diff --git a/src b/src deleted file mode 160000 index 344d90b..0000000 --- a/src +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 344d90be6efcf811527ce49769debd97cf626e4d diff --git a/src5/code/.gitignore b/src5/code/.gitignore new file mode 100644 index 0000000..14ea590 --- /dev/null +++ b/src5/code/.gitignore @@ -0,0 +1,14 @@ +# Windows +[Dd]esktop.ini +Thumbs.db +$RECYCLE.BIN/ + +# macOS +.DS_Store +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes + +# Node.js +node_modules/ diff --git a/src5/code/README.md b/src5/code/README.md new file mode 100644 index 0000000..974b957 --- /dev/null +++ b/src5/code/README.md @@ -0,0 +1,87 @@ +# 校园二手交易小程序 + +## 1. 系统简介 + +本项目是一个功能完善的校园二手交易微信小程序,旨在为大学校园内的学生和教职工提供一个安全、便捷、高效的二手物品交易平台。系统不仅包含了完整的商品交易流程,还融入了丰富的社交和实用功能,构建了一个活跃的校园社区生态。 + +### 主要功能模块 + +- **商品模块**:用户可以轻松发布、编辑、下架自己的二手商品,支持图文描述、价格、分类、交易地点等信息。 +- **交易流程**:实现了从创建订单、买家支付(模拟)、卖家确认、买家收货到交易完成的全闭环流程。 +- **求购广场**:用户可以发布求购信息,寻找自己需要的物品,其他用户可以响应求购。 +- **智能定价**:支持用户上传二手商品,生成闲置商品破损程度分析和定价建议 +- **实时聊天**:买卖双方可以进行一对一的实时沟通,支持文字和图片消息。 +- **校园地图模式**:以地图为载体,直观展示校园内各个交易地点的商品信息,方便用户发现附近的宝贝。 +- **智能推荐**:根据用户的浏览和收藏行为,个性化推荐可能感兴趣的商品。 + +### 技术栈 + +- **前端**:微信小程序原生开发 (WXML, WXSS, JavaScript) +- **后端**:微信小程序云开发 +- **数据库**:微信云开发数据库 +- **核心服务**:微信云函数、云存储 + +--- + +## 2. 环境配置与部署指南 + +请严格按照以下步骤进行配置,以确保项目能成功运行。 + +### 步骤 1:准备工作 + +1. **安装微信开发者工具**:前往 [微信开放平台](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html) 下载并安装最新版的微信开发者工具。 +2. **注册小程序账号**:拥有一个自己的小程序 AppID。个人或企业主体均可。 + +### 步骤 2:导入项目 + +1. 打开微信开发者工具,点击“导入项目”。 +2. **项目目录**:选择本项目的根目录 `ruangong1`。 +3. **AppID**:填写您自己的小程序 AppID。 +4. **项目名称**:自定义即可。 +5. 点击“导入”。 + +### 步骤 3:开通并配置云开发环境 + +1. 在开发者工具的顶部工具栏中,点击“云开发”按钮,打开云开发控制台。 +2. 按照提示**开通云开发**,系统会自动为您创建一个云开发环境。 +3. **记住您的环境 ID**,它通常是一串类似 `your-env-id-xxxxxxxx` 的字符串。 +4. 回到开发者工具的编辑器界面,打开文件 `miniprogram/app.js`。 +5. 找到以下代码块(大约在第 6 行): + ```javascript + this.globalData = { + env: '' // <--- 在这里填入您的环境ID + }; + ``` +6. 将您刚刚获取的**环境 ID** 填入 `env` 字段的引号中。 + +### 步骤 4:部署云函数 + +1. 在开发者工具的左侧文件树中,找到 `cloudfunctions/quickstartFunctions` 目录。 +2. 右键点击该目录,选择“**上传并部署:云端安装依赖**”。 +3. 等待几分钟,直到开发者工具的控制台提示部署成功。 + +### 步骤 5:创建数据库集合 + +这是**非常关键**的一步。项目需要以下数据库集合来存储数据,您必须手动创建它们。 + +1. 打开云开发控制台,切换到“数据库”标签页。 +2. 点击“**+**”号按钮,选择“创建集合”。 +3. 依次创建以下所有集合(**集合名称必须完全一致**): + - `T_user` + - `T_product` + - `T_want` + - `T_order` + - `T_favorites` + - `T_campus_landmarks` + - `T_chat` + - `T_message` + - `T_notify` + - `T_user_behavior` + +4. **设置权限**:为了方便开发和测试,您可以暂时将所有集合的权限设置为“**所有用户可读,仅创建者可读写**”。上线前请根据实际业务需求调整为更严格的权限规则。 + +### 步骤 6:运行项目 + +完成以上所有步骤后,点击开发者工具顶部的“编译”按钮。如果一切顺利,您应该可以在模拟器中看到小程序的启动界面。 + +至此,项目已成功在您的开发环境中运行起来。祝您使用愉快! diff --git a/src5/code/cloudfunctions/quickstartFunctions/config.json b/src5/code/cloudfunctions/quickstartFunctions/config.json new file mode 100644 index 0000000..2ca4704 --- /dev/null +++ b/src5/code/cloudfunctions/quickstartFunctions/config.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "openapi": [ + "wxacode.get" + ] + }, + "timeout": 300, + "envVariables": { + "QQMAP_KEY": "QBJBZ-E433N-DYYFX-SDC5E-EUB3V-MJBOE", + "QQMAP_KEYS": "7YTBZ-DTQHW-RPURE-YWKGQ-UTODK-RZFVP", + "QQMAP_REFERER": "https://servicewechat.com", + "QQMAP_SK": "" + } +} diff --git a/src5/code/cloudfunctions/quickstartFunctions/index.js b/src5/code/cloudfunctions/quickstartFunctions/index.js new file mode 100644 index 0000000..db41dcf --- /dev/null +++ b/src5/code/cloudfunctions/quickstartFunctions/index.js @@ -0,0 +1,3675 @@ +const cloud = require("wx-server-sdk"); +cloud.init({ + env: cloud.DYNAMIC_CURRENT_ENV, +}); + +const axios = require('axios'); +const crypto = require('crypto'); + +const db = cloud.database(); +const _ = db.command; // 数据库命令引用 +// 获取openid +const getOpenId = async () => { + // 获取基础信息 + const wxContext = cloud.getWXContext(); + return { + openid: wxContext.OPENID, + appid: wxContext.APPID, + unionid: wxContext.UNIONID, + }; +}; + +const sendChatMessage = async (event) => { + try { + const wxContext = cloud.getWXContext(); + const fromOpenId = wxContext.OPENID; + const toUserIdParam = String(event?.toUserId || '').trim(); + const toOpenIdParam = String(event?.toOpenId || '').trim(); + const contentType = String(event?.contentType || '').trim(); + const content = String(event?.content || '').trim(); + const imageUrl = String(event?.imageUrl || ''); + const productId = String(event?.productId || ''); + if (!contentType) return { success: false, error: 'contentType required' }; + if (contentType === 'text' && !content) return { success: false, error: 'content required' }; + if (contentType === 'image' && !imageUrl) return { success: false, error: 'imageUrl required' }; + let toUserId = ''; + let toOpenId = ''; + if (toUserIdParam) { + const uById = await db.collection('T_user').doc(toUserIdParam).get(); + const uDoc = uById.data || null; + toUserId = uDoc ? (uDoc._id || toUserIdParam) : toUserIdParam; + toOpenId = uDoc ? (uDoc._openid || '') : toOpenIdParam; + } else if (toOpenIdParam) { + const uByOpen = await db.collection('T_user').where({ _openid: toOpenIdParam }).limit(1).get(); + const uDoc = (uByOpen.data && uByOpen.data[0]) || null; + toOpenId = toOpenIdParam; + toUserId = uDoc ? (uDoc._id || '') : ''; + } + if (!toOpenId && !toUserId) return { success: false, error: 'toUserId or toOpenId required' }; + if (!toOpenId && toUserId) { + const uById2 = await db.collection('T_user').doc(toUserId).get(); + const uDoc2 = uById2.data || null; + toOpenId = uDoc2 ? (uDoc2._openid || '') : ''; + } + if (!toOpenId || toOpenId === fromOpenId) return { success: false, error: 'invalid recipient' }; + const meRes = await db.collection('T_user').where({ _openid: fromOpenId }).limit(1).get(); + const meDoc = (meRes.data && meRes.data[0]) || null; + const fromUserId = meDoc ? (meDoc._id || '') : ''; + const sessionKey = [fromOpenId, toOpenId].sort().join('|'); + const sessionKeyUser = (fromUserId && toUserId) ? [fromUserId, toUserId].sort().join('|') : ''; + const nowTs = Date.now(); + const isSystem = Boolean(event?.isSystem); + const orderId = String(event?.orderId || ''); + const chatDoc = { + sessionKey, + sessionKeyUser, + productId, + fromOpenId, + toOpenId, + fromUserId, + toUserId, + contentType, + content, + imageUrl, + isSystem, + orderId, + timestamp: nowTs, + createTime: new Date() + }; + const addRes = await db.collection('T_chat').add({ data: chatDoc }); + return { success: true, id: addRes._id || '', data: { ...chatDoc, _id: addRes._id || '' } }; + } catch (e) { + return { success: false, error: e.message }; + } +}; + +const getChatMessages = async (event) => { + try { + const wxContext = cloud.getWXContext(); + const myOpenId = wxContext.OPENID; + const toUserIdParam = String(event?.toUserId || '').trim(); + const toOpenIdParam = String(event?.toOpenId || '').trim(); + const limit = Math.max(1, Math.min(200, Number(event?.limit) || 100)); + const before = Number(event?.before) || 0; + let toOpenId = ''; + let toUserId = ''; + if (toUserIdParam) { + const uById = await db.collection('T_user').doc(toUserIdParam).get(); + const uDoc = uById.data || null; + toUserId = uDoc ? (uDoc._id || toUserIdParam) : toUserIdParam; + toOpenId = uDoc ? (uDoc._openid || '') : toOpenIdParam; + } else if (toOpenIdParam) { + const uByOpen = await db.collection('T_user').where({ _openid: toOpenIdParam }).limit(1).get(); + const uDoc = (uByOpen.data && uByOpen.data[0]) || null; + toOpenId = toOpenIdParam; + toUserId = uDoc ? (uDoc._id || '') : ''; + } + if (!toOpenId && !toUserId) return { success: false, error: 'toUserId or toOpenId required' }; + if (!toOpenId && toUserId) { + const uById2 = await db.collection('T_user').doc(toUserId).get(); + const uDoc2 = uById2.data || null; + toOpenId = uDoc2 ? (uDoc2._openid || '') : ''; + } + const sessionKey = [myOpenId, toOpenId].sort().join('|'); + let query = null; + if (toUserId) { + const meRes = await db.collection('T_user').where({ _openid: myOpenId }).limit(1).get(); + const meDoc = (meRes.data && meRes.data[0]) || null; + const myUserId = meDoc ? (meDoc._id || '') : ''; + const sessionKeyUser = (myUserId && toUserId) ? [myUserId, toUserId].sort().join('|') : ''; + query = sessionKeyUser ? db.collection('T_chat').where({ sessionKeyUser }) : db.collection('T_chat').where({ sessionKey }); + } else { + query = db.collection('T_chat').where({ sessionKey }); + } + if (before > 0) { + query = query.where({ timestamp: _.lt(before) }); + } + const ret = await query.orderBy('timestamp', 'asc').limit(limit).get(); + const docs = (ret.data || []).filter(d => d && (d.fromOpenId === myOpenId || d.toOpenId === myOpenId)); + return { success: true, data: docs }; + } catch (e) { + return { success: false, error: e.message }; + } +}; + +const revokeChatMessage = async (event) => { + try { + const wxContext = cloud.getWXContext(); + const myOpenId = wxContext.OPENID; + const id = String(event?.id || '').trim(); + if (!id) return { success: false, error: 'id required' }; + const doc = await db.collection('T_chat').doc(id).get(); + const m = doc.data || null; + if (!m) return { success: false, error: 'not found' }; + if (m.fromOpenId !== myOpenId) return { success: false, error: 'not sender' }; + const now = Date.now(); + if ((now - (m.timestamp || 0)) > 2 * 60 * 1000) return { success: false, error: 'timeout' }; + await db.collection('T_chat').doc(id).update({ data: { isRevoked: true, contentType: 'revoke', content: '' } }); + return { success: true }; + } catch (e) { + return { success: false, error: e.message }; + } +}; + +const listChatSessions = async () => { + try { + const wxContext = cloud.getWXContext(); + const myOpenId = wxContext.OPENID; + const uRes = await db.collection('T_user').where({ _openid: myOpenId }).limit(1).get(); + const me = (uRes.data && uRes.data[0]) || null; + const myUserId = me ? (me._id || '') : ''; + + let msgs = []; + if (myUserId) { + const retUser = await db.collection('T_chat') + .where(_.or([{ fromUserId: myUserId }, { toUserId: myUserId }])) + .orderBy('timestamp', 'desc') + .limit(200) + .get(); + msgs = retUser.data || []; + } + if (!msgs.length) { + const retOpen = await db.collection('T_chat') + .where(_.or([{ fromOpenId: myOpenId }, { toOpenId: myOpenId }])) + .orderBy('timestamp', 'desc') + .limit(200) + .get(); + msgs = retOpen.data || []; + } + + const bySession = {}; + msgs.forEach(m => { + const sk = m.sessionKeyUser || (m.sessionKey || [m.fromOpenId, m.toOpenId].sort().join('|')); + const ts = m.timestamp || 0; + if (!bySession[sk] || ts > (bySession[sk].timestamp || 0)) bySession[sk] = m; + }); + let sessions = Object.values(bySession).map(m => { + const peerUserId = (m.fromUserId === myUserId ? m.toUserId : m.fromUserId) || ''; + const peerOpenId = (m.fromOpenId === myOpenId ? m.toOpenId : m.fromOpenId) || ''; + return { + sessionKey: m.sessionKey || [m.fromOpenId, m.toOpenId].sort().join('|'), + sessionKeyUser: m.sessionKeyUser || ((myUserId && peerUserId) ? [myUserId, peerUserId].sort().join('|') : ''), + peerUserId, + peerOpenId, + lastContentType: m.contentType, + lastContent: m.contentType === 'text' ? m.content : '[图片]', + lastTime: m.timestamp || Date.now(), + peerName: '', + peerAvatar: '', + unreadCount: 0 + }; + }); + + const peerUserIds = sessions.map(s => s.peerUserId).filter(Boolean); + if (peerUserIds.length) { + const uresById = await db.collection('T_user').where({ _id: _.in(peerUserIds) }).get(); + const umap = {}; + (uresById.data || []).forEach(u => { umap[u._id] = u; }); + sessions = sessions.map(s => { + const u = umap[s.peerUserId] || {}; + return { ...s, peerName: u.sname || u.nickName || '用户', peerAvatar: u.avatar || '' }; + }); + } else { + const peerOpenIds = sessions.map(s => s.peerOpenId).filter(Boolean); + if (peerOpenIds.length) { + const uresByOpen = await db.collection('T_user').where({ _openid: _.in(peerOpenIds) }).get(); + const umap2 = {}; + (uresByOpen.data || []).forEach(u => { umap2[u._openid] = u; }); + sessions = sessions.map(s => { + const u = umap2[s.peerOpenId] || {}; + return { ...s, peerName: u.sname || u.nickName || '用户', peerAvatar: u.avatar || '' }; + }); + } + } + + sessions.sort((a, b) => b.lastTime - a.lastTime); + // 计算未读数 + const statesRes = await db.collection('T_chat_read_state').where({ myOpenId }).get(); + const states = statesRes.data || []; + const stateMap = new Map(); + states.forEach(s => { + const key = s.sessionKeyUser || s.sessionKey; + stateMap.set(key, s.lastReadTime || 0); + }); + for (let i = 0; i < sessions.length; i++) { + const s = sessions[i]; + const key = s.sessionKeyUser || s.sessionKey; + const lastRead = stateMap.get(key) || 0; + let q = null; + if (s.sessionKeyUser) { + q = db.collection('T_chat').where({ sessionKeyUser: s.sessionKeyUser, toOpenId: myOpenId, timestamp: _.gt(lastRead) }); + } else { + q = db.collection('T_chat').where({ sessionKey: s.sessionKey, toOpenId: myOpenId, timestamp: _.gt(lastRead) }); + } + try { + const cnt = await q.count(); + s.unreadCount = cnt.total || 0; + } catch (e) { + s.unreadCount = 0; + } + } + return { success: true, data: sessions }; + } catch (e) { + return { success: false, error: e.message }; + } +}; + +const updateChatReadState = async (event) => { + try { + const wxContext = cloud.getWXContext(); + const myOpenId = wxContext.OPENID; + const toUserIdParam = String(event?.toUserId || '').trim(); + const toOpenIdParam = String(event?.toOpenId || '').trim(); + let toOpenId = ''; + let toUserId = ''; + if (toUserIdParam) { + const uById = await db.collection('T_user').doc(toUserIdParam).get(); + const uDoc = uById.data || null; + toUserId = uDoc ? (uDoc._id || toUserIdParam) : toUserIdParam; + toOpenId = uDoc ? (uDoc._openid || '') : toOpenIdParam; + } else if (toOpenIdParam) { + const uByOpen = await db.collection('T_user').where({ _openid: toOpenIdParam }).limit(1).get(); + const uDoc = (uByOpen.data && uByOpen.data[0]) || null; + toOpenId = toOpenIdParam; + toUserId = uDoc ? (uDoc._id || '') : ''; + } + if (!toOpenId && toUserId) { + const uById2 = await db.collection('T_user').doc(toUserId).get(); + const uDoc2 = uById2.data || null; + toOpenId = uDoc2 ? (uDoc2._openid || '') : ''; + } + const meRes = await db.collection('T_user').where({ _openid: myOpenId }).limit(1).get(); + const meDoc = (meRes.data && meRes.data[0]) || null; + const myUserId = meDoc ? (meDoc._id || '') : ''; + const sessionKey = [myOpenId, toOpenId].sort().join('|'); + const sessionKeyUser = (myUserId && toUserId) ? [myUserId, toUserId].sort().join('|') : ''; + const now = Date.now(); + const col = db.collection('T_chat_read_state'); + const where = sessionKeyUser ? { sessionKeyUser, myOpenId } : { sessionKey, myOpenId }; + const exist = await col.where(where).limit(1).get(); + if (exist.data && exist.data[0]) { + await col.doc(exist.data[0]._id).update({ data: { lastReadTime: now } }); + } else { + await col.add({ data: { sessionKey, sessionKeyUser, myOpenId, myUserId, lastReadTime: now, createTime: new Date() } }); + } + return { success: true }; + } catch (e) { + return { success: false, error: e.message }; + } +}; +// 获取小程序二维码 +const getMiniProgramCode = async () => { + // 获取小程序二维码的buffer + const resp = await cloud.openapi.wxacode.get({ + path: "pages/index/index", + }); + const { buffer } = resp; + // 将图片上传云存储空间 + const upload = await cloud.uploadFile({ + cloudPath: "code.png", + fileContent: buffer, + }); + return upload.fileID; +}; + +// 创建集合 +const createCollection = async () => { + try { + // 创建集合 + await db.createCollection("sales"); + await db.collection("sales").add({ + // data 字段表示需新增的 JSON 数据 + data: { + region: "华东", + city: "上海", + sales: 11, + }, + }); + await db.collection("sales").add({ + // data 字段表示需新增的 JSON 数据 + data: { + region: "华东", + city: "南京", + sales: 11, + }, + }); + await db.collection("sales").add({ + // data 字段表示需新增的 JSON 数据 + data: { + region: "华南", + city: "广州", + sales: 22, + }, + }); + await db.collection("sales").add({ + // data 字段表示需新增的 JSON 数据 + data: { + region: "华南", + city: "深圳", + sales: 22, + }, + }); + return { + success: true, + }; + } catch (e) { + // 这里catch到的是该collection已经存在,从业务逻辑上来说是运行成功的,所以catch返回success给前端,避免工具在前端抛出异常 + return { + success: true, + data: "create collection success", + }; + } +}; + +// 查询数据 +const selectRecord = async () => { + // 返回数据库查询结果 + return await db.collection("sales").get(); +}; + +// 更新数据 +const updateRecord = async (event) => { + try { + // 遍历修改数据库信息 + for (let i = 0; i < event.data.length; i++) { + await db + .collection("sales") + .where({ + _id: event.data[i]._id, + }) + .update({ + data: { + sales: event.data[i].sales, + }, + }); + } + return { + success: true, + data: event.data, + }; + } catch (e) { + return { + success: false, + errMsg: e, + }; + } +}; + +// 新增数据 +const insertRecord = async (event) => { + try { + const insertRecord = event.data; + // 插入数据 + await db.collection("sales").add({ + data: { + region: insertRecord.region, + city: insertRecord.city, + sales: Number(insertRecord.sales), + }, + }); + return { + success: true, + data: event.data, + }; + } catch (e) { + return { + success: false, + errMsg: e, + }; + } +}; + +// 删除数据 +const deleteRecord = async (event) => { + try { + await db + .collection("sales") + .where({ + _id: event.data._id, + }) + .remove(); + return { + success: true, + }; + } catch (e) { + return { + success: false, + errMsg: e, + }; + } +}; + +// 通过云函数进行腾讯位置服务逆地理编码(更安全,避免前端域名/密钥限制) +const cloudReverseGeocode = async (event) => { + try { + const lat = Number(event.latitude || (event.location && event.location.latitude)); + const lng = Number(event.longitude || (event.location && event.location.longitude)); + // 组装可用 Keys:优先单 Key,其次多 Key 列表(逗号分隔) + const primaryKey = process.env.QQMAP_KEY || event.key; // 优先使用云环境变量 + const multiKeysStr = process.env.QQMAP_KEYS || event.keys || ''; + const extraKeys = multiKeysStr.split(',').map(k => k.trim()).filter(Boolean); + const keys = []; + if (primaryKey) keys.push(primaryKey); + for (const k of extraKeys) { if (!keys.includes(k)) keys.push(k); } + if (keys.length === 0) { + return { success: false, error: '缺少QQMAP_KEY,请在云环境变量配置或传入key' }; + } + if (!isFinite(lat) || !isFinite(lng)) { + return { success: false, error: '坐标参数无效' }; + } + + // 规范化 Referer:移除所有非 URL 合法字符(含空格、反引号等),避免校验失败 + const refererRaw = process.env.QQMAP_REFERER || event.referer || ''; + // 规范化 Referer:显式去除常见引号字符(包含反引号、中文引号),并移除空白 + const normalizeReferer = (val) => { + let s = String(val || '') + .replace(/[`"'“”‘’]/g, '') + .replace(/\s+/g, ''); + // 二次清理:仅保留 URL 常见合法字符 + s = s.replace(/[^a-zA-Z0-9:\/._-]/g, ''); + // 若为空或不以 http(s) 开头,兜底为微信域 + if (!s || !/^https?:\/\//.test(s)) return 'https://servicewechat.com'; + // 若包含 servicewechat.com,则统一为基础域,避免子路径差异 + if (s.includes('servicewechat.com')) return 'https://servicewechat.com'; + return s; + }; + const referer = normalizeReferer(refererRaw); + const sk = process.env.QQMAP_SK || event.sk || ''; + const urlBase = 'https://apis.map.qq.com'; + const apiPathVariants = ['/ws/geocoder/v1', '/ws/geocoder/v1/']; + let lastError = null; + console.log('[Reverse] using referer:', referer); + for (const k of keys) { + // 仅使用必需参数参与签名,避免含 '=' 的值(如 poi_options)造成歧义 + const base = { key: k, location: `${lat},${lng}` }; + const keysSorted = Object.keys(base).sort(); + const qsPlain = keysSorted.map(p => `${p}=${base[p]}`).join('&'); + const qsEncoded = keysSorted.map(p => `${p}=${encodeURIComponent(String(base[p]))}`).join('&'); + const qsReqBase = qsEncoded; + + for (const apiPath of apiPathVariants) { + let qsReq = qsReqBase; + // 先尝试带签名 + if (sk) { + // 同时尝试“未编码”和“已编码”两种签名算法 + const sigStrPlain = `${apiPath}?${qsPlain}${sk}`; + const sigPlain = crypto.createHash('md5').update(sigStrPlain).digest('hex'); + console.log('[Reverse] try sigPlain', { apiPath, qsPlain, sigPlain }); + let tried = false; + // 先尝试 plain 签名 + try { + const resp = await axios.get(`${urlBase}${apiPath}?${qsReq}&sig=${sigPlain}`, { + timeout: 5000, + headers: referer ? { Referer: referer } : {} + }); + tried = true; + if (resp.data && resp.data.status === 0) { + const r = resp.data.result || {}; + return { + success: true, + data: { + address: r.address || '', + formatted_addresses: r.formatted_addresses || {}, + address_component: r.address_component || {}, + address_reference: r.address_reference || {}, + ad_info: r.ad_info || {}, + location: { lat, lng } + } + }; + } else { + lastError = new Error(resp.data?.message || '逆地理编码失败'); + } + } catch (e) { + lastError = e; + } + + // 若失败再尝试 encoded 签名 + try { + const sigStrEnc = `${apiPath}?${qsEncoded}${sk}`; + const sigEnc = crypto.createHash('md5').update(sigStrEnc).digest('hex'); + console.log('[Reverse] try sigEnc', { apiPath, qsEncoded, sigEnc }); + const resp = await axios.get(`${urlBase}${apiPath}?${qsReq}&sig=${sigEnc}`, { + timeout: 5000, + headers: referer ? { Referer: referer } : {} + }); + if (resp.data && resp.data.status === 0) { + const r = resp.data.result || {}; + return { + success: true, + data: { + address: r.address || '', + formatted_addresses: r.formatted_addresses || {}, + address_component: r.address_component || {}, + address_reference: r.address_reference || {}, + ad_info: r.ad_info || {}, + location: { lat, lng } + } + }; + } else { + lastError = new Error(resp.data?.message || '逆地理编码失败'); + } + } catch (e) { + lastError = e; + } + // 如果上面两种签名都失败,将在后面尝试无签名请求 + } + try { + const resp = await axios.get(`${urlBase}${apiPath}?${qsReq}`, { + timeout: 5000, + headers: referer ? { Referer: referer } : {} + }); + if (resp.data && resp.data.status === 0) { + const r = resp.data.result || {}; + return { + success: true, + data: { + address: r.address || '', + formatted_addresses: r.formatted_addresses || {}, + address_component: r.address_component || {}, + address_reference: r.address_reference || {}, + ad_info: r.ad_info || {}, + location: { lat, lng } + } + }; + } else { + lastError = new Error(resp.data?.message || '逆地理编码失败'); + } + } catch (e) { + lastError = e; + } + + // 若签名失败且开启了签名校验,尝试不带签名的请求(部分控制台配置允许) + try { + const resp = await axios.get(`${urlBase}${apiPath}?${qsReqBase}`, { + timeout: 5000, + headers: referer ? { Referer: referer } : {} + }); + if (resp.data && resp.data.status === 0) { + const r = resp.data.result || {}; + return { + success: true, + data: { + address: r.address || '', + formatted_addresses: r.formatted_addresses || {}, + address_component: r.address_component || {}, + address_reference: r.address_reference || {}, + ad_info: r.ad_info || {}, + location: { lat, lng } + } + }; + } + } catch (e) { + // 保留最后的错误用于返回 + lastError = e; + } + } + } + return { success: false, error: lastError?.message || '逆地理编码失败' }; + } catch (e) { + return { success: false, error: e.message }; + } +}; + +// 正向地理编码:根据地址获取坐标(支持多 Key 与签名) +const cloudGeocode = async (event) => { + try { + const address = String(event.address || '').trim(); + if (!address) { + return { success: false, error: '地址为空' }; + } + const primaryKey = process.env.QQMAP_KEY || event.key; // 优先环境变量 + const multiKeysStr = process.env.QQMAP_KEYS || event.keys || ''; + const extraKeys = multiKeysStr.split(',').map(k => k.trim()).filter(Boolean); + const keys = []; + if (primaryKey) keys.push(primaryKey); + for (const k of extraKeys) { if (!keys.includes(k)) keys.push(k); } + if (keys.length === 0) { + return { success: false, error: '缺少QQMAP_KEY,请在云环境变量配置或传入key' }; + } + const refererRaw = process.env.QQMAP_REFERER || event.referer || ''; + const normalizeReferer = (val) => { + let s = String(val || '') + .replace(/[`"'“”‘’]/g, '') + .replace(/\s+/g, ''); + s = s.replace(/[^a-zA-Z0-9:\/._-]/g, ''); + if (!s || !/^https?:\/\//.test(s)) return 'https://servicewechat.com'; + if (s.includes('servicewechat.com')) return 'https://servicewechat.com'; + return s; + }; + const referer = normalizeReferer(refererRaw); + const sk = process.env.QQMAP_SK || event.sk || ''; + const urlBase = 'https://apis.map.qq.com'; + const apiPathVariants = ['/ws/geocoder/v1', '/ws/geocoder/v1/']; + let lastError = null; + console.log('[Forward] using referer:', referer); + for (const k of keys) { + // 仅使用必需参数参与签名 + const base = { address, key: k }; + const keysSorted = Object.keys(base).sort(); + const qsPlain = keysSorted.map(p => `${p}=${base[p]}`).join('&'); + const qsEncoded = keysSorted.map(p => `${p}=${encodeURIComponent(String(base[p]))}`).join('&'); + const qsReqBase = qsEncoded; + + for (const apiPath of apiPathVariants) { + let qsReq = qsReqBase; + // 先尝试带签名 + if (sk) { + // 同时尝试“未编码”和“已编码”两种签名算法 + const sigStrPlain = `${apiPath}?${qsPlain}${sk}`; + const sigPlain = crypto.createHash('md5').update(sigStrPlain).digest('hex'); + console.log('[Forward] try sigPlain', { apiPath, qsPlain, sigPlain }); + try { + const resp = await axios.get(`${urlBase}${apiPath}?${qsReq}&sig=${sigPlain}`, { + timeout: 5000, + headers: referer ? { Referer: referer } : {} + }); + if (resp.data && resp.data.status === 0 && resp.data.result && resp.data.result.location) { + const loc = resp.data.result.location; + return { success: true, data: { latitude: loc.lat, longitude: loc.lng } }; + } else { + lastError = new Error(resp.data?.message || '正向地理编码失败'); + } + } catch (e) { + lastError = e; + } + + try { + const sigStrEnc = `${apiPath}?${qsEncoded}${sk}`; + const sigEnc = crypto.createHash('md5').update(sigStrEnc).digest('hex'); + console.log('[Forward] try sigEnc', { apiPath, qsEncoded, sigEnc }); + const resp = await axios.get(`${urlBase}${apiPath}?${qsReq}&sig=${sigEnc}`, { + timeout: 5000, + headers: referer ? { Referer: referer } : {} + }); + if (resp.data && resp.data.status === 0 && resp.data.result && resp.data.result.location) { + const loc = resp.data.result.location; + return { success: true, data: { latitude: loc.lat, longitude: loc.lng } }; + } else { + lastError = new Error(resp.data?.message || '正向地理编码失败'); + } + } catch (e) { + lastError = e; + } + } + try { + const resp = await axios.get(`${urlBase}${apiPath}?${qsReq}`, { + timeout: 5000, + headers: referer ? { Referer: referer } : {} + }); + if (resp.data && resp.data.status === 0 && resp.data.result && resp.data.result.location) { + const loc = resp.data.result.location; + return { success: true, data: { latitude: loc.lat, longitude: loc.lng } }; + } else { + lastError = new Error(resp.data?.message || '正向地理编码失败'); + } + } catch (e) { + lastError = e; + } + + // 若签名失败,尝试不带签名 + try { + const resp = await axios.get(`${urlBase}${apiPath}?${qsReqBase}`, { + timeout: 5000, + headers: referer ? { Referer: referer } : {} + }); + if (resp.data && resp.data.status === 0 && resp.data.result && resp.data.result.location) { + const loc = resp.data.result.location; + return { success: true, data: { latitude: loc.lat, longitude: loc.lng } }; + } + } catch (e) { + lastError = e; + } + } + } + return { success: false, error: lastError?.message || '正向地理编码失败' }; + } catch (e) { + return { success: false, error: e.message }; + } +}; + +// 创建通知集合 T_notify +const createNotifyCollection = async () => { + try { + await db.createCollection('T_notify'); + return { success: true }; + } catch (e) { + // 集合可能已存在,视为成功 + return { success: true, data: 'T_notify exists' }; + } +}; + +// 创建聊天集合 T_chat,并为 sessionKey/sessionKeyUser + timestamp 建索引(若环境支持) +const createChatCollection = async () => { + try { + await db.createCollection('T_chat'); + try { + // 索引创建在部分环境可能不支持,失败不影响主要功能 + await db.collection('T_chat').createIndex({ + fields: { + sessionKey: 1, + timestamp: -1 + }, + name: 'idx_session_time' + }); + await db.collection('T_chat').createIndex({ + fields: { + sessionKeyUser: 1, + timestamp: -1 + }, + name: 'idx_session_user_time' + }); + } catch (e) { + // 忽略索引创建失败 + } + return { success: true }; + } catch (e) { + // 集合可能已存在,视为成功 + return { success: true, data: 'T_chat exists' }; + } +}; + +const createMessageCollection = async () => { + try { + await db.createCollection('T_message'); + try { + await db.collection('T_message').createIndex({ + fields: { toUserId: 1, fromUserId: 1 }, + name: 'idx_to_from' + }); + } catch (e) {} + return { success: true }; + } catch (e) { + return { success: true, data: 'T_message exists' }; + } +}; + +// 批量为 T_product 补齐缺失的 sellerUserId(根据 sellerOpenId 关联 T_user) +const backfillSellerUserId = async (event) => { + try { + const batchSize = Math.max(10, Math.min(500, Number(event?.batchSize) || 100)); + const dryRun = Boolean(event?.dryRun); + + const colProduct = db.collection('T_product'); + const colUser = db.collection('T_user'); + + // 统计总数 + const countRes = await colProduct.count(); + const total = countRes.total || 0; + + let processed = 0; + let corrected = 0; + let skipped = 0; + const openIdToUserId = new Map(); + + for (let offset = 0; offset < total; offset += batchSize) { + const page = await colProduct.skip(offset).limit(batchSize).get(); + const items = page.data || []; + for (const p of items) { + processed += 1; + const hasSellerUserId = !!(p.sellerUserId && String(p.sellerUserId).trim() !== ''); + if (hasSellerUserId) { + skipped += 1; + continue; + } + + const sellerOpenId = p.sellerOpenId || ''; + if (!sellerOpenId) { + skipped += 1; + continue; + } + + // 缓存查询,避免重复命中数据库 + let userId = openIdToUserId.get(sellerOpenId); + if (!userId) { + const uRes = await colUser.where({ _openid: sellerOpenId }).limit(1).get(); + const u = (uRes.data && uRes.data[0]) || null; + userId = u ? u._id : undefined; + if (userId) openIdToUserId.set(sellerOpenId, userId); + } + + if (!userId) { + skipped += 1; + continue; + } + + if (!dryRun) { + try { + await colProduct.doc(p._id).update({ + data: { + sellerUserId: userId + } + }); + corrected += 1; + } catch (e) { + // 单条失败不影响整体流程 + console.error('更新商品 sellerUserId 失败:', p._id, e?.message); + } + } else { + corrected += 1; // 试运行只统计可修复数 + } + } + } + + return { + success: true, + message: dryRun ? '试运行完成(未实际写库)' : '回填完成', + total, + processed, + corrected, + skipped, + batchSize, + dryRun + }; + } catch (e) { + return { + success: false, + error: e?.message || 'backfillSellerUserId 执行失败' + }; + } +}; + +// 创建用户地址集合 T_address(用于存储默认地址等) +const createAddressCollection = async () => { + try { + await db.createCollection('T_address'); + return { success: true }; + } catch (e) { + // 已存在也视为成功 + return { success: true, data: 'T_address exists' }; + } +}; + +// 便捷初始化:为当前用户写入一个默认地址(校园地址),便于测试“地址附近交易点” +const seedDefaultAddress = async (event) => { + try { + const wxContext = cloud.getWXContext(); + const openid = wxContext.OPENID; + if (!openid) { + return { success: false, error: '未获取openid' }; + } + + const col = db.collection('T_address'); + // 若已有默认地址则直接返回成功 + const existing = await col.where({ _openid: openid, isDefault: true }).limit(1).get(); + if (existing.data && existing.data.length > 0) { + return { success: true, data: '已有默认地址' }; + } + + const now = new Date(); + await col.add({ + data: { + name: event?.name || '默认地址', + phone: event?.phone || '', + province: event?.province || '天津市', + city: event?.city || '东丽区', + district: event?.district || '', + detail: event?.detail || '中国民航大学', + isDefault: true, + createTime: now + } + }); + return { success: true }; +} catch (e) { + return { success: false, error: e.message }; +} +}; + +// 批量修复商品缺失坐标:优先地标匹配,其次地址正向地理编码 +// 用法:wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'fixProductCoords', limit: 200, dryRun: false } }) +const fixProductCoords = async (event) => { + try { + const limit = Number(event?.limit) > 0 ? Math.min(Number(event.limit), 500) : 200; + const dryRun = !!event?.dryRun; + const col = db.collection('T_product'); + // QQMap 配置(若云环境变量未设,可通过 event 传入) + const qqKey = process.env.QQMAP_KEY || event?.key || ''; + const qqKeys = process.env.QQMAP_KEYS || event?.keys || ''; + const qqSk = process.env.QQMAP_SK || event?.sk || ''; + const qqReferer = process.env.QQMAP_REFERER || event?.referer || 'https://servicewechat.com'; + + // 内置校园地标(云端版,可加入更细的别名以提高匹配率) + const campusLandmarksCloud = [ + { name: '图书馆', latitude: 39.1234, longitude: 117.3365 }, + { name: '体育馆', latitude: 39.1216, longitude: 117.3394 }, + { name: '第一食堂', latitude: 39.1242, longitude: 117.3396 }, + { name: '第二食堂', latitude: 39.1211, longitude: 117.3358 }, + { name: '西门快递点', latitude: 39.1209, longitude: 117.3349 }, + { name: '南门', latitude: 39.1198, longitude: 117.3382 }, + { name: '北门', latitude: 39.1255, longitude: 117.3361 }, + // 常见别称与扩展(示例) + { name: '礼堂', latitude: 39.112869, longitude: 117.349220 }, + { name: '中国民航大学北校区礼堂', latitude: 39.112869, longitude: 117.349220 }, + { name: '北校区礼堂', latitude: 39.112869, longitude: 117.349220 } + ]; + + const normalizeNum = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : null; + }; + + const extractCoordsFromString = (s) => { + if (!s || typeof s !== 'string') return null; + const m = s.match(/(-?\d+\.?\d*)[\s,]+(-?\d+\.?\d*)/); + if (m) { + const lat = normalizeNum(m[1]); + const lng = normalizeNum(m[2]); + if (lat != null && lng != null) return { lat, lng }; + } + return null; + }; + + const simplify = (s) => String(s || '') + .replace(/[()()]/g, '') + .replace(/附近$/g, '') + .trim(); + + const matchLandmark = (names) => { + const cands = (names || []).filter(Boolean).map(v => simplify(v)); + if (cands.length === 0) return null; + for (const candidate of cands) { + const m = campusLandmarksCloud.find(l => candidate.includes(l.name) || l.name.includes(candidate)); + if (m && Number.isFinite(m.latitude) && Number.isFinite(m.longitude)) { + return { lat: m.latitude, lng: m.longitude }; + } + } + return null; + }; + + // 分页获取,筛选出缺坐标的记录 + const countRes = await col.count(); + const total = (countRes && countRes.total) || 0; + const pageSize = 100; + const toFix = []; + for (let skip = 0; skip < Math.max(total, pageSize) && toFix.length < limit; skip += pageSize) { + const page = await col.skip(skip).limit(pageSize).get(); + const batch = (page && page.data) || []; + for (const p of batch) { + const lat = normalizeNum(p.tradeLocationLat ?? p.tradeLat ?? p.lat ?? p.latitude ?? p?.tradeLocation?.latitude); + const lng = normalizeNum(p.tradeLocationLng ?? p.tradeLng ?? p.lng ?? p.longitude ?? p?.tradeLocation?.longitude); + if (!(lat != null && lng != null)) { + toFix.push(p); + } + if (toFix.length >= limit) break; + } + if (!page || !page.data || page.data.length < pageSize) break; + } + + const results = []; + let fixedCount = 0; + for (const p of toFix) { + let latLng = null; + // 1) 解析字符串坐标 + latLng = latLng || extractCoordsFromString(String(p.tradeLocation || '')); + // 2) 地标匹配 + latLng = latLng || matchLandmark([p.tradeLandmarkName, p.tradeLocationName, p.tradeAddress]); + // 3) 地址正向地理编码 + if (!latLng) { + const addr = simplify(p.tradeAddress || '') || `${simplify(p.tradeLandmarkName || p.tradeLocationName || '')} 中国民航大学 东丽校区`; + if (addr) { + try { + const r = await cloudGeocode({ address: addr, key: qqKey, keys: qqKeys, sk: qqSk, referer: qqReferer }); + if (r && r.success && r.data && Number.isFinite(r.data.latitude) && Number.isFinite(r.data.longitude)) { + latLng = { lat: r.data.latitude, lng: r.data.longitude }; + } + } catch (e) { + // 忽略该条错误,继续下一个 + } + } + } + + if (latLng) { + if (!dryRun) { + try { + await col.doc(p._id).update({ data: { tradeLocationLat: latLng.lat, tradeLocationLng: latLng.lng } }); + fixedCount += 1; + results.push({ id: p._id, ok: true, lat: latLng.lat, lng: latLng.lng }); + } catch (e) { + results.push({ id: p._id, ok: false, error: e.message }); + } + } else { + results.push({ id: p._id, ok: true, lat: latLng.lat, lng: latLng.lng, dryRun: true }); + } + } else { + results.push({ id: p._id, ok: false, error: '未能解析或编码地址' }); + } + } + + return { success: true, fixedCount, totalCandidates: toFix.length, dryRun, results }; + } catch (e) { + return { success: false, error: e.message }; + } +}; + +// 创建地标集合 T_campus_landmarks(若不存在) +const createCampusLandmarksCollection = async () => { + try { + await db.createCollection('T_campus_landmarks'); + return { success: true }; + } catch (e) { + // 已存在也视为成功 + return { success: true, data: 'T_campus_landmarks exists' }; + } +}; + +// 规范化/去噪地标名称(用于去重与匹配) +const normalizeLandmarkName = (s) => String(s || '') + .replace(/[()()]/g, '') + .replace(/附近$/g, '') + .replace(/\s+/g, '') + .trim(); + +// 上报/同步单条地标:按规范化名称去重,存在则更新,不存在则新增 +// 用法:wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'upsertCampusLandmark', name, latitude, longitude, address, tags } }) +const upsertCampusLandmark = async (event) => { + try { + const name = String(event?.name || '').trim(); + const latitude = Number(event?.latitude); + const longitude = Number(event?.longitude); + const address = String(event?.address || '').trim(); + const tags = Array.isArray(event?.tags) ? event.tags : []; + const source = event?.source || 'product'; + const productId = String(event?.productId || '').trim(); + const selling = typeof event?.selling === 'boolean' ? event.selling : undefined; + const productCategory = String(event?.productCategory || '').trim(); + const thumbUrl = String(event?.thumbUrl || '').trim(); + + if (!name) return { success: false, error: 'name不能为空' }; + if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) { + return { success: false, error: '坐标无效' }; + } + + const col = db.collection('T_campus_landmarks'); + const normalizedName = normalizeLandmarkName(name); + + // 若集合不存在,尝试创建 + try { await db.collection('T_campus_landmarks').limit(1).get(); } catch (e) { + const msg = String(e?.message || e?.errMsg || ''); + if (/DATABASE_COLLECTION_NOT_EXIST|ResourceNotFound|Db or Table not exist/i.test(msg)) { + await createCampusLandmarksCollection(); + } + } + + const existing = await col.where({ normalizedName }).limit(1).get(); + const now = new Date(); + if (existing.data && existing.data.length > 0) { + const id = existing.data[0]._id; + const prev = existing.data[0] || {}; + const prevIds = Array.isArray(prev.productIds) ? prev.productIds : []; + const nextIds = productId && !prevIds.includes(productId) ? [...prevIds, productId] : prevIds; + const data = { + name, + normalizedName, + latitude, + longitude, + address, + tags, + source, + productIds: nextIds, + selling: selling === undefined ? prev.selling : selling, + productCategory: productCategory ? productCategory : prev.productCategory, + thumbUrl: thumbUrl ? thumbUrl : prev.thumbUrl, + updateTime: now + }; + await col.doc(id).update({ data }); + return { success: true, updated: 1, id }; + } else { + const addRes = await col.add({ + data: { + name, + normalizedName, + latitude, + longitude, + address, + tags, + source, + productIds: productId ? [productId] : [], + selling: selling === undefined ? true : selling, + productCategory, + thumbUrl, + createTime: now, + updateTime: now + } + }); + return { success: true, created: 1, id: addRes._id }; + } + } catch (e) { + return { success: false, error: e.message }; + } +}; + +// 批量从商品集合同步可用地标到 T_campus_landmarks(去重,保留最新坐标) +// 支持:旧字段、字符串坐标解析;若无坐标但有 tradeAddress,则云端正向地理编码补齐 +// 用法:wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'syncProductLandmarks', limit: 500, dryRun: false, key: '', referer: '' } }) +const syncProductLandmarks = async (event) => { + try { + const limit = Number(event?.limit) > 0 ? Math.min(Number(event.limit), 1000) : 500; + const dryRun = !!event?.dryRun; + const col = db.collection('T_product'); + + // 工具:数值规范化与字符串坐标解析、名称简化 + const normalizeNum = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : null; + }; + const parseStringCoords = (s) => { + if (!s || typeof s !== 'string') return null; + const m = s.match(/(-?\d+\.?\d*)[\s,]+(-?\d+\.?\d*)/); + if (!m) return null; + const lat = normalizeNum(m[1]); + const lng = normalizeNum(m[2]); + return lat != null && lng != null ? { lat, lng } : null; + }; + const simplify = (s) => String(s || '') + .replace(/[()()]/g, '') + .replace(/附近$/g, '') + .trim(); + + const countRes = await col.count(); + const total = (countRes && countRes.total) || 0; + const pageSize = 100; + const candidates = []; + for (let skip = 0; skip < Math.max(total, pageSize) && candidates.length < limit; skip += pageSize) { + const page = await col.skip(skip).limit(pageSize).get(); + const batch = (page && page.data) || []; + for (const p of batch) { + // 解析坐标:优先数值字段,其次字符串坐标 + let lat = normalizeNum(p.tradeLocationLat ?? p.tradeLat ?? p.lat ?? p.latitude ?? p?.tradeLocation?.latitude); + let lng = normalizeNum(p.tradeLocationLng ?? p.tradeLng ?? p.lng ?? p.longitude ?? p?.tradeLocation?.longitude); + if (lat == null || lng == null) { + const sc = parseStringCoords(String(p.tradeLocation || p.location || p.coords || '')); + if (sc) { lat = sc.lat; lng = sc.lng; } + } + + // 名称:支持嵌套字段与地址,并做简化 + const rawName = p.tradeLandmarkName || p.tradeLocationName || p?.tradeLocation?.landmarkName || p.tradeAddress || ''; + const name = simplify(rawName); + + if (name && lat != null && lng != null) { + candidates.push({ name, lat, lng, address: String(p.tradeAddress || ''), source: 'product', productId: String(p._id || '') }); + } + if (candidates.length >= limit) break; + } + if (!page || !page.data || page.data.length < pageSize) break; + } + + const results = []; + let created = 0; + let updated = 0; + let geocoded = 0; + + // 若存在缺坐标但有地址的潜在项,尝试地理编码补齐 + const potentials = []; + { + // 重新扫描一次(避免漏掉仅有地址的商品),但不超过 limit + const countRes2 = await col.count(); + const total2 = (countRes2 && countRes2.total) || 0; + const pageSize2 = 100; + for (let skip = 0; skip < Math.max(total2, pageSize2) && potentials.length < limit; skip += pageSize2) { + const page = await col.skip(skip).limit(pageSize2).get(); + const batch = (page && page.data) || []; + for (const p of batch) { + // 名称与地址 + const rawName = p.tradeLandmarkName || p.tradeLocationName || p?.tradeLocation?.landmarkName || p.tradeAddress || ''; + const name = simplify(rawName); + const address = String(p.tradeAddress || '').trim(); + // 坐标(含字符串解析) + let lat = normalizeNum(p.tradeLocationLat ?? p.tradeLat ?? p.lat ?? p.latitude ?? p?.tradeLocation?.latitude); + let lng = normalizeNum(p.tradeLocationLng ?? p.tradeLng ?? p.lng ?? p.longitude ?? p?.tradeLocation?.longitude); + if (lat == null || lng == null) { + const sc = parseStringCoords(String(p.tradeLocation || p.location || p.coords || '')); + if (sc) { lat = sc.lat; lng = sc.lng; } + } + if ((name || address) && (lat == null || lng == null) && address) { + potentials.push({ name, address, lat, lng, productId: String(p._id || '') }); + } + if (potentials.length >= limit) break; + } + if (!page || !page.data || page.data.length < pageSize2) break; + } + } + + // 并发地理编码补齐 + if (potentials.length > 0) { + const concurrency = 6; + let idx = 0; + const worker = async () => { + while (idx < potentials.length) { + const item = potentials[idx++]; + try { + const r = await cloudGeocode({ address: item.address, key: event?.key, keys: event?.keys, referer: event?.referer, sk: event?.sk }); + if (r.success && r.data && typeof r.data.latitude === 'number' && typeof r.data.longitude === 'number') { + item.lat = r.data.latitude; + item.lng = r.data.longitude; + geocoded += 1; + // 若名称有效,直接纳入候选进行 upsert + if (item.name && item.lat != null && item.lng != null) { + candidates.push({ name: item.name, lat: item.lat, lng: item.lng, address: item.address, source: 'product', productId: String(item.productId || '') }); + } + } + } catch (e) { + // 忽略失败,继续 + } + } + }; + const workers = Array.from({ length: Math.min(concurrency, potentials.length) }, () => worker()); + await Promise.allSettled(workers); + } + + for (const c of candidates) { + const normalizedName = normalizeLandmarkName(c.name); + if (dryRun) { + results.push({ name: c.name, normalizedName, latitude: c.lat, longitude: c.lng, dryRun: true }); + continue; + } + try { + // 直接调用内部 upsert 逻辑 + const r = await upsertCampusLandmark({ name: c.name, latitude: c.lat, longitude: c.lng, address: c.address, source: c.source, productId: c.productId }); + if (r.success && r.created) created += r.created; + if (r.success && r.updated) updated += r.updated; + results.push({ name: c.name, normalizedName, latitude: c.lat, longitude: c.lng, ok: r.success, id: r.id }); + } catch (e) { + results.push({ name: c.name, normalizedName, error: e.message }); + } + } + return { success: true, totalCandidates: candidates.length, created, updated, geocoded, dryRun, results }; + } catch (e) { + return { success: false, error: e.message }; + } +}; + +// const getOpenId = require('./getOpenId/index'); +// const getMiniProgramCode = require('./getMiniProgramCode/index'); +// const createCollection = require('./createCollection/index'); +// const selectRecord = require('./selectRecord/index'); +// const updateRecord = require('./updateRecord/index'); +// const sumRecord = require('./sumRecord/index'); +// const fetchGoodsList = require('./fetchGoodsList/index'); +// const genMpQrcode = require('./genMpQrcode/index'); +// 查询T_user表数据 +const queryTUser = async () => { + try { + const result = await db.collection("T_user").get(); + return { + success: true, + data: result.data, + count: result.data.length + }; + } catch (e) { + return { + success: false, + error: e.message + }; + } +}; + +// 根据openid查询用户信息(用于表关联) +const getUserByOpenId = async (event) => { + try { + const wxContext = cloud.getWXContext(); + const openid = event.openid || wxContext.OPENID; + + console.log('查询用户信息,openid:', openid); + + if (!openid) { + return { + success: false, + error: 'openid不能为空' + }; + } + + // 先尝试通过_openid查询(微信云开发自动添加的字段) + let userResult = await db.collection("T_user").where({ + _openid: openid + }).get(); + + // 如果没找到,尝试通过其他方式查询(如果T_user表中有openid字段) + if (userResult.data.length === 0) { + // 这里可以根据实际需求添加其他查询方式 + // 例如:如果有存储openid字段,可以查询 + // userResult = await db.collection("T_user").where({ + // openid: openid + // }).get(); + } + + if (userResult.data.length > 0) { + const userData = userResult.data[0]; + return { + success: true, + data: { + userId: userData._id, + sno: userData.sno || '', + sname: userData.sname || '', + phone: userData.phone || '', + major: userData.major || '', + sushe: userData.sushe || '', + grade: userData.年级 || '', + avatar: userData.avatar || '', + openid: openid + } + }; + } else { + return { + success: false, + error: '未找到用户信息', + openid: openid + }; + } + } catch (e) { + console.error('查询用户信息失败:', e); + return { + success: false, + error: e.message + }; + } +}; + +// 添加测试用户到T_user表 +const addTestUser = async (event) => { + try { + const userData = event.userData || { + sno: "230340151", + sname: "测试用户", + phone: "13800138000", + password: "100997@mkg", + major: "计算机科学", + sushe: "1号楼 101", + 年级: "大三", + avatar: "https://via.placeholder.com/100x100/4285F4/ffffff?text=T" + }; + + const result = await db.collection("T_user").add({ + data: userData + }); + + return { + success: true, + data: result, + message: "测试用户添加成功" + }; + } catch (e) { + return { + success: false, + error: e.message + }; + } +}; + +// AI定价功能 - 调用Coze工作流分析商品图片 +const analyzeProductPrice = async (event) => { + const axios = require('axios'); + const FormData = require('form-data'); + + const COZE_API_TOKEN = 'pat_EB0pYMqiuAjIEnGK3i1e12RWwBjF5iZzrzMgdLQE8jNgZWVykJo6ZPGZ2YESYamq'; + const COZE_UPLOAD_URL = 'https://api.coze.cn/v1/files/upload'; + const COZE_WORKFLOW_URL = 'https://api.coze.cn/v1/workflow/run'; + const WORKFLOW_ID = '7567021771821105167'; + + try { + console.log('========== AI定价功能开始 =========='); + console.log('接收到的参数:', JSON.stringify(event, null, 2)); + + // 获取参数 + const { fileID, originalPrice, imageUrl } = event; + + // 确保originalPrice是数字类型 + const priceNum = originalPrice ? parseFloat(originalPrice) : null; + if (priceNum && isNaN(priceNum)) { + throw new Error('原价必须是有效的数字'); + } + + console.log('接收到的参数详情:'); + console.log(' fileID:', fileID); + console.log(' originalPrice:', originalPrice, '类型:', typeof originalPrice); + console.log(' imageUrl:', imageUrl); + + // 第一步:获取图片文件内容 + let imageBuffer = null; + let fileName = 'image.jpg'; + + if (fileID) { + // 从云存储下载文件 + console.log('步骤1: 从云存储下载文件, fileID:', fileID); + try { + if (!fileID || typeof fileID !== 'string') { + throw new Error('fileID参数无效'); + } + + const result = await cloud.downloadFile({ + fileID: fileID + }); + + if (!result || !result.fileContent) { + throw new Error('下载文件结果为空'); + } + + imageBuffer = result.fileContent; + fileName = fileID.split('/').pop() || 'image.jpg'; + + // 确保imageBuffer是Buffer类型 + if (!Buffer.isBuffer(imageBuffer)) { + imageBuffer = Buffer.from(imageBuffer); + } + + console.log('✅ 文件下载成功,文件名:', fileName, '文件大小:', imageBuffer.length, 'bytes'); + } catch (downloadError) { + console.error('❌ 下载云存储文件失败:', downloadError); + console.error('下载错误详情:', JSON.stringify(downloadError, Object.getOwnPropertyNames(downloadError))); + throw new Error('下载图片文件失败: ' + (downloadError.message || downloadError)); + } + } else if (imageUrl) { + // 从URL下载图片 + console.log('步骤1: 从URL下载图片, imageUrl:', imageUrl); + try { + if (!imageUrl || typeof imageUrl !== 'string') { + throw new Error('imageUrl参数无效'); + } + + const response = await axios.get(imageUrl, { + responseType: 'arraybuffer', + timeout: 30000 // 30秒超时 + }); + + imageBuffer = Buffer.from(response.data); + fileName = imageUrl.split('/').pop().split('?')[0] || 'image.jpg'; + console.log('✅ 图片下载成功,文件名:', fileName, '文件大小:', imageBuffer.length, 'bytes'); + } catch (downloadError) { + console.error('❌ 下载图片失败:', downloadError); + console.error('下载错误详情:', downloadError.response?.data || downloadError.message); + throw new Error('下载图片失败: ' + (downloadError.message || downloadError)); + } + } else { + throw new Error('缺少必要参数:需要提供fileID或imageUrl'); + } + + // 验证图片缓冲区 + if (!imageBuffer || imageBuffer.length === 0) { + throw new Error('图片数据为空'); + } + + // 第二步:上传图片到Coze + console.log('步骤2: 上传图片到Coze'); + let cozeFileId = null; + let uploadResponse = null; // 在外部声明,确保在返回时能访问 + + try { + if (!imageBuffer || imageBuffer.length === 0) { + throw new Error('图片数据为空,无法上传'); + } + + const formData = new FormData(); + formData.append('file', imageBuffer, { + filename: fileName, + contentType: 'image/jpeg' + }); + + console.log('准备上传到Coze,文件大小:', imageBuffer.length, 'bytes'); + + uploadResponse = await axios.post(COZE_UPLOAD_URL, formData, { + headers: { + 'Authorization': `Bearer ${COZE_API_TOKEN}`, + ...formData.getHeaders() + }, + timeout: 60000, // 60秒超时 + maxContentLength: Infinity, + maxBodyLength: Infinity + }); + + console.log('✅ 图片上传到Coze成功'); + console.log('上传响应数据:', JSON.stringify(uploadResponse.data, null, 2)); + + if (uploadResponse.data.code === 0 && uploadResponse.data.data && uploadResponse.data.data.id) { + cozeFileId = uploadResponse.data.data.id; + console.log('✅ 获取到Coze file_id:', cozeFileId); + } else { + throw new Error('上传响应格式错误: ' + JSON.stringify(uploadResponse.data)); + } + } catch (uploadError) { + console.error('❌ 上传图片到Coze失败:', uploadError.response?.data || uploadError.message); + throw new Error('上传图片到Coze失败: ' + (uploadError.response?.data?.msg || uploadError.message)); + } + + // 第三步:调用Coze工作流 + console.log('步骤3: 调用Coze工作流'); + console.log('workflow_id:', WORKFLOW_ID); + console.log('file_id:', cozeFileId); + + try { + // 根据curl示例,input参数应该是数字类型(原价) + // curl示例: "input": 30 + // 不再需要inputText,直接使用priceNum + + // 验证file_id + if (!cozeFileId || typeof cozeFileId !== 'string') { + throw new Error('Coze file_id无效: ' + cozeFileId); + } + + // 根据API文档,image参数应该是字符串,内容是JSON格式的字符串 + // 格式: "{\"file_id\":\"...\"}" + // 注意:需要手动构建JSON字符串,确保格式正确 + const imageParam = JSON.stringify({ file_id: String(cozeFileId) }); + + // 验证image参数格式 + try { + const imageObj = JSON.parse(imageParam); + if (!imageObj.file_id || typeof imageObj.file_id !== 'string') { + throw new Error('image参数格式错误: file_id无效'); + } + console.log('✅ image参数格式验证通过:', imageObj); + } catch (parseError) { + console.error('❌ image参数格式验证失败:', parseError); + throw new Error('image参数格式错误: ' + parseError.message); + } + + // 构建请求对象 + const workflowRequest = { + workflow_id: String(WORKFLOW_ID), + parameters: { + image: imageParam, // JSON字符串: "{\"file_id\":\"7567222758887850038\"}" + input: Number(priceNum || 0) // 数字类型(原价),根据curl示例 + } + }; + + // 验证最终请求格式 + const testSerialize = JSON.stringify(workflowRequest); + console.log('测试序列化结果:', testSerialize); + const testParse = JSON.parse(testSerialize); + console.log('测试解析结果:', JSON.stringify(testParse, null, 2)); + + // 验证解析后的image参数格式 + const parsedImage = JSON.parse(testParse.parameters.image); + console.log('解析后的image对象:', parsedImage); + if (!parsedImage.file_id) { + throw new Error('序列化后image参数格式错误'); + } + + // 打印最终发送的请求(序列化后的格式) + const requestBody = JSON.stringify(workflowRequest); + console.log('========== 请求参数详情 =========='); + console.log('工作流请求数据(序列化前):', JSON.stringify(workflowRequest, null, 2)); + console.log('工作流请求数据(序列化后,将发送给API):', requestBody); + console.log('参数类型检查:'); + console.log(' workflow_id类型:', typeof workflowRequest.workflow_id); + console.log(' workflow_id值:', workflowRequest.workflow_id); + console.log(' image类型:', typeof workflowRequest.parameters.image); + console.log(' image值:', workflowRequest.parameters.image); + console.log(' image值长度:', workflowRequest.parameters.image.length); + console.log(' image值解析:', JSON.parse(workflowRequest.parameters.image)); + console.log(' input类型:', typeof workflowRequest.parameters.input); + console.log(' input值:', workflowRequest.parameters.input); + console.log('==================================='); + + // 验证input是数字类型 + if (typeof workflowRequest.parameters.input !== 'number') { + throw new Error('input参数必须是数字类型,当前类型: ' + typeof workflowRequest.parameters.input); + } + + // 模拟curl命令格式,用于对比 + const curlFormat = JSON.stringify({ + workflow_id: workflowRequest.workflow_id, + parameters: { + image: workflowRequest.parameters.image, + input: workflowRequest.parameters.input + } + }, null, 2); + console.log('预期格式(类似curl):', curlFormat); + console.log('✅ 参数格式验证通过,符合curl示例格式'); + + const workflowResponse = await axios.post(COZE_WORKFLOW_URL, workflowRequest, { + headers: { + 'Authorization': `Bearer ${COZE_API_TOKEN}`, + 'Content-Type': 'application/json' + }, + timeout: 120000 // 120秒超时,因为AI分析可能需要较长时间 + }); + + console.log('✅ Coze工作流HTTP请求成功'); + console.log('HTTP状态码:', workflowResponse.status); + console.log('工作流响应数据:', JSON.stringify(workflowResponse.data, null, 2)); + + // 检查响应状态 + if (workflowResponse.data.code !== 0) { + const errorData = workflowResponse.data; + console.error('❌ Coze API返回错误'); + console.error('错误代码:', errorData.code); + console.error('错误消息:', errorData.msg); + console.error('错误详情:', errorData.detail); + console.error('调试URL:', errorData.debug_url); + + // 如果是参数错误,提供更详细的诊断信息 + if (errorData.code === 4000) { + console.error('参数错误诊断:'); + console.error(' 发送的请求数据:', JSON.stringify(workflowRequest, null, 2)); + console.error(' image参数原始值:', imageParam); + console.error(' input参数原始值:', inputText); + } + + throw new Error(`Coze API错误 (${errorData.code}): ${errorData.msg || '未知错误'}`); + } + + // 解析返回结果 + let parsedResult = {}; + if (workflowResponse.data.data) { + try { + // data字段是字符串,需要解析 + const dataStr = typeof workflowResponse.data.data === 'string' + ? workflowResponse.data.data + : JSON.stringify(workflowResponse.data.data); + + const parsedData = JSON.parse(dataStr); + + // 解析output字段 + if (parsedData.output) { + let outputStr = typeof parsedData.output === 'string' + ? parsedData.output + : JSON.stringify(parsedData.output); + + console.log('步骤3.1: 原始output字符串:', outputStr); + + // 尝试解析output中的JSON字符串 + try { + // 处理多重转义的情况 + // 如果output是被双重转义的JSON字符串,需要先解析一次 + if (outputStr.startsWith('"') && outputStr.endsWith('"')) { + try { + outputStr = JSON.parse(outputStr); + console.log('步骤3.2: 解析外层引号后的output:', outputStr); + } catch (e) { + // 如果解析失败,继续使用原始字符串 + console.warn('步骤3.2: 解析外层引号失败,继续使用原始字符串'); + } + } + + // 处理转义字符 + let cleanedOutput = outputStr; + + // 如果仍然是字符串,尝试多次解析转义 + if (typeof cleanedOutput === 'string') { + // 尝试解析转义的JSON字符串 + try { + // 如果看起来像转义的JSON,尝试解析 + if (cleanedOutput.includes('\\"')) { + cleanedOutput = cleanedOutput.replace(/\\"/g, '"').replace(/\\n/g, '\n'); + } + } catch (e) { + // 忽略解析错误 + } + } + + console.log('步骤3.3: 清理后的output:', cleanedOutput); + console.log('步骤3.3: output类型:', typeof cleanedOutput); + + // 使用正则表达式提取字段值 + // 匹配格式: "name":"值" 或 name:"值" 或 name=值 + const outputContent = typeof cleanedOutput === 'string' ? cleanedOutput : JSON.stringify(cleanedOutput); + + const nameMatch = outputContent.match(/["']?name["']?\s*[:=]\s*["']([^"']+)["']/); + const priceMatch = outputContent.match(/["']?price["']?\s*[:=]\s*["']([^"']+)["']/); + const chengseMatch = outputContent.match(/["']?chengse["']?\s*[:=]\s*["']([^"']+)["']/); + const gradeMatch = outputContent.match(/["']?grade["']?\s*[:=]\s*["']([^"']+)["']/); + const suggMatch = outputContent.match(/["']?sugg["']?\s*[:=]\s*["']([^"']+)["']/); + + console.log('步骤3.4: 正则匹配结果:'); + console.log(' name:', nameMatch ? nameMatch[1] : '未匹配'); + console.log(' price:', priceMatch ? priceMatch[1] : '未匹配'); + console.log(' chengse:', chengseMatch ? chengseMatch[1] : '未匹配'); + console.log(' grade:', gradeMatch ? gradeMatch[1] : '未匹配'); + console.log(' sugg:', suggMatch ? (suggMatch[1].substring(0, 50) + '...') : '未匹配'); + + // 如果正则匹配失败,尝试直接解析为JSON对象 + let parsedOutput = null; + try { + // 尝试构建一个完整的JSON对象 + if (outputContent.includes('name') && outputContent.includes('price')) { + // 尝试提取JSON部分 + const jsonMatch = outputContent.match(/\{[\s\S]*\}/); + if (jsonMatch) { + parsedOutput = JSON.parse(jsonMatch[0]); + console.log('步骤3.5: 成功解析为JSON对象:', parsedOutput); + } else { + // 尝试手动构建JSON字符串 + const jsonStr = '{' + outputContent.replace(/\n/g, ',') + '}'; + try { + parsedOutput = JSON.parse(jsonStr); + console.log('步骤3.5: 成功构建并解析JSON对象:', parsedOutput); + } catch (e) { + // 忽略错误 + } + } + } + } catch (e) { + console.warn('步骤3.5: 解析JSON对象失败:', e.message); + } + + // 优先使用解析后的对象,否则使用正则匹配 + parsedResult = { + productName: parsedOutput?.name || nameMatch?.[1] || '--', + suggestedPrice: parsedOutput?.price || priceMatch?.[1] || '0.00', + conditionLevel: parsedOutput?.chengse || chengseMatch?.[1] || '--', + aiScore: parsedOutput?.grade || gradeMatch?.[1] || '--', + analysisReport: parsedOutput?.sugg || suggMatch?.[1] || 'AI分析完成', + rawOutput: outputContent + }; + + console.log('✅ 步骤3.6: 最终解析结果:', JSON.stringify(parsedResult, null, 2)); + } catch (parseError) { + console.warn('⚠️ 解析output失败,使用原始数据:', parseError.message); + console.warn('解析错误堆栈:', parseError.stack); + parsedResult = { + productName: '--', + suggestedPrice: originalPrice ? (originalPrice * 0.8).toFixed(2) : '0.00', + conditionLevel: '--', + aiScore: '--', + analysisReport: outputStr.substring(0, 200), + rawOutput: outputStr + }; + } + } else { + parsedResult = { + productName: '--', + suggestedPrice: originalPrice ? (originalPrice * 0.8).toFixed(2) : '0.00', + conditionLevel: '--', + aiScore: '--', + analysisReport: 'AI分析完成,但未获取到详细信息', + rawOutput: dataStr + }; + } + } catch (parseError) { + console.error('❌ 解析响应数据失败:', parseError); + parsedResult = { + productName: '--', + suggestedPrice: originalPrice ? (originalPrice * 0.8).toFixed(2) : '0.00', + conditionLevel: '--', + aiScore: '--', + analysisReport: '解析响应数据失败: ' + parseError.message, + rawResponse: workflowResponse.data + }; + } + } else { + console.warn('⚠️ 工作流响应中没有data字段'); + console.warn('响应内容:', JSON.stringify(workflowResponse.data, null, 2)); + + // 如果响应中没有data,返回默认结果 + parsedResult = { + productName: '--', + suggestedPrice: originalPrice ? (originalPrice * 0.8).toFixed(2) : '0.00', + conditionLevel: '--', + aiScore: '--', + analysisReport: 'AI分析完成,但未获取到详细信息', + rawResponse: workflowResponse.data + }; + } + + console.log('========== AI定价功能完成 =========='); + + return { + success: true, + data: parsedResult, + debug: { + uploadResponse: uploadResponse ? uploadResponse.data : null, + workflowResponse: workflowResponse.data + } + }; + + } catch (workflowError) { + console.error('❌ 调用Coze工作流失败'); + console.error('错误对象:', workflowError); + console.error('错误响应:', workflowError.response?.data); + console.error('错误状态码:', workflowError.response?.status); + console.error('错误消息:', workflowError.message); + + // 提取详细的错误信息 + let errorMsg = '调用Coze工作流失败'; + if (workflowError.response?.data) { + const errorData = workflowError.response.data; + errorMsg = errorData.msg || errorData.message || errorMsg; + if (errorData.detail) { + errorMsg += '\n详情: ' + JSON.stringify(errorData.detail); + } + if (errorData.code) { + errorMsg += '\n错误代码: ' + errorData.code; + } + } else { + errorMsg += ': ' + workflowError.message; + } + + throw new Error(errorMsg); + } + + } catch (error) { + console.error('========== AI定价功能出错 =========='); + console.error('错误信息:', error.message); + console.error('错误堆栈:', error.stack); + + return { + success: false, + error: error.message, + details: error.response?.data || error.stack + }; + } +}; + +// 初始化T_category表(如果不存在则创建) +const initCategoryTable = async () => { + try { + // 默认类别数据 + const defaultCategories = [ + { categoryName: '电子产品', description: '手机、电脑、配件', icon: '📱', iconBg: '#2196F3', sort: 1, enabled: true }, + { categoryName: '图书文具', description: '教材、小说、专业书籍、文具', icon: '📚', iconBg: '#4CAF50', sort: 2, enabled: true }, + { categoryName: '服装鞋帽', description: '衣服、鞋子、配饰', icon: '👕', iconBg: '#FF9800', sort: 3, enabled: true }, + { categoryName: '家居用品', description: '桌椅、小家电、生活用品', icon: '🛋️', iconBg: '#795548', sort: 4, enabled: true }, + { categoryName: '运动户外', description: '健身器材、球类运动、户外用品', icon: '⚽', iconBg: '#9C27B0', sort: 5, enabled: true }, + { categoryName: '美妆个护', description: '美妆护肤产品', icon: '💄', iconBg: '#FF6B9D', sort: 6, enabled: true }, + { categoryName: '其他', description: '其他各类商品', icon: '📦', iconBg: '#757575', sort: 7, enabled: true } + ]; + + // 检查T_category表是否存在数据 + const existing = await db.collection("T_category").get(); + + if (existing.data.length === 0) { + // 初始化类别数据(使用循环add,避免batch兼容性问题) + for (const category of defaultCategories) { + await db.collection("T_category").add({ + data: { + ...category, + createTime: new Date(), + updateTime: new Date() + } + }); + } + console.log('T_category表初始化成功'); + } + + return { + success: true, + message: 'T_category表已初始化' + }; + } catch (e) { + console.error('初始化T_category表失败:', e); + return { + success: false, + error: e.message + }; + } +}; + +// 获取所有商品类别(优先从T_category表获取,如果没有则从T_product表提取) +const getProductCategories = async () => { + try { + // 先尝试从T_category表获取 + let categoriesResult = await db.collection("T_category") + .where({ + enabled: true + }) + .orderBy('sort', 'asc') + .get(); + + if (categoriesResult.data.length > 0) { + // 从T_category表获取类别 + const categories = categoriesResult.data.map(item => ({ + id: item.categoryName, + name: item.categoryName, + description: item.description || getCategoryDescription(item.categoryName), + icon: item.icon || getCategoryIcon(item.categoryName), + iconBg: item.iconBg || getCategoryBg(item.categoryName) + })); + + return { + success: true, + data: categories, + source: 'T_category' + }; + } + + // 如果T_category表为空,从T_product表提取(兼容旧数据) + console.log('T_category表为空,从T_product表提取类别'); + const result = await db.collection("T_product").field({ + productCategory: true + }).get(); + + // 提取不重复的类别 + const categoriesSet = new Set(); + result.data.forEach(item => { + if (item.productCategory) { + categoriesSet.add(item.productCategory); + } + }); + + // 转换为数组并返回 + const categories = Array.from(categoriesSet).map(category => ({ + id: category, + name: category, + description: getCategoryDescription(category), + icon: getCategoryIcon(category), + iconBg: getCategoryBg(category) + })); + + // 如果数据库中没有类别,返回默认类别 + if (categories.length === 0) { + return { + success: true, + data: [ + { id: '电子产品', name: '电子产品', description: '手机、电脑、配件', icon: '📱', iconBg: '#2196F3' }, + { id: '图书文具', name: '图书文具', description: '教材、小说、专业书籍、文具', icon: '📚', iconBg: '#4CAF50' }, + { id: '服装鞋帽', name: '服装鞋帽', description: '衣服、鞋子、配饰', icon: '👕', iconBg: '#FF9800' }, + { id: '家居用品', name: '家居用品', description: '桌椅、小家电、生活用品', icon: '🛋️', iconBg: '#795548' }, + { id: '运动户外', name: '运动户外', description: '健身器材、球类运动、户外用品', icon: '⚽', iconBg: '#9C27B0' }, + { id: '美妆个护', name: '美妆个护', description: '美妆护肤产品', icon: '💄', iconBg: '#FF6B9D' }, + { id: '其他', name: '其他', description: '其他各类商品', icon: '📦', iconBg: '#757575' } + ], + source: 'default' + }; + } + + return { + success: true, + data: categories, + source: 'T_product' + }; + } catch (e) { + console.error('获取商品类别失败:', e); + return { + success: false, + error: e.message + }; + } +}; + +// 获取类别描述 +const getCategoryDescription = (category) => { + const descriptions = { + '电子产品': '手机、电脑、配件', + '图书文具': '教材、小说、专业书籍、文具', + '服装鞋帽': '衣服、鞋子、配饰', + '家居用品': '桌椅、小家电、生活用品', + '运动户外': '健身器材、球类运动、户外用品', + '美妆个护': '美妆护肤产品', + '其他': '其他各类商品' + }; + return descriptions[category] || '各类商品'; +}; + +// 获取类别图标(使用emoji) +const getCategoryIcon = (category) => { + const icons = { + '电子产品': '📱', + '图书文具': '📚', + '服装鞋帽': '👕', + '家居用品': '🛋️', + '运动户外': '⚽', + '美妆个护': '💄', + '其他': '📦' + }; + return icons[category] || '📦'; +}; + +// 获取类别背景色 +const getCategoryBg = (category) => { + const colors = { + '电子产品': '#2196F3', + '图书文具': '#4CAF50', + '服装鞋帽': '#FF9800', + '家居用品': '#795548', + '运动户外': '#9C27B0', + '美妆个护': '#FF6B9D', + '其他': '#757575' + }; + return colors[category] || '#757575'; +}; + +// 更新用户兴趣 +const updateUserInterests = async (event) => { + try { + const wxContext = cloud.getWXContext(); + const openid = wxContext.OPENID; + const interests = event.interests || []; + + console.log('更新用户兴趣,openid:', openid, 'interests:', interests); + + if (!openid) { + return { + success: false, + error: 'openid不能为空' + }; + } + + // 查询用户是否存在 + const userResult = await db.collection("T_user").where({ + _openid: openid + }).get(); + + if (userResult.data.length === 0) { + return { + success: false, + error: '用户不存在' + }; + } + + // 更新用户兴趣 + const updateResult = await db.collection("T_user").where({ + _openid: openid + }).update({ + data: { + interests: interests, + updateTime: new Date() + } + }); + + console.log('更新用户兴趣成功:', updateResult); + + return { + success: true, + data: { + interests: interests, + updateResult: updateResult + } + }; + } catch (e) { + console.error('更新用户兴趣失败:', e); + return { + success: false, + error: e.message + }; + } +}; + +// 类别名称映射(兼容旧数据) +const mapCategoryName = (category) => { + const categoryMap = { + '化妆品': '美妆个护', + '二手书': '图书文具', + '运动器材': '运动户外', + '家具家电': '家居用品', + '文具用品': '图书文具', + '其他商品': '其他' + }; + return categoryMap[category] || category; +}; + +// 记录用户行为(浏览、收藏、购买) +const recordUserBehavior = async (event) => { + try { + const wxContext = cloud.getWXContext(); + const openid = wxContext.OPENID; + const { productId, behaviorType, productCategory } = event; // behaviorType: 'view', 'favorite', 'purchase' + + console.log('记录用户行为,参数:', { openid, productId, behaviorType, productCategory }); + + if (!productId || !behaviorType) { + console.error('缺少必要参数:', { productId, behaviorType }); + return { + success: false, + error: '缺少必要参数' + }; + } + + if (!openid) { + console.error('缺少openid'); + return { + success: false, + error: '用户未登录' + }; + } + + // 行为权重:浏览1分,收藏3分,购买5分 + const behaviorWeights = { + 'view': 1, + 'favorite': 3, + 'purchase': 5 + }; + + const score = behaviorWeights[behaviorType] || 1; + + try { + // 检查是否已存在该行为记录 + const existingRecord = await db.collection('T_user_behavior') + .where({ + _openid: openid, + productId: productId, + behaviorType: behaviorType + }) + .get(); + + if (existingRecord.data && existingRecord.data.length > 0) { + // 更新现有记录的时间 + await db.collection('T_user_behavior') + .doc(existingRecord.data[0]._id) + .update({ + data: { + updateTime: new Date(), + count: _.inc(1) // 增加行为次数 + } + }); + console.log('更新用户行为记录成功'); + } else { + // 创建新记录 + const result = await db.collection('T_user_behavior').add({ + data: { + _openid: openid, + productId: productId, + productCategory: productCategory || '', + behaviorType: behaviorType, + score: score, + count: 1, + createTime: new Date(), + updateTime: new Date() + } + }); + console.log('创建用户行为记录成功,ID:', result._id); + } + + return { + success: true, + message: '行为记录成功' + }; + } catch (dbError) { + console.error('数据库操作失败:', dbError); + // 如果集合不存在,尝试创建 + if (dbError.errMsg && (dbError.errMsg.includes('not exist') || dbError.errMsg.includes('not exists') || dbError.errCode === -502005)) { + console.log('集合不存在,尝试创建 T_user_behavior 集合'); + try { + await db.createCollection('T_user_behavior'); + console.log('集合创建成功,重新尝试添加记录'); + + // 重新尝试添加记录 + await db.collection('T_user_behavior').add({ + data: { + _openid: openid, + productId: productId, + productCategory: productCategory || '', + behaviorType: behaviorType, + score: score, + count: 1, + createTime: new Date(), + updateTime: new Date() + } + }); + + return { + success: true, + message: '行为记录成功(集合已创建)' + }; + } catch (createError) { + console.error('创建集合失败:', createError); + return { + success: false, + error: '集合创建失败: ' + createError.message + }; + } + } else { + return { + success: false, + error: dbError.message || '数据库操作失败' + }; + } + } + } catch (e) { + console.error('记录用户行为失败:', e); + return { + success: false, + error: e.message + }; + } +}; + +// 创建用户行为集合 +const createBehaviorCollection = async () => { + try { + // 检查集合是否已存在 + const collections = await db.listCollections(); + const collectionNames = collections.data.map(col => col.name); + + if (collectionNames.includes('T_user_behavior')) { + return { + success: true, + message: "集合已存在", + }; + } + + // 创建 T_user_behavior 集合 + await db.createCollection("T_user_behavior"); + + return { + success: true, + message: "T_user_behavior 集合创建成功", + }; + } catch (e) { + // 如果集合已存在,也返回成功 + if (e.message && e.message.includes('already exists')) { + return { + success: true, + message: "集合已存在", + }; + } + console.error('创建 T_user_behavior 集合失败:', e); + return { + success: false, + error: e.message, + }; + } +}; + +// 管理员登录验证 +const adminLogin = async (event) => { + try { + const { username, password } = event; + + if (!username || !password) { + return { + success: false, + error: '缺少必要参数' + }; + } + + // 查询管理员表(如果不存在,使用默认管理员) + try { + const adminResult = await db.collection('T_admin') + .where({ + username: username, + password: password, + status: 'active' + }) + .get(); + + if (adminResult.data && adminResult.data.length > 0) { + const admin = adminResult.data[0]; + return { + success: true, + adminId: admin._id, + username: admin.username, + name: admin.name || '管理员', + role: admin.role || 'admin' + }; + } + } catch (err) { + // 如果集合不存在,使用默认管理员账号 + if (err.errMsg && err.errMsg.includes('not exist')) { + // 默认管理员账号:admin / admin123 + if (username === 'admin' && password === 'admin123') { + return { + success: true, + adminId: 'default_admin', + username: 'admin', + name: '系统管理员', + role: 'admin' + }; + } + } + } + + return { + success: false, + error: '账号或密码错误' + }; + } catch (e) { + console.error('管理员登录失败:', e); + return { + success: false, + error: e.message + }; + } +}; + +// 获取管理员统计数据 +const getAdminStats = async () => { + try { + console.log('开始获取管理员统计数据...'); + + // 获取商品总数(包括所有状态的商品) + const productsResult = await db.collection('T_product').count(); + const totalProducts = productsResult.total || 0; + console.log('商品总数:', totalProducts); + + // 获取用户总数 + const usersResult = await db.collection('T_user').count(); + const totalUsers = usersResult.total || 0; + console.log('用户总数:', totalUsers); + + // 获取订单总数和总销售额 + let totalOrders = 0; + let totalSales = 0; + try { + const ordersResult = await db.collection('T_order') + .where({ + status: _.in(['已完成', '已支付']) + }) + .get(); + + totalOrders = ordersResult.data.length; + totalSales = ordersResult.data.reduce((sum, order) => { + return sum + (parseFloat(order.totalPrice) || 0); + }, 0); + console.log('订单总数:', totalOrders, '总销售额:', totalSales); + } catch (err) { + console.error('获取订单数据失败:', err); + } + + // 获取最近6个月的月度商品发布量 + const monthlyProducts = await getMonthlyProducts(); + + // 获取最近6个月的月度销售额 + const monthlySales = await getMonthlySales(); + + // 获取商品分类统计 + const categoryStats = await getCategoryStats(); + + const result = { + success: true, + data: { + totalProducts, + totalUsers, + totalSales: Math.round(totalSales * 100) / 100, + totalOrders, + monthlyProducts, + monthlySales, + categoryStats + } + }; + + console.log('统计数据获取成功:', JSON.stringify(result.data, null, 2)); + + return result; + } catch (e) { + console.error('获取统计数据失败:', e); + return { + success: false, + error: e.message + }; + } +}; + +// 获取月度商品发布量 +const getMonthlyProducts = async () => { + try { + const now = new Date(); + const months = []; + + for (let i = 5; i >= 0; i--) { + const date = new Date(now.getFullYear(), now.getMonth() - i, 1); + const nextDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 1); + + const monthStr = `${date.getMonth() + 1}月`; + + const result = await db.collection('T_product') + .where({ + createTime: _.gte(date).and(_.lt(nextDate)) + }) + .count(); + + months.push({ + month: monthStr, + count: result.total || 0 + }); + } + + return months; + } catch (e) { + console.error('获取月度商品发布量失败:', e); + return []; + } +}; + +// 获取月度销售额 +const getMonthlySales = async () => { + try { + const now = new Date(); + const months = []; + + for (let i = 5; i >= 0; i--) { + const date = new Date(now.getFullYear(), now.getMonth() - i, 1); + const nextDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 1); + + const monthStr = `${date.getMonth() + 1}月`; + + try { + const result = await db.collection('T_order') + .where({ + createTime: _.gte(date).and(_.lt(nextDate)), + status: _.in(['已完成', '已支付']) + }) + .get(); + + const sales = result.data.reduce((sum, order) => { + return sum + (parseFloat(order.totalPrice) || 0); + }, 0); + + months.push({ + month: monthStr, + sales: Math.round(sales * 100) / 100 + }); + } catch (err) { + months.push({ + month: monthStr, + sales: 0 + }); + } + } + + return months; + } catch (e) { + console.error('获取月度销售额失败:', e); + return []; + } +}; + +// 获取商品分类统计 +const getCategoryStats = async () => { + try { + const result = await db.collection('T_product') + .field({ + productCategory: true + }) + .get(); + + const categoryMap = {}; + result.data.forEach(item => { + const category = item.productCategory || '其他'; + categoryMap[category] = (categoryMap[category] || 0) + 1; + }); + + return Object.entries(categoryMap).map(([category, count]) => ({ + category, + count + })).sort((a, b) => b.count - a.count).slice(0, 8); + } catch (e) { + console.error('获取分类统计失败:', e); + return []; + } +}; + +// 管理端:聚合求购关键词词云(基于T_want) +const getWantedKeywordCloud = async (event) => { + try { + const limit = event && event.limit ? parseInt(event.limit) : 60; + // 拉取活跃求购数据(字段:productName、purchaseDescription、expectedBrand、expectedModel、productCategory) + const res = await db.collection('T_want') + .field({ productName: true, description: true, purchaseDescription: true, expectedBrand: true, expectedModel: true, productCategory: true, status: true }) + .get(); + + const stopwords = new Set(['求购','求','购买','需要','急需','求助','有没有','二手','全新','新','的','和','及','或','我','我们','联系','微信','电话','价','价格','预算','希望','求一个','一个','一台','一本','一套']); + const freq = {}; + + const toTokens = (text) => { + if (!text || typeof text !== 'string') return []; + const cleaned = text.replace(/[\s\n\r\t]+/g, ' ') + .replace(/[,。、“”‘’!!??::;;、,/\.\-]+/g, ' ') + .trim(); + const parts = cleaned.split(' ').filter(Boolean); + return parts; + }; + + res.data.forEach(item => { + if (item.status && item.status !== 'active') return; // 只统计活跃求购 + const texts = [item.productName, item.purchaseDescription || item.description, item.expectedBrand, item.expectedModel]; + const tokens = texts.flatMap(t => toTokens(t)); + tokens.forEach(tok => { + const token = (tok || '').trim(); + if (!token) return; + if (token.length < 2) return; // 过滤过短词 + if (stopwords.has(token)) return; + // 统一大小写处理英文 + const key = /[A-Za-z]/.test(token) ? token.toLowerCase() : token; + freq[key] = (freq[key] || 0) + 1; + }); + }); + + const entries = Object.entries(freq).map(([text, count]) => ({ text, count })); + entries.sort((a, b) => b.count - a.count); + const top = entries.slice(0, limit); + const max = top.length > 0 ? top[0].count : 1; + const min = top.length > 0 ? top[top.length - 1].count : 1; + const norm = (c) => { + if (max === min) return 1; + return (c - min) / (max - min); + }; + const keywords = top.map(k => ({ + text: k.text, + weight: k.count, + score: norm(k.count) // 0..1 + })); + + return { success: true, data: { keywords, total: entries.length } }; + } catch (e) { + console.error('聚合求购关键词失败:', e); + return { success: false, error: e.message }; + } +}; + +// 计算分类供需统计与发布建议(不分校区,支持个性化权重,并返回类别指引) +const getPublishRecommendations = async (event) => { + try { + const forType = event && event.forType; // 'product' | 'wanted' | admin + const campusFilter = ''; // 不分校区 + const wxContext = cloud.getWXContext(); + const callerOpenId = (event && event.openid) || (wxContext && wxContext.OPENID) || ''; + let interestsFromEvent = Array.isArray(event && event.interests) ? (event.interests || []) : []; + + // 辅助:从用户信息推断校区 + const getCampusFromUser = (user) => { + if (!user) return '未知'; + if (user.campus) return user.campus; + const dorm = user.sushe || ''; + if (/闵行/.test(dorm)) return '闵行校区'; + if (/徐汇/.test(dorm)) return '徐汇校区'; + if (/松江/.test(dorm)) return '松江校区'; + return '未知'; + }; + + // 统计供给(商品在售/交易中),顺便收集sellerUserId用于校区映射 + const productRes = await db.collection('T_product') + .field({ productCategory: true, status: true, transactionMethod: true, salePrice: true, suggestedPrice: true, sellerUserId: true, sellerOpenId: true }) + .get(); + + // 批量获取卖家用户校区 + const sellerIds = [...new Set(productRes.data.map(p => p.sellerUserId).filter(Boolean))]; + let sellerUsers = { }; + if (sellerIds.length > 0) { + try { + const sellerUserDocs = await db.collection('T_user').where({ _id: _.in(sellerIds) }).field({ _id: true, campus: true, sushe: true, interests: true, _openid: true }).get(); + sellerUserDocs.data.forEach(u => { sellerUsers[u._id] = u; }); + } catch (err) { + console.error('批量获取卖家用户信息失败:', err); + } + } + + // 统计需求(求购信息),收集openids用于校区映射 + let wantResData = []; + try { + const wantRes = await db.collection('T_want') + .field({ productCategory: true, _openid: true }) + .get(); + wantResData = wantRes.data || []; + } catch (e) { + wantResData = []; + } + + const wantOpenIds = [...new Set(wantResData.map(w => w._openid).filter(Boolean))]; + let wantUsersByOpenId = { }; + if (wantOpenIds.length > 0) { + try { + const wantUserDocs = await db.collection('T_user').where({ _openid: _.in(wantOpenIds) }).field({ _openid: true, campus: true, sushe: true, interests: true, _id: true }).get(); + wantUserDocs.data.forEach(u => { wantUsersByOpenId[u._openid] = u; }); + } catch (err) { + console.error('批量获取求购用户信息失败:', err); + } + } + + // 如果没有传入兴趣,尝试从当前调用者的用户信息获取 + if (!interestsFromEvent || interestsFromEvent.length === 0) { + try { + const curUser = await db.collection('T_user').where({ _openid: callerOpenId }).field({ interests: true, campus: true, sushe: true }).get(); + const userDoc = (curUser.data && curUser.data[0]) || null; + if (userDoc && Array.isArray(userDoc.interests)) { + interestsFromEvent = userDoc.interests; + } + } catch (err) { + // 忽略错误 + } + } + + // 供需统计(可选校区过滤) + const supplyMap = {}; + const demandMap = {}; + const campusBreakdown = {}; // { 校区: { category -> { supply, demand } } } + + const supplyStatuses = new Set(['在售','交易中','已发布','待卖家确认','待发货','待收货']); + + productRes.data.forEach(item => { + const cat = item.productCategory || '其他'; + const status = item.status || ''; + const isSupply = supplyStatuses.has(status); + const sellerUser = item.sellerUserId ? sellerUsers[item.sellerUserId] : null; + const campus = getCampusFromUser(sellerUser); + + if (isSupply) { + // 总体 + supplyMap[cat] = (supplyMap[cat] || 0) + 1; + // 校区分布 + campusBreakdown[campus] = campusBreakdown[campus] || {}; + campusBreakdown[campus][cat] = campusBreakdown[campus][cat] || { supply: 0, demand: 0 }; + campusBreakdown[campus][cat].supply += 1; + } + }); + + wantResData.forEach(item => { + const cat = item.productCategory || '其他'; + const user = wantUsersByOpenId[item._openid]; + const campus = getCampusFromUser(user); + demandMap[cat] = (demandMap[cat] || 0) + 1; + campusBreakdown[campus] = campusBreakdown[campus] || {}; + campusBreakdown[campus][cat] = campusBreakdown[campus][cat] || { supply: 0, demand: 0 }; + campusBreakdown[campus][cat].demand += 1; + }); + + // 如果指定校区,则使用该校区下的供需统计替换总体视图 + let effectiveSupplyMap = supplyMap; + let effectiveDemandMap = demandMap; + if (campusFilter && campusBreakdown[campusFilter]) { + const campusStats = campusBreakdown[campusFilter]; + effectiveSupplyMap = {}; + effectiveDemandMap = {}; + Object.keys(campusStats).forEach(cat => { + effectiveSupplyMap[cat] = campusStats[cat].supply || 0; + effectiveDemandMap[cat] = campusStats[cat].demand || 0; + }); + } + + // 汇总所有出现过的类别 + const allCats = new Set([...Object.keys(effectiveSupplyMap), ...Object.keys(effectiveDemandMap)]); + const stats = Array.from(allCats).map(cat => { + const supply = effectiveSupplyMap[cat] || 0; + const demand = effectiveDemandMap[cat] || 0; + return { + category: cat, + supply, + demand, + gap: demand - supply + }; + }).sort((a, b) => Math.abs(b.gap) - Math.abs(a.gap)); + + // 价格与交易方式指引 + const categoryGuides = {}; + const methodCountByCat = {}; + const priceByCat = {}; + productRes.data.forEach(item => { + const status = item.status || ''; + if (!supplyStatuses.has(status)) return; + const cat = item.productCategory || '其他'; + const price = (typeof item.salePrice === 'number' && item.salePrice > 0) ? item.salePrice + : ((typeof item.suggestedPrice === 'number' && item.suggestedPrice > 0) ? item.suggestedPrice : null); + if (price !== null) { + priceByCat[cat] = priceByCat[cat] || []; + priceByCat[cat].push(price); + } + const method = item.transactionMethod || '面交'; + methodCountByCat[cat] = methodCountByCat[cat] || {}; + methodCountByCat[cat][method] = (methodCountByCat[cat][method] || 0) + 1; + }); + Object.keys(allCats).forEach(() => {}); // no-op to satisfy lint + for (const cat of allCats) { + const prices = priceByCat[cat] || []; + let guide = { avgPrice: 0, minPrice: 0, maxPrice: 0, typicalPriceRange: '', commonTrade: '面交', methodsRanked: [] }; + if (prices.length > 0) { + const sum = prices.reduce((a, b) => a + b, 0); + const avg = Math.round((sum / prices.length) * 100) / 100; + const min = Math.min(...prices); + const max = Math.max(...prices); + guide.avgPrice = avg; + guide.minPrice = min; + guide.maxPrice = max; + guide.typicalPriceRange = `${Math.round(min)}-${Math.round(max)}`; + } + const mc = methodCountByCat[cat] || {}; + const ranked = Object.entries(mc).sort((a, b) => b[1] - a[1]).map(([m]) => m); + guide.commonTrade = ranked[0] || '面交'; + guide.methodsRanked = ranked; + categoryGuides[cat] = guide; + } + + // 生成建议(支持forType与兴趣权重) + const shortage = stats.filter(s => s.gap >= 3).sort((a, b) => b.gap - a.gap).slice(0, 5); + const oversupply = stats.filter(s => (s.supply - s.demand) >= 3).sort((a, b) => (b.supply - b.demand) - (a.supply - a.demand)).slice(0, 5); + const hotDemand = [...stats].sort((a, b) => b.demand - a.demand).slice(0, 5); + const richSupply = [...stats].sort((a, b) => b.supply - a.supply).slice(0, 5); + + const interestSet = new Set((interestsFromEvent || []).map(i => i)); + let candidates = []; + let baseMessage = ''; + if (forType === 'product') { + candidates = (shortage.length > 0 ? shortage : hotDemand); + baseMessage = shortage.length > 0 ? '以下类别买家需求旺盛、供给偏少,优先发布:' : '以下类别近期需求较高,发布更易成交:'; + } else if (forType === 'wanted') { + candidates = (oversupply.length > 0 ? oversupply : richSupply); + baseMessage = oversupply.length > 0 ? '以下类别货源充足、响应更快,建议求购:' : '以下类别当前货源较多,求购更容易被响应:'; + } else { + candidates = shortage.slice(0, 3); + baseMessage = '供需缺口较大的类别:'; + } + + // 应用兴趣加权 + const scored = candidates.map(c => { + const interestBoost = interestSet.has(c.category) ? 1 : 0; + const score = (Math.abs(c.gap) || 0) + interestBoost; + return { cat: c.category, score }; + }).sort((a, b) => b.score - a.score); + + const recommended = scored.map(s => s.cat); + const message = `${baseMessage}${recommended.join('、')}`; + + return { + success: true, + data: { + stats, + shortage, + oversupply, + hotDemand, + richSupply, + recommended, + message, + campus: '全部校区', + interestsUsed: Array.from(interestSet), + categoryGuides, + campusBreakdown + } + }; + } catch (e) { + console.error('计算发布建议失败:', e); + return { + success: false, + error: e.message + }; + } +}; + +// 管理员:一键推送建议到用户群(不分校区) +const adminPushRecommendations = async (event) => { + try { + const categories = Array.isArray(event.categories) ? event.categories : []; + const target = event.target || 'sellers'; // sellers | buyers | all + const campus = ''; // 不分校区 + const customMessage = event.message || ''; + + // 本地工具:从用户资料推断校区 + const getCampusFromUserLocal = (user) => { + if (!user) return '未知'; + if (user.campus) return user.campus; + const dorm = user.dorm || user.sushe || ''; + if (/闵行/.test(dorm)) return '闵行校区'; + if (/徐汇/.test(dorm)) return '徐汇校区'; + if (/松江/.test(dorm)) return '松江校区'; + return '未知'; + }; + + if (categories.length === 0) { + return { success: false, error: '缺少要推送的类别列表' }; + } + + // 查询用户(按兴趣筛选,不分校区) + const userQuery = {}; + // 拉取可能的兴趣用户(这里不在where里使用数组匹配,改为拉取后过滤,兼容旧数据) + const usersRes = await db.collection('T_user').where(userQuery).field({ _id: true, _openid: true, interests: true, campus: true, sushe: true, notificationEnabled: true, sname: true }).get(); + + // 可能的买家(发布过求购) + let wantUsersOpenIds = new Set(); + try { + const wantRes = await db.collection('T_want').where({ productCategory: _.in(categories) }).field({ _openid: true }).get(); + wantRes.data.forEach(w => { if (w._openid) wantUsersOpenIds.add(w._openid); }); + } catch (err) { + // ignore + } + + // 过滤用户:兴趣命中或是活跃求购用户 + const getCampus = (u) => u.campus || getCampusFromUserLocal(u); + const recipients = []; + usersRes.data.forEach(u => { + const enabled = (u.notificationEnabled !== false); + if (!enabled) return; + const campusOk = true; + const interests = Array.isArray(u.interests) ? u.interests : []; + const interestHit = interests.some(i => categories.includes(i)); + const wantHit = wantUsersOpenIds.has(u._openid); + if (campusOk && (interestHit || (target !== 'sellers' && wantHit) || target === 'all')) { + recipients.push(u); + } + }); + + // 写入通知集合 + let successCount = 0; + for (const user of recipients) { + try { + const content = customMessage || `当前【${categories.join('、')}】类别存在供需变化,建议关注并发布/求购。`; + await db.collection('T_notify').add({ + data: { + userId: user._id, + _openid: user._openid, + type: 'recommendation', + title: '供需发布建议', + content, + categories, + campus: '全部校区', + target, + read: false, + createTime: new Date() + } + }); + successCount += 1; + } catch (err) { + console.error('写入通知失败:', err); + } + } + + return { success: true, pushed: successCount, recipients: recipients.length }; + } catch (e) { + console.error('管理员推送建议失败:', e); + return { success: false, error: e.message }; + } +}; + +// 管理员更新商品 +const adminUpdateProduct = async (event) => { + try { + const { productId, data } = event; + + if (!productId) { + return { + success: false, + error: '缺少商品ID' + }; + } + + await db.collection('T_product').doc(productId).update({ + data: { + ...data, + updateTime: new Date() + } + }); + + return { + success: true, + message: '更新成功' + }; + } catch (e) { + console.error('更新商品失败:', e); + return { + success: false, + error: e.message + }; + } +}; + +// 管理员删除商品(下架) +const adminDeleteProduct = async (event) => { + try { + const { productId } = event; + + if (!productId) { + return { + success: false, + error: '缺少商品ID' + }; + } + + // 更新商品状态为已下架 + await db.collection('T_product').doc(productId).update({ + data: { + status: '已下架', + updateTime: new Date() + } + }); + + return { + success: true, + message: '商品已下架' + }; + } catch (e) { + console.error('下架商品失败:', e); + return { + success: false, + error: e.message + }; + } +}; + +// 管理员获取用户列表 +const adminGetUsers = async (event) => { + try { + const { page = 0, pageSize = 20, keyword = '' } = event; + + let queryCondition = {}; + + if (keyword) { + // 微信小程序云数据库使用db.RegExp + const regex = db.RegExp({ + regexp: keyword, + options: 'i' + }); + queryCondition = _.or([ + { sno: regex }, + { sname: regex }, + { phone: regex } + ]); + } + + const result = await db.collection('T_user') + .where(queryCondition) + .skip(page * pageSize) + .limit(pageSize) + .orderBy('createTime', 'desc') + .get(); + + const countResult = await db.collection('T_user') + .where(queryCondition) + .count(); + + return { + success: true, + data: { + users: result.data, + total: countResult.total, + page: page, + pageSize: pageSize + } + }; + } catch (e) { + console.error('获取用户列表失败:', e); + return { + success: false, + error: e.message + }; + } +}; + +// 管理员更新用户信息 +const adminUpdateUser = async (event) => { + try { + const { userId, data } = event; + + console.log('管理员更新用户信息,userId:', userId, 'data:', JSON.stringify(data, null, 2)); + + if (!userId) { + return { + success: false, + error: '缺少用户ID' + }; + } + + // 检查用户是否存在 + const userDoc = await db.collection('T_user').doc(userId).get(); + if (!userDoc.data) { + return { + success: false, + error: '用户不存在' + }; + } + + // 更新用户信息 + const updateResult = await db.collection('T_user').doc(userId).update({ + data: { + ...data, + updateTime: new Date() + } + }); + + console.log('更新用户信息成功,结果:', updateResult); + + return { + success: true, + message: '更新成功', + data: updateResult + }; + } catch (e) { + console.error('更新用户失败:', e); + return { + success: false, + error: e.message || '更新失败' + }; + } +}; + +// 安全订单状态更新(基于角色和状态流转校验) +const updateOrderStatus = async (event) => { + try { + const { orderId, action, callerUserId } = event || {}; + if (!orderId || !action) { + return { success: false, error: '缺少必要参数:orderId 或 action' }; + } + + const ctx = cloud.getWXContext(); + const callerOpenId = ctx.OPENID || event.openid || ''; + + const orderRes = await db.collection('T_order').doc(orderId).get(); + const order = orderRes.data; + if (!order) { + return { success: false, error: '订单不存在' }; + } + + const flows = { + buyerPay: { from: '待付款', to: '待确认付款', role: 'buyer' }, + sellerConfirmPayment: { from: '待确认付款', to: '待发货', role: 'seller' }, + sellerShip: { from: '待发货', to: '待收货', role: 'seller' }, + buyerConfirmReceipt: { from: '待收货', to: '已完成', role: 'buyer' }, + buyerCancel: { from: '待付款', to: '已取消', role: 'buyer' } + }; + + const flow = flows[action]; + if (!flow) { + return { success: false, error: '不支持的动作' }; + } + + const isSeller = ( + (order.sellerOpenId && order.sellerOpenId === callerOpenId) || + (order.sellerUserId && callerUserId && order.sellerUserId === callerUserId) + ); + const isBuyer = ( + (order.buyerOpenId && order.buyerOpenId === callerOpenId) || + (order.buyerUserId && callerUserId && order.buyerUserId === callerUserId) + ); + + if (flow.role === 'seller' && !isSeller) { + return { success: false, error: '无权执行:仅卖家可进行该操作' }; + } + if (flow.role === 'buyer' && !isBuyer) { + return { success: false, error: '无权执行:仅买家可进行该操作' }; + } + + if (order.status !== flow.from) { + return { success: false, error: `状态不匹配:当前为${order.status},期望${flow.from}` }; + } + + const now = new Date(); + const updates = { status: flow.to, updateTime: now }; + + switch (action) { + case 'buyerPay': + updates.paymentTime = now; + break; + case 'sellerConfirmPayment': + updates.sellerConfirmTime = now; + break; + case 'sellerShip': + updates.shipTime = now; + if (event.shipInfo) updates.shipInfo = event.shipInfo; + break; + case 'buyerConfirmReceipt': + updates.finishTime = now; + break; + case 'buyerCancel': + updates.cancelTime = now; + break; + } + + const updateRes = await db.collection('T_order') + .where({ _id: orderId, status: flow.from }) + .update({ data: updates }); + + if (!updateRes.stats || updateRes.stats.updated !== 1) { + return { success: false, error: '更新失败或状态已变化,请刷新重试' }; + } + + // 写入系统聊天消息(买家/卖家互相通知订单状态) + try { + const buyerOpenId = order.buyerOpenId || ''; + const sellerOpenId = order.sellerOpenId || ''; + if (buyerOpenId && sellerOpenId) { + const sessionKey = [buyerOpenId, sellerOpenId].sort().join('|'); + let fromOpenId = ''; + let toOpenId = ''; + let content = ''; + + if (action === 'buyerPay') { + fromOpenId = buyerOpenId; + toOpenId = sellerOpenId; + content = `[系统] 买家已付款:订单号 ${order.orderNumber},金额 ¥${order.totalPrice}`; + } else if (action === 'sellerConfirmPayment') { + fromOpenId = sellerOpenId; + toOpenId = buyerOpenId; + content = `[系统] 卖家已确认收款:订单号 ${order.orderNumber}`; + } else if (action === 'sellerShip') { + fromOpenId = sellerOpenId; + toOpenId = buyerOpenId; + const shipText = event.shipInfo ? `,物流:${event.shipInfo}` : ''; + content = `[系统] 卖家已发货:订单号 ${order.orderNumber}${shipText}`; + } else if (action === 'buyerConfirmReceipt') { + fromOpenId = buyerOpenId; + toOpenId = sellerOpenId; + content = `[系统] 买家已确认收货:订单号 ${order.orderNumber}`; + } else if (action === 'buyerCancel') { + fromOpenId = buyerOpenId; + toOpenId = sellerOpenId; + content = `[系统] 买家已取消订单:订单号 ${order.orderNumber}`; + } + + if (fromOpenId && toOpenId && content) { + const buyerUserId = order.buyerUserId || ''; + const sellerUserId = order.sellerUserId || ''; + const sessionKeyUser = (buyerUserId && sellerUserId) ? [buyerUserId, sellerUserId].sort().join('|') : ''; + + let fromUserId = ''; + let toUserId = ''; + if (action === 'buyerPay' || action === 'buyerConfirmReceipt' || action === 'buyerCancel') { + fromUserId = buyerUserId; + toUserId = sellerUserId; + } else { + // sellerConfirmPayment / sellerShip + fromUserId = sellerUserId; + toUserId = buyerUserId; + } + + const chatDoc = { + fromOpenId, + toOpenId, + fromUserId, + toUserId, + sessionKey, + sessionKeyUser, + contentType: 'text', + content, + timestamp: Date.now(), + orderId: orderId, + productId: order.productId || (Array.isArray(order.products) && order.products.length === 1 ? order.products[0].productId : ''), + isSystem: true + }; + try { + await db.collection('T_chat').add({ data: chatDoc }); + } catch (e) { + // 如果集合不存在,先创建再重试一次 + if (String(e.errMsg || '').includes('does not exist') || e.errCode === -502005) { + await createChatCollection(); + await db.collection('T_chat').add({ data: chatDoc }); + } else { + throw e; + } + } + } + } + } catch (e) { + // 聊天消息写入失败不影响主流程 + console.error('写入订单状态聊天消息失败:', e); + } + + if (action === 'buyerConfirmReceipt' && order.productId) { + try { + await db.collection('T_product').doc(order.productId).update({ + data: { status: '已售', updateTime: now } + }); + } catch (e) { + console.error('更新商品为已售失败:', e); + return { success: true, warning: '订单完成成功,但商品状态更新失败', newStatus: flow.to }; + } + try { + const pr = await db.collection('T_product').doc(order.productId).get(); + const p = pr.data || {}; + const name = String(p.tradeLandmarkName || p.tradeLocationName || p.tradeAddress || '').trim(); + const lat = Number(p.tradeLocationLat); + const lng = Number(p.tradeLocationLng); + if (name && Number.isFinite(lat) && Number.isFinite(lng)) { + await upsertCampusLandmark({ name, latitude: lat, longitude: lng, address: String(p.tradeAddress || ''), source: 'product', productId: order.productId, selling: false }); + } + } catch (e) { + console.error('同步地标为非在售失败:', e); + } + } + + return { success: true, newStatus: flow.to }; + } catch (e) { + console.error('安全更新订单状态失败:', e); + return { success: false, error: e.message }; + } +}; + +// 创建论坛集合 +const createForumCollection = async () => { + try { + const collections = await db.listCollections(); + const collectionNames = collections.data.map(col => col.name); + + const collectionsToCreate = ['T_forum_post', 'T_forum_comment', 'T_forum_like']; + const created = []; + + for (const collectionName of collectionsToCreate) { + if (!collectionNames.includes(collectionName)) { + try { + await db.createCollection(collectionName); + created.push(collectionName); + console.log(`${collectionName} 集合创建成功`); + } catch (err) { + if (err.message && !err.message.includes('already exists')) { + console.error(`创建 ${collectionName} 失败:`, err); + } + } + } + } + + return { + success: true, + message: created.length > 0 ? `成功创建 ${created.join(', ')} 集合` : "所有集合已存在", + created: created + }; + } catch (e) { + console.error('创建论坛集合失败:', e); + return { + success: false, + error: e.message, + }; + } +}; + +// 计算余弦相似度 +const cosineSimilarity = (vecA, vecB) => { + let dotProduct = 0; + let normA = 0; + let normB = 0; + + const keys = new Set([...Object.keys(vecA), ...Object.keys(vecB)]); + + for (const key of keys) { + const a = vecA[key] || 0; + const b = vecB[key] || 0; + dotProduct += a * b; + normA += a * a; + normB += b * b; + } + + if (normA === 0 || normB === 0) { + return 0; + } + + return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); +}; + +// 基于用户的协同过滤推荐 +const userBasedCF = async (openid, limit = 10) => { + try { + // 获取所有用户行为数据 + const allBehaviors = await db.collection('T_user_behavior').get(); + + if (allBehaviors.data.length === 0) { + return []; + } + + // 构建用户-商品评分矩阵 + const userProductMatrix = {}; + allBehaviors.data.forEach(behavior => { + const userId = behavior._openid; + const productId = behavior.productId; + const score = behavior.score * (behavior.count || 1); // 考虑行为次数 + + if (!userProductMatrix[userId]) { + userProductMatrix[userId] = {}; + } + userProductMatrix[userId][productId] = (userProductMatrix[userId][productId] || 0) + score; + }); + + // 获取当前用户的行为数据 + const currentUserProducts = userProductMatrix[openid] || {}; + + if (Object.keys(currentUserProducts).length === 0) { + return []; // 新用户,没有行为数据 + } + + // 计算当前用户与其他用户的相似度 + const userSimilarities = []; + for (const userId in userProductMatrix) { + if (userId === openid) continue; + + const similarity = cosineSimilarity(currentUserProducts, userProductMatrix[userId]); + if (similarity > 0) { + userSimilarities.push({ + userId: userId, + similarity: similarity + }); + } + } + + // 按相似度排序 + userSimilarities.sort((a, b) => b.similarity - a.similarity); + + // 获取相似用户喜欢的商品(当前用户未交互过的) + const recommendedProducts = {}; + const topSimilarUsers = userSimilarities.slice(0, 20); // 取前20个相似用户 + + for (const similarUser of topSimilarUsers) { + const similarUserProducts = userProductMatrix[similarUser.userId]; + for (const productId in similarUserProducts) { + if (!currentUserProducts[productId]) { + // 当前用户未交互过的商品 + const score = similarUserProducts[productId] * similarUser.similarity; + recommendedProducts[productId] = (recommendedProducts[productId] || 0) + score; + } + } + } + + // 转换为数组并排序 + const productScores = Object.entries(recommendedProducts) + .map(([productId, score]) => ({ productId, score })) + .sort((a, b) => b.score - a.score) + .slice(0, limit); + + return productScores.map(item => item.productId); + } catch (e) { + console.error('基于用户的协同过滤失败:', e); + return []; + } +}; + +// 基于物品的协同过滤推荐 +const itemBasedCF = async (openid, limit = 10) => { + try { + // 获取所有用户行为数据 + const allBehaviors = await db.collection('T_user_behavior').get(); + + if (allBehaviors.data.length === 0) { + return []; + } + + // 构建商品-用户评分矩阵 + const productUserMatrix = {}; + allBehaviors.data.forEach(behavior => { + const productId = behavior.productId; + const userId = behavior._openid; + const score = behavior.score * (behavior.count || 1); + + if (!productUserMatrix[productId]) { + productUserMatrix[productId] = {}; + } + productUserMatrix[productId][userId] = (productUserMatrix[productId][userId] || 0) + score; + }); + + // 获取当前用户交互过的商品 + const currentUserBehaviors = await db.collection('T_user_behavior') + .where({ + _openid: openid + }) + .get(); + + if (currentUserBehaviors.data.length === 0) { + return []; // 新用户 + } + + const currentUserProducts = {}; + currentUserBehaviors.data.forEach(behavior => { + currentUserProducts[behavior.productId] = behavior.score * (behavior.count || 1); + }); + + // 计算当前用户交互过的商品与其他商品的相似度 + const productSimilarities = {}; + + for (const productId in currentUserProducts) { + if (!productUserMatrix[productId]) continue; + + const currentProductUsers = productUserMatrix[productId]; + + // 计算与其他商品的相似度 + for (const otherProductId in productUserMatrix) { + if (otherProductId === productId || currentUserProducts[otherProductId]) { + continue; // 跳过自己或已交互过的商品 + } + + const similarity = cosineSimilarity(currentProductUsers, productUserMatrix[otherProductId]); + if (similarity > 0) { + const weightedScore = currentUserProducts[productId] * similarity; + productSimilarities[otherProductId] = (productSimilarities[otherProductId] || 0) + weightedScore; + } + } + } + + // 转换为数组并排序 + const productScores = Object.entries(productSimilarities) + .map(([productId, score]) => ({ productId, score })) + .sort((a, b) => b.score - a.score) + .slice(0, limit); + + return productScores.map(item => item.productId); + } catch (e) { + console.error('基于物品的协同过滤失败:', e); + return []; + } +}; + +// 根据用户兴趣推荐商品(保留原有功能作为备用) +const getRecommendedProductsByInterest = async (userInterests, limit) => { + const mappedInterests = userInterests.map(cat => mapCategoryName(cat)); + + let queryCondition = { + status: '在售' + }; + + if (mappedInterests.length > 0) { + queryCondition.productCategory = _.in(mappedInterests); + } + + const result = await db.collection("T_product") + .where(queryCondition) + .orderBy('createTime', 'desc') + .limit(limit) + .get(); + + return result.data.map(item => item._id); +}; + +// 根据用户兴趣推荐商品(增强版:结合协同过滤) +const getRecommendedProducts = async (event) => { + try { + const wxContext = cloud.getWXContext(); + const openid = wxContext.OPENID; + const userInterests = event.interests || []; + const limit = event.limit || 8; + const useCF = event.useCF !== false; // 默认使用协同过滤 + + console.log('获取推荐商品,openid:', openid, '使用协同过滤:', useCF); + + let recommendedProductIds = []; + + // 优先使用协同过滤算法 + if (useCF) { + try { + // 尝试基于用户的协同过滤 + const userCFIds = await userBasedCF(openid, limit); + console.log('基于用户的CF推荐数量:', userCFIds.length); + + // 如果基于用户的CF结果不足,尝试基于物品的CF + if (userCFIds.length < limit) { + const itemCFIds = await itemBasedCF(openid, limit - userCFIds.length); + console.log('基于物品的CF推荐数量:', itemCFIds.length); + + // 合并结果,去重 + const allCFIds = [...new Set([...userCFIds, ...itemCFIds])]; + recommendedProductIds = allCFIds.slice(0, limit); + } else { + recommendedProductIds = userCFIds; + } + + console.log('协同过滤推荐商品ID:', recommendedProductIds); + } catch (cfError) { + console.error('协同过滤推荐失败,使用兴趣推荐:', cfError); + recommendedProductIds = []; + } + } + + // 如果协同过滤结果不足,使用兴趣推荐补充 + if (recommendedProductIds.length < limit && userInterests.length > 0) { + const interestIds = await getRecommendedProductsByInterest(userInterests, limit - recommendedProductIds.length); + // 去重并合并 + const allIds = [...new Set([...recommendedProductIds, ...interestIds])]; + recommendedProductIds = allIds.slice(0, limit); + } + + // 如果还是没有足够的推荐,获取热门商品 + if (recommendedProductIds.length < limit) { + const hotProducts = await db.collection("T_product") + .where({ + status: '在售' + }) + .orderBy('createTime', 'desc') + .limit(limit - recommendedProductIds.length) + .get(); + + const hotIds = hotProducts.data.map(item => item._id); + const allIds = [...new Set([...recommendedProductIds, ...hotIds])]; + recommendedProductIds = allIds.slice(0, limit); + } + + // 根据ID获取商品详细信息 + const products = []; + for (const productId of recommendedProductIds) { + try { + const productDoc = await db.collection("T_product").doc(productId).get(); + if (productDoc.data && productDoc.data.status === '在售') { + const item = productDoc.data; + products.push({ + id: item._id, + name: item.productName || '商品名称', + price: item.salePrice || item.suggestedPrice || item.originalPrice || 0, + image: Array.isArray(item.productImage) ? (item.productImage[0] || '') : (item.productImage || 'https://via.placeholder.com/280x200/4285F4/ffffff?text=商品'), + tag: item.productCategory || '其他', + category: item.productCategory || '其他', + description: item.productDescription || '', + createTime: item.createTime + }); + } + } catch (err) { + console.error('获取商品详情失败:', productId, err); + } + } + + console.log('最终推荐商品数量:', products.length); + + return { + success: true, + data: products, + count: products.length, + message: products.length === 0 ? '暂无推荐商品' : '推荐商品加载成功', + algorithm: useCF && recommendedProductIds.length > 0 ? '协同过滤' : '兴趣推荐' + }; + } catch (e) { + console.error('获取推荐商品失败:', e); + return { + success: false, + error: e.message + }; + } +}; + +// 云函数入口函数 +exports.main = async (event, context) => { + try { + // 获取操作类型,支持多种参数格式 + const type = event.type || event.userInfo?.type || (event.tcbContext && event.tcbContext.type); + + console.log('云函数被调用,type:', type); + console.log('event参数:', JSON.stringify(event, null, 2)); + + if (!type) { + return { + success: false, + error: '缺少操作类型参数(type)', + event: event + }; + } + + // 添加调试日志 + console.log('开始执行switch,type值:', type, '类型:', typeof type); + + switch (type) { + case "getOpenId": + return await getOpenId(); + case "getMiniProgramCode": + return await getMiniProgramCode(); + case "createCollection": + return await createCollection(); + case "selectRecord": + return await selectRecord(); + case "updateRecord": + return await updateRecord(event); + case "insertRecord": + return await insertRecord(event); + case "deleteRecord": + return await deleteRecord(event); + case "queryTUser": + return await queryTUser(); + case "addTestUser": + return await addTestUser(event); + case "analyzeProductPrice": + return await analyzeProductPrice(event); + case "getUserByOpenId": + return await getUserByOpenId(event); + case "getProductCategories": + return await getProductCategories(); + case "updateUserInterests": + return await updateUserInterests(event); + case "getRecommendedProducts": + console.log('匹配到getRecommendedProducts case'); + return await getRecommendedProducts(event); + case "getPublishRecommendations": + // 基于分类供需对比的发布建议(商品/求购) + return await getPublishRecommendations(event); + case "adminPushRecommendations": + // 管理员一键推送建议到指定用户群 + return await adminPushRecommendations(event); + case "recordUserBehavior": + return await recordUserBehavior(event); + case "createBehaviorCollection": + return await createBehaviorCollection(); + case "createForumCollection": + return await createForumCollection(); + case "createNotifyCollection": + return await createNotifyCollection(); + case "createChatCollection": + return await createChatCollection(); + case "createMessageCollection": + return await createMessageCollection(); + case "sendChatMessage": + return await sendChatMessage(event); + case "getChatMessages": + return await getChatMessages(event); + case "listChatSessions": + return await listChatSessions(); + case "updateChatReadState": + return await updateChatReadState(event); + case "revokeChatMessage": + return await revokeChatMessage(event); + case "backfillSellerUserId": + // 批量为商品补齐缺失的卖家用户ID(通过 sellerOpenId 关联 T_user) + return await backfillSellerUserId(event); + case "createAddressCollection": + return await createAddressCollection(); + case "createCampusLandmarksCollection": + return await createCampusLandmarksCollection(); + case "seedDefaultAddress": + return await seedDefaultAddress(event); + case "adminLogin": + console.log('匹配到adminLogin case'); + return await adminLogin(event); + case "getAdminStats": + return await getAdminStats(); + case "getWantedKeywordCloud": + // 管理端词云:求购关键词聚合 + return await getWantedKeywordCloud(event); + case "adminUpdateProduct": + return await adminUpdateProduct(event); + case "adminDeleteProduct": + return await adminDeleteProduct(event); + case "adminGetUsers": + return await adminGetUsers(event); + case "adminUpdateUser": + return await adminUpdateUser(event); + case "updateOrderStatus": + // 安全更新订单状态(带角色和状态校验) + return await updateOrderStatus(event); + case "initCategoryTable": + return await initCategoryTable(); + case "cloudReverseGeocode": + return await cloudReverseGeocode(event); + case "cloudGeocode": + return await cloudGeocode(event); + case "fixProductCoords": + // 批量修复商品缺失坐标(地标匹配 + 地址地理编码) + return await fixProductCoords(event); + case "upsertCampusLandmark": + // 上报/同步单条地标 + return await upsertCampusLandmark(event); + case "syncProductLandmarks": + // 批量从商品同步地标到 T_campus_landmarks + return await syncProductLandmarks(event); + default: + console.log('未匹配到任何case,type:', type, 'type类型:', typeof type); + return { + success: false, + error: '未知的操作类型: ' + type + }; + } + } catch (error) { + console.error('========== 云函数入口函数错误 =========='); + console.error('错误信息:', error.message); + console.error('错误堆栈:', error.stack); + console.error('event:', JSON.stringify(event, null, 2)); + + const type = event.type || event.userInfo?.type || (event.tcbContext && event.tcbContext.type); + + return { + success: false, + error: error.message || '云函数执行失败', + details: error.stack, + type: type + }; + } +}; diff --git a/src5/code/cloudfunctions/quickstartFunctions/package-lock.json b/src5/code/cloudfunctions/quickstartFunctions/package-lock.json new file mode 100644 index 0000000..465c719 --- /dev/null +++ b/src5/code/cloudfunctions/quickstartFunctions/package-lock.json @@ -0,0 +1,1915 @@ +{ + "name": "quickstartFunctions", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quickstartFunctions", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@coze/api": "^1.3.7", + "axios": "^1.13.1", + "form-data": "^4.0.4", + "wx-server-sdk": "~2.4.0" + } + }, + "node_modules/@agora-js/media": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@agora-js/media/-/media-4.24.0.tgz", + "integrity": "sha512-foii2klr5+qonLznxN0ZZFejoxLt/W8do79wmIsADPZLw2uZjRP35m0lqUGiLXBKeQ8u3i4UygPzEdFaY26hrw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@agora-js/report": "4.24.0", + "@agora-js/shared": "4.24.0", + "agora-rte-extension": "^1.2.4", + "axios": "^1.8.3", + "webrtc-adapter": "8.2.0" + } + }, + "node_modules/@agora-js/report": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@agora-js/report/-/report-4.24.0.tgz", + "integrity": "sha512-MYbtkdY1Ls0KW0iagUzrPzyvqMWlyCWSC5odEb1SQaraAl7DJeDUkf91a3wxKzrjVah+LCxFxsS4lCFDxvKgNA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@agora-js/shared": "4.24.0", + "axios": "^1.8.3" + } + }, + "node_modules/@agora-js/shared": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/@agora-js/shared/-/shared-4.24.0.tgz", + "integrity": "sha512-Vj67ZcTHZI+1ctWusrEPSSGLM3l6CFiAze/Bi8r7YHRMLivzhZR79nV6GiKvHS3muLAON2YAExznvjPIly6lcg==", + "license": "MIT", + "peer": true, + "dependencies": { + "axios": "^1.8.3", + "ua-parser-js": "^0.7.34" + } + }, + "node_modules/@cloudbase/database": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@cloudbase/database/-/database-1.2.2.tgz", + "integrity": "sha512-14GPoD0vdVnfdN+4rHlMmpkxAekFklt4X2gi33iCuoZUDC62p5LWS7OuTjoronnZ4QPsZPCKm+WsjE8mVD+Hmw==", + "license": "ISC", + "dependencies": { + "bson": "^4.0.3", + "lodash.clonedeep": "4.5.0", + "lodash.set": "4.3.2", + "lodash.unset": "4.5.2" + } + }, + "node_modules/@cloudbase/node-sdk": { + "version": "2.4.7", + "resolved": "https://registry.npmmirror.com/@cloudbase/node-sdk/-/node-sdk-2.4.7.tgz", + "integrity": "sha512-gMtp+25nAJzpXTxpZzN7PTtsTdv6m7SNRszMwPpWB3pwAYyefbuOkR505iv+kYugsX6MkbgKjcCQ/F5dpNMMYw==", + "license": "MIT", + "dependencies": { + "@cloudbase/database": "1.2.2", + "@cloudbase/signature-nodejs": "1.0.0-beta.0", + "@types/retry": "^0.12.0", + "agentkeepalive": "^4.1.3", + "is-regex": "^1.0.4", + "jsonwebtoken": "^8.5.1", + "lodash.merge": "^4.6.1", + "request": "^2.87.0", + "request-promise": "^4.2.5", + "retry": "^0.12.0", + "ts-node": "^8.10.2", + "xml2js": "^0.4.19" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@cloudbase/signature-nodejs": { + "version": "1.0.0-beta.0", + "resolved": "https://registry.npmmirror.com/@cloudbase/signature-nodejs/-/signature-nodejs-1.0.0-beta.0.tgz", + "integrity": "sha512-gpKqwsVk/D2PzvFamYNReymXSdvRSY90eZ1ARf+1wZ8oT6OpK9kr6nmevGykMxN1n17Gn92hBbWqAxU9o3+kAQ==", + "dependencies": { + "@types/clone": "^0.1.30", + "clone": "^2.1.2", + "is-stream": "^2.0.0", + "url": "^0.11.0" + } + }, + "node_modules/@coze/api": { + "version": "1.3.7", + "resolved": "https://registry.npmmirror.com/@coze/api/-/api-1.3.7.tgz", + "integrity": "sha512-r8wEwRFWEc4o0R3kL+rAqnc1r+mxPVjk41mVZqqQVenKMw09ftcIQkpw4IcJKANj6azCWWhV07G8jgo0sqA8FA==", + "license": "MIT", + "dependencies": { + "agora-extension-ai-denoiser": "^1.0.0", + "agora-rtc-sdk-ng": "4.23.2-1", + "agora-rte-extension": "^1.2.4", + "jsonwebtoken": "^9.0.2", + "node-fetch": "^2.x", + "opus-encdec": "^0.1.1", + "reconnecting-websocket": "^4.4.0", + "uuid": "^10.0.0", + "ws": "^8.11.0" + }, + "peerDependencies": { + "axios": "^1.7.1" + } + }, + "node_modules/@coze/api/node_modules/@agora-js/media": { + "version": "4.23.2-1", + "resolved": "https://registry.npmmirror.com/@agora-js/media/-/media-4.23.2-1.tgz", + "integrity": "sha512-d795kSsY/qmQ9OGYn/qSa8XcUhB4nypy5I4SAW+wQ/JJScfF2ZXF/HVc9ECb9NoVurTSRqfOaw8Y/hxA+76cNA==", + "license": "MIT", + "dependencies": { + "@agora-js/report": "4.23.2-1", + "@agora-js/shared": "4.23.2-1", + "agora-rte-extension": "^1.2.4", + "axios": "^1.8.3", + "webrtc-adapter": "8.2.0" + } + }, + "node_modules/@coze/api/node_modules/@agora-js/report": { + "version": "4.23.2-1", + "resolved": "https://registry.npmmirror.com/@agora-js/report/-/report-4.23.2-1.tgz", + "integrity": "sha512-FNDuGb1GKA+J/gBR2oaoArvNnaqXhZV1Si1Qli9GNodrO4SCntz4SSQds7A3BQRMTFTSuQu2tAbkCfJtQBNGuA==", + "license": "MIT", + "dependencies": { + "@agora-js/shared": "4.23.2-1", + "axios": "^1.8.3" + } + }, + "node_modules/@coze/api/node_modules/@agora-js/shared": { + "version": "4.23.2-1", + "resolved": "https://registry.npmmirror.com/@agora-js/shared/-/shared-4.23.2-1.tgz", + "integrity": "sha512-qF0okTndl5mQzfnfV1CymQs3/TY+oxKnnvAxQ/NiJ/Hf/wo+LfgMh2UaJl7xQH5mlhQE8VE2dHMXuU3LpDTbcw==", + "license": "MIT", + "dependencies": { + "axios": "^1.8.3", + "ua-parser-js": "^0.7.34" + } + }, + "node_modules/@coze/api/node_modules/agora-rtc-sdk-ng": { + "version": "4.23.2-1", + "resolved": "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.23.2-1.tgz", + "integrity": "sha512-Tng16+2eVKC+JsBCggey4uXk9a92W7DxwmzPMtCMMsxRgO3EAIhS9EHVjycwyCcRdDanlbeWsrGH5faD4e+eGw==", + "license": "MIT", + "dependencies": { + "@agora-js/media": "4.23.2-1", + "@agora-js/report": "4.23.2-1", + "@agora-js/shared": "4.23.2-1", + "agora-rte-extension": "^1.2.4", + "axios": "^1.8.3", + "formdata-polyfill": "^4.0.7", + "pako": "^2.1.0", + "ua-parser-js": "^0.7.34", + "webrtc-adapter": "8.2.0" + } + }, + "node_modules/@coze/api/node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/@coze/api/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@coze/api/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/clone": { + "version": "0.1.30", + "resolved": "https://registry.npmmirror.com/@types/clone/-/clone-0.1.30.tgz", + "integrity": "sha512-vcxBr+ybljeSiasmdke1cQ9ICxoEwaBgM1OQ/P5h4MPj/kRyLcDl5L8PrftlbyV1kBbJIs3M3x1A1+rcWd4mEA==", + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.5", + "resolved": "https://registry.npmmirror.com/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", + "license": "MIT" + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/agora-extension-ai-denoiser": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/agora-extension-ai-denoiser/-/agora-extension-ai-denoiser-1.1.0.tgz", + "integrity": "sha512-g4klzzz7IQblNt0c+gJ/DM7sa5oMZ/7GWE4c9aqs3snHRAo+CjDieT84iAc1KUhAiY84RrfhyFdtpvbbEoYBcA==", + "license": "MIT", + "peerDependencies": { + "agora-rtc-sdk-ng": ">=4.15.0" + } + }, + "node_modules/agora-rtc-sdk-ng": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.24.0.tgz", + "integrity": "sha512-2apG/07EtsuX21ncSF77q+dr6/kDgu9B/RpKtstCtaq46l4/Eraoecewi4zXRUCY3Im+8dzTIXx6jUwyPdxdHQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@agora-js/media": "4.24.0", + "@agora-js/report": "4.24.0", + "@agora-js/shared": "4.24.0", + "agora-rte-extension": "^1.2.4", + "axios": "^1.8.3", + "formdata-polyfill": "^4.0.7", + "pako": "^2.1.0", + "ua-parser-js": "^0.7.34", + "webrtc-adapter": "8.2.0" + } + }, + "node_modules/agora-rte-extension": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/agora-rte-extension/-/agora-rte-extension-1.2.4.tgz", + "integrity": "sha512-0ovZz1lbe30QraG1cU+ji7EnQ8aUu+Hf3F+a8xPml3wPOyUQEK6CTdxV9kMecr9t+fIDrGeW7wgJTsM1DQE7Nw==", + "license": "ISC" + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.1.tgz", + "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/bson": { + "version": "4.7.2", + "resolved": "https://registry.npmmirror.com/bson/-/bson-4.7.2.tgz", + "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", + "license": "Apache-2.0", + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmmirror.com/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==", + "license": "MIT" + }, + "node_modules/lodash.unset": { + "version": "4.5.2", + "resolved": "https://registry.npmmirror.com/lodash.unset/-/lodash.unset-4.5.2.tgz", + "integrity": "sha512-bwKX88k2JhCV9D1vtE8+naDKlLiGrSmf8zi/Y9ivFHwbmRfA8RxS/aVJ+sIht2XOwqoNr4xUPUkGZpc1sHFEKg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/opus-encdec": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/opus-encdec/-/opus-encdec-0.1.1.tgz", + "integrity": "sha512-TDzyGqYqrwn5UEUNaLsfLGu8Ma+HRNrgLYj7Vx5wfTnafAA21G6Bnm/qTIa3orQi/yZPZYmkdpO/gez4nfA1Rw==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "6.8.8", + "resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-6.8.8.tgz", + "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.0", + "@types/node": "^10.1.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/reconnecting-websocket": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz", + "integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==", + "license": "MIT" + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmmirror.com/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmmirror.com/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", + "deprecated": "request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "license": "ISC", + "dependencies": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "license": "ISC", + "dependencies": { + "lodash": "^4.17.19" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, + "node_modules/sdp": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/sdp/-/sdp-3.2.1.tgz", + "integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "license": "ISC", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tcb-admin-node": { + "version": "1.23.0", + "resolved": "https://registry.npmmirror.com/tcb-admin-node/-/tcb-admin-node-1.23.0.tgz", + "integrity": "sha512-SAbjTqMsSi63SId1BJ4kWdyGJzhxh9Tjvy3YXxcsoaAC2PtASn4UIYsBsiNEUfcn58QEn2tdvCvvf69WLLjjrg==", + "license": "MIT", + "dependencies": { + "@cloudbase/database": "0.9.15", + "@cloudbase/signature-nodejs": "^1.0.0-beta.0", + "is-regex": "^1.0.4", + "jsonwebtoken": "^8.5.1", + "lodash.merge": "^4.6.1", + "request": "^2.87.0", + "xml2js": "^0.4.19" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/tcb-admin-node/node_modules/@cloudbase/database": { + "version": "0.9.15", + "resolved": "https://registry.npmmirror.com/@cloudbase/database/-/database-0.9.15.tgz", + "integrity": "sha512-63e7iIl+van41B39Tw4ScNe9TRCt+5GHjc7q6i8NzkWBLC3U3KlbWo79YHsUHUPI79POpQ8UMlMVo7HXIAO3dg==", + "license": "ISC", + "dependencies": { + "bson": "^4.0.2", + "lodash.clonedeep": "4.5.0", + "lodash.set": "4.3.2", + "lodash.unset": "4.5.2" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmmirror.com/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "license": "MIT", + "dependencies": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.41", + "resolved": "https://registry.npmmirror.com/ua-parser-js/-/ua-parser-js-0.7.41.tgz", + "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmmirror.com/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/url/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webrtc-adapter": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/webrtc-adapter/-/webrtc-adapter-8.2.0.tgz", + "integrity": "sha512-umxCMgedPAVq4Pe/jl3xmelLXLn4XZWFEMR5Iipb5wJ+k1xMX0yC4ZY9CueZUU1MjapFxai1tFGE7R/kotH6Ww==", + "license": "BSD-3-Clause", + "dependencies": { + "sdp": "^3.0.2" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">=3.10.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wx-server-sdk": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/wx-server-sdk/-/wx-server-sdk-2.4.0.tgz", + "integrity": "sha512-+d/OAUgt3LVlIwC/EEd9oHK2VltMqvoSa3Z797sgZ/hBm/Z+bhYBX3PfrRgn41fprzNk49jdbmw8Rkwa4JryIQ==", + "license": "MIT", + "dependencies": { + "@cloudbase/node-sdk": "2.4.7", + "protobufjs": "6.8.8", + "tcb-admin-node": "latest", + "tslib": "^1.9.3" + } + }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmmirror.com/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/src5/code/cloudfunctions/quickstartFunctions/package.json b/src5/code/cloudfunctions/quickstartFunctions/package.json new file mode 100644 index 0000000..3996691 --- /dev/null +++ b/src5/code/cloudfunctions/quickstartFunctions/package.json @@ -0,0 +1,17 @@ +{ + "name": "quickstartFunctions", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@coze/api": "^1.3.7", + "axios": "^1.13.1", + "form-data": "^4.0.4", + "wx-server-sdk": "~2.4.0" + } +} diff --git a/src5/code/cloudfunctions/quickstartFunctions/test-analyze.js b/src5/code/cloudfunctions/quickstartFunctions/test-analyze.js new file mode 100644 index 0000000..53af95a --- /dev/null +++ b/src5/code/cloudfunctions/quickstartFunctions/test-analyze.js @@ -0,0 +1,58 @@ +// 测试AI定价功能的脚本 +// 用于本地测试云函数逻辑 + +const cloud = require("wx-server-sdk"); +cloud.init({ + env: "cloud1-7gmt1869f0703287" // 使用你的环境ID +}); + +// 导入云函数 +const analyzeProductPrice = require('./index.js').analyzeProductPrice; + +async function testAnalyzeProductPrice() { + console.log('========== 开始测试AI定价功能 =========='); + + // 测试用例1: 使用fileID(需要先上传一个图片到云存储) + const testEvent1 = { + type: 'analyzeProductPrice', + fileID: 'cloud://cloud1-7gmt1869f0703287.636c-cloud1-7gmt1869f0703287-1316160869/pricing/test.jpg', + originalPrice: 100 + }; + + // 测试用例2: 使用imageUrl(使用一个公开的图片URL) + const testEvent2 = { + type: 'analyzeProductPrice', + imageUrl: 'https://pic1.zhimg.com/v2-24d60d3e9a24ce8d1f3ee0d9f05f929c_b.jpg', + originalPrice: 100 + }; + + try { + console.log('\n测试用例2: 使用imageUrl测试'); + console.log('测试参数:', JSON.stringify(testEvent2, null, 2)); + + const result = await analyzeProductPrice(testEvent2); + + console.log('\n========== 测试结果 =========='); + console.log(JSON.stringify(result, null, 2)); + + if (result.success) { + console.log('\n✅ 测试成功!'); + console.log('商品名称:', result.data.productName); + console.log('建议价格:', result.data.suggestedPrice); + console.log('商品成色:', result.data.conditionLevel); + console.log('AI评分:', result.data.aiScore); + } else { + console.log('\n❌ 测试失败!'); + console.log('错误信息:', result.error); + console.log('错误详情:', result.details); + } + } catch (error) { + console.error('\n========== 测试异常 =========='); + console.error('错误信息:', error.message); + console.error('错误堆栈:', error.stack); + } +} + +// 运行测试 +testAnalyzeProductPrice(); + diff --git a/src5/code/cloudfunctions/quickstartFunctions/test-coze-workflow.js b/src5/code/cloudfunctions/quickstartFunctions/test-coze-workflow.js new file mode 100644 index 0000000..5d34d77 --- /dev/null +++ b/src5/code/cloudfunctions/quickstartFunctions/test-coze-workflow.js @@ -0,0 +1,28 @@ +// 测试云函数AI定价功能 +async function testCloudFunction() { + try { + console.log('开始测试云函数AI定价功能...'); + + // 模拟云函数调用 - 使用用户提供的具体图片URL + const event = { + type: 'analyzeProductPrice', + imageUrl: 'https://pic1.zhimg.com/v2-24d60d3e9a24ce8d1f3ee0d9f05f929c_b.jpg', + originalPrice: 100 + }; + + // 导入云函数 + const cloudFunction = require('./index.js'); + + console.log('调用云函数analyzeProductPrice...'); + const result = await cloudFunction.main(event, {}); + + console.log('云函数调用结果:', JSON.stringify(result, null, 2)); + + } catch (error) { + console.error('❌ 测试过程中发生错误:', error.message); + console.error('错误堆栈:', error.stack); + } +} + +// 运行测试 +testCloudFunction(); \ No newline at end of file diff --git a/src5/code/image/05c485187dc967e68498ba92b2787d20.jpg b/src5/code/image/05c485187dc967e68498ba92b2787d20.jpg new file mode 100644 index 0000000..0761f9b Binary files /dev/null and b/src5/code/image/05c485187dc967e68498ba92b2787d20.jpg differ diff --git a/src5/code/image/0fa9d0ab28bd29e1.jpg b/src5/code/image/0fa9d0ab28bd29e1.jpg new file mode 100644 index 0000000..2bb0bc3 Binary files /dev/null and b/src5/code/image/0fa9d0ab28bd29e1.jpg differ diff --git a/src5/code/image/408.avif b/src5/code/image/408.avif new file mode 100644 index 0000000..1ae95b4 Binary files /dev/null and b/src5/code/image/408.avif differ diff --git a/src5/code/image/R.jpg b/src5/code/image/R.jpg new file mode 100644 index 0000000..e80c70e Binary files /dev/null and b/src5/code/image/R.jpg differ diff --git a/src5/code/image/login copy 2.jpg b/src5/code/image/login copy 2.jpg new file mode 100644 index 0000000..8135dc0 Binary files /dev/null and b/src5/code/image/login copy 2.jpg differ diff --git a/src5/code/image/login copy.jpg b/src5/code/image/login copy.jpg new file mode 100644 index 0000000..8135dc0 Binary files /dev/null and b/src5/code/image/login copy.jpg differ diff --git a/src5/code/image/login.jpg b/src5/code/image/login.jpg new file mode 100644 index 0000000..8135dc0 Binary files /dev/null and b/src5/code/image/login.jpg differ diff --git a/src5/code/image/login2.jpg b/src5/code/image/login2.jpg new file mode 100644 index 0000000..04e684d Binary files /dev/null and b/src5/code/image/login2.jpg differ diff --git a/src5/code/image/login3.jpg b/src5/code/image/login3.jpg new file mode 100644 index 0000000..1279255 Binary files /dev/null and b/src5/code/image/login3.jpg differ diff --git a/src5/code/image/login4.jpg b/src5/code/image/login4.jpg new file mode 100644 index 0000000..fe9ab87 Binary files /dev/null and b/src5/code/image/login4.jpg differ diff --git a/src5/code/image/minhang.png b/src5/code/image/minhang.png new file mode 100644 index 0000000..5e8eb3d Binary files /dev/null and b/src5/code/image/minhang.png differ diff --git a/src5/code/image/三星.avif b/src5/code/image/三星.avif new file mode 100644 index 0000000..80c73d5 Binary files /dev/null and b/src5/code/image/三星.avif differ diff --git a/src5/code/image/书.jpg b/src5/code/image/书.jpg new file mode 100644 index 0000000..213b49b Binary files /dev/null and b/src5/code/image/书.jpg differ diff --git a/src5/code/image/二手书.avif b/src5/code/image/二手书.avif new file mode 100644 index 0000000..6adb926 Binary files /dev/null and b/src5/code/image/二手书.avif differ diff --git a/src5/code/image/二手书.jpg b/src5/code/image/二手书.jpg new file mode 100644 index 0000000..7e25160 Binary files /dev/null and b/src5/code/image/二手书.jpg differ diff --git a/src5/code/image/冲锋衣.avif b/src5/code/image/冲锋衣.avif new file mode 100644 index 0000000..9e2d54f Binary files /dev/null and b/src5/code/image/冲锋衣.avif differ diff --git a/src5/code/image/冲锋衣2.avif b/src5/code/image/冲锋衣2.avif new file mode 100644 index 0000000..ac9c8ee Binary files /dev/null and b/src5/code/image/冲锋衣2.avif differ diff --git a/src5/code/image/冲锋衣3.webp b/src5/code/image/冲锋衣3.webp new file mode 100644 index 0000000..488f11e Binary files /dev/null and b/src5/code/image/冲锋衣3.webp differ diff --git a/src5/code/image/冲锋衣4.avif b/src5/code/image/冲锋衣4.avif new file mode 100644 index 0000000..13fd9f8 Binary files /dev/null and b/src5/code/image/冲锋衣4.avif differ diff --git a/src5/code/image/华为.avif b/src5/code/image/华为.avif new file mode 100644 index 0000000..54f4385 Binary files /dev/null and b/src5/code/image/华为.avif differ diff --git a/src5/code/image/奇亚籽.jpg b/src5/code/image/奇亚籽.jpg new file mode 100644 index 0000000..6fe0d62 Binary files /dev/null and b/src5/code/image/奇亚籽.jpg differ diff --git a/src5/code/image/套盒.avif b/src5/code/image/套盒.avif new file mode 100644 index 0000000..b566f8c Binary files /dev/null and b/src5/code/image/套盒.avif differ diff --git a/src5/code/image/数学.avif b/src5/code/image/数学.avif new file mode 100644 index 0000000..1e69fbb Binary files /dev/null and b/src5/code/image/数学.avif differ diff --git a/src5/code/image/杯子.jpg b/src5/code/image/杯子.jpg new file mode 100644 index 0000000..f1263fb Binary files /dev/null and b/src5/code/image/杯子.jpg differ diff --git a/src5/code/image/相机.jpg b/src5/code/image/相机.jpg new file mode 100644 index 0000000..1e67e2c Binary files /dev/null and b/src5/code/image/相机.jpg differ diff --git a/src5/code/image/相机.webp b/src5/code/image/相机.webp new file mode 100644 index 0000000..223ddb3 Binary files /dev/null and b/src5/code/image/相机.webp differ diff --git a/src5/code/image/眼影盘.webp b/src5/code/image/眼影盘.webp new file mode 100644 index 0000000..d61ca3f Binary files /dev/null and b/src5/code/image/眼影盘.webp differ diff --git a/src5/code/image/红宝书.avif b/src5/code/image/红宝书.avif new file mode 100644 index 0000000..9a1a7e2 Binary files /dev/null and b/src5/code/image/红宝书.avif differ diff --git a/src5/code/image/花枝小.avif b/src5/code/image/花枝小.avif new file mode 100644 index 0000000..0f511e6 Binary files /dev/null and b/src5/code/image/花枝小.avif differ diff --git a/src5/code/image/苹果.avif b/src5/code/image/苹果.avif new file mode 100644 index 0000000..1c35fb5 Binary files /dev/null and b/src5/code/image/苹果.avif differ diff --git a/src5/code/image/衣服1.avif b/src5/code/image/衣服1.avif new file mode 100644 index 0000000..94be16d Binary files /dev/null and b/src5/code/image/衣服1.avif differ diff --git a/src5/code/image/饼干.jpg b/src5/code/image/饼干.jpg new file mode 100644 index 0000000..6f32e7b Binary files /dev/null and b/src5/code/image/饼干.jpg differ diff --git a/src5/code/image/高数.avif b/src5/code/image/高数.avif new file mode 100644 index 0000000..295d9fe Binary files /dev/null and b/src5/code/image/高数.avif differ diff --git a/src5/code/miniprogram/app.js b/src5/code/miniprogram/app.js new file mode 100644 index 0000000..1edbe76 --- /dev/null +++ b/src5/code/miniprogram/app.js @@ -0,0 +1,21 @@ +// app.js +App({ + onLaunch: function () { + this.globalData = { + // env 参数说明: + // env 参数决定接下来小程序发起的云开发调用(wx.cloud.xxx)会默认请求到哪个云环境的资源 + // 此处请填入环境 ID, 环境 ID 可打开云控制台查看 + // 如不填则使用默认环境(第一个创建的环境) + env: "cloud1-7gmt1869f0703287" + + }; + if (!wx.cloud) { + console.error("请使用 2.2.3 或以上的基础库以使用云能力"); + } else { + wx.cloud.init({ + env: this.globalData.env, + traceUser: true, + }); + } + }, +}); diff --git a/src5/code/miniprogram/app.json b/src5/code/miniprogram/app.json new file mode 100644 index 0000000..4fd7716 --- /dev/null +++ b/src5/code/miniprogram/app.json @@ -0,0 +1,77 @@ +{ + "pages": [ + "pages/index/index", + "pages/main/main", + "pages/market/market", + "pages/cart/cart", + "pages/messages/messages", + "pages/profile/profile", + "pages/recommend-list/recommend-list", + "pages/chat/chat" + ], + "subpackages": [ + { "root": "pages/register", "pages": ["register"] }, + { "root": "pages/interests", "pages": ["interests"] }, + { "root": "pages/myProducts", "pages": ["myProducts"] }, + { "root": "pages/orders", "pages": ["orders"] }, + { "root": "pages/wanted-list", "pages": ["wanted-list"] }, + { "root": "pages/favorites", "pages": ["favorites"] }, + { "root": "pages/profile-edit", "pages": ["profile-edit"] }, + { "root": "pages/security", "pages": ["security"] }, + { "root": "pages/address", "pages": ["address"] }, + { "root": "pages/feedback", "pages": ["feedback"] }, + { "root": "pages/privacy", "pages": ["privacy"] }, + { "root": "pages/buy", "pages": ["buy"] }, + { "root": "pages/purchase", "pages": ["purchase"] }, + + { "root": "pages/admin-login", "pages": ["admin-login"], "independent": true }, + { "root": "pages/admin-dashboard", "pages": ["admin-dashboard"], "independent": true }, + { "root": "pages/admin-products", "pages": ["admin-products"], "independent": true }, + { "root": "pages/admin-users", "pages": ["admin-users"], "independent": true }, + { "root": "pages/admin-user-edit", "pages": ["admin-user-edit"], "independent": true }, + { "root": "pages/product-detail", "pages": ["product-detail"] }, + { "root": "pages/publish", "pages": ["publish"] }, + { "root": "pages/pricing", "pages": ["pricing"] } + ], + "window": { + "navigationBarTextStyle": "white", + "navigationBarTitleText": "校园二手交易", + "navigationBarBackgroundColor": "#4f8bff", + "backgroundColor": "#f0f7ff" + }, + "tabBar": { + "custom": true, + "color": "#999999", + "selectedColor": "#4f8bff", + "backgroundColor": "#ffffff", + "borderStyle": "black", + "list": [ + { "pagePath": "pages/main/main", "text": "首页" }, + { "pagePath": "pages/market/market", "text": "藏宝图" }, + { "pagePath": "pages/cart/cart", "text": "购物车" }, + { "pagePath": "pages/messages/messages", "text": "消息" }, + { "pagePath": "pages/profile/profile", "text": "我的" } + ] + }, + "requiredPrivateInfos": [ + "chooseAddress", + "chooseLocation", + "choosePoi", + "getLocation", + "onLocationChange", + "startLocationUpdate", + "startLocationUpdateBackground" + ], + "requiredBackgroundModes": [ + "location" + ], + "permission": { + "scope.userLocation": { + "desc": "你的位置信息将用于小程序位置接口的效果展示" + } + }, + "style": "v2", + "componentFramework": "glass-easel", + "sitemapLocation": "sitemap.json", + "lazyCodeLoading": "requiredComponents" +} diff --git a/src5/code/miniprogram/app.wxss b/src5/code/miniprogram/app.wxss new file mode 100644 index 0000000..d5dbaa5 --- /dev/null +++ b/src5/code/miniprogram/app.wxss @@ -0,0 +1,52 @@ +/**app.wxss**/ +/* 全局样式重置 */ +page { + height: 100%; + background-color: #f5f5f5; +} + +/* 容器样式 */ +/**app.wxss**/ +.container { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 200rpx 0; + box-sizing: border-box; +} + + +/* 通用文本样式 */ +text { + font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun, sans-serif; +} + +/* 通用按钮样式 */ +button { + border-radius: 8rpx; + font-size: 32rpx; +} + +button::after { + border: none; +} + +/* 通用输入框样式 */ +input { + font-size: 32rpx; + color: #333; +} + +/* 通用图片样式 */ +image { + display: block; +} + +/* 通用滚动条样式 */ +::-webkit-scrollbar { + width: 0; + height: 0; + color: transparent; +} diff --git a/src5/code/miniprogram/components/cloudTipModal/index.js b/src5/code/miniprogram/components/cloudTipModal/index.js new file mode 100644 index 0000000..8e75cc0 --- /dev/null +++ b/src5/code/miniprogram/components/cloudTipModal/index.js @@ -0,0 +1,28 @@ +Component({ + + /** + * 页面的初始数据 + */ + data: { + showTip: false, + }, + properties: { + showTipProps: Boolean, + title:String, + content:String + }, + observers: { + showTipProps: function(showTipProps) { + this.setData({ + showTip: showTipProps + }); + } + }, + methods: { + onClose(){ + this.setData({ + showTip: !this.data.showTip + }); + }, + } +}); diff --git a/src5/code/miniprogram/components/cloudTipModal/index.json b/src5/code/miniprogram/components/cloudTipModal/index.json new file mode 100644 index 0000000..4575d1b --- /dev/null +++ b/src5/code/miniprogram/components/cloudTipModal/index.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "component": true +} \ No newline at end of file diff --git a/src5/code/miniprogram/components/cloudTipModal/index.wxml b/src5/code/miniprogram/components/cloudTipModal/index.wxml new file mode 100644 index 0000000..cc51c4a --- /dev/null +++ b/src5/code/miniprogram/components/cloudTipModal/index.wxml @@ -0,0 +1,10 @@ + + + + + + + {{title}} + {{content}} + + diff --git a/src5/code/miniprogram/components/cloudTipModal/index.wxss b/src5/code/miniprogram/components/cloudTipModal/index.wxss new file mode 100644 index 0000000..862f70c --- /dev/null +++ b/src5/code/miniprogram/components/cloudTipModal/index.wxss @@ -0,0 +1,60 @@ +.install_tip_back { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: rgba(0,0,0,0.4); + z-index: 1; +} +.install_tip_close{ + position:absolute; + right: 10rpx; + top: 10rpx; + width: 32px; + height: 32px; + /* background-color: red; */ +} +.install_tip_detail { + position: fixed; + background-color: white; + right: 0; + bottom: 0; + left: 0; + border-radius: 40rpx 40rpx 0 0; + padding: 50rpx 50rpx 100rpx 50rpx; + z-index: 9; +} + +.install_tip_detail_title { + font-weight: 400; + font-size: 40rpx; + text-align: center; +} + +.install_tip_detail_tip { + font-size: 25rpx; + color: rgba(0,0,0,0.4); + margin-top: 20rpx; + text-align: left; +} +.install_tip_detail_buttons { + padding-top: 50rpx; + display: flex; +} +.install_tip_detail_button { + color: #07C160; + font-weight: 500; + background-color: rgba(0,0,0,0.1); + width: 40%; + text-align: center; + /* height: 90rpx; */ + /* line-height: 90rpx; */ + border-radius: 10rpx; + margin: 0 auto; +} + +.install_tip_detail_button_primary { + background-color: #07C160; + color: #fff; +} \ No newline at end of file diff --git a/src5/code/miniprogram/custom-tab-bar/index.js b/src5/code/miniprogram/custom-tab-bar/index.js new file mode 100644 index 0000000..b1a85da --- /dev/null +++ b/src5/code/miniprogram/custom-tab-bar/index.js @@ -0,0 +1,23 @@ +Component({ + data: { + selected: 0, + tabs: [ + { pagePath: '/pages/main/main' }, + { pagePath: '/pages/market/market' }, + { pagePath: '/pages/cart/cart' }, + { pagePath: '/pages/messages/messages' }, + { pagePath: '/pages/profile/profile' } + ] + }, + methods: { + setSelected(index) { + this.setData({ selected: index }); + }, + onTabTap(e) { + const idx = Number(e.currentTarget.dataset.index); + const tab = this.data.tabs[idx]; + if (!tab) return; + wx.switchTab({ url: tab.pagePath }); + } + } +}); \ No newline at end of file diff --git a/src5/code/miniprogram/custom-tab-bar/index.json b/src5/code/miniprogram/custom-tab-bar/index.json new file mode 100644 index 0000000..32640e0 --- /dev/null +++ b/src5/code/miniprogram/custom-tab-bar/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} \ No newline at end of file diff --git a/src5/code/miniprogram/custom-tab-bar/index.wxml b/src5/code/miniprogram/custom-tab-bar/index.wxml new file mode 100644 index 0000000..efb548c --- /dev/null +++ b/src5/code/miniprogram/custom-tab-bar/index.wxml @@ -0,0 +1,22 @@ + + + 🏠 + 首页 + + + 🗺️ + 藏宝图 + + + 🛒 + 购物车 + + + 📨 + 消息 + + + 👤 + 我的 + + \ No newline at end of file diff --git a/src5/code/miniprogram/custom-tab-bar/index.wxss b/src5/code/miniprogram/custom-tab-bar/index.wxss new file mode 100644 index 0000000..7867ae1 --- /dev/null +++ b/src5/code/miniprogram/custom-tab-bar/index.wxss @@ -0,0 +1,22 @@ +.tab-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + background: #ffffff; + border-top: 1rpx solid #e0e0e0; + padding: 15rpx 0; + z-index: 1000; +} +.tab-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #999; +} +.tab-item.active .tab-text { color: #4f8bff; } +.tab-icon-emoji { font-size: 44rpx; margin-bottom: 8rpx; line-height: 1; } +.tab-text { font-size: 20rpx; } \ No newline at end of file diff --git a/src5/code/miniprogram/envList.js b/src5/code/miniprogram/envList.js new file mode 100644 index 0000000..e9a169e --- /dev/null +++ b/src5/code/miniprogram/envList.js @@ -0,0 +1,6 @@ +const envList = []; +const isMac = false; +module.exports = { + envList, + isMac +}; diff --git a/src5/code/miniprogram/images/仓鼠.png b/src5/code/miniprogram/images/仓鼠.png new file mode 100644 index 0000000..0e74147 Binary files /dev/null and b/src5/code/miniprogram/images/仓鼠.png differ diff --git a/src5/code/miniprogram/images/更多犬种.png b/src5/code/miniprogram/images/更多犬种.png new file mode 100644 index 0000000..067a075 Binary files /dev/null and b/src5/code/miniprogram/images/更多犬种.png differ diff --git a/src5/code/miniprogram/images/羊.png b/src5/code/miniprogram/images/羊.png new file mode 100644 index 0000000..16ac452 Binary files /dev/null and b/src5/code/miniprogram/images/羊.png differ diff --git a/src5/code/miniprogram/images/边牧.png b/src5/code/miniprogram/images/边牧.png new file mode 100644 index 0000000..3f770a2 Binary files /dev/null and b/src5/code/miniprogram/images/边牧.png differ diff --git a/src5/code/miniprogram/pages/address/address.js b/src5/code/miniprogram/pages/address/address.js new file mode 100644 index 0000000..09bf23b --- /dev/null +++ b/src5/code/miniprogram/pages/address/address.js @@ -0,0 +1,200 @@ +// pages/address/address.js +Page({ + /** + * 页面的初始数据 + */ + data: { + addressList: [], + defaultAddressId: null + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + this.loadAddressList(); + }, + + /** + * 生命周期函数--监听页面显示 + */ + onShow() { + this.loadAddressList(); + }, + + /** + * 确保有openid + */ + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getOpenId' + } + }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { + console.error('获取openid失败:', err); + } + } + return openid; + }, + + /** + * 加载地址列表 + */ + async loadAddressList() { + try { + const db = wx.cloud.database(); + const openid = await this.ensureOpenId(); + + if (!openid) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + this.setData({ + addressList: [] + }); + return; + } + + const result = await db.collection('T_address') + .where({ + _openid: openid + }) + .orderBy('isDefault', 'desc') + .orderBy('createTime', 'desc') + .get(); + + let defaultAddressId = null; + if (result.data && result.data.length > 0) { + const defaultAddr = result.data.find(addr => addr.isDefault); + if (defaultAddr) { + defaultAddressId = defaultAddr._id; + } + } + + this.setData({ + addressList: result.data || [], + defaultAddressId: defaultAddressId + }); + } catch (err) { + console.error('加载地址列表失败:', err); + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + } + }, + + /** + * 添加地址 + */ + onAddAddress() { + wx.navigateTo({ + url: '/pages/address-edit/address-edit' + }); + }, + + /** + * 编辑地址 + */ + onEditAddress(e) { + const addressId = e.currentTarget.dataset.id; + wx.navigateTo({ + url: `/pages/address-edit/address-edit?id=${addressId}` + }); + }, + + /** + * 删除地址 + */ + async onDeleteAddress(e) { + const addressId = e.currentTarget.dataset.id; + + wx.showModal({ + title: '确认删除', + content: '确定要删除该地址吗?', + success: async (res) => { + if (res.confirm) { + try { + const db = wx.cloud.database(); + await db.collection('T_address').doc(addressId).remove(); + + wx.showToast({ + title: '删除成功', + icon: 'success' + }); + + this.loadAddressList(); + } catch (err) { + console.error('删除地址失败:', err); + wx.showToast({ + title: '删除失败', + icon: 'none' + }); + } + } + } + }); + }, + + /** + * 设置默认地址 + */ + async onSetDefault(e) { + const addressId = e.currentTarget.dataset.id; + + try { + const db = wx.cloud.database(); + const openid = await this.ensureOpenId(); + + if (!openid) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + return; + } + + // 先将所有地址设为非默认 + await db.collection('T_address') + .where({ + _openid: openid + }) + .update({ + data: { + isDefault: false + } + }); + + // 设置当前地址为默认 + await db.collection('T_address').doc(addressId).update({ + data: { + isDefault: true + } + }); + + wx.showToast({ + title: '设置成功', + icon: 'success' + }); + + this.loadAddressList(); + } catch (err) { + console.error('设置默认地址失败:', err); + wx.showToast({ + title: '设置失败', + icon: 'none' + }); + } + } +}); + diff --git a/src5/code/miniprogram/pages/address/address.json b/src5/code/miniprogram/pages/address/address.json new file mode 100644 index 0000000..da6fcf3 --- /dev/null +++ b/src5/code/miniprogram/pages/address/address.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "收货地址", + "enablePullDownRefresh": true +} + diff --git a/src5/code/miniprogram/pages/address/address.wxml b/src5/code/miniprogram/pages/address/address.wxml new file mode 100644 index 0000000..ffe1018 --- /dev/null +++ b/src5/code/miniprogram/pages/address/address.wxml @@ -0,0 +1,37 @@ + + + + + + + + {{item.name}} + {{item.phone}} + 默认 + + {{item.province}}{{item.city}}{{item.district}}{{item.detail}} + + + + 设为默认 + 默认地址 + + 编辑 + 删除 + + + + + + + + 暂无收货地址 + 快去添加一个地址吧~ + + + + + + + + diff --git a/src5/code/miniprogram/pages/address/address.wxss b/src5/code/miniprogram/pages/address/address.wxss new file mode 100644 index 0000000..8258df4 --- /dev/null +++ b/src5/code/miniprogram/pages/address/address.wxss @@ -0,0 +1,144 @@ +/* pages/address/address.wxss */ +.page-container { + min-height: 100vh; + background-color: #f5f5f5; + padding-bottom: 120rpx; +} + +.address-list { + padding: 20rpx; +} + +.address-item { + background: white; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.address-info { + padding-bottom: 20rpx; + border-bottom: 1rpx solid #f0f0f0; + margin-bottom: 20rpx; +} + +.address-header { + display: flex; + align-items: center; + margin-bottom: 10rpx; +} + +.name { + font-size: 28rpx; + font-weight: bold; + color: #333; + margin-right: 20rpx; +} + +.phone { + font-size: 28rpx; + color: #666; + margin-right: 20rpx; +} + +.default-tag { + font-size: 22rpx; + color: #ff6b6b; + background: #ffe5e5; + padding: 4rpx 12rpx; + border-radius: 4rpx; +} + +.address-detail { + font-size: 26rpx; + color: #666; + line-height: 1.6; +} + +.address-actions { + display: flex; + justify-content: flex-end; + gap: 20rpx; +} + +.action-btn { + font-size: 26rpx; + color: #667eea; + padding: 10rpx 20rpx; + border-radius: 8rpx; + border: 1rpx solid #667eea; +} + +.action-btn:active { + opacity: 0.7; +} + +.action-btn.edit { + color: #07c160; + border-color: #07c160; +} + +.action-btn.delete { + color: #ff6b6b; + border-color: #ff6b6b; +} + +.default-text { + color: #999; + border-color: #e0e0e0; +} + +.empty-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 30rpx; +} + +.empty-icon { + width: 200rpx; + height: 200rpx; + margin-bottom: 30rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 32rpx; + color: #999; + margin-bottom: 10rpx; +} + +.empty-tip { + font-size: 24rpx; + color: #ccc; +} + +.add-button-section { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 20rpx 30rpx; + background: white; + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.add-btn { + width: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 50rpx; + padding: 30rpx; + font-size: 32rpx; + font-weight: bold; + box-shadow: 0 8rpx 25rpx rgba(102, 126, 234, 0.3); +} + +.add-btn:active { + transform: translateY(2rpx); + box-shadow: 0 4rpx 15rpx rgba(102, 126, 234, 0.3); +} + diff --git a/src5/code/miniprogram/pages/admin-dashboard/admin-dashboard.js b/src5/code/miniprogram/pages/admin-dashboard/admin-dashboard.js new file mode 100644 index 0000000..808089b --- /dev/null +++ b/src5/code/miniprogram/pages/admin-dashboard/admin-dashboard.js @@ -0,0 +1,404 @@ +// pages/admin-dashboard/admin-dashboard.js +const { QQMAP_KEY, QQMAP_REFERER } = require('../../utils/config.js'); +Page({ + data: { + adminInfo: {}, + stats: { + totalProducts: 0, + totalUsers: 0, + totalSales: 0, + totalOrders: 0 + }, + monthlyProducts: [], + monthlySales: [], + categoryStats: [], + loading: true, + refreshing: false, + // 供需缺口 Top N 展示与推送(不分校区) + topNOptions: [3,5,8], + topNIndex: 1, // 默认5 + topGaps: [], + gapsLoading: false, + pushInProgress: false, + pushResult: null, + // 求购关键词词云 + keywordsLoading: false, + wantedKeywords: [], + keywordsDisplay: [] + , + // 地标同步工具 + syncDryRun: true, + syncLimit: 300, + syncLandmarksLoading: false, + syncResult: null + }, + + onLoad() { + // 检查管理员登录状态 + const adminInfo = wx.getStorageSync('adminInfo'); + if (!adminInfo) { + wx.redirectTo({ + url: '/pages/admin-login/admin-login' + }); + return; + } + + this.setData({ + adminInfo: adminInfo + }); + + this.loadDashboardData(); + // 加载供需缺口TopN + this.loadSupplyDemandGaps(); + // 加载求购关键词词云 + this.loadWantedKeywords(); + }, + + /** 切换地标同步 dryRun */ + onSyncDryRunToggle(e) { + this.setData({ syncDryRun: !!e.detail.value }); + }, + + /** 设置地标同步 limit */ + onSyncLimitInput(e) { + const v = parseInt(e.detail.value, 10); + if (Number.isFinite(v) && v > 0) { + this.setData({ syncLimit: v }); + } + }, + + /** 一键同步地标 */ + async onSyncLandmarks() { + if (this.data.syncLandmarksLoading) return; + try { + this.setData({ syncLandmarksLoading: true, syncResult: null }); + // 先确保集合存在 + try { + await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'createCampusLandmarksCollection' } }); + } catch (_) { /* 如果已存在或创建失败,继续执行同步 */ } + + const res = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { type: 'syncProductLandmarks', dryRun: this.data.syncDryRun, limit: this.data.syncLimit, key: QQMAP_KEY, referer: QQMAP_REFERER } + }); + const result = res.result || {}; + // 兼容展示字段 + result.candidateCount = result.totalCandidates || result.candidateCount || 0; + result.upserted = (result.created || 0) + (result.updated || 0); + this.setData({ syncResult: result }); + if (result.success) { + const candidates = result.totalCandidates || result.candidateCount || 0; + const upserted = (result.created || 0) + (result.updated || 0); + const msg = this.data.syncDryRun + ? `试运行:候选 ${candidates}` + : `已同步 ${upserted} 条地标`; + wx.showToast({ title: msg, icon: 'success' }); + } else { + wx.showToast({ title: result.error || '同步失败', icon: 'none' }); + } + } catch (err) { + console.error('同步地标失败:', err); + wx.showToast({ title: '同步失败', icon: 'none' }); + } finally { + this.setData({ syncLandmarksLoading: false }); + } + }, + + /** + * 页面显示时刷新数据 + */ + onShow() { + // 每次页面显示时都刷新统计数据,确保数据实时更新 + // 检查是否已经加载过数据(避免首次加载时重复请求) + if (this.data.adminInfo && Object.keys(this.data.adminInfo).length > 0 && !this.data.loading) { + // 静默刷新,不显示加载状态 + this.loadDashboardData(true); + } + }, + + /** + * 加载仪表盘数据 + */ + async loadDashboardData(refresh = false) { + // 如果不是刷新操作,显示加载状态 + if (!refresh) { + this.setData({ + loading: true + }); + } + + try { + // 调用云函数获取统计数据 + const res = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getAdminStats' + } + }); + + if (res.result && res.result.success) { + const stats = res.result.data; + + // 处理月度商品发布量数据 + const monthlyProducts = this.processMonthlyData(stats.monthlyProducts || []); + + // 处理月度销售额数据 + const monthlySales = this.processSalesData(stats.monthlySales || []); + + // 处理分类统计数据 + const categoryStats = this.processCategoryData(stats.categoryStats || []); + + this.setData({ + stats: { + totalProducts: stats.totalProducts || 0, + totalUsers: stats.totalUsers || 0, + totalSales: stats.totalSales || 0, + totalOrders: stats.totalOrders || 0 + }, + monthlyProducts: monthlyProducts, + monthlySales: monthlySales, + categoryStats: categoryStats, + loading: false, + refreshing: false + }); + + console.log('统计数据已更新:', { + totalProducts: stats.totalProducts, + totalUsers: stats.totalUsers, + totalSales: stats.totalSales, + totalOrders: stats.totalOrders + }); + } else { + throw new Error(res.result?.error || '获取数据失败'); + } + } catch (err) { + console.error('加载仪表盘数据失败:', err); + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + this.setData({ + loading: false, + refreshing: false + }); + } + }, + + /** + * 加载供需缺口Top N(全校区合并,不分校区) + */ + async loadSupplyDemandGaps() { + try { + this.setData({ gapsLoading: true }); + const res = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getPublishRecommendations', + forType: 'admin' + } + }); + const result = res.result || {}; + if (result.success && result.data) { + const stats = result.data.stats || []; + const n = this.data.topNOptions[this.data.topNIndex] || 5; + const top = stats.slice(0, n); + this.setData({ topGaps: top }); + } else { + wx.showToast({ title: '获取供需缺口失败', icon: 'none' }); + } + } catch (err) { + console.error('获取供需缺口失败:', err); + wx.showToast({ title: '网络异常', icon: 'none' }); + } finally { + this.setData({ gapsLoading: false }); + } + }, + + /** 选择TopN */ + onTopNChange(e) { + this.setData({ topNIndex: parseInt(e.detail.value) }); + this.loadSupplyDemandGaps(); + }, + + /** 一键推送建议 */ + async onPushRecommendation() { + if (this.data.pushInProgress) return; + try { + this.setData({ pushInProgress: true }); + const categories = (this.data.topGaps || []).map(item => item.category).filter(Boolean); + if (categories.length === 0) { + wx.showToast({ title: '暂无可推送的类别', icon: 'none' }); + return; + } + const res = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'adminPushRecommendations', + categories, + target: 'all' + } + }); + const result = res.result || {}; + if (result.success) { + wx.showToast({ title: `已推送${result.pushed || 0}条`, icon: 'success' }); + } else { + wx.showToast({ title: result.error || '推送失败', icon: 'none' }); + } + this.setData({ pushResult: result }); + } catch (err) { + console.error('推送建议失败:', err); + wx.showToast({ title: '推送失败', icon: 'none' }); + } finally { + this.setData({ pushInProgress: false }); + } + }, + + /** 加载求购关键词词云 */ + async loadWantedKeywords() { + try { + this.setData({ keywordsLoading: true }); + const res = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { type: 'getWantedKeywordCloud', limit: 60 } + }); + const result = res.result || {}; + if (result.success && result.data && Array.isArray(result.data.keywords)) { + const keywords = result.data.keywords; + const colors = ['#3366FF','#33A852','#FBBC04','#EA4335','#7E57C2','#00ACC1','#F06292','#8D6E63']; + const display = keywords.map((k, idx) => { + const size = Math.round(24 + (k.score || 0) * 22); // 24~46rpx + const opacity = (0.6 + (k.score || 0) * 0.4).toFixed(2); // 0.6~1.0 + const color = colors[idx % colors.length]; + return { + text: k.text, + weight: k.weight, + style: `font-size: ${size}rpx; color: ${color}; opacity: ${opacity};` + }; + }); + this.setData({ wantedKeywords: keywords, keywordsDisplay: display }); + } else { + this.setData({ wantedKeywords: [], keywordsDisplay: [] }); + } + } catch (err) { + console.error('加载求购关键词失败:', err); + wx.showToast({ title: '词云加载失败', icon: 'none' }); + } finally { + this.setData({ keywordsLoading: false }); + } + }, + + /** + * 处理月度数据(用于柱状图) + */ + processMonthlyData(data) { + if (!data || data.length === 0) { + // 返回空数据 + const months = ['1月', '2月', '3月', '4月', '5月', '6月']; + return months.map(month => ({ + month: month, + count: 0, + percentage: 0 + })); + } + + const maxCount = Math.max(...data.map(item => item.count), 1); + + return data.map(item => ({ + month: item.month, + count: item.count, + percentage: (item.count / maxCount) * 100 + })); + }, + + /** + * 处理销售额数据(用于折线图) + */ + processSalesData(data) { + if (!data || data.length === 0) { + const months = ['1月', '2月', '3月', '4月', '5月', '6月']; + return months.map((month, index) => ({ + month: month, + sales: 0, + percentage: 0, + position: (index / (months.length - 1)) * 100 + })); + } + + const maxSales = Math.max(...data.map(item => item.sales), 1); + const totalItems = data.length; + + return data.map((item, index) => ({ + month: item.month, + sales: item.sales, + percentage: (item.sales / maxSales) * 100, + position: totalItems > 1 ? (index / (totalItems - 1)) * 100 : 50 + })); + }, + + /** + * 处理分类数据(用于饼图) + */ + processCategoryData(data) { + if (!data || data.length === 0) { + return []; + } + + const total = data.reduce((sum, item) => sum + item.count, 0); + const colors = ['#4285F4', '#34A853', '#FBBC05', '#EA4335', '#9C27B0', '#00BCD4', '#FF9800', '#795548']; + + return data.map((item, index) => ({ + category: item.category, + count: item.count, + percentage: (item.count / total) * 100, + color: colors[index % colors.length] + })); + }, + + /** + * 下拉刷新 + */ + onRefresh() { + this.setData({ + refreshing: true + }); + this.loadDashboardData(); + }, + + /** + * 导航到其他页面 + */ + onNavigateTo(e) { + const page = e.currentTarget.dataset.page; + const pages = { + products: '/pages/admin-products/admin-products', + users: '/pages/admin-users/admin-users', + orders: '/pages/admin-orders/admin-orders' + }; + + if (pages[page]) { + wx.navigateTo({ + url: pages[page] + }); + } + }, + + /** + * 退出登录 + */ + onLogout() { + wx.showModal({ + title: '提示', + content: '确定要退出登录吗?', + success: (res) => { + if (res.confirm) { + wx.removeStorageSync('adminInfo'); + wx.removeStorageSync('adminToken'); + wx.redirectTo({ + url: '/pages/admin-login/admin-login' + }); + } + } + }); + } +}); + diff --git a/src5/code/miniprogram/pages/admin-dashboard/admin-dashboard.json b/src5/code/miniprogram/pages/admin-dashboard/admin-dashboard.json new file mode 100644 index 0000000..ffe67ed --- /dev/null +++ b/src5/code/miniprogram/pages/admin-dashboard/admin-dashboard.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "管理后台", + "navigationBarBackgroundColor": "#667eea", + "navigationBarTextStyle": "white", + "enablePullDownRefresh": true +} + diff --git a/src5/code/miniprogram/pages/admin-dashboard/admin-dashboard.wxml b/src5/code/miniprogram/pages/admin-dashboard/admin-dashboard.wxml new file mode 100644 index 0000000..61fd966 --- /dev/null +++ b/src5/code/miniprogram/pages/admin-dashboard/admin-dashboard.wxml @@ -0,0 +1,220 @@ + + + + + + 管理后台 + + {{adminInfo.name || '管理员'}} + 退出 + + + + + + + + + + 📦 + + {{stats.totalProducts}} + 商品总数 + + + + 👥 + + {{stats.totalUsers}} + 用户总数 + + + + 💰 + + ¥{{stats.totalSales}} + 总销售额 + + + + 📋 + + {{stats.totalOrders}} + 订单总数 + + + + + + + + 供需缺口 Top {{topNOptions[topNIndex]}} + 全校区 + + + + + Top {{topNOptions[topNIndex]}} + + + + + + + + + {{item.category}} + 供 {{item.supply}} · 需 {{item.demand}} + + + {{item.gap >= 0 ? ('缺口 +' + item.gap) : ('过供 ' + (item.gap))}} + + + 暂无数据 + + + + + + + + + + + 月度商品发布量 + 最近6个月 + + + + + + + + {{item.count}} + + {{item.month}} + + + + + + + + + + 月度销售额 + 最近6个月 + + + + + + + + + + + + + + {{item.month}} + + + ¥{{item.sales}} + + + + + + + + + + 商品分类分布 + + + + + + + + {{item.category}}: {{item.count}} + + + + + + + + + + 求购关键词词云 + 按出现频次加权 + + 加载词云... + + + {{item.text}} + + 暂无关键词 + + + + + + + 数据工具 + 地标与地图 + + + + 试运行 + + + + Limit + + + + + + + + + + 候选 + {{syncResult.candidateCount || 0}} + + + {{syncResult.upserted || 0}} 已写入 + + + {{syncResult.error || '执行失败'}} + + + + + + + 📦 + 商品管理 + + + 👥 + 用户管理 + + + 📋 + 订单管理 + + + + + + + + + 加载中... + + + diff --git a/src5/code/miniprogram/pages/admin-dashboard/admin-dashboard.wxss b/src5/code/miniprogram/pages/admin-dashboard/admin-dashboard.wxss new file mode 100644 index 0000000..f05ff3f --- /dev/null +++ b/src5/code/miniprogram/pages/admin-dashboard/admin-dashboard.wxss @@ -0,0 +1,445 @@ +/* pages/admin-dashboard/admin-dashboard.wxss */ +.page-container { + height: 100vh; + display: flex; + flex-direction: column; + background-color: #f5f5f5; +} + +/* 顶部导航栏 */ +.admin-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 30rpx; + padding-top: calc(30rpx + env(safe-area-inset-top)); +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; +} + +.admin-title { + font-size: 36rpx; + font-weight: 600; + color: white; +} + +.admin-info { + display: flex; + align-items: center; + gap: 20rpx; +} + +.admin-name { + font-size: 28rpx; + color: white; +} + +.logout-btn { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.8); + padding: 8rpx 20rpx; + background: rgba(255, 255, 255, 0.2); + border-radius: 20rpx; +} + +.scroll-container { + flex: 1; + padding: 20rpx; +} + +/* 数据概览 */ +.stats-overview { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20rpx; + margin-bottom: 30rpx; +} + +.stat-card { + background: white; + border-radius: 20rpx; + padding: 30rpx; + display: flex; + align-items: center; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); +} + +.stat-icon { + font-size: 60rpx; + margin-right: 20rpx; +} + +.stat-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.stat-value { + font-size: 36rpx; + font-weight: 600; + color: #333; + margin-bottom: 8rpx; +} + +.stat-label { + font-size: 24rpx; + color: #999; +} + +/* 图表区域 */ +.chart-section { + background: white; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 30rpx; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: 600; + color: #333; +} + +.section-subtitle { + font-size: 24rpx; + color: #999; +} + +.chart-container { + width: 100%; +} + +/* 供需缺口 TopN */ +.filters-row { + display: flex; + gap: 20rpx; + margin-bottom: 20rpx; +} + +.filter-picker { + flex: 1; +} + +.gaps-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.gap-item { + display: flex; + justify-content: space-between; + align-items: center; + background: #f8fbff; + border: 1rpx solid #e8ecff; + border-radius: 12rpx; + padding: 18rpx 20rpx; +} + +.gap-left { + display: flex; + flex-direction: column; +} + +.gap-category { + font-size: 28rpx; + color: #333; + font-weight: 600; +} + +.gap-sub { + font-size: 22rpx; + color: #666; +} + +.gap-right { + display: flex; + align-items: center; +} + +.gap-value { + font-size: 26rpx; + font-weight: 600; +} + +.gap-value.pos { color: #EA4335; } +.gap-value.neg { color: #34A853; } + +.empty-tips { + font-size: 24rpx; + color: #999; + text-align: center; + padding: 20rpx 0; +} + +.push-row { margin-top: 20rpx; } +.push-btn { + width: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; + border: none; + border-radius: 12rpx; + padding: 22rpx; + font-size: 28rpx; +} + +/* 柱状图 */ +.bar-chart { + width: 100%; +} + +.chart-bars { + display: flex; + justify-content: space-around; + align-items: flex-end; + height: 300rpx; + padding: 20rpx 0; +} + +.bar-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; +} + +.bar-wrapper { + flex: 1; + width: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + position: relative; +} + +.bar { + width: 60%; + background: linear-gradient(180deg, #667eea 0%, #764ba2 100%); + border-radius: 8rpx 8rpx 0 0; + min-height: 20rpx; + transition: all 0.3s ease; +} + +.bar-value { + position: absolute; + top: -40rpx; + font-size: 22rpx; + color: #666; + font-weight: 600; +} + +.bar-label { + font-size: 22rpx; + color: #999; + margin-top: 15rpx; +} + +/* 折线图 */ +.line-chart { + width: 100%; + height: 300rpx; + position: relative; +} + +.chart-area { + width: 100%; + height: 100%; + position: relative; +} + +.chart-grid { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.grid-line { + height: 1rpx; + background: #e0e0e0; +} + +.chart-line { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 60rpx; +} + +.line-path { + position: relative; + width: 100%; + height: 100%; +} + +.line-point { + position: absolute; + width: 16rpx; + height: 16rpx; + background: #667eea; + border-radius: 50%; + border: 3rpx solid white; + box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.3); + transform: translate(-50%, 50%); +} + +.chart-labels { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 60rpx; + display: flex; + justify-content: space-around; + align-items: center; +} + +.label-item { + font-size: 22rpx; + color: #999; +} + +.chart-values { + position: absolute; + top: -30rpx; + left: 0; + right: 0; + display: flex; + justify-content: space-around; +} + +.value-item { + font-size: 20rpx; + color: #666; +} + +/* 饼图 */ +.pie-chart-container { + width: 100%; +} + +.pie-chart { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.pie-item { + display: flex; + align-items: center; + gap: 20rpx; +} + +.pie-segment { + height: 40rpx; + border-radius: 20rpx; + min-width: 20rpx; +} + +.pie-label { + flex: 1; + display: flex; + align-items: center; + gap: 15rpx; +} + +.label-color { + width: 24rpx; + height: 24rpx; + border-radius: 50%; +} + +.label-text { + font-size: 26rpx; + color: #333; +} + +/* 快捷操作 */ +.quick-actions { + display: flex; + gap: 20rpx; + margin-bottom: 30rpx; +} + +.action-item { + flex: 1; + background: white; + border-radius: 20rpx; + padding: 40rpx 20rpx; + display: flex; + flex-direction: column; + align-items: center; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); +} + +.action-icon { + font-size: 60rpx; + margin-bottom: 15rpx; +} + +.action-text { + font-size: 26rpx; + color: #333; +} + +/* 加载状态 */ +.loading-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loading-content { + text-align: center; +} + +.loading-text { + font-size: 28rpx; + color: #666; +} + +/* 词云 */ +.word-cloud { + display: flex; + flex-wrap: wrap; + gap: 16rpx; + align-items: center; +} + +.word-chip { + background: #f2f3f5; + border-radius: 24rpx; + padding: 10rpx 16rpx; + line-height: 1.2; +} + +.word-cloud-loading { + font-size: 24rpx; + color: #999; +} + diff --git a/src5/code/miniprogram/pages/admin-login/admin-login.js b/src5/code/miniprogram/pages/admin-login/admin-login.js new file mode 100644 index 0000000..3f9ae02 --- /dev/null +++ b/src5/code/miniprogram/pages/admin-login/admin-login.js @@ -0,0 +1,141 @@ +// pages/admin-login/admin-login.js +Page({ + data: { + username: '', + password: '', + isLoginDisabled: false + }, + + onLoad() { + // 检查是否已登录 + const adminInfo = wx.getStorageSync('adminInfo'); + if (adminInfo) { + wx.redirectTo({ + url: '/pages/admin-dashboard/admin-dashboard' + }); + } + }, + + // 账号输入处理 + onInputUsername(e) { + this.setData({ + username: e.detail.value.trim() + }); + this.checkLoginButton(); + }, + + // 密码输入处理 + onInputPassword(e) { + this.setData({ + password: e.detail.value.trim() + }); + this.checkLoginButton(); + }, + + // 检查登录按钮状态 + checkLoginButton() { + this.setData({ + isLoginDisabled: false + }); + }, + + // 登录处理 + onLogin() { + const { username, password } = this.data; + + if (!this.validateInput(username, password)) { + return; + } + + wx.showLoading({ + title: '登录中...', + mask: true + }); + + // 调用云函数验证管理员身份 + wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'adminLogin', + username: username, + password: password + }, + success: (res) => { + wx.hideLoading(); + + if (res.result && res.result.success) { + // 登录成功 + const adminInfo = { + _id: res.result.adminId, + username: res.result.username, + name: res.result.name || '管理员', + role: res.result.role || 'admin' + }; + + wx.setStorageSync('adminInfo', adminInfo); + wx.setStorageSync('adminToken', 'admin_token_' + res.result.adminId); + + wx.showToast({ + title: '登录成功', + icon: 'success', + duration: 1500 + }); + + setTimeout(() => { + wx.redirectTo({ + url: '/pages/admin-dashboard/admin-dashboard' + }); + }, 1500); + } else { + wx.showToast({ + title: res.result?.error || '账号或密码错误', + icon: 'none' + }); + } + }, + fail: (err) => { + wx.hideLoading(); + console.error('管理员登录失败:', err); + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }); + } + }); + }, + + // 输入验证 + validateInput(username, password) { + if (!username) { + wx.showToast({ + title: '请输入管理员账号', + icon: 'none' + }); + return false; + } + + if (!password) { + wx.showToast({ + title: '请输入密码', + icon: 'none' + }); + return false; + } + + if (password.length < 6) { + wx.showToast({ + title: '密码长度不能少于6位', + icon: 'none' + }); + return false; + } + + return true; + }, + + // 返回用户登录 + onBackToUserLogin() { + wx.navigateBack(); + } +}); + diff --git a/src5/code/miniprogram/pages/admin-login/admin-login.json b/src5/code/miniprogram/pages/admin-login/admin-login.json new file mode 100644 index 0000000..b5c9715 --- /dev/null +++ b/src5/code/miniprogram/pages/admin-login/admin-login.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "管理员登录", + "navigationBarBackgroundColor": "#667eea", + "navigationBarTextStyle": "white" +} + diff --git a/src5/code/miniprogram/pages/admin-login/admin-login.wxml b/src5/code/miniprogram/pages/admin-login/admin-login.wxml new file mode 100644 index 0000000..a8ce683 --- /dev/null +++ b/src5/code/miniprogram/pages/admin-login/admin-login.wxml @@ -0,0 +1,52 @@ + + + + + diff --git a/src5/code/miniprogram/pages/admin-login/admin-login.wxss b/src5/code/miniprogram/pages/admin-login/admin-login.wxss new file mode 100644 index 0000000..a4853d5 --- /dev/null +++ b/src5/code/miniprogram/pages/admin-login/admin-login.wxss @@ -0,0 +1,97 @@ +/* pages/admin-login/admin-login.wxss */ +.page-container { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx; +} + +.login-container { + width: 100%; + max-width: 600rpx; +} + +.header { + text-align: center; + margin-bottom: 60rpx; +} + +.admin-icon { + font-size: 120rpx; + display: block; + margin-bottom: 30rpx; +} + +.title { + display: block; + font-size: 48rpx; + font-weight: 600; + color: white; + margin-bottom: 15rpx; +} + +.subtitle { + display: block; + font-size: 26rpx; + color: rgba(255, 255, 255, 0.8); +} + +.login-form { + background: white; + border-radius: 30rpx; + padding: 60rpx 40rpx; + box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3); +} + +.form-group { + margin-bottom: 40rpx; +} + +.label { + display: block; + font-size: 28rpx; + color: #333; + margin-bottom: 15rpx; + font-weight: 500; +} + +.input { + width: 100%; + height: 88rpx; + background: #f5f5f5; + border-radius: 15rpx; + padding: 0 25rpx; + font-size: 28rpx; + color: #333; + box-sizing: border-box; +} + +.login-btn { + width: 100%; + height: 88rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 15rpx; + font-size: 32rpx; + font-weight: 600; + margin-top: 40rpx; + border: none; +} + +.login-btn.disabled { + background: #ccc; +} + +.back-link { + text-align: center; + margin-top: 40rpx; +} + +.link { + color: #667eea; + font-size: 26rpx; + text-decoration: underline; +} + diff --git a/src5/code/miniprogram/pages/admin-products/admin-products.js b/src5/code/miniprogram/pages/admin-products/admin-products.js new file mode 100644 index 0000000..140581d --- /dev/null +++ b/src5/code/miniprogram/pages/admin-products/admin-products.js @@ -0,0 +1,220 @@ +// pages/admin-products/admin-products.js +Page({ + data: { + products: [], + keyword: '', + page: 0, + pageSize: 20, + hasMore: true, + loading: true, + refreshing: false + }, + + onLoad() { + // 检查管理员登录状态 + const adminInfo = wx.getStorageSync('adminInfo'); + if (!adminInfo) { + wx.redirectTo({ + url: '/pages/admin-login/admin-login' + }); + return; + } + + this.loadProducts(); + }, + + /** + * 加载商品列表 + */ + async loadProducts(refresh = false) { + if (refresh) { + this.setData({ + page: 0, + products: [], + hasMore: true + }); + } + + if (!this.data.hasMore && !refresh) { + return; + } + + this.setData({ + loading: true + }); + + try { + const db = wx.cloud.database(); + let queryCondition = {}; + + // 搜索条件 + if (this.data.keyword) { + queryCondition.productName = db.RegExp({ + regexp: this.data.keyword, + options: 'i' + }); + } + + const result = await db.collection('T_product') + .where(queryCondition) + .orderBy('createTime', 'desc') + .skip(this.data.page * this.data.pageSize) + .limit(this.data.pageSize) + .get(); + + const products = result.data.map(item => ({ + ...item, + timeText: this.formatTime(item.createTime) + })); + + this.setData({ + products: refresh ? products : [...this.data.products, ...products], + page: this.data.page + 1, + hasMore: result.data.length === this.data.pageSize, + loading: false, + refreshing: false + }); + } catch (err) { + console.error('加载商品列表失败:', err); + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + this.setData({ + loading: false, + refreshing: false + }); + } + }, + + /** + * 格式化时间 + */ + formatTime(date) { + if (!date) return ''; + const d = new Date(date); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + }, + + /** + * 搜索输入 + */ + onSearchInput(e) { + this.setData({ + keyword: e.detail.value + }); + }, + + /** + * 搜索 + */ + onSearch() { + this.loadProducts(true); + }, + + /** + * 下拉刷新 + */ + onRefresh() { + this.setData({ + refreshing: true + }); + this.loadProducts(true); + }, + + /** + * 加载更多 + */ + onLoadMore() { + if (!this.data.loading && this.data.hasMore) { + this.loadProducts(); + } + }, + + /** + * 编辑商品 + */ + onEditProduct(e) { + const productId = e.currentTarget.dataset.id; + wx.navigateTo({ + url: `/pages/admin-product-edit/admin-product-edit?id=${productId}` + }); + }, + + /** + * 切换商品状态(上架/下架) + */ + async onToggleStatus(e) { + const productId = e.currentTarget.dataset.id; + const currentStatus = e.currentTarget.dataset.status; + const newStatus = currentStatus === '在售' ? '已下架' : '在售'; + + wx.showModal({ + title: '提示', + content: `确定要${newStatus === '在售' ? '上架' : '下架'}该商品吗?`, + success: async (res) => { + if (res.confirm) { + wx.showLoading({ + title: '处理中...', + mask: true + }); + + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'adminUpdateProduct', + productId: productId, + data: { + status: newStatus + } + } + }); + + wx.hideLoading(); + + if (result.result && result.result.success) { + wx.showToast({ + title: '操作成功', + icon: 'success' + }); + this.loadProducts(true); + } else { + wx.showToast({ + title: result.result?.error || '操作失败', + icon: 'none' + }); + } + } catch (err) { + wx.hideLoading(); + console.error('操作失败:', err); + wx.showToast({ + title: '操作失败', + icon: 'none' + }); + } + } + } + }); + }, + + /** + * 添加商品 + */ + onAddProduct() { + wx.navigateTo({ + url: '/pages/admin-product-edit/admin-product-edit' + }); + }, + + /** + * 页面显示时刷新 + */ + onShow() { + const pages = getCurrentPages(); + const prevPage = pages[pages.length - 2]; + if (prevPage && prevPage.route === 'pages/admin-product-edit/admin-product-edit') { + this.loadProducts(true); + } + } +}); diff --git a/src5/code/miniprogram/pages/admin-products/admin-products.json b/src5/code/miniprogram/pages/admin-products/admin-products.json new file mode 100644 index 0000000..550734a --- /dev/null +++ b/src5/code/miniprogram/pages/admin-products/admin-products.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "商品管理", + "navigationBarBackgroundColor": "#667eea", + "navigationBarTextStyle": "white", + "enablePullDownRefresh": true +} diff --git a/src5/code/miniprogram/pages/admin-products/admin-products.wxml b/src5/code/miniprogram/pages/admin-products/admin-products.wxml new file mode 100644 index 0000000..4c5ae53 --- /dev/null +++ b/src5/code/miniprogram/pages/admin-products/admin-products.wxml @@ -0,0 +1,54 @@ + + + + + + 🔍 + + + + + + + + + + + + + {{item.productName}} + ¥{{item.salePrice || item.suggestedPrice || 0}} + + {{item.productCategory || '其他'}} + {{item.status || '未知'}} + + 发布时间: {{item.timeText}} + + + 编辑 + + {{item.status === '在售' ? '下架' : '上架'}} + + + + + + 加载中... + + + 没有更多了 + + + + 📦 + 暂无商品 + + + + + + + + 加载中... + + diff --git a/src5/code/miniprogram/pages/admin-products/admin-products.wxss b/src5/code/miniprogram/pages/admin-products/admin-products.wxss new file mode 100644 index 0000000..b23517c --- /dev/null +++ b/src5/code/miniprogram/pages/admin-products/admin-products.wxss @@ -0,0 +1,206 @@ +/* pages/admin-products/admin-products.wxss */ +.page-container { + height: 100vh; + display: flex; + flex-direction: column; + background-color: #f5f5f5; +} + +.search-header { + background: white; + padding: 20rpx 30rpx; + display: flex; + align-items: center; + gap: 20rpx; + border-bottom: 1rpx solid #e0e0e0; +} + +.search-bar { + flex: 1; + display: flex; + align-items: center; + background: #f5f5f5; + border-radius: 50rpx; + padding: 15rpx 25rpx; +} + +.search-icon { + font-size: 28rpx; + margin-right: 15rpx; +} + +.search-input { + flex: 1; + font-size: 28rpx; +} + +.add-btn { + width: 80rpx; + height: 80rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 40rpx; +} + +.product-list { + flex: 1; + padding: 20rpx; +} + +.product-item { + background: white; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 20rpx; + display: flex; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); +} + +.product-image { + width: 160rpx; + height: 160rpx; + border-radius: 15rpx; + margin-right: 20rpx; + flex-shrink: 0; +} + +.product-info { + flex: 1; + display: flex; + flex-direction: column; +} + +.product-name { + font-size: 30rpx; + font-weight: 600; + color: #333; + margin-bottom: 10rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.product-price { + font-size: 32rpx; + font-weight: 600; + color: #EA4335; + margin-bottom: 15rpx; +} + +.product-meta { + display: flex; + align-items: center; + gap: 20rpx; + margin-bottom: 10rpx; +} + +.product-category { + font-size: 24rpx; + color: #999; + background: #f5f5f5; + padding: 5rpx 15rpx; + border-radius: 10rpx; +} + +.product-status { + font-size: 24rpx; + padding: 5rpx 15rpx; + border-radius: 10rpx; +} + +.product-status.on-sale { + background: #E8F5E9; + color: #4CAF50; +} + +.product-status.off-sale { + background: #FFEBEE; + color: #F44336; +} + +.product-time { + font-size: 22rpx; + color: #999; +} + +.product-actions { + display: flex; + flex-direction: column; + gap: 15rpx; + margin-left: 20rpx; +} + +.action-btn { + padding: 12rpx 24rpx; + border-radius: 10rpx; + font-size: 24rpx; + text-align: center; + min-width: 100rpx; +} + +.action-btn.edit { + background: #E3F2FD; + color: #2196F3; +} + +.action-btn.online { + background: #E8F5E9; + color: #4CAF50; +} + +.action-btn.offline { + background: #FFEBEE; + color: #F44336; +} + +.load-more, +.no-more { + text-align: center; + padding: 30rpx 0; + color: #999; + font-size: 24rpx; +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 0; +} + +.empty-icon { + font-size: 120rpx; + margin-bottom: 30rpx; +} + +.empty-text { + font-size: 32rpx; + color: #666; +} + +.loading-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loading-content { + text-align: center; +} + +.loading-text { + font-size: 28rpx; + color: #666; +} diff --git a/src5/code/miniprogram/pages/admin-user-edit/admin-user-edit.js b/src5/code/miniprogram/pages/admin-user-edit/admin-user-edit.js new file mode 100644 index 0000000..a354c63 --- /dev/null +++ b/src5/code/miniprogram/pages/admin-user-edit/admin-user-edit.js @@ -0,0 +1,306 @@ +// pages/admin-user-edit/admin-user-edit.js +Page({ + data: { + userId: '', + userInfo: { + sno: '', + sname: '', + phone: '', + major: '', + grade: '', + sushe: '', + avatar: '' + }, + newPassword: '', + grades: [ + { label: '大一', value: '大一' }, + { label: '大二', value: '大二' }, + { label: '大三', value: '大三' }, + { label: '大四', value: '大四' }, + { label: '研究生', value: '研究生' }, + { label: '博士生', value: '博士生' } + ], + gradeIndex: 0, + loading: true, + submitting: false, + canSubmit: false + }, + + onLoad(options) { + // 检查管理员登录状态 + const adminInfo = wx.getStorageSync('adminInfo'); + if (!adminInfo) { + wx.redirectTo({ + url: '/pages/admin-login/admin-login' + }); + return; + } + + const userId = options.id; + if (userId) { + this.setData({ + userId: userId + }); + this.loadUserInfo(userId); + } else { + // 新建用户(可选功能) + this.setData({ + loading: false + }); + } + }, + + /** + * 加载用户信息 + */ + async loadUserInfo(userId) { + try { + const db = wx.cloud.database(); + const result = await db.collection('T_user').doc(userId).get(); + + if (result.data) { + const userInfo = result.data; + // 找到年级索引 + const gradeValue = userInfo.年级 || userInfo.grade || ''; + const gradeIndex = this.data.grades.findIndex(grade => grade.value === gradeValue); + + this.setData({ + userInfo: { + sno: userInfo.sno || '', + sname: userInfo.sname || '', + phone: userInfo.phone || '', + major: userInfo.major || '', + grade: gradeValue, + sushe: userInfo.sushe || '', + avatar: userInfo.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U' + }, + gradeIndex: gradeIndex >= 0 ? gradeIndex : 0, + loading: false + }); + + // 检查是否可以提交 + this.checkCanSubmit(); + } else { + throw new Error('用户不存在'); + } + } catch (err) { + console.error('加载用户信息失败:', err); + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + setTimeout(() => { + wx.navigateBack(); + }, 1500); + } + }, + + /** + * 检查是否可以提交 + */ + checkCanSubmit() { + const { userInfo } = this.data; + const canSubmit = !!(userInfo.sno && userInfo.sname && userInfo.phone); + this.setData({ + canSubmit: canSubmit + }); + }, + + /** + * 输入处理 + */ + onInputSno(e) { + this.setData({ + 'userInfo.sno': e.detail.value + }); + this.checkCanSubmit(); + }, + + onInputSname(e) { + this.setData({ + 'userInfo.sname': e.detail.value + }); + this.checkCanSubmit(); + }, + + onInputPhone(e) { + this.setData({ + 'userInfo.phone': e.detail.value + }); + this.checkCanSubmit(); + }, + + onInputMajor(e) { + this.setData({ + 'userInfo.major': e.detail.value + }); + }, + + onInputSushe(e) { + this.setData({ + 'userInfo.sushe': e.detail.value + }); + }, + + onInputPassword(e) { + this.setData({ + newPassword: e.detail.value + }); + }, + + /** + * 年级选择 + */ + onGradeChange(e) { + const index = parseInt(e.detail.value); + this.setData({ + gradeIndex: index, + 'userInfo.grade': this.data.grades[index].value + }); + }, + + /** + * 选择头像 + */ + onChooseAvatar() { + wx.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: async (res) => { + wx.showLoading({ + title: '上传中...', + mask: true + }); + + try { + const filePath = res.tempFilePaths[0]; + const cloudPath = `avatars/${Date.now()}-${Math.random().toString(36).substr(2, 9)}.jpg`; + + const uploadResult = await wx.cloud.uploadFile({ + cloudPath: cloudPath, + filePath: filePath + }); + + this.setData({ + 'userInfo.avatar': uploadResult.fileID + }); + + wx.hideLoading(); + } catch (err) { + console.error('上传头像失败:', err); + wx.hideLoading(); + wx.showToast({ + title: '上传失败', + icon: 'none' + }); + } + } + }); + }, + + /** + * 提交修改 + */ + async onSubmit() { + if (!this.data.canSubmit) { + wx.showToast({ + title: '请填写必填项', + icon: 'none' + }); + return; + } + + // 验证手机号格式 + const phoneRegex = /^1[3-9]\d{9}$/; + if (!phoneRegex.test(this.data.userInfo.phone)) { + wx.showToast({ + title: '请输入正确的手机号', + icon: 'none' + }); + return; + } + + // 如果设置了新密码,验证密码长度 + if (this.data.newPassword && this.data.newPassword.length < 6) { + wx.showToast({ + title: '密码长度不能少于6位', + icon: 'none' + }); + return; + } + + this.setData({ + submitting: true + }); + + try { + // 构建更新数据 + const updateData = { + sno: this.data.userInfo.sno.trim(), + sname: this.data.userInfo.sname.trim(), + phone: this.data.userInfo.phone.trim(), + major: this.data.userInfo.major.trim() || '', + 年级: this.data.userInfo.grade || '', // 保存到数据库时使用中文字段名 + sushe: this.data.userInfo.sushe.trim() || '', + avatar: this.data.userInfo.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U', + updateTime: new Date() + }; + + // 如果设置了新密码,添加到更新数据中 + if (this.data.newPassword) { + updateData.password = this.data.newPassword.trim(); + } + + // 调用云函数更新用户信息 + wx.showLoading({ + title: '保存中...', + mask: true + }); + + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'adminUpdateUser', + userId: this.data.userId, + data: updateData + } + }); + + wx.hideLoading(); + + console.log('更新用户信息结果:', result); + + if (result.result && result.result.success) { + wx.showToast({ + title: '保存成功', + icon: 'success', + duration: 2000 + }); + + setTimeout(() => { + wx.navigateBack(); + }, 2000); + } else { + const errorMsg = result.result?.error || result.errMsg || '保存失败'; + console.error('保存失败:', errorMsg); + wx.showToast({ + title: errorMsg, + icon: 'none', + duration: 3000 + }); + } + } catch (err) { + console.error('保存用户信息失败:', err); + wx.showToast({ + title: err.message || '保存失败', + icon: 'none' + }); + } finally { + this.setData({ + submitting: false + }); + } + } +}); + diff --git a/src5/code/miniprogram/pages/admin-user-edit/admin-user-edit.json b/src5/code/miniprogram/pages/admin-user-edit/admin-user-edit.json new file mode 100644 index 0000000..747f061 --- /dev/null +++ b/src5/code/miniprogram/pages/admin-user-edit/admin-user-edit.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "编辑用户", + "navigationBarBackgroundColor": "#667eea", + "navigationBarTextStyle": "white" +} + diff --git a/src5/code/miniprogram/pages/admin-user-edit/admin-user-edit.wxml b/src5/code/miniprogram/pages/admin-user-edit/admin-user-edit.wxml new file mode 100644 index 0000000..dbaad51 --- /dev/null +++ b/src5/code/miniprogram/pages/admin-user-edit/admin-user-edit.wxml @@ -0,0 +1,123 @@ + + + + + + + + + + + 更换头像 + + + + + + + 基本信息 + + + 学号 + + + + + 姓名 + + + + + 手机号 + + + + + 专业 + + + + + 年级 + + + {{userInfo.grade || '请选择年级'}} + + + + + + + 宿舍 + + + + + + + 密码设置(可选) + + 新密码 + + + + + + + + + + + + + + + + 加载中... + + + diff --git a/src5/code/miniprogram/pages/admin-user-edit/admin-user-edit.wxss b/src5/code/miniprogram/pages/admin-user-edit/admin-user-edit.wxss new file mode 100644 index 0000000..bb374fb --- /dev/null +++ b/src5/code/miniprogram/pages/admin-user-edit/admin-user-edit.wxss @@ -0,0 +1,175 @@ +/* pages/admin-user-edit/admin-user-edit.wxss */ +.page-container { + height: 100vh; + display: flex; + flex-direction: column; + background-color: #f5f5f5; +} + +.scroll-container { + flex: 1; +} + +.form-container { + padding: 30rpx; + padding-bottom: 100rpx; +} + +.form-section { + background: white; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 30rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 30rpx; +} + +.section-label { + font-size: 28rpx; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 20rpx; +} + +/* 头像部分 */ +.avatar-section { + display: flex; + flex-direction: column; + align-items: center; +} + +.avatar { + width: 200rpx; + height: 200rpx; + border-radius: 50%; + margin-bottom: 30rpx; + border: 4rpx solid #f0f0f0; +} + +.avatar-upload { + padding: 15rpx 40rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 50rpx; + font-size: 26rpx; +} + +/* 表单组 */ +.form-group { + margin-bottom: 30rpx; +} + +.form-group:last-child { + margin-bottom: 0; +} + +.label { + display: block; + font-size: 28rpx; + color: #333; + margin-bottom: 15rpx; + font-weight: 500; +} + +.input { + width: 100%; + height: 88rpx; + background: #f5f5f5; + border-radius: 15rpx; + padding: 0 25rpx; + font-size: 28rpx; + color: #333; + box-sizing: border-box; +} + +.input[disabled] { + background: #e0e0e0; + color: #999; +} + +/* 选择器 */ +.picker-input { + width: 100%; + height: 88rpx; + background: #f5f5f5; + border-radius: 15rpx; + padding: 0 25rpx; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 28rpx; + color: #333; +} + +.picker-input .placeholder { + color: #999; +} + +.picker-arrow { + font-size: 32rpx; + color: #999; +} + +/* 提交按钮 */ +.submit-section { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: white; + padding: 20rpx 30rpx; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); + border-top: 1rpx solid #e0e0e0; + box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05); +} + +.submit-btn { + width: 100%; + height: 88rpx; + background: #ccc; + color: white; + border-radius: 50rpx; + font-size: 32rpx; + font-weight: 600; + border: none; + transition: all 0.3s ease; +} + +.submit-btn.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.submit-btn[disabled] { + opacity: 0.6; +} + +/* 加载状态 */ +.loading-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loading-content { + text-align: center; +} + +.loading-text { + font-size: 28rpx; + color: #666; +} + diff --git a/src5/code/miniprogram/pages/admin-users/admin-users.js b/src5/code/miniprogram/pages/admin-users/admin-users.js new file mode 100644 index 0000000..3ae15c4 --- /dev/null +++ b/src5/code/miniprogram/pages/admin-users/admin-users.js @@ -0,0 +1,150 @@ +// pages/admin-users/admin-users.js +Page({ + data: { + users: [], + keyword: '', + page: 0, + pageSize: 20, + hasMore: true, + loading: true, + refreshing: false + }, + + onLoad() { + // 检查管理员登录状态 + const adminInfo = wx.getStorageSync('adminInfo'); + if (!adminInfo) { + wx.redirectTo({ + url: '/pages/admin-login/admin-login' + }); + return; + } + + this.loadUsers(); + }, + + /** + * 加载用户列表 + */ + async loadUsers(refresh = false) { + if (refresh) { + this.setData({ + page: 0, + users: [], + hasMore: true + }); + } + + if (!this.data.hasMore && !refresh) { + return; + } + + this.setData({ + loading: true + }); + + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'adminGetUsers', + page: this.data.page, + pageSize: this.data.pageSize, + keyword: this.data.keyword + } + }); + + if (result.result && result.result.success) { + const users = result.result.data.users.map(item => ({ + ...item, + timeText: this.formatTime(item.createTime) + })); + + this.setData({ + users: refresh ? users : [...this.data.users, ...users], + page: this.data.page + 1, + hasMore: users.length === this.data.pageSize, + loading: false, + refreshing: false + }); + } else { + throw new Error(result.result?.error || '获取用户列表失败'); + } + } catch (err) { + console.error('加载用户列表失败:', err); + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + this.setData({ + loading: false, + refreshing: false + }); + } + }, + + /** + * 格式化时间 + */ + formatTime(date) { + if (!date) return ''; + const d = new Date(date); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + }, + + /** + * 搜索输入 + */ + onSearchInput(e) { + this.setData({ + keyword: e.detail.value + }); + }, + + /** + * 搜索 + */ + onSearch() { + this.loadUsers(true); + }, + + /** + * 下拉刷新 + */ + onRefresh() { + this.setData({ + refreshing: true + }); + this.loadUsers(true); + }, + + /** + * 加载更多 + */ + onLoadMore() { + if (!this.data.loading && this.data.hasMore) { + this.loadUsers(); + } + }, + + /** + * 编辑用户 + */ + onEditUser(e) { + const userId = e.currentTarget.dataset.id; + wx.navigateTo({ + url: `/pages/admin-user-edit/admin-user-edit?id=${userId}` + }); + }, + + /** + * 页面显示时刷新 + */ + onShow() { + const pages = getCurrentPages(); + const prevPage = pages[pages.length - 2]; + if (prevPage && (prevPage.route === 'pages/admin-user-edit/admin-user-edit')) { + this.loadUsers(true); + } + } +}); diff --git a/src5/code/miniprogram/pages/admin-users/admin-users.json b/src5/code/miniprogram/pages/admin-users/admin-users.json new file mode 100644 index 0000000..8709cf0 --- /dev/null +++ b/src5/code/miniprogram/pages/admin-users/admin-users.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "用户管理", + "navigationBarBackgroundColor": "#667eea", + "navigationBarTextStyle": "white", + "enablePullDownRefresh": true +} diff --git a/src5/code/miniprogram/pages/admin-users/admin-users.wxml b/src5/code/miniprogram/pages/admin-users/admin-users.wxml new file mode 100644 index 0000000..84f88cb --- /dev/null +++ b/src5/code/miniprogram/pages/admin-users/admin-users.wxml @@ -0,0 +1,46 @@ + + + + + + 🔍 + + + + + + + + + + + + + + 加载中... + + + 没有更多了 + + + + 👥 + 暂无用户 + + + + + + + + 加载中... + + diff --git a/src5/code/miniprogram/pages/admin-users/admin-users.wxss b/src5/code/miniprogram/pages/admin-users/admin-users.wxss new file mode 100644 index 0000000..4543640 --- /dev/null +++ b/src5/code/miniprogram/pages/admin-users/admin-users.wxss @@ -0,0 +1,145 @@ +/* pages/admin-users/admin-users.wxss */ +.page-container { + height: 100vh; + display: flex; + flex-direction: column; + background-color: #f5f5f5; +} + +.search-header { + background: white; + padding: 20rpx 30rpx; + border-bottom: 1rpx solid #e0e0e0; +} + +.search-bar { + display: flex; + align-items: center; + background: #f5f5f5; + border-radius: 50rpx; + padding: 15rpx 25rpx; +} + +.search-icon { + font-size: 28rpx; + margin-right: 15rpx; +} + +.search-input { + flex: 1; + font-size: 28rpx; +} + +.user-list { + flex: 1; + padding: 20rpx; +} + +.user-item { + background: white; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 20rpx; + display: flex; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); +} + +.user-avatar { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + margin-right: 20rpx; + flex-shrink: 0; +} + +.user-info { + flex: 1; + display: flex; + flex-direction: column; +} + +.user-name { + font-size: 32rpx; + font-weight: 600; + color: #333; + margin-bottom: 10rpx; +} + +.user-sno, +.user-phone, +.user-major { + font-size: 26rpx; + color: #666; + margin-bottom: 8rpx; +} + +.user-time { + font-size: 22rpx; + color: #999; + margin-top: 10rpx; +} + +.user-actions { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 20rpx; +} + +.action-btn { + padding: 15rpx 30rpx; + background: #E3F2FD; + color: #2196F3; + border-radius: 10rpx; + font-size: 26rpx; + text-align: center; + min-width: 100rpx; +} + +.load-more, +.no-more { + text-align: center; + padding: 30rpx 0; + color: #999; + font-size: 24rpx; +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 0; +} + +.empty-icon { + font-size: 120rpx; + margin-bottom: 30rpx; +} + +.empty-text { + font-size: 32rpx; + color: #666; +} + +.loading-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loading-content { + text-align: center; +} + +.loading-text { + font-size: 28rpx; + color: #666; +} diff --git a/src5/code/miniprogram/pages/buy/buy.js b/src5/code/miniprogram/pages/buy/buy.js new file mode 100644 index 0000000..9c4029c --- /dev/null +++ b/src5/code/miniprogram/pages/buy/buy.js @@ -0,0 +1,610 @@ +// pages/buy/buy.js +const reco = require('../../utils/recommendation.js'); +Page({ + /** + * 页面的初始数据 + */ + data: { + // 搜索相关 + searchKeyword: '', + + // 筛选条件 + categories: ['全部分类', '电子产品', '图书文具', '服装鞋帽', '运动户外', '美妆个护', '家居生活', '其他'], + selectedCategory: 0, + categoryNavItems: [ + { id: 0, name: '全部' }, + { id: 1, name: '电子产品' }, + { id: 2, name: '图书文具' }, + { id: 3, name: '服装鞋帽' }, + { id: 4, name: '运动户外' }, + { id: 5, name: '美妆个护' }, + { id: 6, name: '家居生活' }, + { id: 7, name: '其他' } + ], + selectedCategoryIndex: 0, + + priceRanges: ['价格范围', '0-50元', '50-100元', '100-200元', '200-500元', '500元以上'], + selectedPriceRange: 0, + + sortOptions: ['最新发布', '价格从低到高', '价格从高到低', '评分最高'], + selectedSort: 0, + + // 商品列表 + products: [], + filteredProducts: [], + + // 分页相关 + currentPage: 1, + pageSize: 10, + hasMore: true, + + // 页面状态 + refreshing: false, + loading: false + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + if (options && options.q) { + try { + const keyword = decodeURIComponent(options.q); + this.setData({ + searchKeyword: keyword + }); + } catch (e) { + // 保底:直接使用传入值 + this.setData({ + searchKeyword: options.q + }); + } + } + this.loadProducts(); + }, + + /** + * 生命周期函数--监听页面初次渲染完成 + */ + onReady() { + + }, + + /** + * 生命周期函数--监听页面显示 + */ + onShow() { + // 页面显示时刷新数据,确保显示最新发布的商品 + this.loadProducts(true); + }, + + /** + * 页面相关事件处理函数--监听用户下拉动作 + */ + onPullDownRefresh() { + this.onRefresh(); + }, + + /** + * 页面上拉触底事件的处理函数 + */ + onReachBottom() { + this.loadMore(); + }, + + /** + * 用户点击右上角分享 + */ + onShareAppMessage() { + return { + title: '校园二手交易 - 发现好物', + path: '/pages/buy/buy' + } + }, + + /** + * 搜索输入事件 + */ + onSearchInput(e) { + this.setData({ + searchKeyword: e.detail.value + }); + // 实时搜索,不需要点击搜索按钮 + this.filterProducts(); + }, + + /** + * 搜索按钮点击事件 + */ + onSearch() { + // 若存在关键词,则刷新并按关键词从后端查询;否则仅本地筛选 + const kw = (this.data.searchKeyword || '').trim(); + if (kw) { + try { + const reco = require('../../utils/recommendation.js'); + reco.recordSearch(kw, 'product'); + } catch (e) {} + this.loadProducts(true); + } else { + this.filterProducts(); + } + }, + + /** + * 清空搜索 + */ + onClearSearch() { + this.setData({ + searchKeyword: '' + }); + this.filterProducts(); + }, + + /** + * 分类导航点击事件 + */ + onCategoryNavTap(e) { + const index = parseInt(e.currentTarget.dataset.index); + this.setData({ + selectedCategoryIndex: index, + selectedCategory: index + }); + this.filterProducts(); + }, + + /** + * 分类选择事件 + */ + onCategoryChange(e) { + const index = e.detail.value; + this.setData({ + selectedCategory: index, + selectedCategoryIndex: index + }); + this.filterProducts(); + }, + + /** + * 价格范围选择事件 + */ + onPriceRangeChange(e) { + this.setData({ + selectedPriceRange: e.detail.value + }); + this.filterProducts(); + }, + + /** + * 排序方式选择事件 + */ + onSortChange() { + wx.showActionSheet({ + itemList: this.data.sortOptions, + success: (res) => { + this.setData({ + selectedSort: res.tapIndex + }); + this.sortProducts(); + } + }); + }, + + /** + * 商品点击事件 + */ + onProductTap(e) { + const product = e.currentTarget.dataset.product; + try { reco.recordClick(product); } catch (e) {} + wx.navigateTo({ + url: `/pages/product-detail/product-detail?id=${product.id}` + }); + }, + + /** + * 下拉刷新 + */ + onRefresh() { + this.setData({ + refreshing: true, + currentPage: 1, + hasMore: true + }); + + // 重新加载数据 + this.loadProducts(true); + this.setData({ + refreshing: false + }); + wx.stopPullDownRefresh(); + }, + + /** + * 加载更多 + */ + loadMore() { + if (this.data.loading || !this.data.hasMore) { + return; + } + + // 进入加载状态,并推进到下一页,改为真实数据库分页查询 + const nextPage = this.data.currentPage + 1; + this.setData({ + loading: true, + currentPage: nextPage + }); + + // 加载下一页商品 + this.loadProducts(false); + }, + + /** + * 加载商品数据 + */ + loadProducts(refresh = false) { + const db = wx.cloud.database(); + const { currentPage, pageSize, selectedCategory, categories, searchKeyword } = this.data; + const _ = db.command; + + // 如果是刷新,重置页码 + const page = refresh ? 1 : currentPage; + + this.setData({ + loading: true + }); + + // 构建查询条件 + let query = db.collection('T_product').where({ + status: '在售' // 只显示在售商品 + }); + + // 分类筛选 + if (selectedCategory > 0 && categories[selectedCategory]) { + const category = categories[selectedCategory]; + query = query.where({ + productCategory: category + }); + } + + // 关键词搜索(商品名称或描述匹配,忽略大小写) + const kw = (searchKeyword || '').trim(); + if (kw) { + const re = db.RegExp({ regexp: kw, options: 'i' }); + query = query.where( + _.or([ + { productName: re }, + { productDescription: re } + ]) + ); + } + + // 分页查询 + query = query + .orderBy('createTime', 'desc') // 按创建时间倒序 + .skip((page - 1) * pageSize) + .limit(pageSize); + + query.get({ + success: (res) => { + console.log('查询商品成功:', res); + + if (res.data && res.data.length > 0) { + // 处理商品数据 + this.processProducts(res.data).then((processedProducts) => { + // 合并分页结果并按 id 去重,避免 wx:key 重复 + const merged = refresh ? processedProducts : [...this.data.products, ...processedProducts]; + const seen = new Set(); + const products = []; + for (const p of merged) { + const key = p.id || p._id || p._originalData?._id; + if (!key) continue; + if (seen.has(key)) continue; + seen.add(key); + products.push(p); + } + + this.setData({ + products: products, + filteredProducts: products, + currentPage: page, + hasMore: res.data.length === pageSize, + loading: false + }); + + // 应用筛选和排序 + this.filterProducts(); + }); + } else { + // 没有数据 + this.setData({ + hasMore: false, + loading: false + }); + if (refresh) { + this.setData({ + products: [], + filteredProducts: [] + }); + } + } + }, + fail: (err) => { + console.error('查询商品失败:', err); + wx.showToast({ + title: '加载失败,请重试', + icon: 'none', + duration: 2000 + }); + this.setData({ + loading: false + }); + } + }); + }, + + /** + * 筛选商品 + */ + filterProducts() { + let filtered = this.data.products; + + // 关键词搜索 + if (this.data.searchKeyword) { + const keyword = this.data.searchKeyword.toLowerCase(); + filtered = filtered.filter(product => + product.name.toLowerCase().includes(keyword) || + product.description.toLowerCase().includes(keyword) || + product.category.includes(keyword) + ); + } + + // 分类筛选 + if (this.data.selectedCategory > 0) { + const category = this.data.categories[this.data.selectedCategory]; + filtered = filtered.filter(product => product.category === category); + } + + // 价格范围筛选 + if (this.data.selectedPriceRange > 0) { + const priceRange = this.data.priceRanges[this.data.selectedPriceRange]; + filtered = filtered.filter(product => { + const price = parseFloat(product.price); + switch (priceRange) { + case '0-50元': return price <= 50; + case '50-100元': return price > 50 && price <= 100; + case '100-200元': return price > 100 && price <= 200; + case '200-500元': return price > 200 && price <= 500; + case '500元以上': return price > 500; + default: return true; + } + }); + } + + this.setData({ + filteredProducts: filtered + }); + + this.sortProducts(); + }, + + /** + * 处理商品数据,转换格式并获取图片URL + */ + async processProducts(products) { + const processedProducts = await Promise.all(products.map(async (product) => { + // 获取图片临时URL + let imageUrl = '/images/仓鼠.png'; // 默认本地图片,避免外网占位图超时 + if (product.productImage) { + try { + // 如果是云存储路径,获取临时URL + if (product.productImage.startsWith('cloud://')) { + const tempFileURL = await wx.cloud.getTempFileURL({ + fileList: [product.productImage] + }); + if (tempFileURL.fileList && tempFileURL.fileList.length > 0) { + imageUrl = tempFileURL.fileList[0].tempFileURL; + } + } else { + // 如果是网络URL,直接使用 + imageUrl = product.productImage; + } + } catch (err) { + console.error('获取图片URL失败:', err); + } + } + + // 计算折扣 + let discount = null; + if (product.originalPrice > 0 && product.salePrice > 0) { + discount = (product.salePrice / product.originalPrice * 10).toFixed(1); + } + + // 格式化时间 + const publishTime = this.formatTime(product.createTime); + + // 获取卖家信息 + const sellerName = product.sellerInfo?.sname || '用户'; + const sellerAvatar = product.sellerInfo?.avatar || '/images/更多犬种.png'; + + // 返回格式化后的商品数据 + return { + id: product._id, + name: product.productName || '未命名商品', + description: product.productDescription || '', + price: product.salePrice || product.suggestedPrice || 0, + originalPrice: product.originalPrice || null, + discount: discount, + category: product.productCategory || '其他', + condition: product.conditionLevel || '未知', + location: '校园', // 可以根据需要添加location字段 + publishTime: publishTime, + image: imageUrl, + sellerAvatar: sellerAvatar, + sellerName: sellerName, + sellerRating: '4.8', // 可以根据需要添加评分系统 + status: product.status === '在售' ? 'selling' : 'sold', + // 保留原始数据字段,方便后续使用 + _originalData: product + }; + })); + + return processedProducts; + }, + + /** + * 格式化时间显示 + */ + formatTime(date) { + if (!date) return ''; + + const now = new Date(); + const createTime = date instanceof Date ? date : new Date(date); + const diff = now - createTime; + + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (seconds < 60) { + return '刚刚'; + } else if (minutes < 60) { + return `${minutes}分钟前`; + } else if (hours < 24) { + return `${hours}小时前`; + } else if (days < 7) { + return `${days}天前`; + } else { + // 超过7天,显示具体日期 + const month = createTime.getMonth() + 1; + const day = createTime.getDate(); + return `${month}月${day}日`; + } + }, + + /** + * 排序商品 + */ + sortProducts() { + let sorted = [...this.data.filteredProducts]; + + switch (this.data.selectedSort) { + case 0: // 最新发布 + sorted.sort((a, b) => { + const timeA = a._originalData?.createTime || new Date(0); + const timeB = b._originalData?.createTime || new Date(0); + return new Date(timeB) - new Date(timeA); + }); + break; + case 1: // 价格从低到高 + sorted.sort((a, b) => parseFloat(a.price || 0) - parseFloat(b.price || 0)); + break; + case 2: // 价格从高到低 + sorted.sort((a, b) => parseFloat(b.price || 0) - parseFloat(a.price || 0)); + break; + case 3: // 评分最高(暂时使用固定评分) + sorted.sort((a, b) => parseFloat(b.sellerRating || 0) - parseFloat(a.sellerRating || 0)); + break; + } + + this.setData({ + filteredProducts: sorted + }); + }, + + /** + * 生成模拟商品数据 + */ + generateMockProducts(page, pageSize) { + if (page > 3) return []; // 模拟只有3页数据 + + const mockProducts = [ + { + id: 1, + name: 'iPhone 13 Pro Max', + description: '99新,256GB,国行在保,无划痕无磕碰', + price: '5999', + originalPrice: '8999', + discount: '6.7', + category: '电子产品', + condition: '99新', + location: '闵行校区', + publishTime: '2小时前', + image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=400&q=60', + sellerAvatar: 'https://via.placeholder.com/100x100/CCCCCC/444444?text=U', + sellerName: '张同学', + sellerRating: '4.8', + status: 'selling' + }, + { + id: 2, + name: '高等数学教材', + description: '同济大学第七版,几乎全新,有少量笔记', + price: '25', + category: '图书文具', + condition: '95新', + location: '徐汇校区', + publishTime: '1天前', + image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=400&q=60', + sellerAvatar: 'https://via.placeholder.com/100x100/CCCCCC/444444?text=U', + sellerName: '李同学', + sellerRating: '4.9', + status: 'selling' + }, + { + id: 3, + name: 'Nike Air Force 1', + description: '白色经典款,42码,穿过几次,保养很好', + price: '299', + originalPrice: '799', + discount: '3.7', + category: '运动户外', + condition: '85新', + location: '松江校区', + publishTime: '3天前', + image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=400&q=60', + sellerAvatar: 'https://via.placeholder.com/100x100/CCCCCC/444444?text=U', + sellerName: '王同学', + sellerRating: '4.7', + status: 'selling' + }, + { + id: 4, + name: 'MacBook Pro 2021', + description: 'M1芯片,16GB内存,512GB硬盘,性能强劲', + price: '8999', + originalPrice: '12999', + discount: '6.9', + category: '电子产品', + condition: '98新', + location: '闵行校区', + publishTime: '5小时前', + image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=400&q=60', + sellerAvatar: 'https://via.placeholder.com/100x100/CCCCCC/444444?text=U', + sellerName: '赵同学', + sellerRating: '4.9', + status: 'selling' + }, + { + id: 5, + name: 'SK-II神仙水', + description: '230ml,正品保证,还剩大半瓶,个人原因转让', + price: '499', + originalPrice: '1540', + discount: '3.2', + category: '美妆个护', + condition: '90新', + location: '徐汇校区', + publishTime: '1天前', + image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=400&q=60', + sellerAvatar: 'https://via.placeholder.com/100x100/CCCCCC/444444?text=U', + sellerName: '陈同学', + sellerRating: '4.8', + status: 'selling' + } + ]; + + // 根据页码返回数据 + const startIndex = (page - 1) * pageSize; + return mockProducts.slice(startIndex, startIndex + pageSize); + } +}) \ No newline at end of file diff --git a/src5/code/miniprogram/pages/buy/buy.json b/src5/code/miniprogram/pages/buy/buy.json new file mode 100644 index 0000000..3744ae5 --- /dev/null +++ b/src5/code/miniprogram/pages/buy/buy.json @@ -0,0 +1,8 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "商品购买", + "navigationBarBackgroundColor": "#4285F4", + "navigationBarTextStyle": "black", + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/buy/buy.wxml b/src5/code/miniprogram/pages/buy/buy.wxml new file mode 100644 index 0000000..d8ea720 --- /dev/null +++ b/src5/code/miniprogram/pages/buy/buy.wxml @@ -0,0 +1,155 @@ + + + + + + + + + 取消 + + + + + + + + + + {{categories[selectedCategory] || '全部分类'}} + + + + + + + {{priceRanges[selectedPriceRange] || '价格范围'}} + + + + + + + {{sortOptions[selectedSort]}} + + + + + + + + + + + + + + + + 已售出 + + + + + + + + + {{item.sellerName}} + + {{item.sellerRating}} + + + + + + + {{item.name}} + + + + + {{item.description}} + + + + + ¥{{item.price}} + ¥{{item.originalPrice}} + + {{item.discount}}折 + + + + + + {{item.category}} + {{item.condition}} + {{item.location}} + + + + + {{item.publishTime}} + + + + + + + + 加载中... + + + + + 没有更多商品了 + + + + + 📦 + 暂无商品 + 换个筛选条件试试 + + + \ No newline at end of file diff --git a/src5/code/miniprogram/pages/buy/buy.wxss b/src5/code/miniprogram/pages/buy/buy.wxss new file mode 100644 index 0000000..5d89373 --- /dev/null +++ b/src5/code/miniprogram/pages/buy/buy.wxss @@ -0,0 +1,348 @@ +/* 商品购买页面样式 */ +.container { + height: 100vh; + background-color: #f5f5f5; + padding-top: 0; +} + +/* 搜索栏样式 */ +.search-bar { + padding: 0; + background-color: #fff; + border-bottom: 1rpx solid #e0e0e0; +} + +.search-input { + display: flex; + align-items: center; + background-color: #f8f9fa; + border-radius: 0; + padding: 20rpx; + height: 100rpx; +} + +.search-icon { + margin-right: 20rpx; + color: #999; +} + +.search-field { + flex: 1; + font-size: 32rpx; + color: #333; + height: 60rpx; + line-height: 60rpx; +} + +.search-cancel { + padding: 10rpx 20rpx; + background-color: #e9ecef; + border-radius: 30rpx; + font-size: 28rpx; + color: #666; + margin-right: 20rpx; + height: 60rpx; + line-height: 40rpx; +} + +.search-btn { + background-color: #007bff; + color: #fff; + border: none; + border-radius: 30rpx; + padding: 10rpx 30rpx; + font-size: 28rpx; + line-height: 1; + margin-left: 20rpx; + height: 60rpx; + min-width: 120rpx; +} + +/* 分类导航 */ +.category-nav { + display: flex; + white-space: nowrap; + padding: 24rpx 30rpx; + background-color: #fff; + border-bottom: 1rpx solid #f0f0f0; +} + +.category-item { + padding: 12rpx 32rpx; + margin-right: 20rpx; + border-radius: 40rpx; + background-color: #f8f9fa; + border: 1rpx solid #e9ecef; + font-size: 28rpx; + color: #6c757d; + transition: all 0.3s ease; + flex-shrink: 0; +} + +.category-item.active { + background-color: #007bff; + border-color: #007bff; + color: #fff; +} + +.category-text { + font-size: 28rpx; + font-weight: 500; +} + +/* 筛选栏样式 */ +.filter-bar { + display: flex; + background-color: #fff; + border-bottom: 1rpx solid #e0e0e0; + padding: 20rpx; +} + +.filter-item { + flex: 1; + text-align: center; + font-size: 26rpx; + color: #666; +} + +.filter-display { + display: flex; + align-items: center; + justify-content: center; +} + +/* 商品列表样式 */ +.product-list { + height: calc(100vh - 180rpx); +} + +/* 商品网格容器 */ +.product-grid { + display: flex; + flex-wrap: wrap; + padding: 10rpx; + gap: 10rpx; + box-sizing: border-box; +} + +/* 商品卡片样式 */ +.product-card { + width: calc(50% - 8rpx); + background-color: #fff; + border-radius: 8rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); + overflow: hidden; + margin-bottom: 10rpx; + box-sizing: border-box; + flex-shrink: 0; +} + +.product-image-container { + position: relative; + height: 200rpx; +} + +.product-image { + width: 100%; + height: 100%; +} + +.product-status { + position: absolute; + top: 10rpx; + right: 10rpx; + background-color: rgba(0, 0, 0, 0.7); + color: #fff; + padding: 4rpx 8rpx; + border-radius: 10rpx; + font-size: 18rpx; +} + +/* 商品信息样式 */ +.product-info { + padding: 15rpx; +} + +/* 卖家信息样式 */ +.seller-info { + display: flex; + align-items: center; + margin-bottom: 10rpx; +} + +.seller-avatar { + width: 40rpx; + height: 40rpx; + border-radius: 50%; + margin-right: 8rpx; +} + +.seller-name { + font-size: 22rpx; + color: #666; + margin-right: 8rpx; +} + +.seller-rating { + display: flex; + align-items: center; + background-color: #fff8e1; + padding: 2rpx 6rpx; + border-radius: 8rpx; +} + +.rating-text { + font-size: 18rpx; + color: #ff9800; + margin-right: 3rpx; +} + +.star-icon { + color: #ff9800; + font-size: 16rpx; +} + +/* 商品标题样式 */ +.product-title { + margin-bottom: 8rpx; +} + +.title-text { + font-size: 26rpx; + font-weight: bold; + color: #333; + line-height: 1.3; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + overflow: hidden; +} + +/* 商品描述样式 */ +.product-description { + margin-bottom: 10rpx; +} + +.description-text { + font-size: 22rpx; + color: #666; + line-height: 1.4; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + overflow: hidden; +} + +/* 价格信息样式 */ +.price-info { + display: flex; + align-items: center; + margin-bottom: 8rpx; +} + +.current-price { + font-size: 28rpx; + font-weight: bold; + color: #ff4444; + margin-right: 8rpx; +} + +.original-price { + font-size: 20rpx; + color: #999; + text-decoration: line-through; + margin-right: 8rpx; +} + +.discount-badge { + background-color: #ff4444; + color: #fff; + padding: 2rpx 6rpx; + border-radius: 6rpx; + font-size: 18rpx; +} + +/* 商品标签样式 */ +.product-tags { + display: flex; + flex-wrap: wrap; + margin-bottom: 8rpx; +} + +.tag { + font-size: 18rpx; + padding: 2rpx 6rpx; + border-radius: 6rpx; + margin-right: 6rpx; + margin-bottom: 4rpx; +} + +.category-tag { + background-color: #e3f2fd; + color: #1976d2; +} + +.condition-tag { + background-color: #f3e5f5; + color: #7b1fa2; +} + +.location-tag { + background-color: #e8f5e8; + color: #388e3c; +} + +/* 发布时间样式 */ +.publish-time { + font-size: 18rpx; + color: #999; +} + +/* 加载更多样式 */ +.load-more, .no-more { + text-align: center; + padding: 40rpx; + font-size: 26rpx; + color: #999; +} + +/* 空状态样式 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 40rpx; +} + +.empty-image { + width: 200rpx; + height: 200rpx; + margin-bottom: 30rpx; +} + +.empty-text { + font-size: 32rpx; + color: #666; + margin-bottom: 15rpx; +} + +.empty-subtext { + font-size: 26rpx; + color: #999; +} + +/* 响应式调整 */ +@media (max-width: 750rpx) { + .product-card { + margin: 15rpx; + } + + .product-info { + padding: 20rpx; + } + + .product-image-container { + height: 250rpx; + } +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/cart/cart.js b/src5/code/miniprogram/pages/cart/cart.js new file mode 100644 index 0000000..468759b --- /dev/null +++ b/src5/code/miniprogram/pages/cart/cart.js @@ -0,0 +1,279 @@ +// pages/cart/cart.js +const reco = require('../../utils/recommendation.js'); +Page({ + data: { + items: [], + totalPrice: '0.00' + }, + + onShow() { + this.loadCart(); + try { + if (this.getTabBar && this.getTabBar()) { + this.getTabBar().setSelected(2); + } + } catch (e) {} + }, + + loadCart() { + try { + const cart = wx.getStorageSync('cart') || []; + const items = (cart || []).map(it => ({ + id: it.id, + name: it.name || '商品', + price: Number(it.price || 0), + image: it.image || '/images/仓鼠.png', + category: it.category || '其他', + sellerName: it.sellerName || '用户', + count: Number(it.count || 1) + })); + this.setData({ items }); + this.updateTotal(); + } catch (e) { + console.error('加载购物车失败:', e); + } + }, + + updateTotal() { + const sum = this.data.items.reduce((s, it) => s + (Number(it.price) || 0) * (Number(it.count) || 1), 0); + this.setData({ totalPrice: sum.toFixed(2) }); + }, + + incQty(e) { + const idx = e.currentTarget.dataset.index; + const items = [...this.data.items]; + items[idx].count = Math.min(99, (items[idx].count || 1) + 1); + this.setData({ items }); + wx.setStorageSync('cart', items); + this.updateTotal(); + }, + + decQty(e) { + const idx = e.currentTarget.dataset.index; + const items = [...this.data.items]; + items[idx].count = Math.max(1, (items[idx].count || 1) - 1); + this.setData({ items }); + wx.setStorageSync('cart', items); + this.updateTotal(); + }, + + removeItem(e) { + const idx = e.currentTarget.dataset.index; + const items = [...this.data.items]; + items.splice(idx, 1); + this.setData({ items }); + wx.setStorageSync('cart', items); + this.updateTotal(); + }, + + clearCart() { + wx.showModal({ + title: '清空购物车', + content: '确定要清空所有商品吗?', + success: (res) => { + if (res.confirm) { + wx.setStorageSync('cart', []); + this.setData({ items: [], totalPrice: '0.00' }); + } + } + }); + }, + + onItemTap(e) { + try { + const id = e.currentTarget.dataset.id; + const p = (this.data.items || []).find(x => x.id === id); + if (p) { try { reco.recordClick({ _id: p.id, productName: p.name, productCategory: p.category }); } catch (_) {} } + if (!id) return; + wx.navigateTo({ url: `/pages/product-detail/product-detail?id=${id}` }); + } catch (err) { /* 忽略 */ } + }, + + goShopping() { + wx.switchTab({ url: '/pages/main/main' }); + }, + + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'getOpenId' } }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { console.error('获取openid失败:', err); } + } + return openid; + }, + + async checkout() { + const items = this.data.items; + if (!items || items.length === 0) { + wx.showToast({ title: '购物车为空', icon: 'none' }); + return; + } + + wx.showLoading({ title: '创建订单...', mask: true }); + + try { + const db = wx.cloud.database(); + const userInfo = wx.getStorageSync('userInfo') || {}; + const buyerUserId = userInfo._id || ''; + let buyerOpenId = ''; + if (buyerUserId) { + try { + const userRes = await db.collection('T_user').doc(buyerUserId).get(); + buyerOpenId = userRes.data && userRes.data._openid ? userRes.data._openid : ''; + } catch (err) { console.warn('通过用户ID获取openid失败:', err); } + } + if (!buyerOpenId) buyerOpenId = await this.ensureOpenId(); + if (!buyerOpenId) { + wx.hideLoading(); + wx.showToast({ title: '请先登录', icon: 'none' }); + return; + } + + // 拉取商品详情,确认在售,并获取卖家信息 + const productDocs = await Promise.all(items.map(async it => { + try { + const res = await db.collection('T_product').doc(it.id).get(); + return res.data || null; + } catch (e) { return null; } + })); + + // 过滤不可购买的商品 + const valid = []; + const removedIds = []; + for (let i = 0; i < items.length; i++) { + const it = items[i]; + const doc = productDocs[i]; + if (!doc || doc.status !== '在售') { + removedIds.push(it.id); + } else { + valid.push({ it, doc }); + } + } + if (removedIds.length > 0) { + wx.showToast({ title: `部分商品不可购买,已移除`, icon: 'none' }); + } + + if (valid.length === 0) { + wx.hideLoading(); + // 移除不可购买商品 + const still = items.filter(x => !removedIds.includes(x.id)); + wx.setStorageSync('cart', still); + this.setData({ items: still }); + this.updateTotal(); + return; + } + + // 按卖家分组生成订单 + const groups = new Map(); + valid.forEach(({ it, doc }) => { + const key = doc.sellerUserId || doc.sellerOpenId || ('unknown-' + (doc._id || it.id)); + if (!groups.has(key)) groups.set(key, { sellerUserId: doc.sellerUserId || '', sellerOpenId: doc.sellerOpenId || '', sellerName: (doc.sellerInfo && doc.sellerInfo.name) || '用户', sellerPhone: (doc.sellerInfo && doc.sellerInfo.phone) || (doc.contactInfo || ''), items: [] }); + groups.get(key).items.push({ it, doc }); + }); + + const createdOrderIds = []; + for (const [key, group] of groups.entries()) { + const products = group.items.map(({ it, doc }) => ({ + productId: doc._id, + name: doc.productName || it.name, + price: Number(doc.salePrice || doc.suggestedPrice || it.price || 0), + count: Number(it.count || 1), + image: Array.isArray(doc.productImage) ? (doc.productImage[0] || it.image) : (doc.productImage || it.image), + specs: doc.productCategory || it.category || '标准' + })); + const totalPrice = products.reduce((s, p) => s + (p.price || 0) * (p.count || 1), 0); + + const orderData = { + orderNumber: `ORD${Date.now()}${Math.random().toString(36).substr(2, 5).toUpperCase()}`, + status: '待付款', + buyerOpenId: buyerOpenId, + buyerUserId: buyerUserId, + sellerOpenId: group.sellerOpenId, + sellerUserId: group.sellerUserId, + sellerName: group.sellerName, + sellerPhone: group.sellerPhone, + products, + totalPrice, + totalCount: products.reduce((s, p) => s + (Number(p.count) || 0), 0), + transactionMethod: '面交', + createTime: new Date(), + updateTime: new Date() + }; + + const orderRes = await db.collection('T_order').add({ data: orderData }); + createdOrderIds.push(orderRes._id); + + // 将本组商品状态更新为交易中 + await Promise.all(group.items.map(({ doc }) => db.collection('T_product').doc(doc._id).update({ data: { status: '交易中', updateTime: new Date() } }))); + + // 写入卖家通知 + try { + await db.collection('T_notify').add({ data: { + productId: group.items[0].doc._id, + productName: group.items[0].doc.productName || '商品', + orderId: orderRes._id || '', + sellerUserId: group.sellerUserId || '', + sellerOpenId: group.sellerOpenId || '', + type: 'purchase', + content: '有新的购物车订单,请及时处理', + status: 'unread', + createTime: new Date(), + updateTime: new Date() + } }); + } catch (e) { + // 忽略通知错误 + } + + // 在聊天中通知该卖家:买家已下单(购物车结算) + try { + if (buyerOpenId && group.sellerOpenId) { + const sessionKey = [buyerOpenId, group.sellerOpenId].sort().join('|'); + const sessionKeyUser = (buyerUserId && group.sellerUserId) ? [buyerUserId, group.sellerUserId].sort().join('|') : ''; + await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'sendChatMessage', + contentType: 'text', + content: `[系统] 买家已下单(购物车):订单号 ${orderData.orderNumber},合计 ¥${orderData.totalPrice},件数 ${orderData.totalCount}`, + toUserId: group.sellerUserId || '', + toOpenId: group.sellerOpenId || '', + productId: group.items[0].doc._id || '', + orderId: orderRes._id || '', + isSystem: true + } + }); + } + } catch (e) { + console.warn('写入购物车下单系统消息失败:', e); + } + } + + // 结算成功:从购物车移除已下单的商品 + const orderedIds = valid.map(v => v.it.id); + const remaining = items.filter(x => !orderedIds.includes(x.id)); + wx.setStorageSync('cart', remaining); + this.setData({ items: remaining }); + this.updateTotal(); + + wx.hideLoading(); + wx.showModal({ + title: '下单成功', + content: '订单已创建,请前往订单页查看并支付', + showCancel: false, + confirmText: '查看订单', + success: () => { + wx.navigateTo({ url: '/pages/orders/orders?tab=pending' }); + } + }); + } catch (err) { + console.error('购物车结算失败:', err); + wx.hideLoading(); + wx.showToast({ title: '结算失败', icon: 'none' }); + } + } +}); \ No newline at end of file diff --git a/src5/code/miniprogram/pages/cart/cart.json b/src5/code/miniprogram/pages/cart/cart.json new file mode 100644 index 0000000..2261ca7 --- /dev/null +++ b/src5/code/miniprogram/pages/cart/cart.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "购物车", + "usingComponents": {} +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/cart/cart.wxml b/src5/code/miniprogram/pages/cart/cart.wxml new file mode 100644 index 0000000..2a8b07e --- /dev/null +++ b/src5/code/miniprogram/pages/cart/cart.wxml @@ -0,0 +1,41 @@ + + + 购物车 + 共 {{items.length}} 件 + + + + 购物车空空如也,去逛逛吧~ + + + + + + + + {{item.name}} + {{item.category}} + + ¥{{item.price}} + + + {{item.count}} + + + + + + + + + + + 合计: + ¥{{totalPrice}} + + + + + + + \ No newline at end of file diff --git a/src5/code/miniprogram/pages/cart/cart.wxss b/src5/code/miniprogram/pages/cart/cart.wxss new file mode 100644 index 0000000..6efb2cf --- /dev/null +++ b/src5/code/miniprogram/pages/cart/cart.wxss @@ -0,0 +1,23 @@ +.cart-page { padding: 20rpx; } +.cart-header { display:flex; justify-content:space-between; align-items:center; margin-bottom: 20rpx; } +.title { font-size: 32rpx; font-weight: bold; } +.count { font-size: 24rpx; color:#888; } +.empty { text-align:center; color:#666; padding:60rpx 0; } +.go-btn { margin-top: 20rpx; background:#4285F4; color:#fff; border-radius: 12rpx; } +.list { max-height: calc(100vh - 240rpx); } +.item { display:flex; background:#fff; border-radius: 16rpx; overflow:hidden; box-shadow:0 6rpx 18rpx rgba(0,0,0,0.06); margin-bottom: 20rpx; } +.thumb { width: 240rpx; height: 200rpx; } +.info { flex:1; padding: 16rpx; } +.name { font-size: 28rpx; color:#333; display:block; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } +.category { font-size: 22rpx; color:#999; margin-top: 6rpx; } +.price-line { display:flex; justify-content:space-between; align-items:center; margin-top: 12rpx; } +.price { font-size: 30rpx; color:#FF6B35; font-weight:bold; } +.qty { display:flex; align-items:center; } +.minus, .plus { width: 52rpx; height: 52rpx; border-radius: 8rpx; background:#f2f2f2; } +.num { width: 60rpx; text-align:center; font-size: 26rpx; } +.remove { margin-top: 12rpx; background:#ffe5e5; color:#d33; border-radius: 10rpx; } +.bottom-bar { position: fixed; left:0; right:0; bottom:0; background:#fff; box-shadow:0 -6rpx 18rpx rgba(0,0,0,0.06); padding: 16rpx 20rpx; display:flex; justify-content:space-between; align-items:center; } +.total { font-size: 26rpx; } +.total-price { font-size: 32rpx; color:#FF6B35; font-weight:bold; } +.clear-btn { background:#eee; border-radius: 12rpx; margin-right: 12rpx; } +.checkout-btn { background:#34A853; color:#fff; border-radius: 12rpx; } \ No newline at end of file diff --git a/src5/code/miniprogram/pages/chat/chat.js b/src5/code/miniprogram/pages/chat/chat.js new file mode 100644 index 0000000..0f5d89a --- /dev/null +++ b/src5/code/miniprogram/pages/chat/chat.js @@ -0,0 +1,258 @@ +// pages/chat/chat.js +Page({ + data: { + peer: { name: '', avatar: '', openId: '', userId: '' }, + myOpenId: '', + myAvatar: '', + myUserId: '', + productId: '', + sessionKey: '', + sessionKeyUser: '', + messages: [], + textInput: '', + sending: false, + uploading: false, + scrollInto: '', + watcher: null + }, + + async onLoad(options) { + // 参数:toUserId(推荐), toOpenId(兼容), toName, productId + const toOpenId = options.toOpenId || ''; + const toName = decodeURIComponent(options.toName || ''); + const productId = options.productId || ''; + const toUserId = options.toUserId || ''; + + const myOpenId = await this.ensureOpenId(); + const myAvatar = (wx.getStorageSync('userInfo') || {}).avatar || ''; + const myUserId = (wx.getStorageSync('userInfo') || {})._id || ''; + + const sessionKey = [myOpenId, toOpenId].sort().join('|'); + const sessionKeyUser = (toUserId && myUserId) ? [myUserId, toUserId].sort().join('|') : ''; + + this.setData({ + peer: { name: toName || '对话', avatar: '', openId: toOpenId, userId: toUserId }, + myOpenId, + myAvatar, + myUserId, + productId, + sessionKey, + sessionKeyUser + }); + + await this.loadPeerInfo(); + await this.loadMessages(); + this.startWatch(); + }, + + onUnload() { + try { if (this.data.watcher) this.data.watcher.close(); } catch(e){} + }, + + goBack() { wx.navigateBack({ delta: 1 }); }, + + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'getOpenId' } }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { console.error('获取openid失败:', err); } + } + return openid; + }, + + async loadPeerInfo() { + try { + const db = wx.cloud.database(); + let u = null; + if (this.data.peer.userId) { + const byId = await db.collection('T_user').doc(this.data.peer.userId).get(); + u = byId.data || null; + } + if (!u && this.data.peer.openId) { + const byOpen = await db.collection('T_user').where({ _openid: this.data.peer.openId }).limit(1).get(); + u = (byOpen.data && byOpen.data[0]) || null; + } + if (u) { + const name = u.sname || u.nickName || this.data.peer.name || '对话'; + const avatar = u.avatar || this.data.peer.avatar || ''; + const openId = u._openid || this.data.peer.openId || ''; + const userId = u._id || this.data.peer.userId || ''; + this.setData({ peer: { ...this.data.peer, name, avatar, openId, userId } }); + } + } catch(e){} + }, + + async loadMessages() { + try { + const db = wx.cloud.database(); + const res = await db.collection('T_message').where({ toUserId: this.data.peer.userId, fromUserId: this.data.myUserId }).limit(1).get(); + const doc = (res.data && res.data[0]) || null; + if (!doc) { + await db.collection('T_message').add({ data: { toUserId: this.data.peer.userId, fromUserId: this.data.myUserId, message: [] } }); + this.setData({ messages: [] }); + return; + } + const list = (doc.message || []).map(m => ({ ...m, _id: m._id || `${m.timestamp}`, timeText: this.formatTime(m.timestamp) })); + this.setData({ messages: list }); + this.scrollToBottom(); + } catch(err) { console.error('加载消息失败:', err); } + }, + + startWatch() { + try { + const db = wx.cloud.database(); + const watcher = db.collection('T_message').where({ toUserId: this.data.peer.userId, fromUserId: this.data.myUserId }) + .watch({ + onChange: snapshot => { + const doc = (snapshot.docs && snapshot.docs[0]) || null; + const list = doc ? (doc.message || []).map(m => ({ ...m, _id: m._id || `${m.timestamp}`, timeText: this.formatTime(m.timestamp) })) : []; + this.setData({ messages: list }); + this.scrollToBottom(); + }, + onError: err => { this.startPolling(); } + }); + this.setData({ watcher }); + } catch(e) { + this.startPolling(); + } + }, + + startPolling() { + if (this._pollTimer) clearInterval(this._pollTimer); + this._pollTimer = setInterval(()=>this.loadMessages(), 3000); + }, + + onTextInput(e) { this.setData({ textInput: e.detail.value }); }, + + async sendText() { + const content = (this.data.textInput || '').trim(); + if (!content) return; + this.setData({ sending: true }); + try { + await this._sendMessage({ contentType: 'text', content }); + this.setData({ textInput: '' }); + } catch(err) { + wx.showToast({ title: '发送失败', icon: 'none' }); + } finally { + this.setData({ sending: false }); + } + }, + + async chooseImage() { + this.setData({ uploading: true }); + try { + const pick = await wx.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album','camera'] }); + const filePath = pick.tempFilePaths[0]; + const cloudPath = `chat-images/${Date.now()}-${Math.random().toString(36).slice(2)}.jpg`; + const up = await wx.cloud.uploadFile({ cloudPath, filePath }); + // 可直接用 fileID;也尝试获取临时URL用于展示 + let imageUrl = up.fileID; + try { + const tmp = await wx.cloud.getTempFileURL({ fileList: [up.fileID] }); + if (tmp.fileList && tmp.fileList[0] && tmp.fileList[0].tempFileURL) imageUrl = tmp.fileList[0].tempFileURL; + } catch(e){} + await this._sendMessage({ contentType: 'image', content: up.fileID, imageUrl }); + } catch(err) { + wx.showToast({ title: '选择图片失败', icon: 'none' }); + } finally { this.setData({ uploading: false }); } + }, + + async _sendMessage({ contentType, content, imageUrl }) { + try { + const db = wx.cloud.database(); + const _ = db.command; + const msg = { + fromUserId: this.data.myUserId, + fromOpenId: this.data.myOpenId, + contentType, + content, + imageUrl: imageUrl || '', + timestamp: Date.now() + }; + const a = { toUserId: this.data.peer.userId, fromUserId: this.data.myUserId }; + const b = { toUserId: this.data.myUserId, fromUserId: this.data.peer.userId }; + const chkA = await db.collection('T_message').where(a).limit(1).get(); + if (!(chkA.data && chkA.data[0])) { await db.collection('T_message').add({ data: { toUserId: a.toUserId, fromUserId: a.fromUserId, message: [] } }); } + try { await db.collection('T_message').where(a).update({ data: { message: _.push(msg) } }); } + catch(e){ + if (String(e.errMsg||'').includes('does not exist')||e.errCode===-502005){ await wx.cloud.callFunction({ name:'quickstartFunctions', data:{ type:'createMessageCollection' } }); await db.collection('T_message').where(a).update({ data: { message: _.push(msg) } }); } + else { throw e; } + } + const chkB = await db.collection('T_message').where(b).limit(1).get(); + if (!(chkB.data && chkB.data[0])) { await db.collection('T_message').add({ data: { toUserId: b.toUserId, fromUserId: b.fromUserId, message: [] } }); } + try { await db.collection('T_message').where(b).update({ data: { message: _.push(msg) } }); } + catch(e){ + if (String(e.errMsg||'').includes('does not exist')||e.errCode===-502005){ await wx.cloud.callFunction({ name:'quickstartFunctions', data:{ type:'createMessageCollection' } }); await db.collection('T_message').where(b).update({ data: { message: _.push(msg) } }); } + } + const messages = [...this.data.messages, { ...msg, _id: `local-${msg.timestamp}`, timeText: this.formatTime(msg.timestamp) }]; + this.setData({ messages }); + this.scrollToBottom(); + } catch (err) { + console.error('发送消息失败:', err); + throw err; + } + }, + + async loadMore() { + try { + const oldest = this.data.messages[0]?.timestamp || 0; + if (!oldest) return; + const res = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getChatMessages', + toUserId: this.data.peer.userId || '', + toOpenId: this.data.peer.openId || '', + before: oldest, + limit: 50 + } + }); + const ret = res.result || {}; + const list = (ret.data || []).map(m => ({ ...m, timeText: this.formatTime(m.timestamp) })); + const merged = [...list, ...this.data.messages]; + this.setData({ messages: merged }); + } catch (e) {} + }, + + previewImage(e) { + const url = e.currentTarget.dataset.url; + wx.previewImage({ urls: [url], current: url }); + }, + + async onMessageLongPress(e) { + const id = e.currentTarget.dataset.id; + const mine = !!e.currentTarget.dataset.mine; + if (!mine) return; + try { + wx.showActionSheet({ itemList: ['撤回'], success: async (res) => { + if (res.tapIndex === 0) { + try { + const ret = await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'revokeChatMessage', id } }); + if (ret.result && ret.result.success) { + await this.loadMessages(); + } else { + wx.showToast({ title: '撤回失败', icon: 'none' }); + } + } catch (err) { wx.showToast({ title: '撤回失败', icon: 'none' }); } + } + }}); + } catch (e) {} + }, + + scrollToBottom() { + const last = this.data.messages[this.data.messages.length - 1]; + if (last) this.setData({ scrollInto: `msg-${last._id || 'bottom-anchor'}` }); + else this.setData({ scrollInto: 'bottom-anchor' }); + }, + + formatTime(ts) { + const d = new Date(ts); + const pad = n => (n<10?'0'+n:n); + return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; + } +}); \ No newline at end of file diff --git a/src5/code/miniprogram/pages/chat/chat.json b/src5/code/miniprogram/pages/chat/chat.json new file mode 100644 index 0000000..6f85c59 --- /dev/null +++ b/src5/code/miniprogram/pages/chat/chat.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "消息", + "usingComponents": {} +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/chat/chat.wxml b/src5/code/miniprogram/pages/chat/chat.wxml new file mode 100644 index 0000000..ebb99ec --- /dev/null +++ b/src5/code/miniprogram/pages/chat/chat.wxml @@ -0,0 +1,57 @@ + + + 返回 + 聊天 + + + + + + 加载更多 + + + + {{peer.name || '对话'}} + 与对方实时沟通,支持文字和图片 + + + + {{item.timeText}} + + + + {{item.contentType === 'revoke' ? '对方撤回一条消息' : (item.content || '[系统消息]')}} + + + + + + + + + {{item.content}} + + + + + + + + + + + + + + {{textInput}} + + + + + {{content.length}}/500 + + + + + 联系方式(选填) + + + + + + 相关图片(选填,最多3张) + + + + × + + + + + + + + + + + + + + + diff --git a/src5/code/miniprogram/pages/feedback/feedback.wxss b/src5/code/miniprogram/pages/feedback/feedback.wxss new file mode 100644 index 0000000..a4b9bfa --- /dev/null +++ b/src5/code/miniprogram/pages/feedback/feedback.wxss @@ -0,0 +1,146 @@ +/* pages/feedback/feedback.wxss */ +.page-container { + min-height: 100vh; + background-color: #f5f5f5; + padding-bottom: 120rpx; +} + +.form-container { + padding: 30rpx; +} + +.form-section { + background: white; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 30rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.section-title { + display: block; + font-size: 28rpx; + font-weight: bold; + color: #333; + margin-bottom: 20rpx; +} + +.type-list { + display: flex; + flex-wrap: wrap; + gap: 20rpx; +} + +.type-item { + padding: 15rpx 30rpx; + background: #f5f5f5; + border-radius: 50rpx; + font-size: 26rpx; + color: #666; + border: 2rpx solid transparent; +} + +.type-item.active { + background: #667eea; + color: white; + border-color: #667eea; +} + +.content-input { + width: 100%; + min-height: 200rpx; + padding: 20rpx; + background: #f8f8f8; + border-radius: 10rpx; + font-size: 28rpx; + color: #333; + margin-bottom: 10rpx; +} + +.char-count { + display: block; + text-align: right; + font-size: 24rpx; + color: #999; +} + +.contact-input { + width: 100%; + padding: 20rpx; + background: #f8f8f8; + border-radius: 10rpx; + font-size: 28rpx; + color: #333; +} + +.image-list { + display: flex; + flex-wrap: wrap; + gap: 20rpx; +} + +.image-item { + position: relative; + width: 200rpx; + height: 200rpx; +} + +.image { + width: 100%; + height: 100%; + border-radius: 10rpx; +} + +.delete-btn { + position: absolute; + top: -10rpx; + right: -10rpx; + width: 40rpx; + height: 40rpx; + background: #ff6b6b; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + font-weight: bold; +} + +.add-image-btn { + width: 200rpx; + height: 200rpx; + border: 2rpx dashed #ccc; + border-radius: 10rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 60rpx; + color: #999; +} + +.add-image-btn:active { + background: #f5f5f5; +} + +.button-section { + padding: 30rpx; +} + +.submit-btn { + width: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 50rpx; + padding: 30rpx; + font-size: 32rpx; + font-weight: bold; + box-shadow: 0 8rpx 25rpx rgba(102, 126, 234, 0.3); +} + +.submit-btn:active { + transform: translateY(2rpx); + box-shadow: 0 4rpx 15rpx rgba(102, 126, 234, 0.3); +} + diff --git a/src5/code/miniprogram/pages/forum-detail/forum-detail.js b/src5/code/miniprogram/pages/forum-detail/forum-detail.js new file mode 100644 index 0000000..7d78921 --- /dev/null +++ b/src5/code/miniprogram/pages/forum-detail/forum-detail.js @@ -0,0 +1,507 @@ +// pages/forum-detail/forum-detail.js +Page({ + /** + * 页面的初始数据 + */ + data: { + postId: '', + post: null, + comments: [], + inputText: '', + loading: true, + isLiked: false, + categories: [ + { label: '学习交流', value: 'study' }, + { label: '生活分享', value: 'life' }, + { label: '二手交易', value: 'trade' }, + { label: '校园活动', value: 'activity' }, + { label: '求助问答', value: 'help' }, + { label: '其他', value: 'other' } + ] + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + const postId = options.id; + if (!postId) { + wx.showToast({ + title: '帖子不存在', + icon: 'none' + }); + setTimeout(() => { + wx.navigateBack(); + }, 1500); + return; + } + + this.setData({ + postId: postId + }); + + this.loadPostDetail(); + this.loadComments(); + this.checkLikeStatus(); + this.increaseViewCount(); + }, + + /** + * 确保有openid + */ + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getOpenId' + } + }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { + console.error('获取openid失败:', err); + } + } + return openid; + }, + + /** + * 加载帖子详情 + */ + async loadPostDetail() { + try { + const db = wx.cloud.database(); + const result = await db.collection('T_forum_post').doc(this.data.postId).get(); + + if (!result.data) { + throw new Error('帖子不存在'); + } + + const post = result.data; + + // 获取作者信息 + let authorInfo = { + name: '匿名用户', + avatar: 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U' + }; + + if (post.authorOpenId) { + try { + const userResult = await db.collection('T_user') + .where({ + _openid: post.authorOpenId + }) + .get(); + + if (userResult.data && userResult.data.length > 0) { + const user = userResult.data[0]; + authorInfo = { + name: user.sname || user.nickName || '用户', + avatar: user.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U' + }; + } + } catch (err) { + console.error('获取作者信息失败:', err); + } + } + + // 格式化时间 + const timeText = this.formatTime(post.createTime); + + // 获取分类文本 + const categoryItem = this.data.categories.find(cat => cat.value === post.category); + const categoryText = categoryItem ? categoryItem.label : '其他'; + + this.setData({ + post: { + ...post, + authorInfo: authorInfo, + timeText: timeText, + categoryText: categoryText, + images: Array.isArray(post.images) ? post.images : (post.images ? [post.images] : []) + }, + loading: false + }); + } catch (err) { + console.error('加载帖子详情失败:', err); + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + setTimeout(() => { + wx.navigateBack(); + }, 1500); + } + }, + + /** + * 加载评论列表 + */ + async loadComments() { + try { + const db = wx.cloud.database(); + const result = await db.collection('T_forum_comment') + .where({ + postId: this.data.postId + }) + .orderBy('createTime', 'asc') + .get(); + + // 获取评论作者信息 + const comments = await Promise.all(result.data.map(async (comment) => { + let authorInfo = { + name: '匿名用户', + avatar: 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U' + }; + + if (comment.authorOpenId) { + try { + const userResult = await db.collection('T_user') + .where({ + _openid: comment.authorOpenId + }) + .get(); + + if (userResult.data && userResult.data.length > 0) { + const user = userResult.data[0]; + authorInfo = { + name: user.sname || user.nickName || '用户', + avatar: user.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U' + }; + } + } catch (err) { + console.error('获取评论作者信息失败:', err); + } + } + + return { + ...comment, + authorInfo: authorInfo, + timeText: this.formatTime(comment.createTime) + }; + })); + + this.setData({ + comments: comments + }); + } catch (err) { + console.error('加载评论失败:', err); + // 如果集合不存在,创建空列表 + if (err.errMsg && (err.errMsg.includes('not exist') || err.errMsg.includes('not exists') || err.errCode === -502005)) { + this.setData({ + comments: [] + }); + } + } + }, + + /** + * 检查点赞状态 + */ + async checkLikeStatus() { + try { + const openid = await this.ensureOpenId(); + if (!openid) return; + + const db = wx.cloud.database(); + try { + const result = await db.collection('T_forum_like') + .where({ + _openid: openid, + postId: this.data.postId + }) + .get(); + + this.setData({ + isLiked: result.data && result.data.length > 0 + }); + } catch (err) { + // 如果集合不存在,默认为未点赞 + if (err.errMsg && (err.errMsg.includes('not exist') || err.errMsg.includes('not exists') || err.errCode === -502005)) { + this.setData({ + isLiked: false + }); + } + } + } catch (err) { + console.error('检查点赞状态失败:', err); + } + }, + + /** + * 增加浏览量 + */ + async increaseViewCount() { + try { + const db = wx.cloud.database(); + const _ = db.command; + await db.collection('T_forum_post').doc(this.data.postId).update({ + data: { + viewCount: _.inc(1), + updateTime: new Date() + } + }); + } catch (err) { + console.error('增加浏览量失败:', err); + } + }, + + /** + * 格式化时间 + */ + formatTime(date) { + if (!date) return ''; + const d = new Date(date); + const now = new Date(); + const diff = now - d; + + const minute = 60 * 1000; + const hour = 60 * minute; + const day = 24 * hour; + + if (diff < minute) { + return '刚刚'; + } else if (diff < hour) { + return Math.floor(diff / minute) + '分钟前'; + } else if (diff < day) { + return Math.floor(diff / hour) + '小时前'; + } else if (diff < 7 * day) { + return Math.floor(diff / day) + '天前'; + } else { + return `${d.getMonth() + 1}月${d.getDate()}日`; + } + }, + + /** + * 输入框内容变化 + */ + onInputChange(e) { + this.setData({ + inputText: e.detail.value + }); + }, + + /** + * 提交评论 + */ + async onSubmitComment() { + const content = this.data.inputText.trim(); + if (!content) { + return; + } + + const openid = await this.ensureOpenId(); + if (!openid) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + return; + } + + try { + const db = wx.cloud.database(); + const _ = db.command; + + // 添加评论 + await db.collection('T_forum_comment').add({ + data: { + postId: this.data.postId, + authorOpenId: openid, + content: content, + createTime: new Date() + } + }); + + // 更新帖子评论数 + await db.collection('T_forum_post').doc(this.data.postId).update({ + data: { + commentCount: _.inc(1), + updateTime: new Date() + } + }); + + // 清空输入框 + this.setData({ + inputText: '' + }); + + // 重新加载评论和帖子 + this.loadComments(); + this.loadPostDetail(); + } catch (err) { + console.error('提交评论失败:', err); + + // 如果集合不存在,尝试创建 + if (err.errMsg && (err.errMsg.includes('not exist') || err.errMsg.includes('not exists') || err.errCode === -502005)) { + try { + await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'createForumCollection' + } + }); + + // 重新尝试提交 + const db = wx.cloud.database(); + const _ = db.command; + await db.collection('T_forum_comment').add({ + data: { + postId: this.data.postId, + authorOpenId: openid, + content: content, + createTime: new Date() + } + }); + + await db.collection('T_forum_post').doc(this.data.postId).update({ + data: { + commentCount: _.inc(1), + updateTime: new Date() + } + }); + + this.setData({ + inputText: '' + }); + + this.loadComments(); + this.loadPostDetail(); + } catch (createErr) { + console.error('创建集合失败:', createErr); + wx.showToast({ + title: '评论失败', + icon: 'none' + }); + } + } else { + wx.showToast({ + title: '评论失败', + icon: 'none' + }); + } + } + }, + + /** + * 点赞/取消点赞 + */ + async onToggleLike() { + const openid = await this.ensureOpenId(); + if (!openid) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + return; + } + + try { + const db = wx.cloud.database(); + const _ = db.command; + + if (this.data.isLiked) { + // 取消点赞 + try { + const likeResult = await db.collection('T_forum_like') + .where({ + _openid: openid, + postId: this.data.postId + }) + .get(); + + if (likeResult.data && likeResult.data.length > 0) { + await db.collection('T_forum_like').doc(likeResult.data[0]._id).remove(); + } + } catch (err) { + // 如果集合不存在,忽略错误 + if (!err.errMsg || (!err.errMsg.includes('not exist') && !err.errMsg.includes('not exists') && err.errCode !== -502005)) { + throw err; + } + } + + await db.collection('T_forum_post').doc(this.data.postId).update({ + data: { + likeCount: _.inc(-1), + updateTime: new Date() + } + }); + + this.setData({ + isLiked: false + }); + } else { + // 点赞 + try { + await db.collection('T_forum_like').add({ + data: { + postId: this.data.postId, + authorOpenId: openid, + createTime: new Date() + } + }); + } catch (err) { + // 如果集合不存在,尝试创建 + if (err.errMsg && (err.errMsg.includes('not exist') || err.errMsg.includes('not exists') || err.errCode === -502005)) { + await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'createForumCollection' + } + }); + + // 重新尝试点赞 + await db.collection('T_forum_like').add({ + data: { + postId: this.data.postId, + authorOpenId: openid, + createTime: new Date() + } + }); + } else { + throw err; + } + } + + await db.collection('T_forum_post').doc(this.data.postId).update({ + data: { + likeCount: _.inc(1), + updateTime: new Date() + } + }); + + this.setData({ + isLiked: true + }); + } + + // 重新加载帖子 + this.loadPostDetail(); + } catch (err) { + console.error('点赞操作失败:', err); + wx.showToast({ + title: '操作失败', + icon: 'none' + }); + } + }, + + /** + * 预览图片 + */ + onPreviewImage(e) { + const urls = e.currentTarget.dataset.urls; + const current = e.currentTarget.dataset.current; + + wx.previewImage({ + urls: urls, + current: current + }); + } +}); + diff --git a/src5/code/miniprogram/pages/forum-detail/forum-detail.json b/src5/code/miniprogram/pages/forum-detail/forum-detail.json new file mode 100644 index 0000000..f271374 --- /dev/null +++ b/src5/code/miniprogram/pages/forum-detail/forum-detail.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "帖子详情", + "navigationBarBackgroundColor": "#4285F4", + "navigationBarTextStyle": "white" +} + diff --git a/src5/code/miniprogram/pages/forum-detail/forum-detail.wxml b/src5/code/miniprogram/pages/forum-detail/forum-detail.wxml new file mode 100644 index 0000000..9613145 --- /dev/null +++ b/src5/code/miniprogram/pages/forum-detail/forum-detail.wxml @@ -0,0 +1,101 @@ + + + + + + + + + + {{post.categoryText}} + + + + + {{post.title}} + {{post.content}} + + + + + + + + + + + 👁️ + {{post.viewCount || 0}} + + + 💬 + {{post.commentCount || 0}} + + + {{isLiked ? '❤️' : '🤍'}} + {{post.likeCount || 0}} + + + + + + + + 评论 ({{comments.length}}) + + + + + + + {{item.authorInfo.name || '匿名用户'}} + {{item.timeText}} + + {{item.content}} + + + + + 暂无评论,快来抢沙发吧~ + + + + + + + + + + 发送 + + + + + + + + 加载中... + + + + diff --git a/src5/code/miniprogram/pages/forum-detail/forum-detail.wxss b/src5/code/miniprogram/pages/forum-detail/forum-detail.wxss new file mode 100644 index 0000000..9e0ca84 --- /dev/null +++ b/src5/code/miniprogram/pages/forum-detail/forum-detail.wxss @@ -0,0 +1,249 @@ +/* pages/forum-detail/forum-detail.wxss */ +.page-container { + height: 100vh; + display: flex; + flex-direction: column; + background-color: #f5f5f5; +} + +.scroll-container { + flex: 1; + padding-bottom: 120rpx; +} + +/* 帖子容器 */ +.post-container { + background: white; + padding: 30rpx; + margin-bottom: 20rpx; +} + +.post-header { + display: flex; + align-items: center; + margin-bottom: 30rpx; +} + +.user-avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + margin-right: 20rpx; +} + +.user-info { + flex: 1; + display: flex; + flex-direction: column; +} + +.username { + font-size: 30rpx; + font-weight: 600; + color: #333; + margin-bottom: 8rpx; +} + +.post-time { + font-size: 24rpx; + color: #999; +} + +.category-tag { + padding: 10rpx 25rpx; + background: linear-gradient(135deg, #4285F4 0%, #667eea 100%); + color: white; + border-radius: 50rpx; + font-size: 24rpx; +} + +.post-content { + margin-bottom: 30rpx; +} + +.post-title { + font-size: 36rpx; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 20rpx; + line-height: 1.5; +} + +.post-text { + font-size: 30rpx; + color: #666; + line-height: 1.8; + display: block; + margin-bottom: 20rpx; + word-break: break-all; +} + +.post-images { + display: flex; + flex-direction: column; + gap: 15rpx; + margin-top: 20rpx; +} + +.post-image { + width: 100%; + border-radius: 10rpx; +} + +.post-stats { + display: flex; + align-items: center; + gap: 50rpx; + padding-top: 30rpx; + border-top: 1rpx solid #f0f0f0; +} + +.stat-item { + display: flex; + align-items: center; + gap: 10rpx; +} + +.stat-icon { + font-size: 32rpx; +} + +.stat-text { + font-size: 26rpx; + color: #666; +} + +/* 评论区域 */ +.comments-section { + background: white; + padding: 30rpx; + margin-bottom: 20rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: 600; + color: #333; + margin-bottom: 30rpx; + padding-bottom: 20rpx; + border-bottom: 2rpx solid #f0f0f0; +} + +.comment-item { + display: flex; + margin-bottom: 30rpx; +} + +.comment-item:last-child { + margin-bottom: 0; +} + +.comment-avatar { + width: 60rpx; + height: 60rpx; + border-radius: 50%; + margin-right: 20rpx; + flex-shrink: 0; +} + +.comment-content { + flex: 1; +} + +.comment-header { + display: flex; + align-items: center; + margin-bottom: 10rpx; +} + +.comment-author { + font-size: 26rpx; + font-weight: 600; + color: #333; + margin-right: 15rpx; +} + +.comment-time { + font-size: 22rpx; + color: #999; +} + +.comment-text { + font-size: 28rpx; + color: #666; + line-height: 1.6; + word-break: break-all; +} + +.empty-comments { + text-align: center; + padding: 60rpx 0; + color: #999; + font-size: 26rpx; +} + +/* 评论输入框 */ +.comment-input-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: white; + padding: 20rpx 30rpx; + padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); + border-top: 1rpx solid #e0e0e0; + box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05); +} + +.input-wrapper { + display: flex; + align-items: center; + background: #f5f5f5; + border-radius: 50rpx; + padding: 10rpx 20rpx; +} + +.comment-input { + flex: 1; + font-size: 28rpx; + padding: 10rpx 0; + margin-right: 20rpx; +} + +.send-btn { + padding: 12rpx 32rpx; + background: #ccc; + color: #fff; + border-radius: 50rpx; + font-size: 26rpx; + transition: all 0.3s ease; +} + +.send-btn.active { + background: linear-gradient(135deg, #4285F4 0%, #667eea 100%); +} + +/* 加载状态 */ +.loading-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loading-content { + text-align: center; +} + +.loading-text { + font-size: 28rpx; + color: #666; +} + diff --git a/src5/code/miniprogram/pages/forum-publish/forum-publish.js b/src5/code/miniprogram/pages/forum-publish/forum-publish.js new file mode 100644 index 0000000..f048218 --- /dev/null +++ b/src5/code/miniprogram/pages/forum-publish/forum-publish.js @@ -0,0 +1,261 @@ +// pages/forum-publish/forum-publish.js +Page({ + /** + * 页面的初始数据 + */ + data: { + category: 'study', + categories: [ + { label: '学习交流', value: 'study' }, + { label: '生活分享', value: 'life' }, + { label: '二手交易', value: 'trade' }, + { label: '校园活动', value: 'activity' }, + { label: '求助问答', value: 'help' }, + { label: '其他', value: 'other' } + ], + title: '', + content: '', + images: [], + submitting: false + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + const category = options.category || 'study'; + this.setData({ + category: category + }); + }, + + /** + * 确保有openid + */ + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getOpenId' + } + }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { + console.error('获取openid失败:', err); + } + } + return openid; + }, + + /** + * 分类选择 + */ + onCategorySelect(e) { + const category = e.currentTarget.dataset.value; + this.setData({ + category: category + }); + }, + + /** + * 标题输入 + */ + onTitleInput(e) { + this.setData({ + title: e.detail.value + }); + }, + + /** + * 内容输入 + */ + onContentInput(e) { + this.setData({ + content: e.detail.value + }); + }, + + /** + * 选择图片 + */ + onChooseImage() { + const maxCount = 9 - this.data.images.length; + wx.chooseImage({ + count: maxCount, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: async (res) => { + wx.showLoading({ + title: '上传中...', + mask: true + }); + + try { + const uploadPromises = res.tempFilePaths.map(async (filePath) => { + const cloudPath = `forum/${Date.now()}-${Math.random().toString(36).substr(2, 9)}.jpg`; + const uploadResult = await wx.cloud.uploadFile({ + cloudPath: cloudPath, + filePath: filePath + }); + return uploadResult.fileID; + }); + + const fileIDs = await Promise.all(uploadPromises); + + this.setData({ + images: [...this.data.images, ...fileIDs] + }); + + wx.hideLoading(); + } catch (err) { + console.error('上传图片失败:', err); + wx.hideLoading(); + wx.showToast({ + title: '上传失败', + icon: 'none' + }); + } + } + }); + }, + + /** + * 删除图片 + */ + onDeleteImage(e) { + const index = e.currentTarget.dataset.index; + const images = this.data.images; + images.splice(index, 1); + this.setData({ + images: images + }); + }, + + /** + * 检查是否可以提交 + */ + get canSubmit() { + return this.data.title.trim().length > 0 && this.data.content.trim().length > 0; + }, + + /** + * 提交帖子 + */ + async onSubmit() { + if (!this.canSubmit) { + wx.showToast({ + title: '请填写标题和内容', + icon: 'none' + }); + return; + } + + const openid = await this.ensureOpenId(); + if (!openid) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + return; + } + + this.setData({ + submitting: true + }); + + try { + const db = wx.cloud.database(); + + // 创建帖子 + await db.collection('T_forum_post').add({ + data: { + authorOpenId: openid, + category: this.data.category, + title: this.data.title.trim(), + content: this.data.content.trim(), + images: this.data.images, + viewCount: 0, + commentCount: 0, + likeCount: 0, + status: 'published', + createTime: new Date(), + updateTime: new Date() + } + }); + + wx.showToast({ + title: '发布成功', + icon: 'success' + }); + + setTimeout(() => { + wx.navigateBack(); + }, 1500); + } catch (err) { + console.error('发布帖子失败:', err); + + // 如果集合不存在,尝试创建 + if (err.errMsg && (err.errMsg.includes('not exist') || err.errMsg.includes('not exists') || err.errCode === -502005)) { + try { + await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'createForumCollection' + } + }); + + // 重新尝试发布 + const db = wx.cloud.database(); + await db.collection('T_forum_post').add({ + data: { + authorOpenId: openid, + category: this.data.category, + title: this.data.title.trim(), + content: this.data.content.trim(), + images: this.data.images, + viewCount: 0, + commentCount: 0, + likeCount: 0, + status: 'published', + createTime: new Date(), + updateTime: new Date() + } + }); + + wx.showToast({ + title: '发布成功', + icon: 'success' + }); + + setTimeout(() => { + wx.navigateBack(); + }, 1500); + } catch (createErr) { + console.error('创建集合失败:', createErr); + wx.showModal({ + title: '提示', + content: '论坛功能需要初始化数据库集合。请在云开发控制台创建 T_forum_post 集合,或联系管理员。', + showCancel: false, + confirmText: '知道了' + }); + } + } else { + wx.showToast({ + title: '发布失败', + icon: 'none' + }); + } + } finally { + this.setData({ + submitting: false + }); + } + } +}); + diff --git a/src5/code/miniprogram/pages/forum-publish/forum-publish.json b/src5/code/miniprogram/pages/forum-publish/forum-publish.json new file mode 100644 index 0000000..749be94 --- /dev/null +++ b/src5/code/miniprogram/pages/forum-publish/forum-publish.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "发布帖子", + "navigationBarBackgroundColor": "#4285F4", + "navigationBarTextStyle": "white" +} + diff --git a/src5/code/miniprogram/pages/forum-publish/forum-publish.wxml b/src5/code/miniprogram/pages/forum-publish/forum-publish.wxml new file mode 100644 index 0000000..641befb --- /dev/null +++ b/src5/code/miniprogram/pages/forum-publish/forum-publish.wxml @@ -0,0 +1,86 @@ + + + + + + + + + {{item.label}} + + + + + + + + + + + + + + + {{content.length}}/1000 + + + + + + + + + + × + + + + 📷 + 添加图片 + + + + + + + + + + + + + + + 发布中... + + + diff --git a/src5/code/miniprogram/pages/forum-publish/forum-publish.wxss b/src5/code/miniprogram/pages/forum-publish/forum-publish.wxss new file mode 100644 index 0000000..d003909 --- /dev/null +++ b/src5/code/miniprogram/pages/forum-publish/forum-publish.wxss @@ -0,0 +1,174 @@ +/* pages/forum-publish/forum-publish.wxss */ +.page-container { + min-height: 100vh; + background-color: #f5f5f5; + padding-bottom: 40rpx; +} + +.form-container { + padding: 30rpx; +} + +.form-section { + background: white; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 30rpx; +} + +.section-label { + font-size: 28rpx; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 20rpx; +} + +/* 分类选择 */ +.category-grid { + display: flex; + flex-wrap: wrap; + gap: 15rpx; +} + +.category-option { + padding: 15rpx 30rpx; + background: #f5f5f5; + border-radius: 50rpx; + color: #666; + font-size: 26rpx; + transition: all 0.3s ease; +} + +.category-option.active { + background: linear-gradient(135deg, #4285F4 0%, #667eea 100%); + color: white; +} + +/* 标题输入 */ +.title-input { + width: 100%; + font-size: 30rpx; + color: #333; + padding: 20rpx 0; + border-bottom: 1rpx solid #e0e0e0; +} + +/* 内容输入 */ +.content-input { + width: 100%; + min-height: 300rpx; + font-size: 28rpx; + color: #333; + line-height: 1.8; + padding: 20rpx 0; +} + +.char-count { + display: block; + text-align: right; + font-size: 24rpx; + color: #999; + margin-top: 15rpx; +} + +/* 图片上传 */ +.image-upload { + display: flex; + flex-wrap: wrap; + gap: 15rpx; +} + +.image-item { + position: relative; + width: 200rpx; + height: 200rpx; +} + +.uploaded-image { + width: 100%; + height: 100%; + border-radius: 10rpx; +} + +.delete-btn { + position: absolute; + top: -10rpx; + right: -10rpx; + width: 40rpx; + height: 40rpx; + background: #ff4444; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 32rpx; + font-weight: bold; +} + +.upload-btn { + width: 200rpx; + height: 200rpx; + border: 2rpx dashed #ccc; + border-radius: 10rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: #fafafa; +} + +.upload-icon { + font-size: 48rpx; + margin-bottom: 10rpx; +} + +.upload-text { + font-size: 24rpx; + color: #999; +} + +/* 提交按钮 */ +.submit-section { + padding: 0 30rpx; +} + +.submit-btn { + width: 100%; + padding: 25rpx 0; + background: #ccc; + color: white; + border-radius: 50rpx; + font-size: 32rpx; + font-weight: 600; + transition: all 0.3s ease; +} + +.submit-btn.active { + background: linear-gradient(135deg, #4285F4 0%, #667eea 100%); +} + +/* 加载状态 */ +.loading-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loading-content { + text-align: center; +} + +.loading-text { + font-size: 28rpx; + color: #666; +} + diff --git a/src5/code/miniprogram/pages/forum/forum.js b/src5/code/miniprogram/pages/forum/forum.js new file mode 100644 index 0000000..bfd093f --- /dev/null +++ b/src5/code/miniprogram/pages/forum/forum.js @@ -0,0 +1,293 @@ +// pages/forum/forum.js +Page({ + /** + * 页面的初始数据 + */ + data: { + // 当前分类 + currentCategory: 'all', + // 分类列表 + categories: [ + { label: '全部', value: 'all' }, + { label: '学习交流', value: 'study' }, + { label: '生活分享', value: 'life' }, + { label: '二手交易', value: 'trade' }, + { label: '校园活动', value: 'activity' }, + { label: '求助问答', value: 'help' }, + { label: '其他', value: 'other' } + ], + // 帖子列表 + posts: [], + // 分页 + page: 0, + pageSize: 10, + hasMore: true, + // 加载状态 + loading: true, + refreshing: false + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + this.loadPosts(); + }, + + /** + * 确保有openid + */ + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getOpenId' + } + }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { + console.error('获取openid失败:', err); + } + } + return openid; + }, + + /** + * 加载帖子列表 + */ + async loadPosts(refresh = false) { + if (refresh) { + this.setData({ + page: 0, + posts: [], + hasMore: true + }); + } + + if (!this.data.hasMore && !refresh) { + return; + } + + this.setData({ + loading: true + }); + + try { + const db = wx.cloud.database(); + const _ = db.command; + + // 构建查询条件 + let queryCondition = {}; + + // 根据分类筛选 + if (this.data.currentCategory !== 'all') { + queryCondition.category = this.data.currentCategory; + } + + // 查询帖子 + const result = await db.collection('T_forum_post') + .where(queryCondition) + .orderBy('createTime', 'desc') + .skip(this.data.page * this.data.pageSize) + .limit(this.data.pageSize) + .get(); + + // 获取作者信息和格式化数据 + const posts = await Promise.all(result.data.map(async (post) => { + // 获取作者信息 + let authorInfo = { + name: '匿名用户', + avatar: 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U' + }; + + if (post.authorOpenId) { + try { + const userResult = await db.collection('T_user') + .where({ + _openid: post.authorOpenId + }) + .get(); + + if (userResult.data && userResult.data.length > 0) { + const user = userResult.data[0]; + authorInfo = { + name: user.sname || user.nickName || '用户', + avatar: user.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U' + }; + } + } catch (err) { + console.error('获取作者信息失败:', err); + } + } + + // 格式化时间 + const timeText = this.formatTime(post.createTime); + + // 获取分类文本 + const categoryItem = this.data.categories.find(cat => cat.value === post.category); + const categoryText = categoryItem ? categoryItem.label : '其他'; + + return { + ...post, + authorInfo: authorInfo, + timeText: timeText, + categoryText: categoryText, + images: Array.isArray(post.images) ? post.images : (post.images ? [post.images] : []), + viewCount: post.viewCount || 0, + commentCount: post.commentCount || 0, + likeCount: post.likeCount || 0 + }; + })); + + this.setData({ + posts: refresh ? posts : [...this.data.posts, ...posts], + page: this.data.page + 1, + hasMore: result.data.length === this.data.pageSize, + loading: false, + refreshing: false + }); + } catch (err) { + console.error('加载帖子失败:', err); + + // 如果集合不存在,创建空列表 + if (err.errMsg && (err.errMsg.includes('not exist') || err.errMsg.includes('not exists') || err.errCode === -502005)) { + this.setData({ + posts: [], + loading: false, + refreshing: false, + hasMore: false + }); + } else { + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + this.setData({ + loading: false, + refreshing: false + }); + } + } + }, + + /** + * 格式化时间 + */ + formatTime(date) { + if (!date) return ''; + const d = new Date(date); + const now = new Date(); + const diff = now - d; + + const minute = 60 * 1000; + const hour = 60 * minute; + const day = 24 * hour; + + if (diff < minute) { + return '刚刚'; + } else if (diff < hour) { + return Math.floor(diff / minute) + '分钟前'; + } else if (diff < day) { + return Math.floor(diff / hour) + '小时前'; + } else if (diff < 7 * day) { + return Math.floor(diff / day) + '天前'; + } else { + return `${d.getMonth() + 1}月${d.getDate()}日`; + } + }, + + /** + * 分类切换 + */ + onCategoryChange(e) { + const category = e.currentTarget.dataset.category; + if (category === this.data.currentCategory) { + return; + } + + this.setData({ + currentCategory: category + }); + + this.loadPosts(true); + }, + + /** + * 下拉刷新 + */ + onRefresh() { + this.setData({ + refreshing: true + }); + this.loadPosts(true); + }, + + /** + * 加载更多 + */ + onLoadMore() { + if (!this.data.loading && this.data.hasMore) { + this.loadPosts(); + } + }, + + /** + * 点击帖子 + */ + onPostTap(e) { + const postId = e.currentTarget.dataset.id; + wx.navigateTo({ + url: `/pages/forum-detail/forum-detail?id=${postId}` + }); + }, + + /** + * 搜索 + */ + onSearchTap() { + wx.navigateTo({ + url: '/pages/forum-search/forum-search' + }); + }, + + /** + * 发布帖子 + */ + onPublishTap() { + wx.navigateTo({ + url: '/pages/forum-publish/forum-publish' + }); + }, + + /** + * 预览图片 + */ + onPreviewImage(e) { + const urls = e.currentTarget.dataset.urls; + const current = e.currentTarget.dataset.current; + + wx.previewImage({ + urls: urls, + current: current + }); + }, + + /** + * 生命周期函数--监听页面显示 + */ + onShow() { + // 如果从发布页面返回,刷新列表 + const pages = getCurrentPages(); + const prevPage = pages[pages.length - 2]; + if (prevPage && prevPage.route === 'pages/forum-publish/forum-publish') { + this.loadPosts(true); + } + } +}); + diff --git a/src5/code/miniprogram/pages/forum/forum.json b/src5/code/miniprogram/pages/forum/forum.json new file mode 100644 index 0000000..f4247ee --- /dev/null +++ b/src5/code/miniprogram/pages/forum/forum.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "校园论坛", + "navigationBarBackgroundColor": "#4285F4", + "navigationBarTextStyle": "white", + "backgroundColor": "#f5f5f5", + "enablePullDownRefresh": true +} + diff --git a/src5/code/miniprogram/pages/forum/forum.wxml b/src5/code/miniprogram/pages/forum/forum.wxml new file mode 100644 index 0000000..8e361bd --- /dev/null +++ b/src5/code/miniprogram/pages/forum/forum.wxml @@ -0,0 +1,112 @@ + + + + + + 🔍 + 搜索帖子... + + + ✏️ + 发帖 + + + + + + + + {{item.label}} + + + + + + + + + + + + {{item.categoryText}} + + + + + {{item.title}} + {{item.content}} + + + + + + + + + + + + + + 加载中... + + + 没有更多了 + + + + + 📝 + 暂无帖子 + 快来发布第一条帖子吧~ + + + + + + + 加载中... + + + + diff --git a/src5/code/miniprogram/pages/forum/forum.wxss b/src5/code/miniprogram/pages/forum/forum.wxss new file mode 100644 index 0000000..af55a34 --- /dev/null +++ b/src5/code/miniprogram/pages/forum/forum.wxss @@ -0,0 +1,265 @@ +/* pages/forum/forum.wxss */ +.page-container { + height: 100vh; + display: flex; + flex-direction: column; + background-color: #f5f5f5; +} + +/* 顶部搜索栏 */ +.forum-header { + background: linear-gradient(135deg, #4285F4 0%, #667eea 100%); + padding: 20rpx 30rpx; + padding-top: calc(20rpx + env(safe-area-inset-top)); + display: flex; + align-items: center; + gap: 20rpx; +} + +.search-bar { + flex: 1; + display: flex; + align-items: center; + background: rgba(255, 255, 255, 0.9); + border-radius: 50rpx; + padding: 15rpx 25rpx; +} + +.search-icon { + font-size: 28rpx; + margin-right: 15rpx; +} + +.search-placeholder { + flex: 1; + font-size: 28rpx; + color: #999; +} + +.publish-btn { + display: flex; + align-items: center; + background: rgba(255, 255, 255, 0.9); + border-radius: 50rpx; + padding: 15rpx 25rpx; + color: #4285F4; + font-weight: 600; +} + +.publish-icon { + font-size: 28rpx; + margin-right: 8rpx; +} + +.publish-text { + font-size: 28rpx; +} + +/* 分类标签 */ +.category-scroll { + white-space: nowrap; + background: white; + border-bottom: 1rpx solid #e0e0e0; +} + +.category-list { + display: inline-flex; + padding: 20rpx 0; +} + +.category-item { + display: inline-block; + padding: 10rpx 30rpx; + margin: 0 10rpx; + border-radius: 50rpx; + background: #f5f5f5; + color: #666; + font-size: 26rpx; + white-space: nowrap; + transition: all 0.3s ease; +} + +.category-item.active { + background: linear-gradient(135deg, #4285F4 0%, #667eea 100%); + color: white; +} + +/* 帖子列表 */ +.post-list { + flex: 1; + padding: 20rpx; +} + +.post-item { + background: white; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); +} + +/* 帖子头部 */ +.post-header { + display: flex; + align-items: center; + margin-bottom: 20rpx; +} + +.user-avatar { + width: 64rpx; + height: 64rpx; + border-radius: 50%; + margin-right: 20rpx; +} + +.user-info { + flex: 1; + display: flex; + flex-direction: column; +} + +.username { + font-size: 28rpx; + font-weight: 600; + color: #333; + margin-bottom: 8rpx; +} + +.post-time { + font-size: 22rpx; + color: #999; +} + +.category-tag { + padding: 8rpx 20rpx; + background: linear-gradient(135deg, #4285F4 0%, #667eea 100%); + color: white; + border-radius: 20rpx; + font-size: 22rpx; +} + +/* 帖子内容 */ +.post-content { + margin-bottom: 20rpx; +} + +.post-title { + font-size: 32rpx; + font-weight: 600; + color: #333; + display: block; + margin-bottom: 15rpx; + line-height: 1.5; +} + +.post-text { + font-size: 28rpx; + color: #666; + line-height: 1.8; + display: block; + margin-bottom: 15rpx; + word-break: break-all; +} + +/* 帖子图片 */ +.post-images { + display: flex; + flex-wrap: wrap; + gap: 10rpx; + margin-top: 15rpx; +} + +.post-image { + width: 200rpx; + height: 200rpx; + border-radius: 10rpx; +} + +.post-image.single { + width: 100%; + max-width: 500rpx; + height: auto; + max-height: 600rpx; +} + +/* 帖子统计 */ +.post-footer { + display: flex; + align-items: center; + gap: 40rpx; + padding-top: 20rpx; + border-top: 1rpx solid #f0f0f0; +} + +.post-stat { + display: flex; + align-items: center; + gap: 8rpx; +} + +.stat-icon { + font-size: 28rpx; +} + +.stat-text { + font-size: 24rpx; + color: #999; +} + +/* 加载更多 */ +.load-more, +.no-more { + text-align: center; + padding: 30rpx 0; + color: #999; + font-size: 24rpx; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 0; +} + +.empty-icon { + font-size: 120rpx; + margin-bottom: 30rpx; +} + +.empty-text { + font-size: 32rpx; + color: #666; + margin-bottom: 15rpx; +} + +.empty-tip { + font-size: 26rpx; + color: #999; +} + +/* 加载状态 */ +.loading-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.loading-content { + text-align: center; +} + +.loading-text { + font-size: 28rpx; + color: #666; +} + diff --git a/src5/code/miniprogram/pages/index/index.js b/src5/code/miniprogram/pages/index/index.js new file mode 100644 index 0000000..67c599a --- /dev/null +++ b/src5/code/miniprogram/pages/index/index.js @@ -0,0 +1,235 @@ +// index.js +Page({ + data: { + studentId: '', + password: '', + isLoginDisabled: false + }, + + onLoad() { + // 检查本地存储中是否有登录信息 + this.checkLoginStatus(); + }, + + // 检查登录状态 + checkLoginStatus() { + const token = wx.getStorageSync('token'); + const userInfo = wx.getStorageSync('userInfo'); + + if (token && userInfo) { + // 如果已登录,跳转到主页面 + wx.switchTab({ + url: '/pages/main/main' + }); + } + }, + + // 学号/手机号输入处理 + onInputStudentId(e) { + const studentId = e.detail.value.trim(); + this.setData({ + studentId: studentId + }); + this.checkLoginButton(); + }, + + // 密码输入处理 + onInputPassword(e) { + const password = e.detail.value.trim(); + this.setData({ + password: password + }); + this.checkLoginButton(); + }, + + // 检查登录按钮状态 + checkLoginButton() { + // 按钮始终保持可用状态 + this.setData({ + isLoginDisabled: false + }); + }, + + // 登录处理 + onLogin() { + const { studentId, password } = this.data; + + // 简单的前端验证 + if (!this.validateInput(studentId, password)) { + return; + } + + // 显示加载中 + wx.showLoading({ + title: '登录中...', + mask: true + }); + + // 调用云数据库进行真实登录验证 + this.realLogin(studentId, password); + }, + + // 输入验证 + validateInput(studentId, password) { + if (!studentId) { + wx.showToast({ + title: '请输入学号或手机号', + icon: 'none' + }); + return false; + } + + if (!password) { + wx.showToast({ + title: '请输入密码', + icon: 'none' + }); + return false; + } + + if (password.length < 6) { + wx.showToast({ + title: '密码长度不能少于6位', + icon: 'none' + }); + return false; + } + + return true; + }, + + // 真实登录验证(查询云数据库) + realLogin(loginId, password) { + const db = wx.cloud.database(); + + // 查询数据库中的T_user表 + db.collection('T_user').where({ + $or: [ + { phone: loginId }, // 手机号匹配 + { sno: loginId } // 学号匹配 + ], + password: password // 密码匹配 + }).get({ + success: (res) => { + wx.hideLoading(); + + if (res.data.length > 0) { + // 登录成功 + const userData = res.data[0]; + const userInfo = { + _id: userData._id, + sno: userData.sno, + sname: userData.sname, + phone: userData.phone, + major: userData.major, + sushe: userData.sushe, + grade: userData.年级, + avatar: userData.avatar || 'https://via.placeholder.com/100x100/4CAF50/ffffff?text=U' + }; + + // 清除旧的用户相关缓存(切换账号时清除) + wx.removeStorageSync('openid'); + wx.removeStorageSync('userStats'); + wx.removeStorageSync('notificationEnabled'); + + // 保存登录信息到本地存储 + wx.setStorageSync('token', 'user_token_' + userData._id); + wx.setStorageSync('userInfo', userInfo); + + wx.showToast({ + title: '登录成功', + icon: 'success', + duration: 1500 + }); + + // 跳转到主页面 + setTimeout(() => { + wx.switchTab({ + url: '/pages/main/main' + }); + }, 1500); + } else { + // 登录失败 + wx.showToast({ + title: '账号或密码错误', + icon: 'none' + }); + } + }, + fail: (err) => { + wx.hideLoading(); + console.error('登录查询失败:', err); + wx.showToast({ + title: '网络错误,请重试', + icon: 'none' + }); + } + }); + }, + + // 注册处理 + onRegister() { + wx.navigateTo({ + url: '/pages/register/register' + }); + }, + + // 忘记密码处理 + onForgotPassword() { + wx.navigateTo({ + url: '/pages/forgot-password/forgot-password' + }); + }, + + // 管理员登录 + onAdminLogin() { + wx.navigateTo({ + url: '/pages/admin-login/admin-login' + }); + }, + + // 微信一键登录 + onGetPhoneNumber(e) { + if (e.detail.errMsg === 'getPhoneNumber:ok') { + // 用户同意授权 + const { encryptedData, iv } = e.detail; + + wx.showLoading({ + title: '登录中...', + mask: true + }); + + // 模拟微信登录 + setTimeout(() => { + const userInfo = { + studentId: 'wx_' + Date.now().toString().slice(-6), + nickname: '微信用户', + avatar: 'https://via.placeholder.com/100x100/07C160/ffffff?text=W', + campus: '微信用户', + department: '微信用户' + }; + + wx.setStorageSync('token', 'wx_token_' + Date.now()); + wx.setStorageSync('userInfo', userInfo); + + wx.hideLoading(); + wx.showToast({ + title: '微信登录成功', + icon: 'success' + }); + + setTimeout(() => { + wx.switchTab({ + url: '/pages/main/main' + }); + }, 1500); + }, 1500); + } else { + // 用户拒绝授权 + wx.showToast({ + title: '授权失败', + icon: 'none' + }); + } + } +}) diff --git a/src5/code/miniprogram/pages/index/index.json b/src5/code/miniprogram/pages/index/index.json new file mode 100644 index 0000000..b55b5a2 --- /dev/null +++ b/src5/code/miniprogram/pages/index/index.json @@ -0,0 +1,4 @@ +{ + "usingComponents": { + } +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/index/index.wxml b/src5/code/miniprogram/pages/index/index.wxml new file mode 100644 index 0000000..7670742 --- /dev/null +++ b/src5/code/miniprogram/pages/index/index.wxml @@ -0,0 +1,76 @@ + + + + diff --git a/src5/code/miniprogram/pages/index/index.wxss b/src5/code/miniprogram/pages/index/index.wxss new file mode 100644 index 0000000..b8c9bf4 --- /dev/null +++ b/src5/code/miniprogram/pages/index/index.wxss @@ -0,0 +1,249 @@ +/**index.wxss**/ +page { + height: 100vh; + position: relative; +} + +.page-container { + height: 100vh; + position: relative; +} + +.background-image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; +} + +.login-container { + flex: 1; + display: flex; + flex-direction: column; + padding: 60rpx 40rpx 40rpx; + box-sizing: border-box; +} + +/* 头部样式 */ +.header { + text-align: center; + margin-bottom: 80rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.logo { + width: 200rpx; + height: 200rpx; + border-radius: 80rpx; + background: rgba(255, 255, 255, 0.2); + margin-bottom: 30rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.title { + display: block; + font-size: 50rpx; + font-weight: bold; + color: rgb(0, 0, 0); + margin-bottom: 16rpx; +} + +.subtitle { + display: block; + font-size: 40rpx; + color: rgba(0, 0, 0, 0.8); +} + +/* 登录表单样式 */ +.login-form { + background: rgba(255, 254, 254, 0.95); + border-radius: 24rpx; + padding: 60rpx 40rpx; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.15); + margin-bottom: 40rpx; + backdrop-filter: blur(10rpx); + border: 1rpx solid rgba(255, 255, 255, 0.3); +} + +.form-group { + margin-bottom: 40rpx; +} + +.label { + display: block; + font-size: 28rpx; + color: #333333; + margin-bottom: 20rpx; + font-weight: 500; +} + +.input { + width: 100%; + height: 80rpx; + border: 2rpx solid #e0e0e0; + border-radius: 12rpx; + padding: 0 24rpx; + font-size: 28rpx; + box-sizing: border-box; + background: #fafafa; + transition: all 0.3s ease; +} + +.input:focus { + border-color: #ffffff; + background: #ffffff; + box-shadow: 0 0 0 4rpx rgba(76, 175, 80, 0.1); +} + +/* 登录按钮样式 */ +.login-btn { + width: 100%; + height: 88rpx; + background: linear-gradient(135deg, rgb(118, 116, 247) 0%, rgb(148, 125, 250) 100%); + border-radius: 44rpx; + color: #ffffff; + font-size: 32rpx; + font-weight: 600; + margin-top: 20rpx; + transition: all 0.3s ease; +} + +.login-btn:active { + transform: scale(0.98); + opacity: 0.9; +} + +.login-btn.disabled { + background: #cccccc; + color: #999999; + transform: none; + opacity: 0.6; +} + +/* 注册链接样式 */ +.register-link { + text-align: center; + margin: 40rpx 0 20rpx; + font-size: 26rpx; + color: #666666; +} + +.link { + color: #a4acefff; + font-weight: 500; + margin-left: 10rpx; +} + +.link:active { + opacity: 0.7; +} + +/* 忘记密码样式 */ +.forgot-password { + text-align: center; + margin-bottom: 20rpx; +} + +/* 管理员登录链接样式 */ +.admin-login-link { + text-align: center; + margin-top: 20rpx; +} + +.admin-login-link .link { + color: #667eea; + font-weight: 500; + font-size: 26rpx; +} + +/* 快速登录样式 */ +.quick-login { + background: rgba(255, 255, 255, 0.1); + border-radius: 24rpx; + padding: 40rpx; + backdrop-filter: blur(10rpx); +} + +.divider { + position: relative; + text-align: center; + margin-bottom: 40rpx; +} + +.divider::before { + content: ''; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1rpx; + background: rgba(255, 255, 255, 0.3); +} + +.divider-text { + display: inline-block; + background: rgba(255, 255, 255, 0.1); + padding: 0 20rpx; + font-size: 24rpx; + color: rgba(255, 255, 255, 0.8); + position: relative; +} + +.quick-login-buttons { + display: flex; + justify-content: center; +} + +.quick-btn { + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.9); + border: none; + border-radius: 44rpx; + padding: 20rpx 40rpx; + font-size: 28rpx; + color: #333333; + transition: all 0.3s ease; +} + +.quick-btn:active { + transform: scale(0.95); + background: rgba(255, 255, 255, 1); +} + +.quick-icon { + width: 40rpx; + height: 40rpx; + margin-right: 16rpx; +} + +.quick-btn.wechat { + background: rgba(255, 255, 255, 0.9); +} + +.quick-btn.wechat:active { + background: rgba(255, 255, 255, 1); +} + +/* 响应式设计 */ +@media (max-width: 750rpx) { + .login-container { + padding: 40rpx 30rpx 30rpx; + } + + .login-form { + padding: 40rpx 30rpx; + } + + .header { + margin-bottom: 60rpx; + } +} diff --git a/src5/code/miniprogram/pages/interests/interests.js b/src5/code/miniprogram/pages/interests/interests.js new file mode 100644 index 0000000..755768c --- /dev/null +++ b/src5/code/miniprogram/pages/interests/interests.js @@ -0,0 +1,448 @@ +// pages/interests/interests.js +const reco = require('../../utils/recommendation.js'); + +Page({ + + /** + * 页面的初始数据 + */ + data: { + // 商品类别数据(默认类别,使用中文类别名作为ID) + // 注意:类别名称必须与数据库中T_product表的productCategory字段值一致 + categories: [ + { + id: '电子产品', + name: '电子产品', + description: '手机、电脑、配件', + icon: '💻', + iconBg: '#2196F3' + }, + { + id: '图书文具', + name: '图书文具', + description: '教材、小说、专业书籍、文具', + icon: '📖', + iconBg: '#4CAF50' + }, + { + id: '服装鞋帽', + name: '服装鞋帽', + description: '衣服、鞋子、配饰', + icon: '👔', + iconBg: '#FF9800' + }, + { + id: '家居用品', + name: '家居用品', + description: '桌椅、小家电、生活用品', + icon: '🏠', + iconBg: '#795548' + }, + { + id: '运动户外', + name: '运动户外', + description: '健身器材、球类运动、户外用品', + icon: '🏃', + iconBg: '#9C27B0' + }, + { + id: '美妆个护', + name: '美妆个护', + description: '美妆护肤产品', + icon: '💅', + iconBg: '#FF6B9D' + }, + { + id: '其他', + name: '其他', + description: '其他各类商品', + icon: '📦', + iconBg: '#757575' + } + ], + // 已选择的商品类别 + selectedInterests: [], + // 类别选中状态映射 + categorySelectedMap: {} + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + // 延迟执行所有初始化操作,避免立即执行导致的问题 + setTimeout(() => { + try { + // 先初始化选中状态映射(使用默认类别) + this.updateCategoriesSelection(); + + // 加载商品类别列表(已禁用云函数调用) + this.loadCategories(); + + // 从本地存储获取用户信息,检查是否已有选择的兴趣 + const userInfo = wx.getStorageSync('userInfo'); + if (userInfo && userInfo.interests && userInfo.interests.length > 0) { + this.setData({ + selectedInterests: userInfo.interests + }); + // 更新选中状态 + this.updateCategoriesSelection(); + } + } catch (e) { + console.error('onLoad初始化失败:', e); + } + }, 100); + }, + + /** + * 从云数据库加载商品类别 + */ + loadCategories() { + // 暂时禁用云函数调用,直接使用前端默认类别数据 + // 这样可以确保图标正确显示 + console.log('使用前端默认类别数据'); + + // 直接使用data中定义的默认类别 + this.setData({ + categories: this.data.categories + }); + + // 更新选中状态映射 + this.updateCategoriesSelection(); + + // 如果需要从云数据库加载,取消下面的注释 + /* + wx.showLoading({ + title: '加载类别中...', + mask: true + }); + + wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getProductCategories' + }, + success: (res) => { + wx.hideLoading(); + console.log('获取商品类别成功:', res); + + if (res.result && res.result.success && res.result.data && res.result.data.length > 0) { + // 使用从数据库获取的类别,但强制使用前端图标映射 + const iconMap = { + '电子产品': '💻', + '图书文具': '📖', + '服装鞋帽': '👔', + '家居用品': '🏠', + '运动户外': '🏃', + '美妆个护': '💅', + '其他': '📦' + }; + + const categories = res.result.data.map(item => ({ + id: item.id || item.name, + name: item.name, + description: item.description || '各类商品', + icon: iconMap[item.name] || item.icon || '📦', // 优先使用前端图标映射 + iconBg: item.iconBg || '#757575' + })); + + console.log('从数据库获取的类别:', categories); + + this.setData({ + categories: categories + }); + + // 更新选中状态映射 + this.updateCategoriesSelection(); + } else { + console.warn('数据库中没有商品类别,使用默认类别'); + } + }, + fail: (err) => { + wx.hideLoading(); + console.error('获取商品类别失败:', err); + } + }); + */ + }, + + /** + * 更新商品类别的选中状态 + */ + updateCategoriesSelection() { + try { + const categorySelectedMap = {}; + const categories = this.data.categories || []; + const selectedInterests = this.data.selectedInterests || []; + + categories.forEach(category => { + // 检查是否选中(支持多种匹配方式) + const isSelected = selectedInterests.some(selected => { + // 直接匹配ID + if (selected === category.id) return true; + // 匹配名称 + if (selected === category.name) return true; + return false; + }); + + categorySelectedMap[category.id] = isSelected; + }); + + // 使用同步方式更新,不传入任何回调 + this.setData({ + categorySelectedMap: categorySelectedMap + }); + } catch (e) { + console.error('更新选中状态失败:', e); + } + }, + + /** + * 商品类别点击事件 + */ + onCategoryTap(e) { + // 立即阻止所有默认行为和事件冒泡 + if (e) { + try { + if (typeof e.preventDefault === 'function') e.preventDefault(); + if (typeof e.stopPropagation === 'function') e.stopPropagation(); + if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation(); + } catch (err) { + // 静默处理 + } + } + + // 获取类别ID + let categoryId = null; + try { + categoryId = e && e.currentTarget && e.currentTarget.dataset && e.currentTarget.dataset.id; + } catch (err) { + return false; + } + + if (!categoryId) { + return false; + } + + // 使用防抖,避免快速连续点击 + const now = Date.now(); + if (this._lastTapTime && now - this._lastTapTime < 300) { + return false; + } + this._lastTapTime = now; + + // 复制当前选择列表 + let selectedInterests = [...(this.data.selectedInterests || [])]; + + // 切换选择状态 + let index = selectedInterests.indexOf(categoryId); + + if (index === -1) { + const category = (this.data.categories || []).find(cat => cat.id === categoryId); + if (category) { + index = selectedInterests.findIndex(selected => selected === category.name || selected === category.id); + } + } + + if (index > -1) { + selectedInterests.splice(index, 1); + } else { + if (selectedInterests.length >= 2) { + wx.showToast({ + title: '最多只能选择2种商品类别', + icon: 'none', + duration: 2000 + }); + return false; + } + selectedInterests.push(categoryId); + } + + // 直接计算选中状态映射 + const categorySelectedMap = {}; + (this.data.categories || []).forEach(category => { + categorySelectedMap[category.id] = selectedInterests.indexOf(category.id) > -1 || + selectedInterests.indexOf(category.name) > -1; + }); + + // 直接同步更新数据,不使用任何延迟或回调 + try { + this.setData({ + selectedInterests: selectedInterests, + categorySelectedMap: categorySelectedMap + }); + } catch (error) { + console.error('更新数据失败:', error); + } + + return false; + }, + + /** + * 保存用户兴趣到数据库 + */ + saveUserInterests(interests) { + return new Promise((resolve, reject) => { + wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'updateUserInterests', + interests: interests + }, + success: (res) => { + console.log('保存用户兴趣成功:', res); + if (res.result && res.result.success) { + // 更新本地存储的用户信息 + const userInfo = wx.getStorageSync('userInfo') || {}; + userInfo.interests = interests; + wx.setStorageSync('userInfo', userInfo); + try { reco.recordInterests(interests); } catch (e) {} + resolve(res.result); + } else { + // 即使云函数失败,也保存到本地,确保用户可以继续使用 + const userInfo = wx.getStorageSync('userInfo') || {}; + userInfo.interests = interests; + wx.setStorageSync('userInfo', userInfo); + console.warn('云函数保存失败,已保存到本地:', res.result?.error); + try { reco.recordInterests(interests); } catch (e) {} + resolve({ success: true, localOnly: true }); + } + }, + fail: (err) => { + console.error('保存用户兴趣失败:', err); + // 即使云函数失败,也保存到本地,确保用户可以继续使用 + const userInfo = wx.getStorageSync('userInfo') || {}; + userInfo.interests = interests; + wx.setStorageSync('userInfo', userInfo); + console.warn('云函数调用失败,已保存到本地'); + try { reco.recordInterests(interests); } catch (e) {} + resolve({ success: true, localOnly: true }); + } + }); + }); + }, + + /** + * 确认选择 + */ + onConfirm() { + if (this.data.selectedInterests.length === 0) { + wx.showToast({ + title: '请至少选择一个商品类别', + icon: 'none' + }); + return; + } + + // 显示加载中 + wx.showLoading({ + title: '保存兴趣中...', + }); + + // 保存用户兴趣到数据库 + this.saveUserInterests(this.data.selectedInterests) + .then(() => { + wx.hideLoading(); + // 显示注册成功提示 + wx.showToast({ + title: '兴趣保存成功', + icon: 'success', + duration: 2000 + }); + + // 直接切换到首页 Tab + wx.switchTab({ url: '/pages/main/main' }); + }) + .catch((err) => { + wx.hideLoading(); + console.error('保存兴趣失败:', err); + wx.showToast({ + title: '保存失败,请重试', + icon: 'none' + }); + }); + }, + + /** + * 跳过选择 + */ + onSkip() { + // 显示加载中 + wx.showLoading({ + title: '处理中...', + }); + + // 保存空兴趣数组到数据库 + this.saveUserInterests([]) + .then(() => { + wx.hideLoading(); + // 显示注册成功提示 + wx.showToast({ + title: '注册成功', + icon: 'success', + duration: 2000 + }); + + // 直接切换到首页 Tab + wx.switchTab({ url: '/pages/main/main' }); + }) + .catch((err) => { + wx.hideLoading(); + console.error('跳过选择失败:', err); + wx.showToast({ + title: '处理失败,请重试', + icon: 'none' + }); + }); + }, + + /** + * 生命周期函数--监听页面初次渲染完成 + */ + onReady() { + + }, + + /** + * 生命周期函数--监听页面显示 + */ + onShow() { + // 完全禁用onShow中的所有操作,避免任何可能的跳转或闪烁 + // 不执行任何操作,确保页面稳定 + }, + + /** + * 生命周期函数--监听页面隐藏 + */ + onHide() { + + }, + + /** + * 生命周期函数--监听页面卸载 + */ + onUnload() { + + }, + + /** + * 页面相关事件处理函数--监听用户下拉动作 + */ + onPullDownRefresh() { + + }, + + /** + * 页面上拉触底事件的处理函数 + */ + onReachBottom() { + + }, + + /** + * 用户点击右上角分享 + */ + onShareAppMessage() { + + } +}) \ No newline at end of file diff --git a/src5/code/miniprogram/pages/interests/interests.json b/src5/code/miniprogram/pages/interests/interests.json new file mode 100644 index 0000000..e235707 --- /dev/null +++ b/src5/code/miniprogram/pages/interests/interests.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "选择兴趣", + "enablePullDownRefresh": false +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/interests/interests.wxml b/src5/code/miniprogram/pages/interests/interests.wxml new file mode 100644 index 0000000..4adbb7b --- /dev/null +++ b/src5/code/miniprogram/pages/interests/interests.wxml @@ -0,0 +1,74 @@ + + + + + + + 🎯 + + 欢迎来到校园二手交易平台! + 请选择您感兴趣的商品类别 + 我们将根据您的选择为您推荐相关商品 + + + + + 选择您感兴趣的商品类别 + (最多选择2个,用于个性化商品推荐) + + + + + {{category.icon}} + + {{category.name}} + {{category.description}} + + + + + + 正在加载类别... + + + + + + 已选择 {{selectedInterests.length}}/2 个商品类别 + 系统将根据您的选择为您推荐相关商品 + + + + + + + + + + + + 完成此步骤后,即可开始使用平台 + + + \ No newline at end of file diff --git a/src5/code/miniprogram/pages/interests/interests.wxss b/src5/code/miniprogram/pages/interests/interests.wxss new file mode 100644 index 0000000..203ff63 --- /dev/null +++ b/src5/code/miniprogram/pages/interests/interests.wxss @@ -0,0 +1,318 @@ +/* pages/interests/interests.wxss */ +.page-container { + min-height: 100vh; + background: linear-gradient(135deg, #7672fdff 0%, #9bc3ffff 100%); + padding: 40rpx 30rpx; + box-sizing: border-box; +} + +.interests-container { + background: white; + border-radius: 20rpx; + padding: 40rpx 30rpx; + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1); +} + +/* 头部样式 */ +.header { + text-align: center; + margin-bottom: 50rpx; + padding-top: 20rpx; +} + +.welcome-icon-wrapper { + width: 140rpx; + height: 140rpx; + margin: 0 auto 25rpx; + background: linear-gradient(135deg, #4285F4 0%, #34A853 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 20rpx rgba(66, 133, 244, 0.3); +} + +.welcome-emoji { + font-size: 80rpx; + line-height: 1; +} + +.title { + display: block; + font-size: 38rpx; + font-weight: bold; + color: #333; + margin-bottom: 15rpx; +} + +.question { + display: block; + font-size: 32rpx; + color: #4285F4; + font-weight: 600; + margin-bottom: 10rpx; +} + +.subtitle { + display: block; + font-size: 26rpx; + color: #999; + margin-top: 10rpx; +} + +/* 商品类别选择区域 */ +.interests-section { + margin-bottom: 40rpx; +} + +.section-title { + display: block; + font-size: 32rpx; + font-weight: bold; + color: #333; + margin-bottom: 10rpx; +} + +.section-subtitle { + display: block; + font-size: 26rpx; + color: #999; + margin-bottom: 30rpx; +} + +.interests-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20rpx; +} + +.interest-card { + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + border: 3rpx solid #e9ecef; + border-radius: 20rpx; + padding: 35rpx 20rpx; + text-align: center; + transition: all 0.3s ease; + cursor: pointer; + position: relative; + overflow: hidden; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); +} + +.interest-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 6rpx; + background: linear-gradient(90deg, #4285F4 0%, #34A853 50%, #EA4335 100%); + opacity: 0; + transition: opacity 0.3s ease; +} + +.interest-card:active { + transform: scale(0.95); +} + +.interest-card.selected { + background: linear-gradient(135deg, #4285F4 0%, #5C9FF7 100%); + border-color: #4285F4; + transform: translateY(-5rpx); + box-shadow: 0 12rpx 30rpx rgba(66, 133, 244, 0.4); +} + +.interest-card.selected::before { + opacity: 1; +} + +.interest-card.selected .interest-name, +.interest-card.selected .interest-desc { + color: white; +} + +.interest-card.selected .interest-icon-wrapper { + transform: scale(1.15) rotate(5deg); + box-shadow: 0 12rpx 32rpx rgba(66, 133, 244, 0.4), 0 0 0 4rpx rgba(255, 255, 255, 0.3); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.85)) !important; + animation: iconPulse 0.6s ease-out; +} + +.interest-card.selected .interest-icon { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.9)) !important; + transform: scale(1.2) rotate(-5deg); + box-shadow: 0 8rpx 24rpx rgba(66, 133, 244, 0.5), inset 0 2rpx 8rpx rgba(255, 255, 255, 0.5); + filter: drop-shadow(0 4rpx 8rpx rgba(66, 133, 244, 0.3)); +} + +@keyframes iconPulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.25) rotate(8deg); + } + 100% { + transform: scale(1.15) rotate(5deg); + } +} + +.interest-icon-wrapper { + width: 120rpx; + height: 120rpx; + margin: 0 auto 20rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 28rpx; + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + position: relative; + overflow: hidden; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.7)); + box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.12); +} + +.interest-icon-wrapper::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.1)); + pointer-events: none; + border-radius: 28rpx; + opacity: 0; + transition: opacity 0.3s ease; +} + +.interest-card:active .interest-icon-wrapper::before { + opacity: 1; +} + +.interest-icon { + width: 96rpx; + height: 96rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 56rpx; + line-height: 1; + text-align: center; + border-radius: 24rpx; + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15), inset 0 2rpx 4rpx rgba(255, 255, 255, 0.3); + position: relative; + z-index: 1; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.85)); + backdrop-filter: blur(10rpx); + font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Android Emoji", sans-serif; + -webkit-font-smoothing: antialiased; + user-select: none; + white-space: nowrap; + overflow: hidden; +} + +.interest-name { + display: block; + font-size: 30rpx; + font-weight: 600; + color: #333; + margin-bottom: 8rpx; + transition: color 0.3s ease; +} + +.interest-desc { + display: block; + font-size: 24rpx; + color: #666; + transition: color 0.3s ease; +} + +/* 选择提示 */ +.selection-hint { + background: linear-gradient(135deg, #e8f4fd 0%, #f0f9ff 100%); + border: 2rpx solid #b3e0ff; + border-radius: 15rpx; + padding: 25rpx; + margin-bottom: 30rpx; + text-align: center; + box-shadow: 0 4rpx 12rpx rgba(0, 102, 204, 0.1); +} + +.hint-text { + display: block; + font-size: 30rpx; + color: #0066cc; + font-weight: 600; + margin-bottom: 8rpx; +} + +.hint-tip { + display: block; + font-size: 24rpx; + color: #666; + line-height: 1.6; +} + +/* 底部操作按钮 */ +.action-buttons { + display: flex; + gap: 20rpx; + margin-bottom: 30rpx; +} + +.skip-btn { + flex: 1; + background: #f8f9fa; + color: #666; + border: 2rpx solid #e9ecef; + border-radius: 50rpx; + font-size: 28rpx; + height: 80rpx; + line-height: 80rpx; +} + +.confirm-btn { + flex: 1; + background: linear-gradient(135deg, #4285F4 0%, #34A853 100%); + color: white; + border: none; + border-radius: 50rpx; + font-size: 30rpx; + height: 88rpx; + line-height: 88rpx; + font-weight: 600; + box-shadow: 0 8rpx 20rpx rgba(66, 133, 244, 0.3); + transition: all 0.3s ease; +} + +.confirm-btn:active { + transform: scale(0.98); + box-shadow: 0 4rpx 12rpx rgba(66, 133, 244, 0.2); +} + +.confirm-btn.disabled { + background: #e0e0e0; + color: #999; + box-shadow: none; +} + +/* 进度提示 */ +.progress-hint { + text-align: center; + padding: 20rpx; +} + +.progress-text { + font-size: 24rpx; + color: #999; +} + +/* 空状态提示 */ +.empty-tip { + text-align: center; + padding: 60rpx 20rpx; + color: #999; + font-size: 28rpx; +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/logs/logs.js b/src5/code/miniprogram/pages/logs/logs.js new file mode 100644 index 0000000..85f6aac --- /dev/null +++ b/src5/code/miniprogram/pages/logs/logs.js @@ -0,0 +1,18 @@ +// logs.js +const util = require('../../utils/util.js') + +Page({ + data: { + logs: [] + }, + onLoad() { + this.setData({ + logs: (wx.getStorageSync('logs') || []).map(log => { + return { + date: util.formatTime(new Date(log)), + timeStamp: log + } + }) + }) + } +}) diff --git a/src5/code/miniprogram/pages/logs/logs.json b/src5/code/miniprogram/pages/logs/logs.json new file mode 100644 index 0000000..b55b5a2 --- /dev/null +++ b/src5/code/miniprogram/pages/logs/logs.json @@ -0,0 +1,4 @@ +{ + "usingComponents": { + } +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/logs/logs.wxml b/src5/code/miniprogram/pages/logs/logs.wxml new file mode 100644 index 0000000..85cf1bf --- /dev/null +++ b/src5/code/miniprogram/pages/logs/logs.wxml @@ -0,0 +1,6 @@ + + + + {{index + 1}}. {{log.date}} + + diff --git a/src5/code/miniprogram/pages/logs/logs.wxss b/src5/code/miniprogram/pages/logs/logs.wxss new file mode 100644 index 0000000..33f9d9e --- /dev/null +++ b/src5/code/miniprogram/pages/logs/logs.wxss @@ -0,0 +1,16 @@ +page { + height: 100vh; + display: flex; + flex-direction: column; +} +.scrollarea { + flex: 1; + overflow-y: hidden; +} +.log-item { + margin-top: 20rpx; + text-align: center; +} +.log-item:last-child { + padding-bottom: env(safe-area-inset-bottom); +} diff --git a/src5/code/miniprogram/pages/main/main.js b/src5/code/miniprogram/pages/main/main.js new file mode 100644 index 0000000..531e012 --- /dev/null +++ b/src5/code/miniprogram/pages/main/main.js @@ -0,0 +1,730 @@ +// pages/main/main.js +const reco = require('../../utils/recommendation.js'); +Page({ + + /** + * 页面的初始数据 + */ + data: { + currentTab: 'home', + userInfo: {}, + searchText: '', + hasMessageAlert: false, + + // 推荐商品数据 + recommendProducts: [ + { + id: 1, + name: '二手iPhone 13', + price: '2999', + image: '/images/仓鼠.png', + tag: '电子产品' + }, + { + id: 2, + name: 'Java编程思想', + price: '35', + image: '/images/更多犬种.png', + tag: '二手书' + }, + { + id: 3, + name: '耐克运动鞋', + price: '180', + image: '/images/边牧.png', + tag: '服装鞋帽' + }, + { + id: 4, + name: '戴尔笔记本电脑', + price: '3200', + image: '/images/羊.png', + tag: '电子产品' + } + ], + + // 热门求购数据 + hotWanted: [ + { + id: 1, + title: '求购二手iPad Pro', + budget: '2500', + time: '2小时前' + }, + { + id: 2, + title: '急需高数教材', + budget: '30', + time: '5小时前' + }, + { + id: 3, + title: '求购健身器材', + budget: '200', + time: '1天前' + } + ] + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + this.loadUserInfo(); + this.loadRecommendations(); + this.loadHotWanted(); + }, + + /** + * 生命周期函数--监听页面显示 + */ + onShow() { + this.loadUserInfo(); + this.loadRecommendations(); + this.loadUnreadStatus(); + try { + if (this.getTabBar && this.getTabBar()) { + this.getTabBar().setSelected(0); + } + } catch (e) {} + }, + + /** + * 加载用户信息 + */ + loadUserInfo() { + const userInfo = wx.getStorageSync('userInfo') || {}; + // 兜底替换占位域名,避免渲染层网络超时 + if (typeof userInfo.avatar === 'string' && userInfo.avatar.startsWith('https://via.placeholder.com')) { + userInfo.avatar = '/images/更多犬种.png'; + } + this.setData({ userInfo }); + }, + + /** + * 加载推荐商品(实时:基于兴趣 + 行为权重) + */ + loadRecommendations() { + const userInfo = wx.getStorageSync('userInfo') || {}; + const userInterests = userInfo.interests || []; + const privacy = wx.getStorageSync('privacySettings') || {}; + const allowRecommend = privacy.allowRecommend !== false; + console.log('用户兴趣:', userInterests); + + wx.showLoading({ title: '加载推荐中...', mask: false }); + + if (!allowRecommend) { + wx.hideLoading(); + this.loadDefaultProducts(); + return; + } + + this.loadRecommendationsFromMoreList(userInterests) + .then(() => wx.hideLoading()) + .catch(() => { + this.loadRecommendationsFromBehavior(userInterests) + .then(() => wx.hideLoading()) + .catch(() => { + wx.hideLoading(); + this.loadDefaultProducts(); + }); + }); + }, + + /** + * 基于本地行为权重实时生成推荐 + */ + async loadRecommendationsFromBehavior(userInterests) { + const db = wx.cloud.database(); + const _ = db.command; + const weights = reco.getWeights(); + const last = wx.getStorageSync('lastInteraction') || {}; + // 强优先使用用户选择的兴趣;若为空再回退到行为权重 + let interests = Array.isArray(userInterests) ? userInterests.filter(Boolean) : []; + if (last.category && interests.indexOf(last.category) === -1) interests.unshift(last.category); + if (interests.length === 0) { + const cats = Object.keys(weights.categories || {}).sort((a,b)=> (weights.categories[b]||0) - (weights.categories[a]||0)); + interests = cats.slice(0, 3); + } + interests = [...new Set(interests)].filter(Boolean); + let where = { status: '在售' }; + if (interests.length > 0) { + where = _.and([ + { status: '在售' }, + { productCategory: _.in(interests) } + ]); + } + const res = await db.collection('T_product') + .where(where) + .orderBy('createTime', 'desc') + .limit(100) + .get(); + let raw = res.data || []; + if (last.productId) raw = raw.filter(x => x._id !== last.productId); + const list = raw.map(item => ({ + id: item._id, + name: item.productName || '商品', + price: item.salePrice || item.suggestedPrice || item.originalPrice || 0, + image: Array.isArray(item.productImage) ? (item.productImage[0] || '/images/仓鼠.png') : (item.productImage || '/images/仓鼠.png'), + tag: item.productCategory || '其他', + productName: item.productName, + productDescription: item.productDescription || item.description || '', + productCategory: item.productCategory, + createTime: item.createTime + })); + const sorted = reco.reorderProductsByWeights(list).slice(0, 12); + this.setData({ recommendProducts: sorted }); + wx.hideLoading(); + console.log('行为权重推荐完成,数量:', sorted.length); + }, + + /** + * 与“更多推荐”统一的推荐源:按兴趣过滤,分页取全量后按权重排序,首页取前12个 + */ + async loadRecommendationsFromMoreList(userInterests) { + const interests = await this.resolveInterestsForRecommend(); + const db = wx.cloud.database(); + const _ = db.command; + let where = { status: '在售' }; + if (interests.length > 0) { + where = _.and([{ status: '在售' }, { productCategory: _.in(interests) }]); + } + const countRes = await db.collection('T_product').where(where).count(); + const total = countRes.total || 0; + const pageSize = 100; + const all = []; + for (let skip = 0; skip < Math.max(total, pageSize * 2); skip += pageSize) { + const page = await db.collection('T_product') + .where(where) + .orderBy('createTime', 'desc') + .skip(skip) + .limit(pageSize) + .get(); + if (page && Array.isArray(page.data)) all.push(...page.data); + if (!page || !page.data || page.data.length < pageSize) break; + } + const list = all.map(item => ({ + id: item._id, + name: item.productName || '商品', + price: item.salePrice || item.suggestedPrice || item.originalPrice || 0, + image: Array.isArray(item.productImage) ? (item.productImage[0] || '/images/仓鼠.png') : (item.productImage || '/images/仓鼠.png'), + tag: item.productCategory || '其他', + productName: item.productName, + productDescription: item.productDescription || item.description || '', + productCategory: item.productCategory, + createTime: item.createTime, + viewCount: item.viewCount || 0 + })); + const sorted = reco.reorderProductsByWeights(list); + const top12 = sorted.slice(0, 12); + this.setData({ recommendProducts: top12 }); + }, + + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'getOpenId' } }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (_) {} + } + return openid; + }, + + async resolveInterestsForRecommend() { + const db = wx.cloud.database(); + const userInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = userInfo._id || ''; + let openid = null; + if (loggedInUserId) { + try { + const r = await db.collection('T_user').doc(loggedInUserId).get(); + openid = r.data && r.data._openid; + } catch (_) {} + } + if (!openid) { + openid = await this.ensureOpenId(); + } + let favRes = { data: [] }; + try { + if (loggedInUserId) { + favRes = await db.collection('T_favorites').where({ userId: loggedInUserId }).orderBy('createTime', 'desc').limit(300).get(); + } else if (openid) { + favRes = await db.collection('T_favorites').where({ _openid: openid }).orderBy('createTime', 'desc').limit(300).get(); + } + } catch (_) {} + const counts = {}; + (favRes.data || []).forEach(f => { + const c = f.productCategory || f.category; + if (c) counts[c] = (counts[c] || 0) + 1; + }); + const favTop = Object.keys(counts).sort((a,b)=> (counts[b]||0) - (counts[a]||0)).slice(0,3); + const baseInterests = Array.isArray(userInfo.interests) ? userInfo.interests.filter(Boolean) : []; + const interests = favTop.length > 0 ? favTop : baseInterests; + return [...new Set(interests)].filter(Boolean); + }, + + /** + * 加载空商品数据(当数据库中没有商品时) + */ + loadEmptyProducts() { + const emptyProducts = [ + { + id: 'empty', + name: '暂无推荐商品', + price: '0.00', + tag: '其他', + image: '/images/更多犬种.png', + isEmpty: true + } + ]; + + this.setData({ + recommendProducts: emptyProducts + }); + }, + + /** + * 加载默认商品数据(当云数据库查询失败时) + */ + loadDefaultProducts() { + const defaultProducts = [ + { + id: 'error', + name: '加载失败,请稍后重试', + price: '0.00', + tag: '其他', + image: '/images/仓鼠.png', + isError: true + } + ]; + + this.setData({ + recommendProducts: defaultProducts + }); + + wx.showToast({ + title: '推荐加载失败', + icon: 'none', + duration: 2000 + }); + }, + + /** + * 搜索输入事件 + */ + onSearchInput(e) { + this.setData({ + searchText: e.detail.value + }); + }, + + /** + * 搜索确认:选择搜商品或搜求购并跳转 + */ + onSearchConfirm(e) { + const keyword = (e && e.detail && e.detail.value ? e.detail.value : this.data.searchText || '').trim(); + if (!keyword) { + wx.showToast({ + title: '请输入搜索关键词', + icon: 'none' + }); + return; + } + wx.showActionSheet({ + itemList: ['搜商品', '搜求购'], + success: (res) => { + const idx = res.tapIndex; + const q = encodeURIComponent(keyword); + if (idx === 0) { + // 记录商品搜索行为 + reco.recordSearch(keyword, 'product'); + wx.navigateTo({ + url: `/pages/buy/buy?q=${q}` + }); + } else if (idx === 1) { + // 记录求购搜索行为 + reco.recordSearch(keyword, 'wanted'); + wx.navigateTo({ + url: `/pages/wanted-list/wanted-list?q=${q}` + }); + } + } + }); + }, + + /** + * 功能导航点击事件 + */ + onNavigateTo(e) { + const page = e.currentTarget.dataset.page; + + switch(page) { + case 'pricing': + wx.navigateTo({ + url: '/pages/pricing/pricing' + }); + break; + case 'buy': + wx.navigateTo({ + url: '/pages/buy/buy' + }); + break; + case 'wanted': + wx.navigateTo({ + url: '/pages/purchase/purchase' + }); + break; + case 'publish': + wx.navigateTo({ + url: '/pages/publish/publish' + }); + break; + } + }, + + /** + * 商品点击事件 + */ + onProductTap(e) { + const productId = e.currentTarget.dataset.id; + try { + const product = e.currentTarget.dataset.product; + if (product) { + reco.recordClick(product); + } + } catch (err) {} + wx.navigateTo({ + url: `/pages/product-detail/product-detail?id=${productId}` + }); + }, + + /** + * 加载热门求购(点击量前三) + */ + async loadHotWanted() { + try { + const db = wx.cloud.database(); + + // 查询所有活跃的求购信息(微信云数据库不支持多个orderBy,先用viewCount排序) + const result = await db.collection('T_want') + .where({ + status: 'active' + }) + .orderBy('viewCount', 'desc') + .limit(20) // 先获取更多数据,然后在客户端排序 + .get(); + + console.log('热门求购查询结果:', result); + + if (result.data && result.data.length > 0) { + // 在客户端进行排序:先按点击量降序,点击量相同则按时间降序(时间更晚的在前) + const sortedData = result.data.sort((a, b) => { + const viewCountA = a.viewCount || 0; + const viewCountB = b.viewCount || 0; + + // 如果点击量不同,按点击量降序 + if (viewCountA !== viewCountB) { + return viewCountB - viewCountA; + } + + // 点击量相同,按时间降序(时间更晚的在前) + const timeA = new Date(a.createTime).getTime(); + const timeB = new Date(b.createTime).getTime(); + return timeB - timeA; + }); + + // 取前3条 + const hotWanted = sortedData.slice(0, 3).map(item => ({ + id: item._id, + title: item.productName || '求购商品', + budget: item.expectedPrice || 0, + time: this.formatTime(item.createTime), + viewCount: item.viewCount || 0, + category: item.productCategory || '其他' + })); + + this.setData({ + hotWanted: hotWanted + }); + } else { + // 如果没有数据,显示空状态 + this.setData({ + hotWanted: [] + }); + } + } catch (err) { + console.error('加载热门求购失败:', err); + // 失败时使用默认数据 + this.setData({ + hotWanted: [] + }); + } + }, + + /** + * 格式化时间 + */ + formatTime(date) { + if (!date) return ''; + const d = new Date(date); + const now = new Date(); + const diff = now - d; + + const minute = 60 * 1000; + const hour = 60 * minute; + const day = 24 * hour; + + if (diff < minute) { + return '刚刚'; + } else if (diff < hour) { + return Math.floor(diff / minute) + '分钟前'; + } else if (diff < day) { + return Math.floor(diff / hour) + '小时前'; + } else if (diff < 7 * day) { + return Math.floor(diff / day) + '天前'; + } else { + return d.getMonth() + 1 + '月' + d.getDate() + '日'; + } + }, + + /** + * 求购点击事件(增加点击量) + */ + async onWantedTap(e) { + const wantedId = e.currentTarget.dataset.id; + const wanted = this.data.hotWanted.find(w => w.id === wantedId); + + if (!wanted) return; + + // 增加点击量 + try { + const db = wx.cloud.database(); + const _ = db.command; + await db.collection('T_want').doc(wantedId).update({ + data: { + viewCount: _.inc(1), + updateTime: new Date() + } + }); + + // 更新本地数据 + const updatedWanted = this.data.hotWanted.map(item => { + if (item.id === wantedId) { + return { + ...item, + viewCount: (item.viewCount || 0) + 1 + }; + } + return item; + }); + + this.setData({ + hotWanted: updatedWanted + }); + + // 跳转到求购详情页面(或显示详情) + wx.navigateTo({ + url: `/pages/wanted-list/wanted-list?id=${wantedId}` + }); + } catch (err) { + console.error('更新点击量失败:', err); + // 即使更新失败也跳转 + wx.navigateTo({ + url: `/pages/wanted-list/wanted-list?id=${wantedId}` + }); + } + }, + + /** + * 跳转到求购列表页面 + */ + onViewMoreWanted() { + wx.navigateTo({ + url: '/pages/wanted-list/wanted-list' + }); + }, + + /** + * 底部导航切换 + */ + onTabChange(e) { + const tab = e.currentTarget.dataset.tab; + + if (tab === this.data.currentTab) { + return; + } + + this.setData({ + currentTab: tab + }); + + // 根据不同的tab加载不同的内容 + switch(tab) { + case 'home': + this.loadHomeContent(); + break; + case 'market': + this.loadMarketContent(); + break; + case 'cart': + // 跳转到购物车页面 + wx.navigateTo({ + url: '/pages/cart/cart' + }); + // 保持当前tab状态 + setTimeout(() => { + this.setData({ currentTab: 'home' }); + }, 100); + break; + case 'message': + // 跳转到消息列表页面 + wx.navigateTo({ + url: '/pages/messages/messages' + }); + // 保持首页tab高亮 + setTimeout(() => { + this.setData({ currentTab: 'home' }); + }, 100); + break; + case 'profile': + // 跳转到个人中心页面 + wx.navigateTo({ + url: '/pages/profile/profile' + }); + // 保持当前tab状态 + setTimeout(() => { + this.setData({ + currentTab: 'home' + }); + }, 100); + break; + } + }, + + /** + * 加载首页内容 + */ + loadHomeContent() { + console.log('加载首页内容'); + }, + + /** + * 加载市场内容 + */ + loadMarketContent() { + // 跳转到市场页面(地图/列表) + wx.navigateTo({ + url: '/pages/market/market' + }); + // 保持首页tab高亮 + setTimeout(() => { + this.setData({ currentTab: 'home' }); + }, 100); + }, + + /** + * 加载消息内容 + */ + loadMessageContent() { + // 已改为跳转到消息列表页 + wx.navigateTo({ + url: '/pages/messages/messages' + }); + }, + + async loadUnreadStatus() { + try { + const res = await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'listChatSessions' } }); + const list = (res.result?.data || []); + const total = list.reduce((sum, s) => sum + (Number(s.unreadCount) || 0), 0); + this.setData({ hasMessageAlert: total > 0 }); + } catch (e) { + this.setData({ hasMessageAlert: false }); + } + }, + + /** + * 加载个人中心内容 + */ + loadProfileContent() { + console.log('加载个人中心内容'); + }, + + /** + * 用户头像点击事件 + */ + onUserProfile() { + wx.showActionSheet({ + itemList: ['查看资料', '设置', '退出登录'], + success: (res) => { + switch(res.tapIndex) { + case 0: + wx.showToast({ + title: '查看用户资料', + icon: 'none' + }); + break; + case 1: + wx.showToast({ + title: '打开设置', + icon: 'none' + }); + break; + case 2: + this.onLogout(); + break; + } + } + }); + }, + + /** + * 退出登录 + */ + onLogout() { + wx.showModal({ + title: '确认退出', + content: '确定要退出登录吗?', + success: (res) => { + if (res.confirm) { + // 清除登录信息 + wx.removeStorageSync('userInfo'); + wx.removeStorageSync('token'); + + // 跳转到登录页面 + wx.redirectTo({ + url: '/pages/index/index' + }); + } + } + }); + }, + + /** + * 页面相关事件处理函数--监听用户下拉动作 + */ + onPullDownRefresh() { + console.log('下拉刷新'); + + // 立即停止下拉刷新,不显示任何提示 + wx.stopPullDownRefresh(); + }, + + /** + * 页面上拉触底事件的处理函数 + */ + onReachBottom() { + console.log('上拉触底'); + // 不执行任何操作,避免显示加载提示 + }, + /** + * 更多推荐入口 + */ + onViewMoreRecommendations() { + wx.navigateTo({ url: '/pages/recommend-list/recommend-list' }); + } +}) \ No newline at end of file diff --git a/src5/code/miniprogram/pages/main/main.json b/src5/code/miniprogram/pages/main/main.json new file mode 100644 index 0000000..b5ccc71 --- /dev/null +++ b/src5/code/miniprogram/pages/main/main.json @@ -0,0 +1,7 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "校园二手交易", + "navigationBarBackgroundColor": "#4f8bff", + "navigationBarTextStyle": "white", + "enablePullDownRefresh": true +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/main/main.wxml b/src5/code/miniprogram/pages/main/main.wxml new file mode 100644 index 0000000..d28e970 --- /dev/null +++ b/src5/code/miniprogram/pages/main/main.wxml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + CAUC市集 + 发现校园好物,心动立刻带回家 + + 闲置 + 限量 + 人气 + + + + + + + + + 出闲置 + + + 🤖 + AI定价 + + + + 发布商品 + + + + + + + 找闲置 + + + 🛍️ + 商品购买 + + + 📣 + 求购 + + + + + + + + + + 猜您感兴趣 + 基于你的最近浏览与兴趣为你推荐 + + + 更多推荐 + + + + + + + + + {{item.name}} + ¥{{item.price}} + {{item.tag}} + + + + + + + + + + + 热门求购 + 大家都在找什么闲置 + + + 更多 + + + + + + {{item.title}} + + 预算: ¥{{item.budget}} + {{item.time}} + + + + 暂无热门求购 + + + + + + \ No newline at end of file diff --git a/src5/code/miniprogram/pages/main/main.wxss b/src5/code/miniprogram/pages/main/main.wxss new file mode 100644 index 0000000..b849eff --- /dev/null +++ b/src5/code/miniprogram/pages/main/main.wxss @@ -0,0 +1,364 @@ +/* pages/main/main.wxss */ + +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #f0f7ff 0%, #eef5ff 100%); + padding-bottom: 100rpx; +} + +/* 顶部导航栏 */ +.header { + display: flex; + align-items: center; + padding: 20rpx 30rpx; + background: linear-gradient(135deg, #5ba8ff 0%, #7ec8ff 100%); + color: #ffffff; +} + +.search-bar { + flex: 1; + display: flex; + align-items: center; + background: rgba(255, 255, 255, 0.95); + border-radius: 50rpx; + padding: 15rpx 25rpx; + margin-right: 20rpx; + box-shadow: 0 6rpx 16rpx rgba(91, 168, 255, 0.25); +} + +.search-icon { + width: 32rpx; + height: 32rpx; + margin-right: 15rpx; +} + +.search-input { + flex: 1; + font-size: 28rpx; + color: #333; +} + +.user-info { + width: 80rpx; + height: 80rpx; +} + +.avatar { + width: 100%; + height: 100%; + border-radius: 50%; + border: 3rpx solid #fff; + box-shadow: 0 6rpx 16rpx rgba(91,168,255,0.25); +} + +.hero-banner { + margin: 20rpx; + border-radius: 24rpx; + background: linear-gradient(135deg, #eaf3ff 0%, #f2f7ff 100%); + padding: 30rpx; + box-shadow: 0 8rpx 24rpx rgba(91,168,255,0.18); +} +.hero-content { display:flex; flex-direction:column; gap:12rpx; } +.hero-title { font-size: 40rpx; font-weight: 800; color: #3b82f6; } +.hero-subtitle { font-size: 26rpx; color: #4979c6; } +.hero-tags { display:flex; gap: 12rpx; margin-top: 8rpx; } +.hero-tag { font-size: 22rpx; color: #fff; background: #9ec5ff; padding: 8rpx 18rpx; border-radius: 999rpx; box-shadow: 0 6rpx 16rpx rgba(158, 197, 255, 0.4); } + + +/* 功能导航 */ +.nav-section { + background: #ffffff; + margin: 20rpx; + border-radius: 20rpx; + padding: 30rpx; + box-shadow: 0 8rpx 24rpx rgba(91, 168, 255, 0.16); +} + +.function-group { + margin-bottom: 40rpx; +} + +.function-group:last-child { + margin-bottom: 0; +} + +.group-title { + font-size: 30rpx; + font-weight: 800; + color: #2a4a7a; + margin-bottom: 20rpx; + display: block; + padding-left: 16rpx; + border-left: 10rpx solid #4f8bff; +} + +.nav-grid { + display: flex; + justify-content: space-around; + flex-wrap: wrap; +} + +.nav-item { + display: flex; + flex-direction: column; + align-items: center; + width: 160rpx; + padding: 20rpx 0; + margin-bottom: 20rpx; + border-radius: 24rpx; + background: #f0f7ff; + box-shadow: 0 8rpx 24rpx rgba(91,168,255,0.16); +} + +.nav-icon { + width: 80rpx; + height: 80rpx; + margin-bottom: 15rpx; +} + +.nav-icon-emoji { + font-size: 64rpx; + margin-bottom: 15rpx; + display: block; + line-height: 1; + animation: float 3s ease-in-out infinite; +} + +.theme-pricing { background: linear-gradient(135deg, #e6f0ff 0%, #d9eaff 100%); } +.theme-publish { background: linear-gradient(135deg, #eaf3ff 0%, #e0efff 100%); } +.theme-buy { background: linear-gradient(135deg, #e6f7ff 0%, #e1f0ff 100%); } +.theme-wanted { background: linear-gradient(135deg, #edf6ff 0%, #e0f2ff 100%); } + +.nav-text { font-weight: 600; color: #3d4d66; } + +@keyframes float { + 0% { transform: translateY(0); } + 50% { transform: translateY(-6rpx); } + 100% { transform: translateY(0); } +} + +.nav-text { + font-size: 24rpx; + color: #333; + text-align: center; +} + +/* 推荐商品区域 */ +.recommend-section { + background: #ffffff; + margin: 20rpx; + border-radius: 20rpx; + padding: 30rpx; + box-shadow: 0 8rpx 24rpx rgba(91, 168, 255, 0.16); +} + +.section-header { + margin-bottom: 25rpx; + display: flex; + justify-content: space-between; + align-items: center; +} + +.header-left { + flex: 1; +} + +.more-btn { + display: flex; + align-items: center; + color: #4285F4; + font-size: 26rpx; + padding: 8rpx 16rpx; + border-radius: 20rpx; + transition: background-color 0.2s ease; +} + +.more-btn:active { + background-color: rgba(66, 133, 244, 0.1); +} + +.more-arrow { + margin-left: 4rpx; + font-size: 32rpx; + font-weight: bold; +} + +.section-title { + font-size: 34rpx; + font-weight: 800; + color: #2a4a7a; + display: block; + margin-bottom: 10rpx; +} + +.section-subtitle { + font-size: 24rpx; + color: #5e7ea6; +} + +.recommend-scroll { + white-space: nowrap; +} + +.recommend-list { + display: inline-flex; +} + +.product-card { + display: inline-block; + width: 280rpx; + margin-right: 20rpx; + background: linear-gradient(180deg, #fff 0%, #f5f9ff 100%); + border-radius: 20rpx; + overflow: hidden; + box-shadow: 0 10rpx 26rpx rgba(91,168,255,0.16); +} + +.product-image { + width: 100%; + height: 200rpx; + border-bottom-left-radius: 24rpx; + border-bottom-right-radius: 24rpx; +} + +.product-info { + padding: 20rpx; +} + +.product-name { + font-size: 26rpx; + color: #333; + display: block; + margin-bottom: 10rpx; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.product-price { + font-size: 28rpx; + color: #3b82f6; + font-weight: 800; + display: block; + margin-bottom: 8rpx; +} + +.product-tag { + font-size: 20rpx; + color: #fff; + background: #9ec5ff; + padding: 6rpx 14rpx; + border-radius: 999rpx; + box-shadow: 0 6rpx 16rpx rgba(158, 197, 255, 0.3); +} + +/* 热门求购 */ +.wanted-section { + background: #ffffff; + margin: 20rpx; + border-radius: 20rpx; + padding: 30rpx; + box-shadow: 0 8rpx 24rpx rgba(91, 168, 255, 0.16); +} + +.wanted-list { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.wanted-item { + background: linear-gradient(180deg, #fff 0%, #f6fbff 100%); + padding: 25rpx; + border-radius: 18rpx; + border-left: 8rpx solid #9ec5ff; + box-shadow: 0 8rpx 24rpx rgba(91,168,255,0.12); +} + +.wanted-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 10rpx; +} + +.wanted-title { + font-size: 30rpx; + color: #2a4a7a; + font-weight: 800; + display: block; + margin-bottom: 10rpx; +} + +.wanted-price { + font-size: 24rpx; + color: #3b82f6; + font-weight: 700; +} + +.wanted-time { + font-size: 22rpx; + color: #999; +} + +.empty-wanted { + text-align: center; + padding: 40rpx 0; + color: #999; + font-size: 26rpx; +} + +/* 底部导航栏 */ +.tab-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + background: #ffffff; + border-top: 1rpx solid #e0e0e0; + padding: 15rpx 0; + z-index: 1000; +} + +.tab-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.tab-icon { + width: 48rpx; + height: 48rpx; + margin-bottom: 8rpx; +} + +.tab-icon-emoji { + font-size: 44rpx; + margin-bottom: 8rpx; + display: block; + line-height: 1; +} + +.tab-text { + font-size: 20rpx; + color: #999; +} + +.tab-item.active .tab-text { + color: #4285F4; +} +.red-dot { position:absolute; top:6rpx; right:20rpx; width:16rpx; height:16rpx; background-color:#ff3b30; border-radius:50%; box-shadow:0 2rpx 6rpx rgba(255,59,48,0.4); } + +/* 响应式设计 */ +@media (max-width: 750rpx) { + .nav-item { + width: 25%; + } + + .product-card { + width: 250rpx; + } +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/market/market.js b/src5/code/miniprogram/pages/market/market.js new file mode 100644 index 0000000..030aff9 --- /dev/null +++ b/src5/code/miniprogram/pages/market/market.js @@ -0,0 +1,889 @@ +// pages/market/market.js +const { haversineDistance, formatDistance } = require('../../utils/geo.js'); +const { geocode } = require('../../utils/geocoder.js'); +const { officialTradeSpots } = require('../../utils/campusMap.js'); +const reco = require('../../utils/recommendation.js'); +const normalizeStatus = (s) => { + const raw = String(s || '').trim(); + const lower = raw.toLowerCase(); + if (raw === '在售' || lower === 'selling') return 'selling'; + if (raw === '已售' || raw === '已售出' || lower === 'sold') return 'sold'; + if (raw === '已下架' || raw.includes('下架') || lower === 'off') return 'off'; + if (raw === '交易中' || raw === '待确认付款' || raw === '待发货' || raw === '待收货' || raw === '待付款' || lower === 'transacting') return 'transacting'; + return 'selling'; +}; +const DISPLAY_LIMIT = 100; + +Page({ + data: { + categories: ['全部', '电子产品', '图书文具', '服装鞋帽', '家居用品', '运动户外', '美妆个护', '其他'], + categoryIndex: 0, + + + // 默认中心:中国民航大学(天津·东丽校区)图书馆附近 + center: { latitude: 39.1234, longitude: 117.3365 }, + myLocation: { latitude: null, longitude: null }, + mapScale: 16, + mapRuntimeScale: 16, + // 为商品标记分配稳定的 markerId,避免每次刷新导致气泡重建 + productMarkerIdMap: {}, + nextProductMarkerId: 1, + + allProducts: [], + visibleProducts: [], + displayMarkers: [], + markerProductMap: {}, + landmarkMarkerMap: {}, + landmarkCategoryMap: {}, + // 数据库校园地标标记 + campusLandmarks: [], + campusLandmarksForDisplay: [], + campusLandmarkMarkers: [], + // 用户默认地址坐标,以及其附近推荐交易点标记 + userAddressLoc: { latitude: null, longitude: null }, + tradeSpotMarkers: [], + // 距离扫描圈与弹窗 + scanCircles: [], + nearestModalVisible: false, + nearestProducts: [], + // 半径选择 + scanRadiusOptions: [300, 500, 1000, 2000, 3000], + scanRadiusLabels: ['300m', '500m', '1km', '2km', '3km'], + scanRadiusIndex: 2, + scanRadius: 1000, + nearestModalTitle: '距我最近(1km内)', + + }, + + onLoad() { + // 初始化地图上下文,避免后续获取中心失败 + try { this.mapCtx = wx.createMapContext('marketMap'); } catch (_) {} + this.initLocation(); + this.loadProducts(); + this.loadCampusLandmarksMarkers(); + // 不再叠加推荐交易点,仅显示商品交易地点 + }, + + onShow() { + // 返回页面时再次尝试定位,避免首次拒绝权限后不再更新 + this.initLocation(); + // 保持地图与列表同步,仅展示商品标记 + this.loadProducts(); + this.loadCampusLandmarksMarkers(); + try { + if (this.getTabBar && this.getTabBar()) { + this.getTabBar().setSelected(1); + } + } catch (e) {} + }, + + /** 读取 T_campus_landmarks 并生成地标标记 */ + async loadCampusLandmarksMarkers() { + const db = wx.cloud.database(); + try { + const res = await db.collection('T_campus_landmarks').limit(500).get(); + const list = (res.data || []) + .filter(x => Number.isFinite(Number(x.latitude)) && Number.isFinite(Number(x.longitude))) + .filter(x => (typeof x.selling === 'boolean') ? x.selling === true : true); + + // 并发拉取商品信息:缩略图与分类 + const resolveProductInfo = async (pid) => { + if (!pid) return { thumbUrl: '', category: '', status: '', statusNormalized: 'off' }; + try { + const pr = await db.collection('T_product').doc(pid).get(); + const p = pr.data || {}; + let url = ''; + if (Array.isArray(p.productImage) && p.productImage.length > 0) url = p.productImage[0]; + else if (typeof p.productImage === 'string') url = p.productImage; + else if (Array.isArray(p.imageUrls) && p.imageUrls.length > 0) url = p.imageUrls[0]; + else if (typeof p.imageUrl === 'string') url = p.imageUrl; + const category = p.productCategory || p.category || ''; + const status = p.status || ''; + const statusNormalized = normalizeStatus(status); + return { thumbUrl: (url || ''), category, status, statusNormalized }; + } catch (_) { + return { thumbUrl: '', category: '', status: '', statusNormalized: 'off' }; + } + }; + const toLocalIcon = async (url) => { + if (!url) return '/images/更多犬种.png'; + try { + if (url.startsWith('cloud://')) { + const t = await wx.cloud.getTempFileURL({ fileList: [url] }); + const temp = (t.fileList && t.fileList[0] && t.fileList[0].tempFileURL) || ''; + if (!temp) return '/images/更多犬种.png'; + const info = await wx.getImageInfo({ src: temp }); + return info && info.path ? info.path : '/images/更多犬种.png'; + } + const info = await wx.getImageInfo({ src: url }); + return info && info.path ? info.path : '/images/更多犬种.png'; + } catch (_) { + return '/images/更多犬种.png'; + } + }; + + const concurrency = 6; + let idx = 0; + const landmarks = []; + const markers = []; + const landmarkMarkerMap = {}; + const landmarkCategoryMap = {}; + const worker = async () => { + while (idx < list.length) { + // capture index before awaits to keep ids stable per item + const i = idx++; + const x = list[i]; + const name = x.name || x.normalizedName || ''; + const latitude = Number(x.latitude); + const longitude = Number(x.longitude); + const address = x.address || ''; + const productIds = Array.isArray(x.productIds) ? x.productIds : []; + let chosenPid = ''; + let info = { thumbUrl: '', category: '', statusNormalized: 'off' }; + const landmarkSelling = (typeof x.selling === 'boolean') ? x.selling : undefined; + const storedCategory = x.productCategory || ''; + const storedThumb = x.thumbUrl || ''; + if (landmarkSelling === true) { + chosenPid = productIds[0] || ''; + if (chosenPid) info = await resolveProductInfo(chosenPid); + } else if (landmarkSelling === false) { + continue; + } else { + for (let k = 0; k < Math.min(productIds.length, 5); k++) { + const pidTry = productIds[k]; + const iTry = await resolveProductInfo(pidTry); + if (iTry.statusNormalized === 'selling') { + chosenPid = pidTry; + info = iTry; + break; + } + } + } + const resolvedThumb = info.thumbUrl || storedThumb; + const resolvedCategory = info.category || storedCategory; + const iconPath = await toLocalIcon(resolvedThumb); + const markerId = 5000 + i; + // 将 bubble 使用的缩略图也改为本地路径,避免网络超时 + landmarks.push({ name, latitude, longitude, address, _markerId: markerId, thumbUrl: iconPath, firstProductId: chosenPid, productCategory: resolvedCategory }); + markers.push({ + id: markerId, + latitude, + longitude, + width: 32, + height: 32, + iconPath, + // 去掉文字callout,保留自定义图片气泡(cover-view slot="callout") + }); + if (chosenPid) landmarkMarkerMap[markerId] = chosenPid; + if (resolvedCategory) landmarkCategoryMap[markerId] = resolvedCategory; + } + }; + const workers = Array.from({ length: Math.min(concurrency, list.length) }, () => worker()); + await Promise.allSettled(workers); + + this.setData({ campusLandmarks: landmarks, campusLandmarkMarkers: markers, landmarkMarkerMap, landmarkCategoryMap }); + this.updateMarkers(); + } catch (e) { + // 集合未创建时忽略,不影响商品显示 + console.warn('读取校园地标失败或集合不存在:', e); + } + }, + + initLocation() { + wx.getLocation({ + type: 'gcj02', + isHighAccuracy: true, + highAccuracyExpireTime: 10000, + success: (res) => { + const my = { latitude: res.latitude, longitude: res.longitude }; + // 不自动移动视角,仅记录“我的位置” + this.setData({ myLocation: my }); + }, + fail: (err) => { + console.warn('市场页获取当前位置失败:', err); + wx.showToast({ title: '定位失败,请检查权限', icon: 'none' }); + // 保持默认中心,不强制覆盖 + } + }); + }, + + async loadProducts() { + const db = wx.cloud.database(); + try { + const col = db.collection('T_product'); + // 分页拉取全量商品 + const countRes = await col.count(); + const total = (countRes && countRes.total) || 0; + const pageSize = 100; + const raw = []; + for (let skip = 0; skip < Math.max(total, pageSize); skip += pageSize) { + const page = await col.skip(skip).limit(pageSize).get(); + if (page && Array.isArray(page.data)) raw.push(...page.data); + if (!page || !page.data || page.data.length < pageSize) break; // 最后一页 + } + // 尝试为缺少坐标的商品补齐坐标(支持旧字段名、地标名称匹配与地址编码) + let products = raw.slice(); + try { + const { campusLandmarks } = require('../../utils/campusMap.js'); + + const normalizeNum = (v) => { + const n = Number(v); + return Number.isFinite(n) ? n : null; + }; + const simplify = (s) => String(s || '') + .replace(/[()()]/g, '') + .replace(/附近$/g, '') + .trim(); + // 读取数据库中的地标,合并到匹配源 + let allLandmarks = Array.isArray(campusLandmarks) ? campusLandmarks.slice() : []; + try { + const lr = await db.collection('T_campus_landmarks').limit(500).get(); + const dyn = (lr.data || []) + .filter(x => Number.isFinite(Number(x.latitude)) && Number.isFinite(Number(x.longitude))) + .map(x => ({ name: x.name || x.normalizedName || '', latitude: Number(x.latitude), longitude: Number(x.longitude) })); + if (dyn.length > 0) allLandmarks = [...dyn, ...allLandmarks]; + } catch (_) { /* 集合不存在或读取失败时,沿用内置地标 */ } + + const matchLandmark = (p) => { + const names = [p.tradeLandmarkName, p.tradeLocationName, p.tradeAddress] + .filter(Boolean) + .map(s => simplify(s)); + if (names.length === 0) return null; + for (const candidate of names) { + // 双向包含,尽量模糊匹配(支持数据库地标 + 内置地标) + const m = allLandmarks.find(l => candidate.includes(l.name) || l.name.includes(candidate)); + if (m && Number.isFinite(m.latitude) && Number.isFinite(m.longitude)) { + return { lat: m.latitude, lng: m.longitude }; + } + } + return null; + }; + + products = products.map(p => { + // 直接复制旧字段或嵌套字段 + const directLat = normalizeNum(p.tradeLocationLat ?? p.tradeLat ?? p.lat ?? p.latitude ?? p?.tradeLocation?.latitude); + const directLng = normalizeNum(p.tradeLocationLng ?? p.tradeLng ?? p.lng ?? p.longitude ?? p?.tradeLocation?.longitude); + if (directLat != null && directLng != null) { + p.tradeLocationLat = directLat; + p.tradeLocationLng = directLng; + return p; + } + // 从字符串坐标解析 "lat,lng" 或 "lat lng" + if (typeof p.tradeLocation === 'string') { + const m = String(p.tradeLocation).match(/(-?\d+\.?\d*)[\s,]+(-?\d+\.?\d*)/); + if (m) { + const lat = normalizeNum(m[1]); + const lng = normalizeNum(m[2]); + if (lat != null && lng != null) { + p.tradeLocationLat = lat; + p.tradeLocationLng = lng; + return p; + } + } + } + // 地标名称匹配 + const lm = matchLandmark(p); + if (lm) { + p.tradeLocationLat = lm.lat; + p.tradeLocationLng = lm.lng; + } + return p; + }); + } catch (e) { + console.warn('校园地标或坐标补齐失败,继续显示原数据:', e); + } + // 仍无坐标但有地址时,尝试正向地理编码补齐(限制并发,避免卡顿) + try { + const missingAll = products.filter(p => ( + !Number.isFinite(Number(p.tradeLocationLat)) || !Number.isFinite(Number(p.tradeLocationLng)) + ) && p.tradeAddress && String(p.tradeAddress).trim() !== ''); + const sortedMissing = missingAll.sort((a,b)=> { + const ta = new Date(a.createTime || 0).getTime(); + const tb = new Date(b.createTime || 0).getTime(); + return tb - ta; + }); + const missing = sortedMissing.slice(0, 40); + if (missing.length > 0) { + const { geocode } = require('../../utils/geocoder.js'); + const concurrency = 5; + let index = 0; + const worker = async () => { + while (index < missing.length) { + const current = missing[index++]; + try { + const loc = await geocode(String(current.tradeAddress)); + if (loc && typeof loc.latitude === 'number' && typeof loc.longitude === 'number') { + current.tradeLocationLat = loc.latitude; + current.tradeLocationLng = loc.longitude; + } + } catch (e) { + // 忽略失败,继续下一个 + } + } + }; + const workers = Array.from({ length: concurrency }, () => worker()); + await Promise.allSettled(workers); + } + } catch (e) { + console.warn('批量正向地理编码异常:', e); + } + // 计算距离与列表字段(如果无法获取我的定位,则用中心点作为参考) + const my = this.data.myLocation; + const ref = (my && my.latitude) ? my : this.data.center; + const formatted = products.map(p => { + let distanceText = ''; + const plat = Number(p.tradeLocationLat); + const plng = Number(p.tradeLocationLng); + if (Number.isFinite(plat) && Number.isFinite(plng)) { + const d = haversineDistance(ref.latitude, ref.longitude, plat, plng); + distanceText = formatDistance(d); + p._distance = d; + } else { + p._distance = Infinity; + distanceText = '未知'; + } + // 默认使用本地占位图片,避免网络超时 + let thumbUrl = '/images/仓鼠.png'; + if (p.productImage) { + if (Array.isArray(p.productImage) && p.productImage.length > 0) { + thumbUrl = p.productImage[0]; + } else if (typeof p.productImage === 'string') { + thumbUrl = p.productImage; + } + } + const statusNormalized = normalizeStatus(p.status); + return { ...p, distanceText, thumbUrl, statusNormalized }; + }); + this.setData({ allProducts: formatted }); + this.applyFilters(); + } catch (e) { + console.error('加载市场商品失败:', e); + } + }, + + applyFilters() { + const { categoryIndex, categories } = this.data; + const category = categories[categoryIndex]; + let list = this.data.allProducts.slice(); + + if (category && category !== '全部') { + list = list.filter(p => p.productCategory === category); + } + list = list.filter(p => (p.statusNormalized || 'selling') === 'selling'); + // 仅显示具备有效坐标的商品(字符串坐标也接受) + list = list.filter(p => Number.isFinite(Number(p.tradeLocationLat)) && Number.isFinite(Number(p.tradeLocationLng))); + + this.setData({ visibleProducts: list }); + this.updateMarkers(); + }, + + // 简易聚类:基于缩放将附近点归为一个cluster + updateMarkers() { + const { visibleProducts, campusLandmarkMarkers, tradeSpotMarkers, productMarkerIdMap, landmarkCategoryMap } = this.data; + const mapScale = (typeof this.data.mapRuntimeScale === 'number') ? this.data.mapRuntimeScale : this.data.mapScale; + const categories = this.data.categories || []; + const selectedCategory = categories[this.data.categoryIndex] || '全部'; + // 使用稳定的 markerId,避免覆盖层频繁销毁/重建导致闪烁 + const markerMap = {}; + const idByPid = { ...productMarkerIdMap }; + let nextId = this.data.nextProductMarkerId || 1; + const withMarkerIds = visibleProducts.map(p => { + const pid = p._id; + let mkid = idByPid[pid]; + if (!mkid) { mkid = nextId++; idByPid[pid] = mkid; } + if (p._markerId === mkid) return p; + return { ...p, _markerId: mkid }; + }); + const limited = withMarkerIds.slice().sort((a, b) => { + const ta = new Date(a.updateTime || a.createTime || 0).getTime(); + const tb = new Date(b.updateTime || b.createTime || 0).getTime(); + return tb - ta; + }).slice(0, DISPLAY_LIMIT); + limited.forEach(p => { markerMap[p._markerId] = p._id; }); + // 仅更新映射与 displayMarkers;在需要显示气泡时同步 visibleProducts(包含 _markerId) + this.setData({ markerProductMap: markerMap, productMarkerIdMap: idByPid, nextProductMarkerId: nextId }); + // 根据分类筛选校园地标标记与气泡 + const campusMarkersFiltered = (selectedCategory && selectedCategory !== '全部') + ? campusLandmarkMarkers.filter(m => landmarkCategoryMap[m.id] === selectedCategory) + : campusLandmarkMarkers; + const campusLandmarksFiltered = (selectedCategory && selectedCategory !== '全部') + ? this.data.campusLandmarks.filter(l => l.productCategory === selectedCategory) + : this.data.campusLandmarks; + if (mapScale >= 16) { + const productMarkers = limited + .filter(p => p.tradeLocationLat && p.tradeLocationLng) + .map(p => ({ + id: p._markerId, + latitude: Number(p.tradeLocationLat), + longitude: Number(p.tradeLocationLng), + width: 32, + height: 32, + iconPath: '/images/仓鼠.png' + })); + const merged = productMarkers.concat(campusMarkersFiltered); + const includeSpots = Array.isArray(tradeSpotMarkers) && tradeSpotMarkers.length > 0 && merged.length === 0; + const finalMarkers = includeSpots ? merged.concat(tradeSpotMarkers) : merged; + this.setData({ displayMarkers: finalMarkers, visibleProducts: limited, campusLandmarksForDisplay: campusLandmarksFiltered }); + return; + } + // 低缩放:聚类 + const grid = new Map(); + const keyFor = (lat, lng) => `${Math.round(lat * 10)}/${Math.round(lng * 10)}`; + limited.forEach(p => { + if (!p.tradeLocationLat || !p.tradeLocationLng) return; + const key = keyFor(p.tradeLocationLat, p.tradeLocationLng); + if (!grid.has(key)) grid.set(key, []); + grid.get(key).push(p); + }); + const clusterMarkers = []; + for (const [key, arr] of grid.entries()) { + if (arr.length === 1) { + const p = arr[0]; + clusterMarkers.push({ + id: p._markerId, + latitude: Number(p.tradeLocationLat), + longitude: Number(p.tradeLocationLng), + width: 32, + height: 32, + iconPath: '/images/仓鼠.png', + }); + } else { + // 聚类点:显示数量 + const lat = arr.reduce((s, x) => s + Number(x.tradeLocationLat), 0) / arr.length; + const lng = arr.reduce((s, x) => s + Number(x.tradeLocationLng), 0) / arr.length; + clusterMarkers.push({ + id: Date.now() + Math.random(), + latitude: lat, + longitude: lng, + width: 40, + height: 40, + label: { content: `${arr.length}`, color: '#fff', bgColor: '#4285F4', padding: 6, borderRadius: 20 } + }); + } + } + const merged = clusterMarkers.concat(campusMarkersFiltered); + const includeSpotsLow = Array.isArray(tradeSpotMarkers) && tradeSpotMarkers.length > 0 && merged.length === 0; + const finalMarkers = includeSpotsLow ? merged.concat(tradeSpotMarkers) : merged; + this.setData({ displayMarkers: finalMarkers, visibleProducts: limited, campusLandmarksForDisplay: campusLandmarksFiltered }); + }, + + onCategoryChange(e) { + const idx = parseInt(e.detail.value, 10); + const cat = this.data.categories[idx]; + this.setData({ categoryIndex: idx }); + this.applyFilters(); + if (cat && cat !== '全部') { + try { reco.recordInterests([cat], 3); } catch (_) {} + } + }, + + onScanRadiusChange(e) { + const idx = parseInt(e.detail.value, 10); + const radius = this.data.scanRadiusOptions[idx] || 1000; + const label = this.data.scanRadiusLabels[idx] || '1km'; + this.setData({ scanRadiusIndex: idx, scanRadius: radius, nearestModalTitle: `距我最近(${label}内)` }); + }, + + onToggleMode() {}, + + onRegionChange(e) { + // 仅在结束事件中更新运行时缩放与标记,不再写入受控中心,避免视角抽搐 + if (e.type === 'end') { + if (typeof e.detail.scale === 'number') { + this.setData({ mapRuntimeScale: e.detail.scale }); + } + this.scheduleUpdateMarkers(); + } + }, + + scheduleUpdateMarkers() { + if (this._updateMarkersTimer) clearTimeout(this._updateMarkersTimer); + this._updateMarkersTimer = setTimeout(() => { + this._updateMarkersTimer = null; + try { this.updateMarkers(); } catch (_) {} + }, 120); + }, + + onMarkerTap(e) { + const id = e.markerId; + const pid = this.data.markerProductMap[id]; + const p = this.data.visibleProducts.find(x => x._id === pid); + if (p) { + try { reco.recordClick(p); } catch (_) {} + wx.navigateTo({ url: `/pages/product-detail/product-detail?id=${p._id}` }); + return; + } + // 地标标记:跳转到关联商品详情(使用第一个productId) + const lpid = this.data.landmarkMarkerMap[id]; + if (lpid) { + const lp = this.data.allProducts.find(x => x._id === lpid); + if (lp) { try { reco.recordClick(lp); } catch (_) {} } else { + const cat = this.data.landmarkCategoryMap[id]; + if (cat) { try { reco.recordInterests([cat], 1); } catch (_) {} } + } + wx.navigateTo({ url: `/pages/product-detail/product-detail?id=${lpid}` }); + return; + } + // 其他非商品标记(聚类/推荐点):不再自动居中或放大,保持用户视角自由 + }, + + onLandmarkBubbleTap(e) { + const pid = (e?.currentTarget?.dataset?.pid) || ''; + if (pid) { + wx.navigateTo({ url: `/pages/product-detail/product-detail?id=${pid}` }); + } + }, + + onListTap(e) { + const id = e.currentTarget.dataset.id; + const np = Array.isArray(this.data.nearestProducts) ? this.data.nearestProducts.find(x => x._id === id) : null; + const vp = Array.isArray(this.data.visibleProducts) ? this.data.visibleProducts.find(x => x._id === id) : null; + const ap = Array.isArray(this.data.allProducts) ? this.data.allProducts.find(x => x._id === id) : null; + const p = np || vp || ap; + if (!p) return; + try { reco.recordClick(p); } catch (_) {} + this.setData({ nearestModalVisible: false }); + wx.navigateTo({ url: `/pages/product-detail/product-detail?id=${p._id}` }); + }, + + onResetCenter() { + if (this.data.myLocation && this.data.myLocation.latitude) { + this.setData({ center: this.data.myLocation, mapScale: 16, mapRuntimeScale: 16 }); + return; + } + // 若尚未有定位信息,主动尝试获取一次 + wx.getLocation({ + type: 'gcj02', + isHighAccuracy: true, + highAccuracyExpireTime: 10000, + success: (res) => { + const center = { latitude: res.latitude, longitude: res.longitude }; + this.setData({ myLocation: center, center, mapScale: 16, mapRuntimeScale: 16 }); + }, + fail: () => { + wx.showToast({ title: '无法获取定位,请在设置中授权', icon: 'none' }); + // 可引导打开设置 + if (wx.openSetting) { + wx.openSetting({}); + } + } + }); + }, + + async onNearestScan() { + try { + const my = this.data.myLocation && this.data.myLocation.latitude ? this.data.myLocation : this.data.center; + if (!my || typeof my.latitude !== 'number' || typeof my.longitude !== 'number') { + wx.showToast({ title: '定位信息缺失', icon: 'none' }); + return; + } + const radius = this.data.scanRadius || 1000; + const label = (this.data.scanRadiusLabels && this.data.scanRadiusLabels[this.data.scanRadiusIndex]) + || (radius >= 1000 ? `${(radius/1000).toFixed(0)}km` : `${radius}m`); + const circle = [{ latitude: Number(my.latitude), longitude: Number(my.longitude), radius: radius, color: '#4f8bff80', fillColor: '#4f8bff20', strokeWidth: 2 }]; + + // 以当前显示的标记为准,避免因数据尚未加载完全导致误判 + const { displayMarkers, markerProductMap, landmarkMarkerMap } = this.data; + const nearMarkers = (Array.isArray(displayMarkers) ? displayMarkers : []).filter(m => { + if (typeof m.latitude !== 'number' || typeof m.longitude !== 'number') return false; + const d = haversineDistance(my.latitude, my.longitude, m.latitude, m.longitude); + return d <= radius; + }); + + // 根据标记映射回商品ID + const candidateIds = []; + nearMarkers.forEach(m => { + const pid = markerProductMap[m.id] || landmarkMarkerMap[m.id] || ''; + if (pid) candidateIds.push(pid); + }); + // 去重 + const uniqIds = [...new Set(candidateIds)]; + + // 组装商品数据:优先从已加载列表查找,缺失时读库 + const inAll = Array.isArray(this.data.allProducts) ? this.data.allProducts : []; + const inVisible = Array.isArray(this.data.visibleProducts) ? this.data.visibleProducts : []; + const byId = new Map(); + inAll.forEach(p => byId.set(p._id, p)); + inVisible.forEach(p => { if (!byId.has(p._id)) byId.set(p._id, p); }); + + const db = wx.cloud.database(); + const resolved = []; + for (const pid of uniqIds) { + let p = byId.get(pid); + if (!p) { + try { + const pr = await db.collection('T_product').doc(pid).get(); + p = pr.data || null; + } catch (_) { p = null; } + } + if (!p) continue; + const mk = nearMarkers.find(m => (markerProductMap[m.id] === pid) || (landmarkMarkerMap[m.id] === pid)); + const plat = mk ? Number(mk.latitude) : Number(p.tradeLocationLat ?? p.lat ?? p?.tradeLocation?.latitude); + const plng = mk ? Number(mk.longitude) : Number(p.tradeLocationLng ?? p.lng ?? p?.tradeLocation?.longitude); + if (!Number.isFinite(plat) || !Number.isFinite(plng)) continue; + const d = haversineDistance(my.latitude, my.longitude, plat, plng); + const thumbUrl = Array.isArray(p.productImage) ? (p.productImage[0] || '/images/仓鼠.png') : (p.productImage || '/images/仓鼠.png'); + resolved.push({ ...p, thumbUrl, _distance: d, _distanceText: formatDistance(d) }); + } + + const nearby = resolved.sort((a,b)=> a._distance - b._distance).slice(0, 40); + if (nearby.length === 0) { + wx.showToast({ title: `${label}内暂无商品`, icon: 'none' }); + } + this.setData({ scanCircles: circle, nearestProducts: nearby, nearestModalVisible: nearby.length > 0, nearestModalTitle: `距我最近(${label}内)` }); + } catch (e) { + console.error('最近商品扫描失败:', e); + wx.showToast({ title: '扫描失败', icon: 'none' }); + } + }, + + onCloseNearestModal() { + this.setData({ nearestModalVisible: false }); + }, + + async onGeoServiceCheck() { + try { + wx.showLoading({ title: '诊断中…', mask: true }); + const { QQMAP_KEY, QQMAP_REFERER } = require('../../utils/config.js'); + const { geocode, reverseGeocode } = require('../../utils/geocoder.js'); + + const keyOk = !!(QQMAP_KEY && typeof QQMAP_KEY === 'string' && QQMAP_KEY.length >= 10); + const keyStatus = keyOk ? '已配置' : '未配置或无效'; + + // 正向地理编码测试 + const testAddr = '天津市东丽区中国民航大学'; + let geocodeMsg = ''; + try { + const loc = await geocode(testAddr); + geocodeMsg = `成功 (${loc.latitude?.toFixed(5)}, ${loc.longitude?.toFixed(5)})`; + } catch (e) { + geocodeMsg = `失败:${e?.message || '请求错误'}`; + } + + // 逆地理编码测试(优先云函数,失败走前端兜底) + const ref = this.data.center; + let reverseMsg = ''; + try { + const addr = await reverseGeocode({ latitude: ref.latitude, longitude: ref.longitude }); + reverseMsg = `成功(${String(addr).slice(0, 28)})`; + } catch (e) { + reverseMsg = `失败:${e?.message || '请求错误'}`; + } + + // 云函数直测(可区分云端配置情况) + let cloudMsg = ''; + try { + const r = await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'cloudReverseGeocode', latitude: ref.latitude, longitude: ref.longitude, key: QQMAP_KEY, referer: QQMAP_REFERER } }); + cloudMsg = (r.result && r.result.success) ? '成功' : `失败:${r.result?.error || '未知错误'}`; + } catch (e) { + cloudMsg = `调用失败:${e?.message || '网络错误'}`; + } + + wx.hideLoading(); + const content = `Key:${keyStatus}\n正向编码:${geocodeMsg}\n逆向编码:${reverseMsg}\n云函数:${cloudMsg}\n\n产品:总数 ${DISPLAY_LIMIT},可见 ${this.data.visibleProducts.length},标记 ${this.data.displayMarkers.length}\n\n如失败:在小程序后台将 request 合法域名添加 https://apis.map.qq.com,并确认 Key 有效。`; + wx.showModal({ title: '地理服务诊断', content, showCancel: false }); + } catch (e) { + wx.hideLoading(); + wx.showToast({ title: '诊断异常,请稍后重试', icon: 'none' }); + } + } + , + /** + * 回填地标分类到 T_campus_landmarks + * 依据地标的第一个关联商品(productIds[0])的分类写入 productCategory 字段 + */ + async onBackfillLandmarkCategories() { + try { + const confirm = await new Promise((resolve) => { + wx.showModal({ + title: '回填地标分类', + content: '将读取地标关联商品分类并写入地标表,仅执行一次性数据修复。是否继续?', + confirmText: '继续', + cancelText: '取消', + success: (res) => resolve(res.confirm), + fail: () => resolve(false) + }); + }); + if (!confirm) return; + + wx.showLoading({ title: '回填中…', mask: true }); + const db = wx.cloud.database(); + const colLandmarks = db.collection('T_campus_landmarks'); + const colProducts = db.collection('T_product'); + + // 分页读取全部地标 + const countRes = await colLandmarks.count(); + const total = (countRes && countRes.total) || 0; + const pageSize = 100; + const docs = []; + for (let skip = 0; skip < Math.max(total, pageSize); skip += pageSize) { + const page = await colLandmarks.skip(skip).limit(pageSize).get(); + if (page && Array.isArray(page.data)) docs.push(...page.data); + if (!page || !page.data || page.data.length < pageSize) break; + } + + let updated = 0, skipped = 0, failed = 0; + const concurrency = 5; + let idx = 0; + const worker = async () => { + while (idx < docs.length) { + const i = idx++; + const d = docs[i]; + const pid = Array.isArray(d.productIds) && d.productIds.length > 0 ? d.productIds[0] : ''; + // 已有分类则跳过 + if (d.productCategory && typeof d.productCategory === 'string' && d.productCategory.trim()) { + skipped++; + continue; + } + if (!pid) { skipped++; continue; } + try { + const pr = await colProducts.doc(pid).get(); + const p = pr.data || {}; + const category = p.productCategory || p.category || ''; + if (!category) { skipped++; continue; } + await colLandmarks.doc(d._id).update({ data: { productCategory: category } }); + updated++; + } catch (e) { + console.warn('回填失败:', d._id, e); + failed++; + } + } + }; + const workers = Array.from({ length: Math.min(concurrency, docs.length) }, () => worker()); + await Promise.allSettled(workers); + + wx.hideLoading(); + wx.showModal({ title: '回填完成', content: `成功 ${updated} 条\n跳过 ${skipped} 条\n失败 ${failed} 条`, showCancel: false }); + + // 重新加载地标,使筛选与展示即时生效 + await this.loadCampusLandmarksMarkers(); + } catch (e) { + wx.hideLoading(); + wx.showToast({ title: '回填异常,请稍后重试', icon: 'none' }); + } + }, + /** + * 加载用户默认地址坐标,并在地图上叠加“地址附近的交易点”标记 + */ + async loadUserTradeSpotsNearby() { + try { + const db = wx.cloud.database(); + // 获取 openid + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const r = await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'getOpenId' } }); + openid = r?.result?.openid || ''; + if (openid) wx.setStorageSync('openid', openid); + } catch (_) { /* 忽略 */ } + } + // 查询默认地址(或最近添加的地址) + const res = await db.collection('T_address') + .where(openid ? { _openid: openid } : {}) + .orderBy('isDefault', 'desc') + .orderBy('createTime', 'desc') + .limit(1) + .get(); + const addr = (res.data && res.data[0]) || null; + // 兜底方法:以当前中心(我的定位或校园中心)叠加推荐交易点,保证有标记可见 + const makeFallbackByCenter = () => { + const center = this.data.myLocation && this.data.myLocation.latitude ? this.data.myLocation : this.data.center; + const iconFor = (name) => { + if (String(name).includes('食堂')) return '/images/羊.png'; + if (String(name).includes('图书馆')) return '/images/边牧.png'; + return '/images/更多犬种.png'; + }; + const nearSpots = officialTradeSpots.filter(s => haversineDistance(center.latitude, center.longitude, s.latitude, s.longitude) <= 1500); + const spotMarkers = nearSpots.map((s, i) => ({ + id: 9000 + i, + latitude: s.latitude, + longitude: s.longitude, + width: 36, + height: 36, + iconPath: iconFor(s.name), + // 去掉文字callout,避免遮挡视野 + })); + this.setData({ userAddressLoc: center, tradeSpotMarkers: spotMarkers }); + this.updateMarkers(); + }; + if (!addr) { + makeFallbackByCenter(); + return; + } + const addrStr = `${addr.province || ''}${addr.city || ''}${addr.district || ''}${addr.detail || ''}`.trim(); + if (!addrStr) { + makeFallbackByCenter(); + return; + } + // 正向编码得到坐标 + let loc = null; + try { + loc = await geocode(addrStr); + } catch (_) { /* 忽略 */ } + if (!loc || typeof loc.latitude !== 'number' || typeof loc.longitude !== 'number') { + makeFallbackByCenter(); + return; + } + let userLoc = { latitude: loc.latitude, longitude: loc.longitude }; + // 若是校园地址但正向编码坐标与校园中心相差过大,进行容错回归校园中心(图书馆) + try { + const centerSpot = officialTradeSpots.find(s => String(s.name).includes('图书馆')) || officialTradeSpots[0]; + const campusKeywords = ['中国民航大学', '民航大学', '东丽校区']; + const isCampusAddr = campusKeywords.some(k => addrStr.includes(k)); + if (isCampusAddr && centerSpot && typeof centerSpot.latitude === 'number' && typeof centerSpot.longitude === 'number') { + const dist = haversineDistance(userLoc.latitude, userLoc.longitude, centerSpot.latitude, centerSpot.longitude); + // 若偏差超过 1.5km,采用校园中心,保证推荐交易点可见 + if (dist > 1500) { + userLoc = { latitude: centerSpot.latitude, longitude: centerSpot.longitude }; + } + } + } catch (_) { /* 忽略容错 */ } + this.setData({ userAddressLoc: userLoc }); + + // 距离 1.5km 内的官方诚信交易点 + const iconFor = (name) => { + if (String(name).includes('食堂')) return '/images/羊.png'; + if (String(name).includes('图书馆')) return '/images/边牧.png'; + return '/images/更多犬种.png'; + }; + const nearSpots = officialTradeSpots.filter(s => haversineDistance(userLoc.latitude, userLoc.longitude, s.latitude, s.longitude) <= 1500); + const spotMarkers = nearSpots.map((s, i) => ({ + id: 9000 + i, + latitude: s.latitude, + longitude: s.longitude, + width: 36, + height: 36, + iconPath: iconFor(s.name), + // 去掉文字callout,避免遮挡视野 + })); + this.setData({ tradeSpotMarkers: spotMarkers }); + // 更新地图标注叠加显示 + this.updateMarkers(); + } catch (e) { + // 集合不存在时,自动尝试初始化 T_address 集合 + const msg = String(e?.message || e?.errMsg || ''); + const code = e?.errCode || e?.code; + if (code === -502005 || /DATABASE_COLLECTION_NOT_EXIST|ResourceNotFound|Db or Table not exist/i.test(msg)) { + try { + await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'createAddressCollection' } }); + wx.showToast({ title: '已初始化地址库,请重试', icon: 'none' }); + } catch (_) { /* 忽略 */ } + } + console.warn('加载地址附近交易点失败:', e); + // 失败时也使用中心兜底,确保图上有标记 + const center = this.data.myLocation && this.data.myLocation.latitude ? this.data.myLocation : this.data.center; + const nearSpots = officialTradeSpots.filter(s => haversineDistance(center.latitude, center.longitude, s.latitude, s.longitude) <= 1500); + const spotMarkers = nearSpots.map((s, i) => ({ + id: 9000 + i, + latitude: s.latitude, + longitude: s.longitude, + width: 36, + height: 36, + iconPath: '/images/更多犬种.png', + // 去掉文字callout,避免遮挡视野 + })); + this.setData({ userAddressLoc: center, tradeSpotMarkers: spotMarkers }); + this.updateMarkers(); + } + } +}); \ No newline at end of file diff --git a/src5/code/miniprogram/pages/market/market.json b/src5/code/miniprogram/pages/market/market.json new file mode 100644 index 0000000..ede9990 --- /dev/null +++ b/src5/code/miniprogram/pages/market/market.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "校园市场", + "enablePullDownRefresh": false +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/market/market.wxml b/src5/code/miniprogram/pages/market/market.wxml new file mode 100644 index 0000000..801b659 --- /dev/null +++ b/src5/code/miniprogram/pages/market/market.wxml @@ -0,0 +1,50 @@ + + + + + 分类:{{categories[categoryIndex] || '全部'}} + + + + + + + + + + + + + + + + + + 半径:{{scanRadiusLabels[scanRadiusIndex]}} + + + + + + + + + + + + {{nearestModalTitle}} + + + + + + + {{item.productName || '商品'}} + {{item._distanceText || (item._distance || 0) + 'm'}} + + + + + \ No newline at end of file diff --git a/src5/code/miniprogram/pages/market/market.wxss b/src5/code/miniprogram/pages/market/market.wxss new file mode 100644 index 0000000..a94dace --- /dev/null +++ b/src5/code/miniprogram/pages/market/market.wxss @@ -0,0 +1,103 @@ +.market-container { + position: relative; + width: 100%; + height: 100vh; + margin: 0; + padding: 0; +} + +.filters { + position: absolute; + top: 20rpx; + left: 20rpx; + z-index: 10; + display: flex; + align-items: center; + gap: 16rpx; + background: rgba(255,255,255,0.95); + border-radius: 12rpx; + padding: 12rpx 16rpx; + box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.08); +} + +.filter-item { + background: #f8f9fa; + border-radius: 8rpx; + padding: 8rpx 12rpx; +} + +.filter-display { + font-size: 24rpx; + color: #333; +} + +.price-input { + width: 140rpx; + height: 60rpx; + padding: 0 12rpx; + background: #fff; + border: 1rpx solid #e0e0e0; + border-radius: 8rpx; +} + +.dash { color: #999; } + +.map-wrapper { position: relative; width: 100%; height: 100%; } +.toolbar { + position: absolute; + right: 20rpx; + bottom: 140rpx; + display: flex; + gap: 12rpx; + background: rgba(255,255,255,0.95); + border-radius: 12rpx; + padding: 12rpx; + box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.08); + z-index: 1100; +} + +.radius-picker { background: #f8f9fa; border-radius: 10rpx; padding: 6rpx 10rpx; } +.radius-display { font-size: 22rpx; color: #2a4a7a; } + +.nearest-modal { + position: absolute; + left: 5%; + right: 5%; + bottom: 140rpx; + background: #ffffff; + border-radius: 16rpx; + box-shadow: 0 10rpx 26rpx rgba(0,0,0,0.12); + z-index: 1100; + max-height: 44vh; + display: flex; + flex-direction: column; +} +.nearest-header { display:flex; justify-content: space-between; align-items:center; padding: 16rpx; border-bottom: 1rpx solid #eef2f7; } +.nearest-title { font-weight: 700; color: #2a4a7a; } +.nearest-list { height: 36vh; } +.nearest-item { display:flex; align-items:center; padding: 16rpx; border-bottom: 1rpx solid #f0f4fa; } +.nearest-thumb { width: 72rpx; height: 72rpx; border-radius: 8rpx; background: #f0f0f0; margin-right: 12rpx; } +.nearest-info { display:flex; flex-direction:column; gap:6rpx; } +.nearest-name { color: #333; font-weight: 600; } +.nearest-dist { color: #4f8bff; } + +/* 地图自定义气泡样式(商品缩略图) */ +.bubble { + display: flex; + align-items: center; + justify-content: center; + width: 72rpx; + height: 72rpx; + border-radius: 36rpx; + box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.15); + background: #fff; + border: 2rpx solid #e0e0e0; +} +.bubble-img { + width: 64rpx; + height: 64rpx; + border-radius: 32rpx; + background: #f0f0f0; +} + +/* 删除列表相关样式,保留地图全屏展示 */ \ No newline at end of file diff --git a/src5/code/miniprogram/pages/messages/messages.js b/src5/code/miniprogram/pages/messages/messages.js new file mode 100644 index 0000000..36ade2a --- /dev/null +++ b/src5/code/miniprogram/pages/messages/messages.js @@ -0,0 +1,118 @@ +Page({ + data: { + myOpenId: '', + myUserId: '', + sessions: [], + loading: true + }, + + async onLoad() { + const myOpenId = await this.ensureOpenId(); + const myUserId = (wx.getStorageSync('userInfo') || {})._id || ''; + this.setData({ myOpenId, myUserId }); + await this.loadSessions(); + this.startPolling(); + }, + + onShow() { + try { + if (this.getTabBar && this.getTabBar()) { + this.getTabBar().setSelected(3); + } + } catch (e) {} + }, + + onUnload() { + if (this._pollTimer) clearInterval(this._pollTimer); + }, + + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'getOpenId' } }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { + console.error('获取openid失败:', err); + } + } + return openid; + }, + + startPolling() { + if (this._pollTimer) clearInterval(this._pollTimer); + this._pollTimer = setInterval(() => this.loadSessions(), 5000); + }, + + async loadSessions() { + try { + const db = wx.cloud.database(); + const _ = db.command; + // 双向聚合:同时查询“我发给对方”和“对方发给我”的会话文档 + const ret = await db.collection('T_message').where(_.or([ + { fromUserId: this.data.myUserId }, + { toUserId: this.data.myUserId } + ])).get(); + const docs = ret.data || []; + + // 以对方 userId 归并会话,取最新一条消息作为预览 + const byPeer = {}; + for (const d of docs) { + const msgs = Array.isArray(d.message) ? d.message : []; + const last = msgs[msgs.length - 1] || {}; + const peerUserId = d.fromUserId === this.data.myUserId ? d.toUserId : d.fromUserId; + if (!peerUserId) continue; + const sessionKey = [this.data.myUserId, peerUserId].sort().join('|'); + const prev = byPeer[peerUserId]; + const cur = { + sessionKey, + peerUserId, + peerOpenId: '', + lastContentType: last.contentType || 'text', + lastContent: last.content || '', + lastTime: last.timestamp || Date.now(), + timeText: this.formatTime(last.timestamp || Date.now()), + peerName: '', + peerAvatar: '', + unreadCount: 0 + }; + if (!prev || cur.lastTime > prev.lastTime) byPeer[peerUserId] = cur; + } + + let sessions = Object.values(byPeer); + const peerIds = sessions.map(s => s.peerUserId).filter(Boolean); + if (peerIds.length) { + const ures = await db.collection('T_user').where({ _id: _.in(peerIds) }).get(); + const umap = {}; + (ures.data || []).forEach(u => { umap[u._id] = u; }); + sessions = sessions.map(s => { + const u = umap[s.peerUserId] || {}; + return { ...s, peerName: u.sname || u.nickName || '用户', peerAvatar: u.avatar || '', peerOpenId: u._openid || '' }; + }); + } + sessions.sort((a,b)=>b.lastTime - a.lastTime); + this.setData({ sessions, loading: false }); + } catch (err) { + console.error('加载会话失败:', err); + this.setData({ loading: false }); + } + }, + + openChat(e) { + const userid = e.currentTarget.dataset.userid; + const name = e.currentTarget.dataset.name || '用户'; + // 同时携带 openid 以兼容旧数据 + const openid = e.currentTarget.dataset.openid || ''; + const params = `toUserId=${userid}&toOpenId=${openid}&toName=${encodeURIComponent(name)}`; + wx.navigateTo({ url: `/pages/chat/chat?${params}` }); + }, + + formatTime(ts) { + const d = new Date(ts); + const pad = n => (n < 10 ? '0' + n : n); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; + } +}); \ No newline at end of file diff --git a/src5/code/miniprogram/pages/messages/messages.json b/src5/code/miniprogram/pages/messages/messages.json new file mode 100644 index 0000000..d15c93d --- /dev/null +++ b/src5/code/miniprogram/pages/messages/messages.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "消息列表", + "usingComponents": {} +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/messages/messages.wxml b/src5/code/miniprogram/pages/messages/messages.wxml new file mode 100644 index 0000000..f97c95b --- /dev/null +++ b/src5/code/miniprogram/pages/messages/messages.wxml @@ -0,0 +1,23 @@ + + + 最近联系 + + + + + + + {{item.unreadCount}} + + + + {{item.peerName || '用户'}} + {{item.timeText}} + + {{item.lastContentType === 'text' ? item.lastContent : '[图片]'}} + + + + 暂无会话 + + \ No newline at end of file diff --git a/src5/code/miniprogram/pages/messages/messages.wxss b/src5/code/miniprogram/pages/messages/messages.wxss new file mode 100644 index 0000000..e7c395f --- /dev/null +++ b/src5/code/miniprogram/pages/messages/messages.wxss @@ -0,0 +1,14 @@ +.messages-page { display:flex; flex-direction:column; height:100vh; background:#f7f7f7; } +.header { padding:20rpx; background:#fff; box-shadow:0 4rpx 12rpx rgba(0,0,0,0.05); } +.title { font-size:30rpx; font-weight:600; color:#333; } +.session-list { flex:1; } +.session-item { display:flex; align-items:center; padding:20rpx; border-bottom:1rpx solid #eee; background:#fff; } +.avatar { width:64rpx; height:64rpx; border-radius:50%; margin-right:16rpx; } +.avatar-wrap { position: relative; margin-right:16rpx; } +.badge { position:absolute; top:-4rpx; right:-4rpx; min-width:28rpx; height:28rpx; background:#ff3b30; color:#fff; border-radius:14rpx; font-size:20rpx; display:flex; align-items:center; justify-content:center; padding:0 8rpx; box-shadow:0 2rpx 6rpx rgba(255,59,48,0.4); } +.info { flex:1; display:flex; flex-direction:column; } +.row { display:flex; justify-content:space-between; align-items:center; } +.name { font-size:28rpx; color:#333; font-weight:600; } +.time { font-size:22rpx; color:#999; } +.preview { font-size:24rpx; color:#666; margin-top:6rpx; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +.empty { text-align:center; color:#999; padding:40rpx; } \ No newline at end of file diff --git a/src5/code/miniprogram/pages/myProducts/myProduct.json b/src5/code/miniprogram/pages/myProducts/myProduct.json new file mode 100644 index 0000000..3a7b76b --- /dev/null +++ b/src5/code/miniprogram/pages/myProducts/myProduct.json @@ -0,0 +1,8 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "我的商品", + "navigationBarBackgroundColor": "#4285F4", + "navigationBarTextStyle": "white", + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/myProducts/myProducts.js b/src5/code/miniprogram/pages/myProducts/myProducts.js new file mode 100644 index 0000000..63794cc --- /dev/null +++ b/src5/code/miniprogram/pages/myProducts/myProducts.js @@ -0,0 +1,603 @@ +// pages/myProducts/myProducts.js +Page({ + + /** + * 页面的初始数据 + */ + data: { + // 商品分类 + categories: ['全部', '电子产品', '图书文具', '服装鞋帽', '运动户外', '美妆个护', '家居生活', '其他'], + selectedCategory: 0, + + // 商品状态筛选 + statusFilter: ['全部', '在售', '交易中', '已售出', '已下架'], + selectedStatus: 0, + + // 排序选项 + sortOptions: ['默认排序', '发布时间', '价格从低到高', '价格从高到低'], + selectedSort: 0, + + // 商品列表数据 + products: [], + filteredProducts: [], + + // 加载状态 + isLoading: true, + error: '', + + // 分页相关 + currentPage: 1, + pageSize: 10, + hasMore: true + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + // 清除可能缓存的旧openid,确保使用登录的用户ID获取正确的openid + const userInfo = wx.getStorageSync('userInfo') || {}; + if (userInfo._id) { + // 如果有登录的用户ID,清除旧的openid缓存,强制重新获取 + wx.removeStorageSync('openid'); + } + this.loadMyProducts(); + }, + + /** + * 生命周期函数--监听页面显示 + */ + onShow() { + // 每次显示页面时强制重新加载数据,确保数据最新 + // 清除可能缓存的旧openid,确保使用登录的用户ID获取正确的openid + const userInfo = wx.getStorageSync('userInfo') || {}; + if (userInfo._id) { + // 如果有登录的用户ID,清除旧的openid缓存,强制重新获取 + wx.removeStorageSync('openid'); + } + this.loadMyProducts(); + }, + + /** + * 确保有openid + */ + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getOpenId' + } + }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { + console.error('获取openid失败:', err); + } + } + return openid; + }, + + /** + * 加载我的商品列表 + */ + async loadMyProducts() { + this.setData({ + isLoading: true, + error: '' + }); + + try { + const db = wx.cloud.database(); + let openid = null; + + // 强制使用登录时保存的用户ID来获取openid + const userInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = userInfo._id; + + if (!loggedInUserId) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + this.setData({ + products: [], + filteredProducts: [], + isLoading: false, + hasMore: false, + error: '请先登录' + }); + return; + } + + // 强制使用登录的用户ID查询用户信息,获取其_openid + try { + const userResult = await db.collection('T_user') + .doc(loggedInUserId) + .get(); + + if (userResult.data && userResult.data._openid) { + openid = userResult.data._openid; + console.log('通过登录用户ID获取到正确的openid:', openid); + console.log('登录的用户ID:', loggedInUserId); + console.log('登录的用户信息:', userInfo); + } else { + console.error('用户记录中没有_openid:', userResult.data); + wx.showToast({ + title: '获取用户信息失败', + icon: 'none' + }); + this.setData({ + products: [], + filteredProducts: [], + isLoading: false, + hasMore: false, + error: '获取用户信息失败' + }); + return; + } + } catch (err) { + console.error('通过用户ID获取openid失败:', err); + wx.showToast({ + title: '获取用户信息失败', + icon: 'none' + }); + this.setData({ + products: [], + filteredProducts: [], + isLoading: false, + hasMore: false, + error: '获取用户信息失败' + }); + return; + } + + console.log('查询我的商品,使用openid:', openid); + console.log('登录的用户ID:', loggedInUserId); + + // 查询当前用户的商品 + // 如果登录了账号(有loggedInUserId),强制只使用sellerUserId查询,确保精确匹配 + // 如果没有登录的用户ID,才使用sellerOpenId和_openid(向后兼容) + const _ = db.command; + let result; + + if (loggedInUserId) { + // 强制使用sellerUserId查询,确保只显示当前登录账号发布的商品 + try { + result = await db.collection('T_product') + .where({ + sellerUserId: loggedInUserId + }) + .orderBy('createTime', 'desc') + .get(); + + console.log('通过sellerUserId查询商品,用户ID:', loggedInUserId, '查询到:', result.data ? result.data.length : 0); + + // 如果没有查询到数据,直接返回空列表(新账号应该返回空) + if (!result.data || result.data.length === 0) { + console.log('当前账号没有发布过商品,返回空列表'); + result = { data: [] }; + } + } catch (err) { + console.error('通过sellerUserId查询商品失败:', err); + // 即使查询失败,也不回退到openid查询,直接返回空列表 + result = { data: [] }; + } + } else { + // 如果没有登录的用户ID,使用sellerOpenId和_openid(向后兼容) + console.log('没有登录的用户ID,使用sellerOpenId和_openid查询'); + try { + result = await db.collection('T_product') + .where( + _.or([ + { sellerOpenId: openid }, + { _openid: openid } + ]) + ) + .orderBy('createTime', 'desc') + .get(); + } catch (err) { + console.error('查询商品失败:', err); + result = { data: [] }; + } + } + + console.log('我的商品查询结果:', result); + console.log('查询到的商品数量:', result.data ? result.data.length : 0); + + if (result.data && result.data.length > 0) { + // 将数据库数据转换为页面需要的格式 + const products = result.data.map((item, index) => { + // 状态映射:在售/交易中/已售出/已下架 -> selling/transacting/sold/off + let status = 'selling'; + if (item.status === '交易中' || item.status === '待确认付款' || item.status === '待发货' || item.status === '待收货') { + status = 'transacting'; + } else if (item.status === '已售' || item.status === '已售出') { + status = 'sold'; + } else if (item.status === '已下架') { + status = 'off'; + } + + // 格式化时间 + const publishTime = this.formatTime(item.createTime); + + return { + id: item._id, + name: item.productName || '商品名称', + price: item.salePrice || item.suggestedPrice || item.originalPrice || 0, + originalPrice: item.originalPrice || 0, + image: item.productImage || 'https://via.placeholder.com/600x400/cccccc/ffffff?text=Product', + category: item.productCategory || '其他', + status: status, + publishTime: publishTime, + createTime: item.createTime ? new Date(item.createTime).getTime() : Date.now(), // 保存原始时间戳用于排序 + views: item.viewCount || 0, + likes: item.likeCount || 0, + comments: 0 // 评论数暂时设为0,如果有评论集合可以查询 + }; + }); + + console.log('转换后的商品列表:', products); + + this.setData({ + products: products, + isLoading: false, + hasMore: false + }); + + // 应用筛选 + this.filterProducts(); + } else { + console.log('没有查询到商品数据'); + this.setData({ + products: [], + filteredProducts: [], + isLoading: false, + hasMore: false + }); + } + } catch (err) { + console.error('加载我的商品失败:', err); + console.error('错误详情:', JSON.stringify(err, null, 2)); + this.setData({ + error: '加载失败,请重试', + isLoading: false, + products: [], + filteredProducts: [] + }); + wx.showToast({ + title: '加载失败: ' + (err.errMsg || '未知错误'), + icon: 'none', + duration: 3000 + }); + } + }, + + /** + * 格式化时间 + */ + formatTime(date) { + if (!date) return ''; + const d = new Date(date); + const now = new Date(); + const diff = now - d; + + const minute = 60 * 1000; + const hour = 60 * minute; + const day = 24 * hour; + + if (diff < minute) { + return '刚刚'; + } else if (diff < hour) { + return Math.floor(diff / minute) + '分钟前'; + } else if (diff < day) { + return Math.floor(diff / hour) + '小时前'; + } else if (diff < 7 * day) { + return Math.floor(diff / day) + '天前'; + } else { + return d.getMonth() + 1 + '月' + d.getDate() + '日'; + } + }, + + /** + * 切换商品分类 + */ + onCategoryChange(e) { + const index = e.currentTarget.dataset.index; + this.setData({ + selectedCategory: index, + currentPage: 1 + }); + this.filterProducts(); + }, + + /** + * 切换商品状态筛选 + */ + onStatusChange(e) { + const index = e.currentTarget.dataset.index; + this.setData({ + selectedStatus: index, + currentPage: 1 + }); + this.filterProducts(); + }, + + /** + * 切换排序方式 + */ + onSortChange(e) { + const index = e.currentTarget.dataset.index; + this.setData({ + selectedSort: index + }); + this.filterProducts(); + }, + + /** + * 根据筛选条件过滤商品 + */ + filterProducts() { + let filtered = [...this.data.products]; + + // 按分类筛选 + if (this.data.selectedCategory !== 0) { + const category = this.data.categories[this.data.selectedCategory]; + filtered = filtered.filter(item => item.category === category); + } + + // 按状态筛选 + if (this.data.selectedStatus !== 0) { + const statusMap = { + 1: 'selling', // 在售 + 2: 'transacting', // 交易中 + 3: 'sold', // 已售出 + 4: 'off' // 已下架 + }; + const status = statusMap[this.data.selectedStatus]; + filtered = filtered.filter(item => item.status === status); + } + + // 排序 + switch (this.data.selectedSort) { + case 1: // 发布时间(最新的在前) + filtered.sort((a, b) => { + const timeA = a.createTime || 0; + const timeB = b.createTime || 0; + return timeB - timeA; // 降序:最新的在前 + }); + break; + case 2: // 价格从低到高 + filtered.sort((a, b) => parseFloat(a.price) - parseFloat(b.price)); + break; + case 3: // 价格从高到低 + filtered.sort((a, b) => parseFloat(b.price) - parseFloat(a.price)); + break; + default: // 默认排序(保持数据库返回的顺序) + // 保持原顺序 + break; + } + + this.setData({ + filteredProducts: filtered + }); + }, + + /** + * 查看商品详情 + */ + onProductDetail(e) { + const productId = e.currentTarget.dataset.id; + wx.navigateTo({ + url: `/pages/product-detail/product-detail?id=${productId}` + }); + }, + + /** + * 处理交易:跳转到订单页面(卖家视角) + */ + onHandleTransaction(e) { + const productId = e.currentTarget.dataset.id; + // 跳转到订单页并切换为卖家视角;可附带productId用于后续扩展筛选 + wx.navigateTo({ + url: `/pages/orders/orders?role=seller&productId=${productId}` + }); + }, + + /** + * 下架商品 + */ + async onOffShelf(e) { + const productId = e.currentTarget.dataset.id; + + wx.showModal({ + title: '确认下架', + content: '确定要下架该商品吗?下架后可重新上架。', + success: async (res) => { + if (res.confirm) { + try { + const db = wx.cloud.database(); + + // 更新数据库中的商品状态 + await db.collection('T_product').doc(productId).update({ + data: { + status: '已下架', + updateTime: new Date() + } + }); + try { + const pr = await db.collection('T_product').doc(productId).get(); + const p = pr.data || {}; + const name = p.tradeLandmarkName || (p.tradeLocation && p.tradeLocation.landmarkName) || p.tradeAddress || ''; + const lat = Number(p.tradeLocationLat ?? (p.tradeLocation && p.tradeLocation.latitude)); + const lng = Number(p.tradeLocationLng ?? (p.tradeLocation && p.tradeLocation.longitude)); + const selling = false; + if (name && Number.isFinite(lat) && Number.isFinite(lng)) { + await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { type: 'upsertCampusLandmark', name, latitude: lat, longitude: lng, address: p.tradeAddress || '', source: 'product', productId, selling, productCategory: p.productCategory || '', thumbUrl: (Array.isArray(p.productImage) ? p.productImage[0] : (typeof p.productImage === 'string' ? p.productImage : '')) } + }); + } + } catch (_) {} + + // 重新加载商品列表 + await this.loadMyProducts(); + + wx.showToast({ + title: '商品已下架', + icon: 'success' + }); + } catch (err) { + console.error('下架商品失败:', err); + wx.showToast({ + title: '操作失败', + icon: 'none' + }); + } + } + } + }); + }, + + /** + * 重新上架商品 + */ + async onReShelf(e) { + const productId = e.currentTarget.dataset.id; + + wx.showModal({ + title: '重新上架', + content: '确定要重新上架该商品吗?', + success: async (res) => { + if (res.confirm) { + try { + const db = wx.cloud.database(); + + // 更新数据库中的商品状态 + await db.collection('T_product').doc(productId).update({ + data: { + status: '在售', + updateTime: new Date() + } + }); + try { + const pr = await db.collection('T_product').doc(productId).get(); + const p = pr.data || {}; + const name = p.tradeLandmarkName || (p.tradeLocation && p.tradeLocation.landmarkName) || p.tradeAddress || ''; + const lat = Number(p.tradeLocationLat ?? (p.tradeLocation && p.tradeLocation.latitude)); + const lng = Number(p.tradeLocationLng ?? (p.tradeLocation && p.tradeLocation.longitude)); + const selling = true; + if (name && Number.isFinite(lat) && Number.isFinite(lng)) { + await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { type: 'upsertCampusLandmark', name, latitude: lat, longitude: lng, address: p.tradeAddress || '', source: 'product', productId, selling, productCategory: p.productCategory || '', thumbUrl: (Array.isArray(p.productImage) ? p.productImage[0] : (typeof p.productImage === 'string' ? p.productImage : '')) } + }); + } + } catch (_) {} + + // 重新加载商品列表 + await this.loadMyProducts(); + + wx.showToast({ + title: '商品已重新上架', + icon: 'success' + }); + } catch (err) { + console.error('重新上架商品失败:', err); + wx.showToast({ + title: '操作失败', + icon: 'none' + }); + } + } + } + }); + }, + + /** + * 编辑商品 + */ + onEditProduct(e) { + const productId = e.currentTarget.dataset.id; + wx.navigateTo({ + url: `/pages/publish/publish?mode=edit&id=${productId}` + }); + }, + + /** + * 删除商品 + */ + async onDeleteProduct(e) { + const productId = e.currentTarget.dataset.id; + + wx.showModal({ + title: '确认删除', + content: '确定要删除该商品吗?删除后无法恢复。', + success: async (res) => { + if (res.confirm) { + try { + const db = wx.cloud.database(); + + // 从数据库中删除商品 + await db.collection('T_product').doc(productId).remove(); + + // 重新加载商品列表(这样会自动移除不存在的商品) + await this.loadMyProducts(); + + wx.showToast({ + title: '商品已删除', + icon: 'success' + }); + } catch (err) { + console.error('删除商品失败:', err); + wx.showToast({ + title: '删除失败', + icon: 'none' + }); + } + } + } + }); + }, + + /** + * 上拉加载更多 + */ + onReachBottom() { + if (this.data.hasMore && !this.data.isLoading) { + // 实际项目中这里应该请求下一页数据 + wx.showToast({ + title: '没有更多数据了', + icon: 'none' + }); + } + }, + + /** + * 下拉刷新 + */ + async onPullDownRefresh() { + await this.loadMyProducts(); + wx.stopPullDownRefresh(); + }, + + /** + * 返回上一页 + */ + onBack() { + wx.navigateBack(); + }, + + /** + * 去发布商品 + */ + onPublish() { + wx.navigateTo({ + url: '/pages/publish/publish' + }); + } +}); \ No newline at end of file diff --git a/src5/code/miniprogram/pages/myProducts/myProducts.json b/src5/code/miniprogram/pages/myProducts/myProducts.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/src5/code/miniprogram/pages/myProducts/myProducts.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/myProducts/myProducts.wxml b/src5/code/miniprogram/pages/myProducts/myProducts.wxml new file mode 100644 index 0000000..7010cb7 --- /dev/null +++ b/src5/code/miniprogram/pages/myProducts/myProducts.wxml @@ -0,0 +1,139 @@ + + + + + + + 加载中... + + + + + + {{error}} + + + + + + + + + {{item}} + + + + + + + 状态: + + + {{item}} + + + + + + 排序: + + + {{item}} + + + + + + + + + + 暂无商品 + + + + + + + + + + + + 已售出 + + + 交易中 + + + 已下架 + + + + + + + {{item.name}} + + + + {{item.category}} + {{item.publishTime}} + + + + + 👁 + {{item.views}} + + + ❤️ + {{item.likes}} + + + 💬 + {{item.comments}} + + + + + + + ¥{{item.price}} + ¥{{item.originalPrice}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src5/code/miniprogram/pages/myProducts/myProducts.wxss b/src5/code/miniprogram/pages/myProducts/myProducts.wxss new file mode 100644 index 0000000..898734d --- /dev/null +++ b/src5/code/miniprogram/pages/myProducts/myProducts.wxss @@ -0,0 +1,470 @@ +/* pages/myProducts/myProducts.wxss */ +/* 全局样式 - 与小程序保持一致 */ +page { + height: 100%; + background-color: #f5f5f5; +} + +.page-container { + min-height: 100vh; + background-color: #f5f5f5; +} + +/* 头部样式 - 使用小程序主色调 */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + height: 88rpx; + padding: 0 30rpx; + background-color: #4285F4; + color: white; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); +} + +.back-btn { + width: 88rpx; + height: 88rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.back-icon { + font-size: 40rpx; + font-weight: bold; + color: white; +} + +.header-title { + font-size: 36rpx; + font-weight: bold; + color: white; +} + +.header-right { + width: 88rpx; +} + +/* 内容区域 */ +.content { + padding-top: 88rpx; +} + +/* 分类滚动 - 与main页面风格一致 */ +.category-scroll { + display: flex; + white-space: nowrap; + background-color: white; + padding: 20rpx 0; + border-bottom: 1rpx solid #eee; +} + +.category-item { + display: inline-flex; + padding: 0 30rpx; + height: 60rpx; + align-items: center; + justify-content: center; + font-size: 28rpx; + color: #333; + position: relative; +} + +.category-item.selected { + color: #4285F4; + font-weight: bold; +} + +.category-item.selected::after { + content: ''; + position: absolute; + bottom: 0; + left: 30%; + width: 40%; + height: 6rpx; + background-color: #4285F4; + border-radius: 3rpx; +} + +/* 筛选区域 - 卡片式设计 */ +.filter-section { + background-color: white; + padding: 20rpx 30rpx; + margin-bottom: 20rpx; + border-bottom: 1rpx solid #eee; + border-radius: 0 0 10rpx 10rpx; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); +} + +.filter-group, +.sort-group { + display: flex; + align-items: center; + margin-bottom: 10rpx; +} + +.sort-group { + margin-bottom: 0; +} + +.filter-label { + font-size: 28rpx; + color: #666; + margin-right: 20rpx; + width: 80rpx; +} + +.filter-options { + display: flex; + flex-wrap: wrap; + flex: 1; +} + +.filter-item { + padding: 8rpx 20rpx; + margin-right: 20rpx; + margin-bottom: 10rpx; + background-color: #f8f9fa; + border-radius: 20rpx; + font-size: 26rpx; + color: #666; + transition: all 0.3s ease; + border: 1rpx solid #e9ecef; +} + +.filter-item.selected { + background-color: #4285F4; + color: white; + border-color: #4285F4; +} + +/* 加载状态 */ +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; +} + +.loading-spinner { + width: 60rpx; + height: 60rpx; + border: 8rpx solid #f3f3f3; + border-top: 8rpx solid #4285F4; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + margin-top: 20rpx; + font-size: 28rpx; + color: #999; +} + +/* 错误状态 */ +.error-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; + padding: 40rpx; + text-align: center; +} + +.error-icon { + font-size: 80rpx; + margin-bottom: 20rpx; + color: #ff6b6b; +} + +.error-text { + font-size: 28rpx; + color: #666; + margin-bottom: 30rpx; +} + +.retry-btn { + background-color: #4285F4; + color: white; + font-size: 28rpx; + padding: 0 40rpx; + border-radius: 44rpx; + box-shadow: 0 4rpx 12rpx rgba(66, 133, 244, 0.3); +} + +.retry-btn:active { + transform: scale(0.95); + opacity: 0.9; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; + background-color: white; + margin: 20rpx; + border-radius: 16rpx; + padding: 40rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.empty-image { + width: 200rpx; + height: 200rpx; + margin-bottom: 30rpx; + opacity: 0.5; +} + +.empty-text { + font-size: 28rpx; + color: #999; + margin-bottom: 30rpx; +} + +.publish-btn { + background-color: #4285F4; + color: white; + font-size: 28rpx; + padding: 0 60rpx; + border-radius: 44rpx; + box-shadow: 0 4rpx 12rpx rgba(66, 133, 244, 0.3); +} + +.publish-btn:active { + transform: scale(0.95); + opacity: 0.9; +} + +/* 商品列表 */ +.product-list { + padding: 0 20rpx 20rpx; +} + +.product-item { + background-color: white; + border-radius: 16rpx; + margin-bottom: 20rpx; + overflow: hidden; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.product-info { + display: flex; + padding: 20rpx; +} + +.product-image-container { + position: relative; + width: 180rpx; + height: 180rpx; + margin-right: 20rpx; + border-radius: 8rpx; + overflow: hidden; +} + +.product-image { + width: 100%; + height: 100%; + border-radius: 8rpx; +} + +.product-status { + position: absolute; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.6); + color: white; + padding: 8rpx 16rpx; + font-size: 24rpx; + border-radius: 8rpx 0 8rpx 0; +} + +.product-status.transacting { + background-color: rgba(255, 152, 0, 0.6); +} + +.product-status.off { + background-color: rgba(150, 150, 150, 0.6); +} + +.product-details { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.product-title { + font-size: 30rpx; + color: #333; + line-height: 44rpx; + margin-bottom: 10rpx; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.product-meta { + display: flex; + justify-content: space-between; + font-size: 24rpx; + color: #999; + margin-bottom: 10rpx; +} + +.product-stats { + display: flex; + align-items: center; +} + +.stat-item { + display: flex; + align-items: center; + margin-right: 20rpx; + font-size: 24rpx; + color: #999; +} + +.stat-icon { + margin-right: 4rpx; +} + +.price-info { + display: flex; + align-items: baseline; + justify-content: flex-end; +} + +.current-price { + font-size: 36rpx; + font-weight: bold; + color: #ff6b81; +} + +.original-price { + font-size: 24rpx; + color: #999; + text-decoration: line-through; + margin-left: 10rpx; +} + +/* 操作按钮 - 优化样式,确保更小并在同一行排列 */ +.action-buttons { + display: flex; + padding: 12rpx 15rpx; + border-top: 1rpx solid #f0f0f0; + justify-content: flex-end; +} + +.action-btn { + flex: 0 0 auto; /* 不自动拉伸,保持按钮固定大小 */ + margin: 0 6rpx; + font-size: 22rpx; /* 进一步减小字体大小 */ + line-height: 50rpx; /* 进一步减小按钮高度 */ + padding: 0 25rpx; /* 添加水平内边距,确保文字不会太拥挤 */ + border-radius: 8rpx; + transition: all 0.3s ease; + box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1); + border: none; + white-space: nowrap; /* 确保按钮文字不会换行 */ +} + +/* 增加按钮点击效果 */ +.action-btn:active { + transform: scale(0.95); + opacity: 0.9; +} + +/* 保持与小程序整体色彩一致 */ +.edit-btn { + background-color: #4285F4; /* 主色调 */ + color: white; +} + +.off-btn { + background-color: #ff9800; /* 下架按钮使用橙色 */ + color: white; +} + +.on-btn { + background-color: #34C759; /* 上架按钮使用绿色 */ + color: white; +} + +.delete-btn { + background-color: #ff6b81; /* 删除按钮使用红色,与订单页面一致 */ + color: white; +} + +.view-btn { + background-color: #2196f3; /* 查看按钮使用蓝色变体 */ + color: white; +} + +/* 响应式调整 - 确保小屏幕上按钮仍能在一行显示 */ +@media screen and (max-width: 320px) { + .filter-options { + flex-direction: column; + } + + .filter-item { + margin-right: 0; + margin-bottom: 10rpx; + } + + .product-info { + flex-direction: column; + } + + .product-image-container { + width: 100%; + height: 300rpx; + margin-right: 0; + margin-bottom: 20rpx; + } + + .price-info { + justify-content: flex-start; + } + + .action-buttons { + padding: 8rpx 10rpx; + overflow-x: auto; /* 允许在极端情况下水平滚动 */ + } + + .action-btn { + font-size: 20rpx; + line-height: 45rpx; + margin: 0 4rpx; + padding: 0 20rpx; + } +} + +/* 文本样式 - 与app.wxss保持一致 */ +text { + font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun, sans-serif; +} + +/* 滚动条样式 */ +::-webkit-scrollbar { + width: 0; + height: 0; + color: transparent; +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/orders/orders.js b/src5/code/miniprogram/pages/orders/orders.js new file mode 100644 index 0000000..f68b794 --- /dev/null +++ b/src5/code/miniprogram/pages/orders/orders.js @@ -0,0 +1,587 @@ +// pages/orders/orders.js +Page({ + /** + * 页面的初始数据 + */ + data: { + // 当前选中的标签 + activeTab: 'all', + // 视角:buyer(买家)/ seller(卖家) + viewRole: 'buyer', + + // 订单数据 + orders: [], + + // 加载状态 + isLoading: true + }, + + /** + * 生命周期函数--监听页面加载 + */ + async onLoad(options) { + // 可以从选项中获取初始标签 + if (options.tab) { + this.setData({ + activeTab: options.tab + }); + } + // 支持通过参数设置初始视角(buyer/seller) + if (options.role && (options.role === 'buyer' || options.role === 'seller')) { + this.setData({ viewRole: options.role }); + } + // 清除可能缓存的旧openid,确保使用登录的用户ID获取正确的openid + const userInfo = wx.getStorageSync('userInfo') || {}; + if (userInfo._id) { + // 如果有登录的用户ID,清除旧的openid缓存,强制重新获取 + wx.removeStorageSync('openid'); + } + await this.loadOrders(); + }, + + /** + * 生命周期函数--监听页面显示 + */ + async onShow() { + // 页面显示时强制重新加载订单数据 + // 清除可能缓存的旧openid,确保使用登录的用户ID获取正确的openid + const userInfo = wx.getStorageSync('userInfo') || {}; + if (userInfo._id) { + // 如果有登录的用户ID,清除旧的openid缓存,强制重新获取 + wx.removeStorageSync('openid'); + } + await this.loadOrders(); + }, + + /** + * 页面相关事件处理函数--监听用户下拉动作 + */ + async onPullDownRefresh() { + await this.loadOrders(); + }, + + /** + * 切换订单标签 + */ + switchTab(e) { + const tab = e.currentTarget.dataset.tab; + this.setData({ + activeTab: tab, + isLoading: true + }); + this.loadOrders(); + }, + + /** + * 切换视角(买家/卖家) + */ + switchRole(e) { + const role = e.currentTarget.dataset.role; + if (!role || (role !== 'buyer' && role !== 'seller')) return; + this.setData({ + viewRole: role, + isLoading: true + }); + this.loadOrders(); + }, + + /** + * 确保有openid + */ + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getOpenId' + } + }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { + console.error('获取openid失败:', err); + } + } + return openid; + }, + + /** + * 加载订单数据 + */ + async loadOrders() { + this.setData({ isLoading: true }); + + try { + const db = wx.cloud.database(); + let openid = null; + + // 优先使用登录时保存的用户ID来获取openid + const userInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = userInfo._id; + + if (loggedInUserId) { + try { + // 使用登录的用户ID查询用户信息,获取其_openid + const userResult = await db.collection('T_user') + .doc(loggedInUserId) + .get(); + + if (userResult.data && userResult.data._openid) { + openid = userResult.data._openid; + } + } catch (err) { + console.error('通过用户ID获取openid失败:', err); + } + } + + // 如果无法通过用户ID获取openid,尝试从缓存获取 + if (!openid) { + openid = await this.ensureOpenId(); + } + + if (!openid) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + this.setData({ + orders: [], + isLoading: false + }); + wx.stopPullDownRefresh(); + return; + } + + // 查询订单:根据视角(买家/卖家)选择查询条件 + const _ = db.command; + let query; + if (this.data.viewRole === 'seller') { + if (loggedInUserId) { + query = db.collection('T_order').where({ + sellerUserId: loggedInUserId + }); + console.log('卖家视角,使用sellerUserId查询订单:', loggedInUserId); + } else { + query = db.collection('T_order').where( + _.or([ + { sellerOpenId: openid }, + { _openid: openid } + ]) + ); + console.log('卖家视角,使用sellerOpenId和_openid查询订单'); + } + } else { + // 买家视角 + if (loggedInUserId) { + query = db.collection('T_order').where({ + buyerUserId: loggedInUserId + }); + console.log('买家视角,使用buyerUserId查询订单:', loggedInUserId); + } else { + query = db.collection('T_order').where( + _.or([ + { buyerOpenId: openid }, + { _openid: openid } + ]) + ); + console.log('买家视角,使用buyerOpenId和_openid查询订单'); + } + } + + // 根据标签筛选订单状态 + if (this.data.activeTab !== 'all') { + const tab = this.data.activeTab; + if (tab === 'paid') { + // “待发货”页签同时包含“待确认付款”和“待发货”两种状态 + query = query.where({ + status: _.in(['待确认付款', '待发货']) + }); + } else { + const statusMap = { + 'pending': '待付款', + 'shipped': '待收货', + 'completed': '已完成', + 'cancelled': '已取消' + }; + const status = statusMap[tab]; + if (status) { + query = query.where({ status }); + } + } + } + + const result = await query + .orderBy('createTime', 'desc') + .get(); + + console.log('订单查询结果:', result); + + if (result.data && result.data.length > 0) { + // 格式化订单数据 + const orders = result.data.map(item => { + // 状态映射 + const statusMap = { + '待付款': { status: 'pending', statusText: '待付款' }, + '待确认付款': { status: 'paid', statusText: '待卖家确认' }, + '待发货': { status: 'paid', statusText: '待发货' }, + '待收货': { status: 'shipped', statusText: '待收货' }, + '已完成': { status: 'completed', statusText: '已完成' }, + '已取消': { status: 'cancelled', statusText: '已取消' } + }; + + const statusInfo = statusMap[item.status] || { status: 'pending', statusText: '待付款' }; + + // 格式化时间 + const orderTime = item.createTime ? this.formatTime(item.createTime) : ''; + + // 处理商品列表 + let products = []; + let totalPrice = 0; + let totalCount = 0; + + if (item.products && Array.isArray(item.products)) { + products = item.products; + totalCount = products.length; + totalPrice = products.reduce((sum, p) => sum + (parseFloat(p.price) || 0) * (p.count || 1), 0); + } else if (item.productId) { + // 兼容单个商品的情况 + products = [{ + productId: item.productId, + name: item.productName || '商品', + price: item.price || 0, + count: 1, + image: item.productImage || 'https://via.placeholder.com/600x400/cccccc/666666?text=商品图' + }]; + totalCount = 1; + totalPrice = parseFloat(item.price) || 0; + } + + return { + id: item._id, + orderNumber: item.orderNumber || item._id, + orderTime: orderTime, + status: statusInfo.status, + statusText: statusInfo.statusText, + totalCount: totalCount, + totalPrice: totalPrice, + products: products + }; + }); + + this.setData({ + orders: orders, + isLoading: false + }); + } else { + this.setData({ + orders: [], + isLoading: false + }); + } + + wx.stopPullDownRefresh(); + } catch (err) { + console.error('加载订单失败:', err); + this.setData({ + orders: [], + isLoading: false + }); + wx.stopPullDownRefresh(); + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + } + }, + + /** + * 格式化时间 + */ + formatTime(date) { + if (!date) return ''; + const d = new Date(date); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hours = String(d.getHours()).padStart(2, '0'); + const minutes = String(d.getMinutes()).padStart(2, '0'); + const seconds = String(d.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + }, + + /** + * 取消订单 + */ + async cancelOrder(e) { + const orderId = e.currentTarget.dataset.id; + wx.showModal({ + title: '取消订单', + content: '确定要取消该订单吗?', + success: async (res) => { + if (res.confirm) { + try { + const userInfo = wx.getStorageSync('userInfo') || {}; + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'updateOrderStatus', + orderId, + action: 'buyerCancel', + callerUserId: userInfo._id || '' + } + }); + if (!result?.result?.success) { + throw new Error(result?.result?.error || '取消失败'); + } + + wx.showToast({ + title: '订单已取消', + icon: 'success' + }); + // 标记订单信息已变更,驱动个人中心红点提醒 + wx.setStorageSync('orderChanged', true); + + // 重新加载订单数据 + await this.loadOrders(); + } catch (err) { + console.error('取消订单失败:', err); + wx.showToast({ + title: '操作失败', + icon: 'none' + }); + } + } + } + }); + }, + + /** + * 去付款 + */ + payOrder(e) { + const orderId = e.currentTarget.dataset.id; + console.log('开始支付流程,订单ID:', orderId); + + // 显示加载中状态 + wx.showLoading({ + title: '处理中...', + }); + + // 1. 查找当前订单信息 + const order = this.data.orders.find(item => item.id === orderId); + if (!order) { + wx.hideLoading(); + wx.showToast({ + title: '订单信息不存在', + icon: 'error' + }); + console.error('未找到订单信息:', orderId); + return; + } + console.log('找到订单信息:', order); + + // 2. 纯逻辑模拟支付,无需真实微信支付 + setTimeout(async () => { + try { + const userInfo = wx.getStorageSync('userInfo') || {}; + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'updateOrderStatus', + orderId, + action: 'buyerPay', + callerUserId: userInfo._id || '' + } + }); + if (!result?.result?.success) { + throw new Error(result?.result?.error || '支付更新失败'); + } + wx.hideLoading(); + wx.showToast({ + title: '支付成功(模拟)', + icon: 'success' + }); + // 标记订单信息已变更,驱动个人中心红点提醒 + wx.setStorageSync('orderChanged', true); + // 刷新订单列表 + await this.loadOrders(); + // 可选:跳转详情页 + this.viewOrderDetail(e); + } catch (err) { + console.error('模拟支付更新订单失败:', err); + wx.hideLoading(); + wx.showToast({ + title: '支付失败(网络)', + icon: 'none' + }); + } + }, 600); + }, + + /** + * 卖家确认付款 + */ + async confirmPayment(e) { + const orderId = e.currentTarget.dataset.id; + console.log('卖家确认收款,订单ID:', orderId); + wx.showModal({ + title: '确认收款', + content: '确认已收到买家付款吗?', + success: async (res) => { + if (res.confirm) { + try { + wx.showLoading({ title: '更新中...' }); + const userInfo = wx.getStorageSync('userInfo') || {}; + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'updateOrderStatus', + orderId, + action: 'sellerConfirmPayment', + callerUserId: userInfo._id || '' + } + }); + if (!result?.result?.success) { + throw new Error(result?.result?.error || '确认收款失败'); + } + wx.hideLoading(); + wx.showToast({ title: '已确认收款', icon: 'success' }); + // 标记订单信息已变更,驱动个人中心红点提醒 + wx.setStorageSync('orderChanged', true); + await this.loadOrders(); + } catch (err) { + console.error('确认付款失败:', err); + wx.hideLoading(); + wx.showToast({ + title: '操作失败', + icon: 'none' + }); + } + } + } + }); + }, + + /** + * 卖家发货 + */ + async shipOrder(e) { + const orderId = e.currentTarget.dataset.id; + wx.showModal({ + title: '发货', + content: '确认发货吗?', + success: async (res) => { + if (res.confirm) { + try { + const userInfo = wx.getStorageSync('userInfo') || {}; + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'updateOrderStatus', + orderId, + action: 'sellerShip', + callerUserId: userInfo._id || '' + } + }); + if (!result?.result?.success) { + throw new Error(result?.result?.error || '发货失败'); + } + wx.showToast({ + title: '已发货', + icon: 'success' + }); + // 标记订单信息已变更,驱动个人中心红点提醒 + wx.setStorageSync('orderChanged', true); + await this.loadOrders(); + } catch (err) { + console.error('发货失败:', err); + wx.showToast({ + title: '操作失败', + icon: 'none' + }); + } + } + } + }); + }, + + /** + * 确认收货 + */ + async confirmReceipt(e) { + const orderId = e.currentTarget.dataset.id; + wx.showModal({ + title: '确认收货', + content: '确认已收到商品吗?', + success: async (res) => { + if (res.confirm) { + try { + const userInfo = wx.getStorageSync('userInfo') || {}; + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'updateOrderStatus', + orderId, + action: 'buyerConfirmReceipt', + callerUserId: userInfo._id || '' + } + }); + if (!result?.result?.success) { + throw new Error(result?.result?.error || '确认收货失败'); + } + + wx.showToast({ + title: '已确认收货', + icon: 'success' + }); + // 标记订单信息已变更,驱动个人中心红点提醒 + wx.setStorageSync('orderChanged', true); + // 重新加载订单数据 + await this.loadOrders(); + } catch (err) { + console.error('确认收货失败:', err); + wx.showToast({ + title: '操作失败', + icon: 'none' + }); + } + } + } + }); + }, + + /** + * 查看订单详情 + */ + viewOrderDetail(e) { + const orderId = e.currentTarget.dataset.id; + wx.navigateTo({ + url: `/pages/order-detail/order-detail?id=${orderId}` + }); + }, + + /** + * 再次购买 + */ + buyAgain(e) { + const orderId = e.currentTarget.dataset.id; + wx.showToast({ + title: '将商品加入购物车', + icon: 'none' + }); + // 这里可以实现再次购买的逻辑 + }, + + /** + * 去购物 + */ + goShopping() { + wx.switchTab({ + url: '/pages/main/main' + }); + } +}); \ No newline at end of file diff --git a/src5/code/miniprogram/pages/orders/orders.json b/src5/code/miniprogram/pages/orders/orders.json new file mode 100644 index 0000000..1c08ff3 --- /dev/null +++ b/src5/code/miniprogram/pages/orders/orders.json @@ -0,0 +1,8 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "我的订单", + "navigationBarBackgroundColor": "#f5f5f5", + "navigationBarTextStyle": "black", + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/orders/orders.wxml b/src5/code/miniprogram/pages/orders/orders.wxml new file mode 100644 index 0000000..62aa2be --- /dev/null +++ b/src5/code/miniprogram/pages/orders/orders.wxml @@ -0,0 +1,101 @@ + + + + + 我的购买 + 我的出售 + + + + 全部 + 待付款 + 待发货 + 待收货 + 已完成 + 已取消 + + + + + + + + 加载中... + + + + + + 暂无订单 + 去逛逛,发现心仪商品 + + + + + + + + + + 订单号:{{item.orderNumber}} + {{item.orderTime}} + + {{item.statusText}} + + + + + + + + {{product.name}} + {{product.specs || '规格:标准'}} + + ¥{{product.price}} + x{{product.count}} + + + + + + + + + + 共{{item.totalCount}}件商品 + + + 合计: + ¥ + {{item.totalPrice}} + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src5/code/miniprogram/pages/orders/orders.wxss b/src5/code/miniprogram/pages/orders/orders.wxss new file mode 100644 index 0000000..3bffbf1 --- /dev/null +++ b/src5/code/miniprogram/pages/orders/orders.wxss @@ -0,0 +1,394 @@ +/* pages/orders/orders.wxss *//* 订单页面样式 */ +.orders-container { + padding-bottom: 100rpx; + background-color: #f5f5f5; +} + +/* 顶部视角切换固定 */ +.role-tabs { + display: flex; + background-color: #fff; + border-bottom: 1px solid #e0e0e0; + padding: 0 20rpx; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 200; +} + +.role-tab { + flex: 1; + text-align: center; + padding: 24rpx 0; + font-size: 28rpx; + color: #666; +} + +.role-tab.active { + color: #667eea; + font-weight: 600; +} + +/* 订单状态筛选标签 */ +.order-tabs { + display: flex; + background-color: #fff; + border-bottom: 1px solid #e0e0e0; + padding: 0 20rpx; + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); + position: fixed; + top: 80rpx; /* 位于视角切换条下方 */ + left: 0; + right: 0; + z-index: 100; +} + +.tab-item { + flex: 1; + text-align: center; + padding: 28rpx 0; + font-size: 28rpx; + color: #666; + position: relative; +} + +.tab-item.active { + color: #667eea; +} + +.tab-item.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 30%; + width: 40%; + height: 6rpx; + background-color: #667eea; + border-radius: 3rpx; +} + +/* 订单列表 */ +.order-list { + margin-top: 170rpx; /* 顶部两条固定栏的总高度 */ +} + +/* 加载状态 */ +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 100rpx 0; +} + +.loading-spinner { + width: 60rpx; + height: 60rpx; + border: 6rpx solid #f3f3f3; + border-top: 6rpx solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + margin-top: 20rpx; + font-size: 28rpx; + color: #999; +} + +/* 空状态 */ +.empty-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 150rpx 0; + background-color: #fff; + margin: 20rpx; + border-radius: 20rpx; +} + +.empty-icon { + width: 200rpx; + height: 200rpx; + opacity: 0.5; +} + +.empty-text { + margin-top: 40rpx; + font-size: 32rpx; + color: #333; +} + +.empty-subtext { + margin-top: 20rpx; + font-size: 28rpx; + color: #999; +} + +.go-shopping-btn { + margin-top: 40rpx; + background-color: #667eea; + color: #fff; + border-radius: 40rpx; + font-size: 28rpx; + padding: 20rpx 60rpx; +} + +/* 订单项 */ +.order-item { + background-color: #fff; + margin: 20rpx; + border-radius: 20rpx; + overflow: hidden; +} + +/* 订单头部 */ +.order-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 28rpx 32rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.order-info { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.order-number { + font-size: 26rpx; + color: #999; +} + +.order-time { + font-size: 24rpx; + color: #ccc; +} + +.order-status { + font-size: 28rpx; + font-weight: 500; +} + +.order-status.pending { + color: #ff9500; +} + +.order-status.paid { + color: #5ac8fa; +} + +.order-status.shipped { + color: #34c759; +} + +.order-status.completed { + color: #667eea; +} + +.order-status.cancelled { + color: #999; +} + +/* 订单商品 */ +.order-products { + padding: 28rpx 32rpx; +} + +.product-item { + display: flex; + gap: 24rpx; + padding: 16rpx 0; +} + +.product-image { + width: 180rpx; + height: 180rpx; + border-radius: 12rpx; + background-color: #f5f5f5; +} + +.product-info { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.product-name { + font-size: 28rpx; + color: #333; + line-height: 40rpx; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.product-specs { + font-size: 24rpx; + color: #999; + margin: 8rpx 0; +} + +.product-price-count { + display: flex; + justify-content: space-between; + align-items: center; +} + +.product-price { + font-size: 28rpx; + color: #ff6b81; + font-weight: 500; +} + +.product-count { + font-size: 26rpx; + color: #999; +} + +/* 订单尾部 */ +.order-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 30rpx 32rpx; + border-top: 1rpx solid #f0f0f0; + background-color: #fafafa; +} + +.order-total { + display: flex; + flex-direction: column; + gap: 10rpx; +} + +.total-info { + display: flex; + align-items: center; +} + +.total-label { + font-size: 26rpx; + color: #666; +} + +.price-container { + display: flex; + align-items: baseline; +} + +.total-price-label { + font-size: 26rpx; + color: #666; + margin-right: 8rpx; +} + +.currency-symbol { + font-size: 24rpx; + color: #ff6b81; + font-weight: bold; +} + +.total-price-value { + font-size: 32rpx; + color: #ff6b81; + font-weight: bold; +} + +.order-actions { + display: flex; + gap: 20rpx; + flex-wrap: wrap; + justify-content: flex-end; +} + +.action-btn { + padding: 0 32rpx; + font-size: 26rpx; + line-height: 68rpx; + border-radius: 34rpx; + margin: 0; + min-width: 160rpx; + text-align: center; +} + +.action-btn.primary { + background-color: #667eea; + color: #fff; + font-weight: 500; + box-shadow: 0 2rpx 10rpx rgba(102, 126, 234, 0.2); +} + +.action-btn.secondary { + background-color: #fff; + color: #666; + border: 1rpx solid #e0e0e0; +} + +/* 按钮点击效果 */ +.action-btn:active { + opacity: 0.8; +} + +/* 响应式调整优化 */ +@media (max-width: 375px) { + .product-item { + flex-direction: column; + align-items: flex-start; + } + + .product-image { + width: 100%; + height: 300rpx; + } + + .order-footer { + flex-direction: column; + align-items: flex-start; + gap: 20rpx; + } + + .order-actions { + width: 100%; + justify-content: flex-start; + } + + .action-btn { + flex: 1; + min-width: auto; + margin-right: 12rpx; + } + + .action-btn:last-child { + margin-right: 0; + } +} +.role-tabs { + display: flex; + gap: 16rpx; + padding: 24rpx 24rpx 0 24rpx; +} +.role-tab { + padding: 12rpx 24rpx; + border-radius: 8rpx; + background: #f5f5f7; + color: #333; + font-size: 26rpx; +} +.role-tab.active { + background: #1677ff; + color: #fff; +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/pricing/pricing.js b/src5/code/miniprogram/pages/pricing/pricing.js new file mode 100644 index 0000000..a4763cc --- /dev/null +++ b/src5/code/miniprogram/pages/pricing/pricing.js @@ -0,0 +1,548 @@ +// pages/pricing/pricing.js +Page({ + /** + * 页面的初始数据 + */ + data: { + // 图片相关 + imagePath: '', // 临时路径 + imageFileID: '', // 云存储路径(上传后) + showResult: false, + isAnalyzing: false, + + // 原价信息 + originalPrice: '', + + // AI定价结果 + suggestedPrice: '0.00', + conditionLevel: '--', + marketRange: '--', + aiScore: '--', + analysisReport: '请先上传商品图片进行AI分析', + + // AI生成的商品信息 + productName: '--', + productCategory: '--', + productDescription: '请先进行AI分析以生成商品信息', + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + console.log('AI定价页面加载'); + }, + + /** + * 选择图片 + */ + onChooseImage() { + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: ['album', 'camera'], + maxDuration: 30, + camera: 'back', + success: (res) => { + const tempFilePath = res.tempFiles[0].tempFilePath; + this.setData({ + imagePath: tempFilePath, + imageFileID: '', // 重置云存储路径 + showResult: false // 重置结果,等待用户点击智能定价按钮 + }); + + wx.showToast({ + title: '图片选择成功', + icon: 'success', + duration: 1500 + }); + }, + fail: (err) => { + console.error('选择图片失败:', err); + wx.showToast({ + title: '选择图片失败', + icon: 'none' + }); + } + }); + }, + + /** + * 拍照上传 + */ + onTakePhoto() { + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: ['camera'], + maxDuration: 30, + camera: 'back', + success: (res) => { + const tempFilePath = res.tempFiles[0].tempFilePath; + this.setData({ + imagePath: tempFilePath, + imageFileID: '', // 重置云存储路径 + showResult: false // 重置结果,等待用户点击智能定价按钮 + }); + + wx.showToast({ + title: '图片选择成功', + icon: 'success', + duration: 1500 + }); + }, + fail: (err) => { + console.error('拍照失败:', err); + wx.showToast({ + title: '拍照失败', + icon: 'none' + }); + } + }); + }, + + /** + * 智能定价 + */ + onAIPricing() { + if (!this.data.imagePath) { + wx.showToast({ + title: '请先选择图片', + icon: 'none' + }); + return; + } + + if (!this.data.originalPrice) { + wx.showToast({ + title: '请先输入商品原价', + icon: 'none' + }); + return; + } + + // 直接开始AI分析 + this.startAIAnalysis(); + }, + + /** + * 开始AI分析 + */ + startAIAnalysis() { + if (!this.data.imagePath || !this.data.originalPrice) { + wx.showToast({ + title: '请先上传图片并输入原价', + icon: 'none' + }); + return; + } + + this.setData({ + isAnalyzing: true, + showResult: false + }); + + wx.showLoading({ + title: 'AI分析中...', + mask: true + }); + + // 先上传图片到云存储 + const cloudPath = `pricing/${Date.now()}_${Math.random().toString(36).substr(2, 9)}.jpg`; + + wx.cloud.uploadFile({ + cloudPath: cloudPath, + filePath: this.data.imagePath, + success: (uploadRes) => { + console.log('图片上传成功:', uploadRes); + + // 保存云存储路径 + const imageFileID = uploadRes.fileID; + + // 调用云函数进行AI分析 + wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'analyzeProductPrice', + fileID: imageFileID, + originalPrice: parseFloat(this.data.originalPrice) + }, + success: (res) => { + console.log('云函数调用成功:', res); + console.log('完整响应:', JSON.stringify(res, null, 2)); + wx.hideLoading(); + + // 检查是否有错误 + if (res.result) { + if (res.result.success) { + const resultData = res.result.data; + + console.log('解析结果数据:', JSON.stringify(resultData, null, 2)); + + this.setData({ + isAnalyzing: false, + showResult: true, + imageFileID: imageFileID, // 保存云存储路径 + suggestedPrice: resultData.suggestedPrice || '0.00', + conditionLevel: resultData.conditionLevel || '--', + aiScore: resultData.aiScore || '--', + analysisReport: resultData.analysisReport || 'AI分析完成', + productName: resultData.productName || '--', + productCategory: resultData.productCategory || '--', + productDescription: resultData.analysisReport || '请先进行AI分析以生成商品信息', + marketRange: `¥${(parseFloat(resultData.suggestedPrice || 0) * 0.9).toFixed(2)}-¥${(parseFloat(resultData.suggestedPrice || 0) * 1.1).toFixed(2)}` + }); + + wx.showToast({ + title: 'AI分析完成', + icon: 'success' + }); + } else { + // 云函数返回了错误 + const errorMsg = res.result.error || 'AI分析失败'; + console.error('AI分析失败:', res.result); + + wx.showModal({ + title: 'AI分析失败', + content: errorMsg + '\n\n详细信息请查看控制台日志', + showCancel: false, + confirmText: '知道了' + }); + + this.setData({ + isAnalyzing: false + }); + } + } else { + // 响应格式异常 + console.error('响应格式异常:', res); + wx.showToast({ + title: '响应格式异常', + icon: 'none', + duration: 3000 + }); + this.setData({ + isAnalyzing: false + }); + } + }, + fail: (err) => { + console.error('云函数调用失败:', err); + console.error('错误详情:', JSON.stringify(err, null, 2)); + wx.hideLoading(); + + // 显示更详细的错误信息 + let errorMsg = '调用失败,请重试'; + if (err.errMsg) { + errorMsg = err.errMsg; + } else if (err.message) { + errorMsg = err.message; + } + + wx.showModal({ + title: '调用失败', + content: errorMsg + '\n\n请检查:\n1. 云函数是否已部署\n2. 网络连接是否正常\n3. 查看控制台日志', + showCancel: false, + confirmText: '知道了' + }); + + this.setData({ + isAnalyzing: false + }); + } + }); + }, + fail: (err) => { + console.error('图片上传失败:', err); + wx.hideLoading(); + wx.showToast({ + title: '图片上传失败', + icon: 'none' + }); + this.setData({ + isAnalyzing: false + }); + } + }); + }, + + /** + * 生成模拟AI定价结果 + */ + generateMockAIPricing() { + const originalPrice = parseFloat(this.data.originalPrice); + if (!originalPrice || originalPrice <= 0) { + return { + price: '0.00', + condition: '--', + range: '--', + score: '--', + report: '请输入有效的商品原价', + productName: '--', + productCategory: '--', + productDescription: '请输入有效的商品原价' + }; + } + + // 基于原价生成合理的二手价格范围 + const depreciationRates = [0.3, 0.4, 0.5, 0.6, 0.7]; // 折旧率:30%-70% + const conditionFactors = [0.9, 0.8, 0.7, 0.6, 0.5]; // 成色系数 + + const randomDepreciation = depreciationRates[Math.floor(Math.random() * depreciationRates.length)]; + const randomCondition = conditionFactors[Math.floor(Math.random() * conditionFactors.length)]; + + // 计算建议价格 + const basePrice = originalPrice * (1 - randomDepreciation); + const suggestedPrice = basePrice * randomCondition; + + // 生成价格范围(±10%) + const minPrice = suggestedPrice * 0.9; + const maxPrice = suggestedPrice * 1.1; + + // 成色描述 + const conditions = ['全新', '95新', '9成新', '85新', '8成新', '7成新']; + const conditionIndex = Math.floor(randomCondition * 5); + + // AI评分(基于折旧率和成色) + const aiScore = Math.floor(60 + (1 - randomDepreciation) * 20 + randomCondition * 20); + + // 分析报告 + const reports = [ + `基于原价¥${originalPrice.toFixed(2)}分析,商品折旧率约${(randomDepreciation * 100).toFixed(0)}%,建议二手价格为¥${suggestedPrice.toFixed(2)}`, + `商品原价¥${originalPrice.toFixed(2)},当前成色${conditions[conditionIndex]},市场参考价格区间为¥${minPrice.toFixed(2)}-${maxPrice.toFixed(2)}`, + `AI分析:原价¥${originalPrice.toFixed(2)}的商品,考虑到${conditions[conditionIndex]}的成色,建议定价为¥${suggestedPrice.toFixed(2)}`, + `根据原价¥${originalPrice.toFixed(2)}和商品状况评估,二手市场合理价格为¥${suggestedPrice.toFixed(2)},价格浮动范围±10%` + ]; + + const randomReportIndex = Math.floor(Math.random() * reports.length); + + // 生成商品信息 + const productInfo = this.generateProductInfo(originalPrice, conditions[conditionIndex]); + + return { + price: suggestedPrice.toFixed(2), + condition: conditions[conditionIndex], + range: `¥${minPrice.toFixed(2)}-${maxPrice.toFixed(2)}`, + score: aiScore, + report: reports[randomReportIndex], + ...productInfo + }; + }, + + /** + * 生成商品信息 + */ + generateProductInfo(originalPrice, condition) { + // 商品类别库 + const categories = [ + { name: '电子产品', items: ['iPhone 13', 'MacBook Pro', 'iPad Air', 'AirPods Pro', 'Apple Watch'] }, + { name: '服装鞋帽', items: ['Nike运动鞋', 'Adidas卫衣', '优衣库T恤', 'Zara外套', '李宁运动裤'] }, + { name: '图书文具', items: ['Python编程书', '英语四级词汇', '考研数学真题', '精美笔记本', '钢笔套装'] }, + { name: '生活用品', items: ['保温杯', '电动牙刷', '吹风机', '台灯', '收纳盒'] }, + { name: '运动户外', items: ['篮球', '羽毛球拍', '瑜伽垫', '登山包', '滑板'] }, + { name: '美妆个护', items: ['口红', '粉底液', '面膜', '洗发水', '香水'] } + ]; + + // 根据原价范围选择类别 + let selectedCategory; + if (originalPrice < 100) { + selectedCategory = categories[2]; // 图书文具 + } else if (originalPrice < 500) { + selectedCategory = categories[3]; // 生活用品 + } else if (originalPrice < 2000) { + selectedCategory = categories[4]; // 运动户外 + } else { + selectedCategory = categories[0]; // 电子产品 + } + + const randomItemIndex = Math.floor(Math.random() * selectedCategory.items.length); + const productName = selectedCategory.items[randomItemIndex]; + + // 生成商品描述 + const descriptions = [ + `这是一款${condition}的${productName},外观保存良好,功能正常使用。适合日常使用,性价比较高。`, + `${productName},${condition}成色,无明显划痕和损坏。经过测试各项功能正常,可以放心使用。`, + `商品为${condition}的${productName},包装配件齐全。使用痕迹轻微,保养得当,值得推荐。`, + `这款${productName}保持${condition}状态,性能稳定可靠。适合学生或上班族使用,实用性强。` + ]; + + const randomDescIndex = Math.floor(Math.random() * descriptions.length); + + return { + productName: productName, + productCategory: selectedCategory.name, + productDescription: descriptions[randomDescIndex] + }; + }, + + /** + * 原价输入 + */ + onOriginalPriceInput(e) { + this.setData({ + originalPrice: e.detail.value + }); + }, + + /** + * 保存商品信息 + */ + onSaveProduct() { + if (!this.validateForm()) { + return; + } + + wx.showLoading({ + title: '保存中...' + }); + + // 模拟保存过程 + setTimeout(() => { + wx.hideLoading(); + wx.showToast({ + title: '保存成功', + icon: 'success' + }); + + // 返回主界面 + setTimeout(() => { + wx.navigateBack(); + }, 1500); + }, 2000); + }, + + /** + * 立即发布 - 跳转到发布商品页面 + */ + onPublishProduct() { + // 验证是否有AI分析结果 + if (!this.data.showResult) { + wx.showToast({ + title: '请先进行AI分析', + icon: 'none' + }); + return; + } + + // 优先使用云存储路径,如果没有则使用临时路径 + const imagePath = this.data.imageFileID || this.data.imagePath; + + // 准备跳转参数 + const params = { + imagePath: imagePath, // 使用云存储路径或临时路径 + productName: this.data.productName, + productCategory: this.data.productCategory, + productDescription: this.data.productDescription, + originalPrice: this.data.originalPrice, + suggestedPrice: this.data.suggestedPrice, + priceRange: this.data.marketRange, + conditionLevel: this.data.conditionLevel, + aiScore: this.data.aiScore, + analysisReport: this.data.analysisReport + }; + + // 编码参数(处理特殊字符和长文本) + const queryString = Object.keys(params) + .filter(key => params[key] !== undefined && params[key] !== null && params[key] !== '') + .map(key => { + const value = params[key]; + // 对于长文本,可能需要截断,但这里先完整传递 + return `${key}=${encodeURIComponent(value)}`; + }) + .join('&'); + + console.log('跳转到发布页面,参数:', params); + + // 跳转到发布商品页面 + wx.navigateTo({ + url: `/pages/publish/publish?${queryString}`, + fail: (err) => { + console.error('跳转失败:', err); + wx.showToast({ + title: '跳转失败,请重试', + icon: 'none' + }); + } + }); + }, + + /** + * 表单验证 + */ + validateForm() { + if (!this.data.imagePath) { + wx.showToast({ + title: '请上传商品图片', + icon: 'none' + }); + return false; + } + + if (!this.data.originalPrice || parseFloat(this.data.originalPrice) <= 0) { + wx.showToast({ + title: '请输入有效的商品原价', + icon: 'none' + }); + return false; + } + + if (!this.data.showResult) { + wx.showToast({ + title: '请先进行AI分析', + icon: 'none' + }); + return false; + } + + return true; + }, + + /** + * 生命周期函数--监听页面初次渲染完成 + */ + onReady() { + + }, + + /** + * 生命周期函数--监听页面显示 + */ + onShow() { + + }, + + /** + * 生命周期函数--监听页面隐藏 + */ + onHide() { + + }, + + /** + * 生命周期函数--监听页面卸载 + */ + onUnload() { + + }, + + /** + * 页面相关事件处理函数--监听用户下拉动作 + */ + onPullDownRefresh() { + + }, + + /** + * 页面上拉触底事件的处理函数 + */ + onReachBottom() { + + }, + + /** + * 用户点击右上角分享 + */ + onShareAppMessage() { + + } +}) \ No newline at end of file diff --git a/src5/code/miniprogram/pages/pricing/pricing.json b/src5/code/miniprogram/pages/pricing/pricing.json new file mode 100644 index 0000000..fca1044 --- /dev/null +++ b/src5/code/miniprogram/pages/pricing/pricing.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "AI智能定价", + "navigationBarBackgroundColor": "#4285F4", + "navigationBarTextStyle": "white", + "backgroundColor": "#f5f5f5", + "enablePullDownRefresh": false +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/pricing/pricing.wxml b/src5/code/miniprogram/pages/pricing/pricing.wxml new file mode 100644 index 0000000..3aaabae --- /dev/null +++ b/src5/code/miniprogram/pages/pricing/pricing.wxml @@ -0,0 +1,137 @@ + + + + + + + 商品上传 + 上传商品图片进行AI智能定价 + + + + + + + + + 点击更换图片 + + + + + 点击上传商品图片 + 支持 JPG、PNG 格式 + + + + + + + 商品原价 + + ¥ + + + + + + + + + + + + + + + + + + + + AI定价结果 + 基于图像分析和市场数据 + + + + + 建议价格 + ¥{{suggestedPrice}} + + + + + 商品成色 + {{conditionLevel}} + + + 市场参考 + {{marketRange}} + + + AI评分 + {{aiScore}}分 + + + + + AI分析报告 + {{analysisReport}} + + + + + + + + AI智能识别 + 基于图像识别自动生成商品信息 + + + + + 商品名称 + {{productName}} + + + + 商品类别 + {{productCategory}} + + + + 商品描述 + {{productDescription}} + + + + + + + + + + + + + + + + AI正在分析商品图片... + 请稍候,这可能需要几秒钟 + + + + \ No newline at end of file diff --git a/src5/code/miniprogram/pages/pricing/pricing.wxss b/src5/code/miniprogram/pages/pricing/pricing.wxss new file mode 100644 index 0000000..a84f2be --- /dev/null +++ b/src5/code/miniprogram/pages/pricing/pricing.wxss @@ -0,0 +1,400 @@ +/* pages/pricing/pricing.wxss */ + +.page-container { + min-height: 100vh; + background-color: #f5f5f5; + padding: 20rpx; +} + +/* 通用样式 */ +.section-header { + margin-bottom: 30rpx; +} + +.section-title { + font-size: 32rpx; + font-weight: bold; + color: #333; + display: block; + margin-bottom: 10rpx; +} + +.section-subtitle { + font-size: 24rpx; + color: #999; +} + +/* 商品上传模块 */ +.upload-section { + background: white; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 30rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); +} + +.upload-area { + display: flex; + flex-direction: column; + align-items: center; +} + +.image-preview { + width: 300rpx; + height: 300rpx; + border: 4rpx dashed #ddd; + border-radius: 20rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 30rpx; + background: #fafafa; + overflow: hidden; + position: relative; +} + +/* 原价输入区域 */ +.original-price-section { + width: 100%; + margin-bottom: 30rpx; + padding: 20rpx; + background: #f8f9fa; + border-radius: 15rpx; + border: 2rpx solid #e0e0e0; +} + +.price-input-container { + display: flex; + align-items: center; + justify-content: space-between; +} + +.price-input-container .price-label { + font-size: 28rpx; + font-weight: bold; + color: #333; + margin-right: 20rpx; +} + +.price-input-container .price-input-group { + flex: 1; + display: flex; + align-items: center; + background: white; + border: 2rpx solid #4285F4; + border-radius: 10rpx; + padding: 15rpx 20rpx; + max-width: 250rpx; +} + +.price-input-container .price-symbol { + font-size: 28rpx; + color: #4285F4; + font-weight: bold; + margin-right: 10rpx; +} + +.price-input-container .price-input { + flex: 1; + font-size: 28rpx; + color: #333; + font-weight: bold; +} + +.image-preview:active { + background: #f0f0f0; +} + +.preview-content { + width: 100%; + height: 100%; + position: relative; +} + +.preview-image { + width: 100%; + height: 100%; +} + +.preview-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.7); + padding: 20rpx; + text-align: center; +} + +.preview-text { + color: white; + font-size: 24rpx; +} + +.upload-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40rpx; +} + +.camera-icon { + width: 80rpx; + height: 80rpx; + margin-bottom: 20rpx; + opacity: 0.6; +} + +.placeholder-text { + font-size: 28rpx; + color: #666; + margin-bottom: 10rpx; +} + +.placeholder-hint { + font-size: 22rpx; + color: #999; +} + +.button-group { + display: flex; + gap: 20rpx; + width: 100%; + justify-content: center; +} + +.camera-btn, .ai-pricing-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 20rpx 40rpx; + border-radius: 50rpx; + font-size: 26rpx; + border: none; + color: white; +} + +.camera-btn { + background: #34A853; +} + +.ai-pricing-btn { + background: linear-gradient(135deg, #FF6B35, #4285F4); + font-weight: bold; + box-shadow: 0 4rpx 15rpx rgba(255, 107, 53, 0.3); +} + +.ai-pricing-btn[disabled] { + background: #ccc; + color: #999; + box-shadow: none; +} + +.btn-icon { + width: 32rpx; + height: 32rpx; + margin-right: 10rpx; +} + +/* AI定价结果模块 */ +.result-section { + background: white; + border-radius: 20rpx; + padding: 30rpx; + margin-bottom: 30rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); +} + +.result-group { + margin-bottom: 40rpx; +} + +.pricing-result { + border: 2rpx dashed #4285F4; + border-radius: 15rpx; + padding: 30rpx; + background: #f8f9ff; +} + +.price-display { + text-align: center; + margin-bottom: 30rpx; + padding-bottom: 30rpx; + border-bottom: 1rpx solid #e0e0e0; +} + +.price-label { + font-size: 24rpx; + color: #666; + display: block; + margin-bottom: 10rpx; +} + +.price-value { + font-size: 48rpx; + font-weight: bold; + color: #FF6B35; +} + +.price-details { + display: flex; + justify-content: space-around; + margin-bottom: 30rpx; +} + +.detail-item { + display: flex; + flex-direction: column; + align-items: center; +} + +.detail-label { + font-size: 22rpx; + color: #999; + margin-bottom: 8rpx; +} + +.detail-value { + font-size: 26rpx; + color: #333; + font-weight: bold; +} + +.ai-analysis { + background: white; + border-radius: 10rpx; + padding: 20rpx; + border: 1rpx solid #e0e0e0; +} + +.analysis-title { + font-size: 24rpx; + font-weight: bold; + color: #333; + margin-bottom: 15rpx; +} + +.analysis-content { + font-size: 22rpx; + color: #666; + line-height: 1.6; +} + +/* AI生成的商品信息模块 */ +.product-info-group { + margin-bottom: 40rpx; +} + +.product-info { + background: #f8f9ff; + border-radius: 15rpx; + padding: 30rpx; + border: 2rpx solid #4285F4; +} + +.info-item { + margin-bottom: 25rpx; + display: flex; + flex-direction: column; +} + +.info-item:last-child { + margin-bottom: 0; +} + +.info-label { + font-size: 24rpx; + color: #666; + font-weight: bold; + margin-bottom: 8rpx; +} + +.info-value { + font-size: 28rpx; + color: #333; + font-weight: bold; + background: white; + padding: 15rpx 20rpx; + border-radius: 8rpx; + border: 1rpx solid #e0e0e0; +} + +.info-description { + font-size: 26rpx; + color: #333; + line-height: 1.6; + background: white; + padding: 20rpx; + border-radius: 8rpx; + border: 1rpx solid #e0e0e0; + min-height: 120rpx; +} + +/* 操作按钮 */ +.action-buttons { + display: flex; + gap: 20rpx; +} + +.save-btn, .publish-btn { + flex: 1; + padding: 25rpx; + border-radius: 50rpx; + font-size: 28rpx; + font-weight: bold; + border: none; +} + +.save-btn { + background: #f8f9fa; + color: #333; + border: 2rpx solid #ddd; +} + +.publish-btn { + background: linear-gradient(135deg, #4285F4, #34A853); + color: white; +} + +/* 加载状态 */ +.loading-section { + background: white; + border-radius: 20rpx; + padding: 60rpx 30rpx; + text-align: center; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); +} + +.loading-content { + display: flex; + flex-direction: column; + align-items: center; +} + +.loading-icon { + width: 100rpx; + height: 100rpx; + margin-bottom: 30rpx; + animation: rotate 2s linear infinite; +} + +.loading-text { + font-size: 28rpx; + color: #333; + margin-bottom: 10rpx; +} + +.loading-subtext { + font-size: 22rpx; + color: #999; +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/privacy/privacy.js b/src5/code/miniprogram/pages/privacy/privacy.js new file mode 100644 index 0000000..f1d3f12 --- /dev/null +++ b/src5/code/miniprogram/pages/privacy/privacy.js @@ -0,0 +1,200 @@ +// pages/privacy/privacy.js +Page({ + /** + * 页面的初始数据 + */ + data: { + showPhone: true, + showEmail: true, + allowSearch: true, + allowRecommend: true + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + this.loadPrivacySettings(); + }, + + /** + * 确保有openid + */ + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getOpenId' + } + }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { + console.error('获取openid失败:', err); + } + } + return openid; + }, + + /** + * 加载隐私设置 + */ + async loadPrivacySettings() { + try { + const db = wx.cloud.database(); + const openid = await this.ensureOpenId(); + + if (openid) { + const userResult = await db.collection('T_user') + .where({ + _openid: openid + }) + .get(); + + if (userResult.data && userResult.data.length > 0) { + const userData = userResult.data[0]; + const privacySettings = userData.privacySettings || {}; + + this.setData({ + showPhone: privacySettings.showPhone !== false, + showEmail: privacySettings.showEmail !== false, + allowSearch: privacySettings.allowSearch !== false, + allowRecommend: privacySettings.allowRecommend !== false + }); + + // 更新本地存储 + wx.setStorageSync('privacySettings', { + showPhone: privacySettings.showPhone !== false, + showEmail: privacySettings.showEmail !== false, + allowSearch: privacySettings.allowSearch !== false, + allowRecommend: privacySettings.allowRecommend !== false + }); + } + } + + // 从本地存储获取(如果数据库中没有) + const localSettings = wx.getStorageSync('privacySettings') || {}; + if (localSettings.showPhone !== undefined) { + this.setData({ + showPhone: localSettings.showPhone, + showEmail: localSettings.showEmail, + allowSearch: localSettings.allowSearch, + allowRecommend: localSettings.allowRecommend + }); + } + } catch (err) { + console.error('加载隐私设置失败:', err); + } + }, + + /** + * 显示手机号开关 + */ + async onShowPhoneChange(e) { + const value = e.detail.value; + await this.updatePrivacySetting('showPhone', value); + this.setData({ + showPhone: value + }); + }, + + /** + * 显示邮箱开关 + */ + async onShowEmailChange(e) { + const value = e.detail.value; + await this.updatePrivacySetting('showEmail', value); + this.setData({ + showEmail: value + }); + }, + + /** + * 允许搜索开关 + */ + async onAllowSearchChange(e) { + const value = e.detail.value; + await this.updatePrivacySetting('allowSearch', value); + this.setData({ + allowSearch: value + }); + }, + + /** + * 允许推荐开关 + */ + async onAllowRecommendChange(e) { + const value = e.detail.value; + await this.updatePrivacySetting('allowRecommend', value); + this.setData({ + allowRecommend: value + }); + }, + + /** + * 更新隐私设置 + */ + async updatePrivacySetting(key, value) { + try { + const db = wx.cloud.database(); + const openid = await this.ensureOpenId(); + + if (!openid) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + return; + } + + if (openid) { + const currentSettings = this.data; + const privacySettings = { + showPhone: currentSettings.showPhone, + showEmail: currentSettings.showEmail, + allowSearch: currentSettings.allowSearch, + allowRecommend: currentSettings.allowRecommend, + [key]: value + }; + + await db.collection('T_user') + .where({ + _openid: openid + }) + .update({ + data: { + privacySettings: privacySettings, + updateTime: new Date() + } + }); + } + + // 更新本地存储 + const localSettings = { + showPhone: this.data.showPhone, + showEmail: this.data.showEmail, + allowSearch: this.data.allowSearch, + allowRecommend: this.data.allowRecommend, + [key]: value + }; + wx.setStorageSync('privacySettings', localSettings); + + wx.showToast({ + title: '设置已保存', + icon: 'success' + }); + } catch (err) { + console.error('更新隐私设置失败:', err); + wx.showToast({ + title: '保存失败', + icon: 'none' + }); + } + } +}); + diff --git a/src5/code/miniprogram/pages/privacy/privacy.json b/src5/code/miniprogram/pages/privacy/privacy.json new file mode 100644 index 0000000..095faca --- /dev/null +++ b/src5/code/miniprogram/pages/privacy/privacy.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "隐私设置" +} + diff --git a/src5/code/miniprogram/pages/privacy/privacy.wxml b/src5/code/miniprogram/pages/privacy/privacy.wxml new file mode 100644 index 0000000..f9772ee --- /dev/null +++ b/src5/code/miniprogram/pages/privacy/privacy.wxml @@ -0,0 +1,53 @@ + + + + + 隐私信息 + + + 显示手机号 + 其他用户可以看到您的手机号 + + + + + + 显示邮箱 + 其他用户可以看到您的邮箱 + + + + + + + + + 推荐与搜索 + + + 允许被搜索 + 允许其他用户通过搜索找到您 + + + + + + 允许推荐 + 系统可以根据您的兴趣推荐商品 + + + + + + + + + + + 隐私说明 + • 我们会严格保护您的隐私信息 + • 您可以根据需要调整隐私设置 + • 关闭某些设置可能会影响部分功能的使用 + + + diff --git a/src5/code/miniprogram/pages/privacy/privacy.wxss b/src5/code/miniprogram/pages/privacy/privacy.wxss new file mode 100644 index 0000000..6bc8288 --- /dev/null +++ b/src5/code/miniprogram/pages/privacy/privacy.wxss @@ -0,0 +1,83 @@ +/* pages/privacy/privacy.wxss */ +.page-container { + min-height: 100vh; + background-color: #f5f5f5; + padding-bottom: 100rpx; +} + +.menu-section { + padding: 30rpx; +} + +.menu-group { + background: white; + border-radius: 20rpx; + margin-bottom: 30rpx; + overflow: hidden; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.group-title { + display: block; + padding: 30rpx 30rpx 20rpx; + font-size: 28rpx; + font-weight: bold; + color: #333; + border-bottom: 1rpx solid #f0f0f0; +} + +.menu-list { + padding: 0; +} + +.menu-item { + display: flex; + flex-direction: column; + padding: 30rpx; + border-bottom: 1rpx solid #f8f8f8; +} + +.menu-item:last-child { + border-bottom: none; +} + +.menu-text { + font-size: 28rpx; + color: #333; + font-weight: bold; + margin-bottom: 10rpx; +} + +.menu-desc { + font-size: 24rpx; + color: #999; + margin-bottom: 20rpx; +} + +.menu-switch { + align-self: flex-end; + margin-top: -50rpx; +} + +.tips-section { + margin: 30rpx; + padding: 30rpx; + background: white; + border-radius: 20rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.tips-title { + font-size: 28rpx; + font-weight: bold; + color: #333; + margin-bottom: 20rpx; +} + +.tips-item { + font-size: 24rpx; + color: #666; + line-height: 1.8; + margin-bottom: 10rpx; +} + diff --git a/src5/code/miniprogram/pages/product-detail/product-detail.js b/src5/code/miniprogram/pages/product-detail/product-detail.js new file mode 100644 index 0000000..a146a6d --- /dev/null +++ b/src5/code/miniprogram/pages/product-detail/product-detail.js @@ -0,0 +1,1282 @@ +// pages/product-detail/product-detail.js +const reco = require('../../utils/recommendation.js'); +Page({ + /** + * 页面的初始数据 + */ + data: { + productId: '', + product: null, + loading: true, + + // 图片相关 + imageUrls: [], + currentImageIndex: 0, + + // 购物车数量 + cartCount: 0, + + // 收藏状态 + isFavorite: false, + + // 是否已添加到购物车 + inCart: false, + + // 位置信息与推荐交易点 + buyerLocation: null, // { latitude, longitude } + tradeLocation: null, // { latitude, longitude, name } + distanceText: '', // "距你 Xkm" + recommendedSpot: null, // { name, latitude, longitude, score } + landmarks: [], // 校园官方交易点 + buyerAddress: '', + tradeAddress: '' + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + const productId = options.id; + if (!productId) { + wx.showToast({ + title: '商品不存在', + icon: 'none' + }); + setTimeout(() => { + wx.navigateBack(); + }, 1500); + return; + } + + this.setData({ + productId: productId + }); + + this.loadProductDetail(); + this.loadCartCount(); + this.loadFavoriteStatus(); + + // 初始化定位,仅用于计算与交易地点距离 + this.initBuyerLocation(); + }, + + /** + * 确保有openid + */ + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getOpenId' + } + }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { + console.error('获取openid失败:', err); + } + } + return openid; + }, + + /** + * 加载收藏状态 + */ + async loadFavoriteStatus() { + try { + const db = wx.cloud.database(); + const openid = await this.ensureOpenId(); + const userInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = userInfo._id || ''; + + if (!openid) { + this.setData({ + isFavorite: false + }); + return; + } + + // 从数据库查询是否已收藏(优先按用户ID,其次按openid) + try { + let result; + if (loggedInUserId) { + result = await db.collection('T_favorites') + .where({ userId: loggedInUserId, productId: this.data.productId }) + .get(); + } else { + result = await db.collection('T_favorites') + .where({ _openid: openid, productId: this.data.productId }) + .get(); + } + const isFavorite = result.data && result.data.length > 0; + this.setData({ isFavorite }); + } catch (err) { + // 如果集合不存在,认为未收藏 + if (err.errMsg && err.errMsg.includes('not exist')) { + this.setData({ + isFavorite: false + }); + } else { + // 其他错误,从本地存储读取 + const favorites = wx.getStorageSync('favorites') || []; + const isFavorite = favorites.includes(this.data.productId); + this.setData({ + isFavorite: isFavorite + }); + } + } + } catch (err) { + console.error('加载收藏状态失败:', err); + // 失败时从本地存储读取 + try { + const favorites = wx.getStorageSync('favorites') || []; + const isFavorite = favorites.includes(this.data.productId); + this.setData({ + isFavorite: isFavorite + }); + } catch (e) { + this.setData({ + isFavorite: false + }); + } + } + }, + + /** + * 加载商品详情 + */ + async loadProductDetail() { + wx.showLoading({ + title: '加载中...', + mask: true + }); + + try { + const db = wx.cloud.database(); + const result = await db.collection('T_product').doc(this.data.productId).get(); + + if (!result.data) { + throw new Error('商品不存在'); + } + + const product = result.data; + + console.log('========== 商品详情加载 =========='); + console.log('商品ID:', this.data.productId); + console.log('完整商品数据:', JSON.stringify(product, null, 2)); + console.log('商品图片字段(productImage):', product.productImage); + console.log('图片字段类型:', typeof product.productImage); + console.log('是否为数组:', Array.isArray(product.productImage)); + + // 获取图片URL + let imageUrls = []; + + // 获取图片数据(支持多种格式) + let rawImages = []; + + if (product.productImage) { + if (Array.isArray(product.productImage)) { + rawImages = product.productImage.filter(img => img); // 过滤空值 + } else if (typeof product.productImage === 'string') { + rawImages = [product.productImage]; + } + } + + // 兼容其他可能的字段名 + if (rawImages.length === 0) { + if (product.imageUrls && Array.isArray(product.imageUrls)) { + rawImages = product.imageUrls.filter(img => img); + } else if (product.imageUrl) { + rawImages = [product.imageUrl]; + } + } + + console.log('原始图片数据:', rawImages); + console.log('原始图片数量:', rawImages.length); + + if (rawImages.length === 0) { + console.warn('⚠️ 商品没有图片数据'); + imageUrls.push('https://via.placeholder.com/600x400/cccccc/ffffff?text=Product'); + } else { + // 处理每张图片 + for (let i = 0; i < rawImages.length; i++) { + const img = rawImages[i]; + console.log(`\n处理图片 ${i + 1}/${rawImages.length}:`, img); + + if (!img || typeof img !== 'string') { + console.warn(`图片 ${i + 1} 无效,跳过`); + continue; + } + + // 云存储路径处理 + if (img.startsWith('cloud://')) { + console.log('检测到云存储路径,获取临时URL...'); + try { + const tempFileURL = await wx.cloud.getTempFileURL({ + fileList: [img] + }); + + console.log('getTempFileURL返回:', tempFileURL); + + if (tempFileURL && tempFileURL.fileList && tempFileURL.fileList.length > 0) { + const tempURL = tempFileURL.fileList[0].tempFileURL; + if (tempURL) { + console.log(`✅ 成功获取临时URL: ${tempURL}`); + imageUrls.push(tempURL); + } else { + console.warn(`⚠️ 临时URL为空`); + // 尝试直接使用原路径 + imageUrls.push(img); + } + } else { + console.warn('⚠️ fileList为空或不存在'); + // 尝试直接使用原路径 + imageUrls.push(img); + } + } catch (err) { + console.error(`❌ 获取临时URL失败:`, err); + console.error('错误详情:', JSON.stringify(err, null, 2)); + // 即使出错,也尝试使用原路径 + imageUrls.push(img); + } + } + // HTTP/HTTPS网络URL + else if (img.startsWith('http://') || img.startsWith('https://')) { + console.log('使用网络URL:', img); + imageUrls.push(img); + } + // 相对路径 + else if (img.startsWith('/')) { + console.log('使用相对路径:', img); + imageUrls.push(img); + } + // 其他情况,直接尝试使用 + else { + console.log('使用其他格式路径:', img); + imageUrls.push(img); + } + } + } + + // 确保至少有一张图片 + if (imageUrls.length === 0) { + console.warn('⚠️ 所有图片处理失败,使用默认图片'); + imageUrls.push('https://via.placeholder.com/750x750/E0E0E0/999999?text=暂无图片'); + } + + // 如果图片URLs为空或者都是无效的,添加一个测试图片 + const validUrls = imageUrls.filter(url => url && url !== 'https://via.placeholder.com/750x750/E0E0E0/999999?text=暂无图片'); + if (validUrls.length === 0 && imageUrls.length > 0) { + console.warn('⚠️ 所有图片URL无效,尝试使用测试图片'); + imageUrls.push('https://via.placeholder.com/750x750/4285F4/ffffff?text=测试图片'); + } + + console.log('\n========== 图片处理结果 =========='); + console.log('最终图片URLs:', imageUrls); + console.log('图片URLs数量:', imageUrls.length); + console.log('第一张图片URL:', imageUrls[0]); + console.log('=====================================\n'); + + // 确保 imageUrls 不为空 + if (!imageUrls || imageUrls.length === 0) { + console.error('❌ imageUrls为空,设置默认值'); + imageUrls = ['https://via.placeholder.com/750x750/4285F4/ffffff?text=暂无图片']; + } + + // 获取卖家信息 - 优先从product.sellerInfo获取;其次 sellerUserId;最后 sellerOpenId + let sellerInfo = { + name: '用户', + avatar: 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U', + sno: '', + phone: '', + userId: '', + openId: '' + }; + + // 1) 若 product.sellerInfo 存在,直接使用,同时补充 userId/openId 以便跳转聊天 + if (product.sellerInfo) { + sellerInfo = { + name: product.sellerInfo.sname || product.sellerInfo.name || '用户', + avatar: product.sellerInfo.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U', + sno: product.sellerInfo.sno || '', + phone: product.sellerInfo.phone || '', + userId: product.sellerInfo.userId || product.sellerUserId || '', + openId: product.sellerInfo.openId || product.sellerOpenId || '' + }; + } else if (product.sellerUserId) { + // 2) 优先按 sellerUserId 查询 T_user,避免 openid 变更导致关联错误 + try { + const byId = await db.collection('T_user').doc(product.sellerUserId).get(); + const seller = byId.data || null; + if (seller) { + sellerInfo = { + name: seller.sname || seller.nickName || '用户', + avatar: seller.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U', + sno: seller.sno || '', + phone: seller.phone || '', + userId: seller._id || product.sellerUserId, + openId: seller._openid || product.sellerOpenId || '' + }; + } + } catch (err) { + console.error('通过 sellerUserId 获取卖家信息失败:', err); + } + } else if (product.sellerOpenId) { + // 3) 回退按 sellerOpenId 查询 T_user + try { + const sellerResult = await db.collection('T_user') + .where({ _openid: product.sellerOpenId }) + .limit(1) + .get(); + const seller = (sellerResult.data && sellerResult.data[0]) || null; + if (seller) { + sellerInfo = { + name: seller.sname || seller.nickName || '用户', + avatar: seller.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U', + sno: seller.sno || '', + phone: seller.phone || '', + userId: seller._id || '', + openId: seller._openid || product.sellerOpenId + }; + } + } catch (err) { + console.error('通过 sellerOpenId 获取卖家信息失败:', err); + } + } + + console.log('卖家信息:', sellerInfo); + + // 格式化时间 + const publishTime = this.formatTime(product.createTime); + + this.setData({ + product: { + ...product, + imageUrls: imageUrls, + sellerInfo: sellerInfo, + publishTime: publishTime, + conditionText: product.conditionLevel || '未知', + aiScoreText: product.aiScore || '--', + priceRangeText: product.priceRange || '--' + }, + imageUrls: imageUrls, + loading: false + }); + + wx.hideLoading(); + + // 同步交易地点并计算距离与推荐点 + this.updateTradeLocationFromProduct(product); + await this.resolveTradeAddress(); + this.updateDistanceAndRecommend(); + + // 记录用户浏览行为(云端 + 本地权重) + this.recordBehavior('view', product.productCategory); + try { reco.recordView(product); } catch (e) {} + } catch (err) { + console.error('加载商品详情失败:', err); + wx.hideLoading(); + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + setTimeout(() => { + wx.navigateBack(); + }, 1500); + } + }, + + /** + * 初始化买家定位 + */ + initBuyerLocation() { + try { + wx.getLocation({ + type: 'gcj02', + success: (res) => { + this.setData({ + buyerLocation: { + latitude: res.latitude, + longitude: res.longitude + } + }); + this.resolveBuyerAddress(); + this.updateDistanceAndRecommend(); + }, + fail: (err) => { + console.warn('获取定位失败,距离功能将受限:', err); + } + }); + } catch (err) { + console.warn('定位异常:', err); + } + }, + + /** + * 加载校园官方交易点 + */ + async loadLandmarks() { + try { + const db = wx.cloud.database(); + const res = await db.collection('T_campus_landmarks').get(); + const list = (res.data || []).map(it => ({ + name: it.name || it.landmarkName || '交易点', + latitude: it.lat || it.latitude, + longitude: it.lng || it.longitude + })).filter(x => typeof x.latitude === 'number' && typeof x.longitude === 'number'); + // 已不使用推荐点,保留方法但不更新UI + if (list.length > 0) { + this.setData({ landmarks: list }); + return; + } + } catch (err) { + console.warn('加载官方交易点失败:', err); + // 无交易点时不影响主流程 + } + // 数据库为空或异常时,回退到内置校园官方交易点(仅作为备用数据,不用于推荐) + try { + const { officialTradeSpots } = require('../../utils/campusMap.js'); + const spots = (officialTradeSpots || []).map(s => ({ + name: s.name, + latitude: s.latitude, + longitude: s.longitude + })).filter(x => typeof x.latitude === 'number' && typeof x.longitude === 'number'); + if (spots.length > 0) { + this.setData({ landmarks: spots }); + } + } catch (e) { + console.warn('本地交易点加载失败:', e); + } + }, + + /** + * 从商品同步交易地点 + */ + updateTradeLocationFromProduct(product) { + if (!product) return; + const lat = product.tradeLocationLat; + const lng = product.tradeLocationLng; + const name = product.tradeLandmarkName || product.tradeLocationName || ''; + if (typeof lat === 'number' && typeof lng === 'number') { + this.setData({ + tradeLocation: { latitude: lat, longitude: lng, name }, + tradeAddress: product.tradeAddress || this.data.tradeAddress + }); + } else { + // 坐标缺失时,尝试根据校园地标名匹配坐标 + if (name) { + try { + const { campusLandmarks } = require('../../utils/campusMap.js'); + const m = campusLandmarks.find(l => String(name).includes(l.name)); + if (m && typeof m.latitude === 'number' && typeof m.longitude === 'number') { + this.setData({ tradeLocation: { latitude: m.latitude, longitude: m.longitude, name } }); + return; + } + } catch (_) {} + } + this.setData({ tradeLocation: null }); + } + }, + + async resolveBuyerAddress() { + const buyer = this.data.buyerLocation; + if (!buyer || typeof buyer.latitude !== 'number' || typeof buyer.longitude !== 'number') return; + try { + const { reverseGeocode } = require('../../utils/geocoder.js'); + const addr = await reverseGeocode(buyer); + this.setData({ buyerAddress: addr }); + } catch (e) { + console.warn('解析我的位置地址失败:', e); + } + }, + + async resolveTradeAddress() { + const loc = this.data.tradeLocation; + if (!loc || typeof loc.latitude !== 'number' || typeof loc.longitude !== 'number') return; + try { + const { reverseGeocode } = require('../../utils/geocoder.js'); + const addr = await reverseGeocode(loc); + this.setData({ tradeAddress: addr }); + } catch (e) { + console.warn('解析交易地点地址失败:', e); + } + }, + + /** + * 计算距离与推荐交易点 + */ + updateDistanceAndRecommend() { + const buyer = this.data.buyerLocation; + const trade = this.data.tradeLocation; + if (buyer && trade) { + const km = this.calcDistanceKm(buyer.latitude, buyer.longitude, trade.latitude, trade.longitude); + this.setData({ distanceText: this.formatDistance(km) }); + // 不再计算推荐交易点,仅展示商品交易地点 + this.setData({ recommendedSpot: null }); + } + }, + + /** + * Haversine 距离计算(单位:km) + */ + calcDistanceKm(lat1, lon1, lat2, lon2) { + const toRad = (d) => d * Math.PI / 180; + const R = 6371; // 地球半径(km) + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + const a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * + Math.sin(dLon/2) * Math.sin(dLon/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + }, + + /** + * 距离显示格式化 + */ + formatDistance(km) { + if (km < 1) { + const m = Math.round(km * 1000); + return `${m}m`; + } + return `${km.toFixed(1)}km`; + }, + + /** + * 导航到商品交易地点 + */ + onNavigateToTradeLocation() { + const loc = this.data.tradeLocation; + if (!loc) { + wx.showToast({ title: '暂无交易地点', icon: 'none' }); + return; + } + wx.openLocation({ + latitude: loc.latitude, + longitude: loc.longitude, + name: loc.name ? `交易地点:${loc.name}` : '交易地点', + scale: 17 + }); + }, + + /** + * 导航到推荐安全交易点 + */ + onNavigateToRecommendedSpot() { + const spot = this.data.recommendedSpot; + if (!spot) { + wx.showToast({ title: '暂无推荐地点', icon: 'none' }); + return; + } + wx.openLocation({ + latitude: spot.latitude, + longitude: spot.longitude, + name: spot.name ? `推荐交易点:${spot.name}` : '推荐交易点', + scale: 17 + }); + }, + + /** + * 格式化时间 + */ + formatTime(date) { + if (!date) return ''; + const d = new Date(date); + const now = new Date(); + const diff = now - d; + + const minute = 60 * 1000; + const hour = 60 * minute; + const day = 24 * hour; + + if (diff < minute) { + return '刚刚'; + } else if (diff < hour) { + return Math.floor(diff / minute) + '分钟前'; + } else if (diff < day) { + return Math.floor(diff / hour) + '小时前'; + } else if (diff < 7 * day) { + return Math.floor(diff / day) + '天前'; + } else { + return d.getMonth() + 1 + '月' + d.getDate() + '日'; + } + }, + + /** + * 图片加载错误处理 + */ + onImageError(e) { + const index = e.currentTarget.dataset.index; + const failedUrl = this.data.imageUrls[index]; + + console.error('========== 图片加载失败 =========='); + console.error('图片索引:', index); + console.error('失败URL:', failedUrl); + console.error('错误对象:', e.detail); + console.error('====================================='); + + wx.showToast({ + title: '图片加载失败', + icon: 'none', + duration: 2000 + }); + + // 如果失败的是云存储临时URL,尝试重新获取 + if (failedUrl && failedUrl.includes('myqcloud.com')) { + console.log('尝试重新获取云存储图片URL...'); + // 可以在这里添加重试逻辑 + } + }, + + /** + * 图片轮播切换 + */ + onImageChange(e) { + this.setData({ + currentImageIndex: e.detail.current + }); + }, + + /** + * 预览图片 + */ + onPreviewImage(e) { + const index = e.currentTarget.dataset.index !== undefined ? e.currentTarget.dataset.index : this.data.currentImageIndex; + const imageUrls = this.data.imageUrls || []; + + if (!imageUrls || imageUrls.length === 0) { + wx.showToast({ + title: '暂无图片', + icon: 'none' + }); + return; + } + + // 确保索引有效 + const currentIndex = index >= 0 && index < imageUrls.length ? index : 0; + + wx.previewImage({ + urls: imageUrls, + current: imageUrls[currentIndex], + fail: (err) => { + console.error('预览图片失败:', err); + wx.showToast({ + title: '预览失败', + icon: 'none' + }); + } + }); + }, + + /** + * 联系卖家 + */ + onContactSeller() { + const sellerInfo = this.data.product?.sellerInfo; + const sellerOpenId = sellerInfo?.openId || this.data.product?.sellerOpenId || ''; + const sellerUserId = this.data.product?.sellerUserId || sellerInfo?.userId || ''; + const sellerName = sellerInfo?.name || '用户'; + + if (!sellerOpenId) { + wx.showToast({ title: '卖家信息不存在', icon: 'none' }); + return; + } + + // 跳转到与卖家的聊天窗口 + const params = `toUserId=${sellerUserId}&toOpenId=${sellerOpenId}&toName=${encodeURIComponent(sellerName)}&productId=${this.data.productId}`; + const url = `/pages/chat/chat?${params}`; + wx.navigateTo({ url }); + }, + + /** + * 加入购物车 + */ + onAddToCart() { + if (this.data.product.status !== '在售') { + wx.showToast({ + title: '商品已下架或已售出', + icon: 'none' + }); + return; + } + + try { + // 获取购物车数据 + let cart = wx.getStorageSync('cart') || []; + + // 检查商品是否已在购物车中 + const existingIndex = cart.findIndex(item => item.id === this.data.productId); + + if (existingIndex >= 0) { + wx.showToast({ + title: '已在购物车中', + icon: 'none' + }); + return; + } + + // 添加到购物车 + const cartItem = { + id: this.data.productId, + name: this.data.product.productName, + price: this.data.product.salePrice || this.data.product.suggestedPrice, + image: this.data.imageUrls[0] || 'https://via.placeholder.com/600x400/cccccc/666666?text=商品图', + category: this.data.product.productCategory, + sellerName: this.data.product.sellerInfo?.name || '用户', + addTime: new Date(), + count: 1 + }; + + cart.push(cartItem); + wx.setStorageSync('cart', cart); + + this.setData({ + inCart: true, + cartCount: cart.length + }); + + wx.showToast({ + title: '已加入购物车', + icon: 'success' + }); + } catch (err) { + console.error('加入购物车失败:', err); + wx.showToast({ + title: '操作失败', + icon: 'none' + }); + } + }, + + /** + * 加载购物车数量 + */ + loadCartCount() { + try { + const cart = wx.getStorageSync('cart') || []; + const inCart = cart.some(item => item.id === this.data.productId); + + this.setData({ + cartCount: cart.length, + inCart: inCart + }); + } catch (err) { + console.error('加载购物车失败:', err); + } + }, + + /** + * 立即购买 + */ + onBuyNow() { + if (this.data.product.status !== '在售') { + wx.showToast({ + title: '商品已下架或已售出', + icon: 'none' + }); + return; + } + + // 显示确认购买对话框 + wx.showModal({ + title: '确认购买', + content: `确定要购买"${this.data.product.productName}"吗?\n价格:¥${this.data.product.salePrice || this.data.product.suggestedPrice}`, + confirmText: '确认购买', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + this.processPayment(); + } + } + }); + }, + + /** + * 处理支付(模拟支付流程) + */ + async processPayment() { + wx.showLoading({ + title: '处理中...', + mask: true + }); + + try { + const db = wx.cloud.database(); + + // 获取当前用户openid和userId + let buyerOpenId = null; + let buyerUserId = null; + + // 优先使用登录的用户ID获取openid + const userInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = userInfo._id; + + if (loggedInUserId) { + try { + // 使用登录的用户ID查询用户信息,获取其_openid + const userResult = await db.collection('T_user') + .doc(loggedInUserId) + .get(); + + if (userResult.data && userResult.data._openid) { + buyerOpenId = userResult.data._openid; + buyerUserId = loggedInUserId; + } + } catch (err) { + console.error('通过用户ID获取openid失败:', err); + } + } + + // 如果无法通过用户ID获取openid,尝试从缓存获取 + if (!buyerOpenId) { + buyerOpenId = await this.ensureOpenId(); + } + + if (!buyerOpenId) { + wx.hideLoading(); + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + return; + } + + const product = this.data.product; + + // 获取卖家信息(从商品信息中) + let sellerUserId = null; + if (product?.sellerUserId) { + sellerUserId = product.sellerUserId; + } + + // 构建订单数据 + const orderData = { + // 订单基本信息 + orderNumber: `ORD${Date.now()}${Math.random().toString(36).substr(2, 5).toUpperCase()}`, + status: '待付款', // 待付款、待发货、待收货、已完成、已取消 + + // 商品信息 + productId: this.data.productId, + productName: product?.productName || '商品', + productImage: product?.productImage || (this.data.imageUrls && this.data.imageUrls[0]) || 'https://via.placeholder.com/600x400/cccccc/ffffff?text=Product', + // 上面默认图片替换为远程占位图 + // 注意:订单中图片建议存云端真实地址 + productCategory: product?.productCategory || '其他', + + // 价格信息 + price: product?.salePrice || product?.suggestedPrice || product?.originalPrice || 0, + originalPrice: product?.originalPrice || 0, + + // 买家信息 + buyerOpenId: buyerOpenId, + buyerUserId: buyerUserId || '', // 添加buyerUserId字段,用于精确区分买家 + + // 卖家信息 + sellerOpenId: product?.sellerOpenId || '', + sellerUserId: sellerUserId || '', // 添加sellerUserId字段,用于精确区分卖家 + sellerName: product?.sellerInfo?.name || '用户', + sellerPhone: product?.sellerInfo?.phone || product?.contactInfo || '', + + // 商品信息(数组格式,兼容订单可能有多个商品) + products: [{ + productId: this.data.productId, + name: product?.productName || '商品', + price: product?.salePrice || product?.suggestedPrice || product?.originalPrice || 0, + count: 1, + image: product?.productImage || (this.data.imageUrls && this.data.imageUrls[0]) || 'https://via.placeholder.com/600x400/cccccc/666666?text=商品图', + specs: product?.productCategory || '标准' + }], + + // 订单金额 + totalPrice: product?.salePrice || product?.suggestedPrice || product?.originalPrice || 0, + totalCount: 1, + + // 交易方式 + transactionMethod: product?.transactionMethod || '面交', + + // 时间信息 + createTime: new Date(), + updateTime: new Date() + }; + + console.log('创建订单数据:', orderData); + + // 同时执行:1. 创建订单 2. 更新商品状态为“交易中” + const [orderResult, productResult] = await Promise.all([ + // 创建订单 + db.collection('T_order').add({ + data: orderData + }), + // 更新商品状态为交易中(买家已下单,进入交易流程) + db.collection('T_product').doc(this.data.productId).update({ + data: { + status: '交易中', + updateTime: new Date() + } + }) + ]); + + console.log('订单创建成功:', orderResult); + console.log('商品状态更新成功:', productResult); + + // 写入卖家通知:提示该商品正在被购买 + try { + const notifyData = { + productId: this.data.productId, + productName: product?.productName || '商品', + orderId: orderResult._id || '', + sellerUserId: sellerUserId || '', + sellerOpenId: product?.sellerOpenId || '', + type: 'purchase', + content: '您的商品正在被购买,请在“我的商品”中处理交易', + status: 'unread', + createTime: new Date(), + updateTime: new Date() + }; + await db.collection('T_notify').add({ data: notifyData }); + console.log('卖家通知已写入:', notifyData); + } catch (notifyErr) { + console.warn('写入卖家通知失败,尝试创建集合后重试:', notifyErr); + if (notifyErr && (notifyErr.errMsg?.includes('not exist') || notifyErr.errCode === -502005)) { + try { + await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { type: 'createNotifyCollection' } + }); + // 重新尝试写入 + const notifyDataRetry = { + productId: this.data.productId, + productName: product?.productName || '商品', + orderId: orderResult._id || '', + sellerUserId: sellerUserId || '', + sellerOpenId: product?.sellerOpenId || '', + type: 'purchase', + content: '您的商品正在被购买,请在“我的商品”中处理交易', + status: 'unread', + createTime: new Date(), + updateTime: new Date() + }; + await db.collection('T_notify').add({ data: notifyDataRetry }); + console.log('卖家通知集合创建后写入成功'); + } catch (createErr) { + console.error('创建通知集合或重试写入失败:', createErr); + } + } + } + + // 在聊天中通知卖家:买家已下单 + try { + const buyerId = buyerOpenId; + const sellerId = product?.sellerOpenId || ''; + const buyerUserId2 = buyerUserId || ''; + const sellerUserId2 = sellerUserId || ''; + if (buyerId && sellerId) { + const sessionKey = [buyerId, sellerId].sort().join('|'); + const sessionKeyUser = (buyerUserId2 && sellerUserId2) ? [buyerUserId2, sellerUserId2].sort().join('|') : ''; + await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'sendChatMessage', + contentType: 'text', + content: `[系统] 买家已下单:订单号 ${orderData.orderNumber},商品 ${orderData.productName},金额 ¥${orderData.totalPrice}`, + toUserId: sellerUserId2 || '', + toOpenId: sellerId || '', + productId: this.data.productId || '', + orderId: orderResult._id || '', + isSystem: true + } + }); + } + } catch (e) { + console.error('写入下单系统消息失败:', e); + } + + wx.hideLoading(); + + wx.showModal({ + title: '购买成功', + content: '订单已创建,请尽快联系卖家完成交易', + showCancel: false, + confirmText: '查看订单', + success: (res) => { + if (res.confirm) { + // 记录购买行为 + this.recordBehavior('purchase', this.data.product?.productCategory); + + // 跳转到订单页面 + wx.navigateTo({ + url: '/pages/orders/orders' + }); + } else { + // 刷新商品详情 + this.loadProductDetail(); + } + } + }); + } catch (err) { + console.error('购买失败:', err); + console.error('错误详情:', JSON.stringify(err, null, 2)); + wx.hideLoading(); + + let errorMsg = '购买失败,请重试'; + if (err.errMsg) { + if (err.errMsg.includes('network') || err.errMsg.includes('Failed to fetch')) { + errorMsg = '网络连接失败,请检查网络设置'; + } else if (err.errMsg.includes('permission')) { + errorMsg = '权限不足,请联系管理员'; + } else if (err.errMsg.includes('not exist')) { + errorMsg = '订单功能需要初始化,请稍后再试'; + } else { + errorMsg = '购买失败:' + err.errMsg; + } + } + + wx.showModal({ + title: '购买失败', + content: errorMsg, + showCancel: false, + confirmText: '知道了' + }); + } + }, + + /** + * 收藏/取消收藏 + */ + async onToggleFavorite() { + try { + const db = wx.cloud.database(); + const openid = await this.ensureOpenId(); + const userInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = userInfo._id || ''; + + if (!openid) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + return; + } + + // 先查询是否已收藏 + let existingResult; + try { + existingResult = await db.collection('T_favorites') + .where({ + _openid: openid, + productId: this.data.productId + }) + .get(); + } catch (err) { + // 如果集合不存在,继续执行添加收藏操作(创建集合) + if (err.errMsg && err.errMsg.includes('not exist')) { + existingResult = { data: [] }; + } else { + throw err; + } + } + + const isFavorite = existingResult && existingResult.data && existingResult.data.length > 0; + const favoriteRecord = isFavorite ? existingResult.data[0] : null; + + if (isFavorite && favoriteRecord) { + // 取消收藏:从数据库删除 + try { + await db.collection('T_favorites').doc(favoriteRecord._id).remove(); + + this.setData({ + isFavorite: false + }); + try { reco.recordUnfavorite(this.data.product); } catch (e) {} + + wx.showToast({ + title: '已取消收藏', + icon: 'success' + }); + } catch (err) { + console.error('取消收藏失败:', err); + wx.showToast({ + title: '操作失败', + icon: 'none' + }); + } + } else { + // 添加收藏:保存到数据库 + const product = this.data.product; + + const favoriteData = { + productId: this.data.productId, + productName: product?.productName || '商品', + productImage: product?.productImage || (this.data.imageUrls && this.data.imageUrls[0]) || 'https://via.placeholder.com/600x400/cccccc/ffffff?text=Product', + productCategory: product?.productCategory || '其他', + productPrice: product?.salePrice || product?.suggestedPrice || product?.originalPrice || 0, + userId: loggedInUserId, + createTime: new Date(), + updateTime: new Date() + }; + + try { + await db.collection('T_favorites').add({ + data: favoriteData + }); + + this.setData({ + isFavorite: true + }); + + // 记录收藏行为 + this.recordBehavior('favorite', this.data.product?.productCategory); + try { reco.recordFavorite(this.data.product); } catch (e) {} + + wx.showToast({ + title: '已收藏', + icon: 'success' + }); + } catch (err) { + console.error('添加收藏失败:', err); + console.error('错误详情:', JSON.stringify(err, null, 2)); + + // 如果集合不存在,尝试创建(第一次添加时) + if (err.errMsg && err.errMsg.includes('not exist')) { + // 集合不存在,但我们已经在 add 操作中,这个错误可能表示需要先创建集合 + // 可以提示用户或静默处理 + console.warn('收藏集合不存在,需要先在云数据库中创建 T_favorites 集合'); + wx.showToast({ + title: '收藏功能需要初始化,请稍后再试', + icon: 'none', + duration: 3000 + }); + } else { + let errorMsg = '操作失败'; + if (err.errMsg) { + if (err.errMsg.includes('network') || err.errMsg.includes('Failed to fetch')) { + errorMsg = '网络连接失败'; + } else if (err.errMsg.includes('permission')) { + errorMsg = '权限不足'; + } else { + errorMsg = '操作失败:' + err.errMsg; + } + } + wx.showToast({ + title: errorMsg, + icon: 'none' + }); + } + } + } + } catch (err) { + console.error('收藏操作失败:', err); + console.error('错误详情:', JSON.stringify(err, null, 2)); + + let errorMsg = '操作失败'; + if (err.errMsg) { + if (err.errMsg.includes('network') || err.errMsg.includes('Failed to fetch')) { + errorMsg = '网络连接失败'; + } else if (err.errMsg.includes('permission')) { + errorMsg = '权限不足'; + } else { + errorMsg = '操作失败:' + err.errMsg; + } + } + + wx.showToast({ + title: errorMsg, + icon: 'none' + }); + } + }, + + /** + * 记录用户行为(用于协同过滤推荐) + */ + async recordBehavior(behaviorType, productCategory) { + try { + const openid = await this.ensureOpenId(); + if (!openid || !this.data.productId) { + console.warn('记录用户行为失败:缺少openid或productId', { openid, productId: this.data.productId }); + return; + } + + console.log('开始记录用户行为:', { behaviorType, productId: this.data.productId, productCategory }); + + // 异步记录,不阻塞主流程 + wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'recordUserBehavior', + productId: this.data.productId, + behaviorType: behaviorType, + productCategory: productCategory || '' + }, + success: (res) => { + console.log('用户行为记录成功:', behaviorType, res); + if (res.result && !res.result.success) { + console.error('用户行为记录失败:', res.result.error); + // 如果是因为集合不存在,尝试创建 + if (res.result.error && res.result.error.includes('not exist')) { + this.createBehaviorCollection(); + } + } + }, + fail: (err) => { + console.error('用户行为记录失败:', err); + // 如果是因为集合不存在,尝试创建 + if (err.errMsg && err.errMsg.includes('not exist')) { + this.createBehaviorCollection(); + } + } + }); + } catch (err) { + console.error('记录用户行为异常:', err); + } + }, + + /** + * 创建用户行为集合 + */ + async createBehaviorCollection() { + try { + console.log('尝试创建 T_user_behavior 集合'); + await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'createBehaviorCollection' + }, + success: (res) => { + console.log('创建集合结果:', res); + }, + fail: (err) => { + console.error('创建集合失败:', err); + } + }); + } catch (err) { + console.error('创建集合异常:', err); + } + }, + + /** + * 分享商品 + */ + onShareAppMessage() { + return { + title: `推荐商品:${this.data.product?.productName || '商品'}`, + path: `/pages/product-detail/product-detail?id=${this.data.productId}`, + imageUrl: this.data.imageUrls[0] || '' + }; + }, + + /** + * 查看购物车 + */ + onViewCart() { + wx.navigateTo({ + url: '/pages/cart/cart' + }); + } +}); + diff --git a/src5/code/miniprogram/pages/product-detail/product-detail.json b/src5/code/miniprogram/pages/product-detail/product-detail.json new file mode 100644 index 0000000..71d00ee --- /dev/null +++ b/src5/code/miniprogram/pages/product-detail/product-detail.json @@ -0,0 +1,9 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "商品详情", + "navigationBarBackgroundColor": "#4285F4", + "navigationBarTextStyle": "white", + "enablePullDownRefresh": false, + "disableScroll": false +} + diff --git a/src5/code/miniprogram/pages/product-detail/product-detail.wxml b/src5/code/miniprogram/pages/product-detail/product-detail.wxml new file mode 100644 index 0000000..a0e58a9 --- /dev/null +++ b/src5/code/miniprogram/pages/product-detail/product-detail.wxml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + 暂无图片 + + + + + {{product.status === '已售' ? '已售出' : '已下架'}} + + + + + + + + ¥{{product.salePrice || product.suggestedPrice || 0}} + ¥{{product.originalPrice}} + + + ❤️ + 🤍 + + + + + {{product.productName}} + + + + {{product.productCategory}} + {{product.conditionText}} + ⭐ {{product.aiScore}}分 + + + + + + 商品信息 + + 商品描述 + {{product.productDescription || '暂无描述'}} + + + + 价格范围 + {{product.priceRangeText}} + + + + AI分析报告 + {{product.analysisReport}} + + + + 交易方式 + {{product.transactionMethod || '面交'}} + + + + + + 交易地点 + + 约定地点 + {{product.tradeLandmarkName || '未设置'}} + + + 真实地址 + {{tradeAddress || '未解析'}} + + + 距你 + {{distanceText}} + + + + + + 建议在校园公共区域当面验货,注意财物安全。 + + + + + + 卖家信息 + + + + {{product.sellerInfo.name}} + 学号:{{product.sellerInfo.sno}} + 发布于 {{product.publishTime}} + + + 联系 + + + + + + + + + + + 🛒 + {{cartCount}} + + + {{isFavorite ? '❤️' : '🤍'}} + + + + + + + + + + + + 加载中... + + + diff --git a/src5/code/miniprogram/pages/product-detail/product-detail.wxss b/src5/code/miniprogram/pages/product-detail/product-detail.wxss new file mode 100644 index 0000000..38680c8 --- /dev/null +++ b/src5/code/miniprogram/pages/product-detail/product-detail.wxss @@ -0,0 +1,573 @@ +/* pages/product-detail/product-detail.wxss */ +page { + height: 100%; +} + +.scroll-container { + height: calc(100vh - env(safe-area-inset-top)); + padding-top: 0; + padding-bottom: calc(150rpx + env(safe-area-inset-bottom)); +} + +.container { + width: 100%; + padding-top: 0; + padding-bottom: 40rpx; + background: linear-gradient(to bottom, #f8f9fa 0%, #f5f5f5 100%); + box-sizing: border-box; +} + +/* 图片轮播区域 */ +.image-section { + position: relative; + width: 100%; + height: auto; + min-height: 500rpx; + margin-top: 0; + padding-top: 0; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + overflow: hidden; +} + +.detail_swiper { + width: 100%; + height: 100%; +} + +.swiper-item { + width: 100%; + height: auto; + min-height: 500rpx; +} + +.image-wrapper { + width: 100%; + min-height: 500rpx; + display: flex; + align-items: flex-start; + justify-content: center; + background-color: #f8f9fa; + overflow: hidden; + box-sizing: border-box; +} + +.slide-image { + width: 100%; + height: auto; + display: block; + min-height: 500rpx; +} + +.no-image-placeholder { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: #f5f5f5; +} + +.placeholder-text { + font-size: 28rpx; + color: #999; +} + +.status-badge { + position: absolute; + top: 30rpx; + right: 30rpx; + background: linear-gradient(135deg, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.5) 100%); + backdrop-filter: blur(10rpx); + color: #fff; + padding: 12rpx 24rpx; + border-radius: 40rpx; + font-size: 24rpx; + font-weight: 500; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2); + z-index: 10; +} + +/* 商品基本信息 */ +.info-section { + width: 100%; + box-sizing: border-box; + background: linear-gradient(to bottom, #fff 0%, #fafafa 100%); + padding: 40rpx 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.04); + border-radius: 0; +} + +.price-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24rpx; + padding-bottom: 24rpx; + border-bottom: 1rpx solid rgba(0, 0, 0, 0.05); +} + +.price-group { + display: flex; + align-items: baseline; + gap: 20rpx; +} + +.current-price { + font-size: 56rpx; + font-weight: 700; + background: linear-gradient(135deg, #FF6B6B 0%, #FF4444 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: -1rpx; +} + +.original-price { + font-size: 28rpx; + color: #bbb; + text-decoration: line-through; + font-weight: 400; +} + +.favorite-btn { + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #ffeef0 0%, #ffe0e6 100%); + border-radius: 50%; + font-size: 40rpx; + transition: transform 0.2s ease; +} + +.favorite-btn:active { + transform: scale(0.95); +} + +.product-title { + font-size: 36rpx; + font-weight: 600; + color: #1a1a1a; + line-height: 1.6; + margin-bottom: 24rpx; + letter-spacing: 0.5rpx; + width: 100%; + box-sizing: border-box; + word-break: break-all; + overflow-wrap: break-word; +} + +.product-tags { + display: flex; + flex-wrap: wrap; + gap: 12rpx; +} + +.tag { + padding: 10rpx 20rpx; + border-radius: 30rpx; + font-size: 24rpx; + font-weight: 500; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); + transition: transform 0.2s ease; +} + +.tag:active { + transform: scale(0.95); +} + +.category-tag { + background: linear-gradient(135deg, #E3F2FD 0%, #BBDEFB 100%); + color: #1565C0; + border: 1rpx solid rgba(21, 101, 192, 0.2); +} + +.condition-tag { + background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%); + color: #E65100; + border: 1rpx solid rgba(230, 81, 0, 0.2); +} + +.score-tag { + background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%); + color: #6A1B9A; + border: 1rpx solid rgba(106, 27, 154, 0.2); +} + +/* 详细信息区域 */ +.detail-section { + width: 100%; + box-sizing: border-box; + background: #fff; + padding: 40rpx 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.04); + border-radius: 0; +} + +.section-title { + font-size: 34rpx; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 30rpx; + padding-bottom: 20rpx; + border-bottom: 2rpx solid #f0f0f0; + position: relative; +} + +.section-title::after { + content: ''; + position: absolute; + bottom: -2rpx; + left: 0; + width: 60rpx; + height: 4rpx; + background: linear-gradient(90deg, #4285F4 0%, #764ba2 100%); + border-radius: 2rpx; +} + +.detail-item { + width: 100%; + box-sizing: border-box; + margin-bottom: 30rpx; + padding: 20rpx; + background: #fafafa; + border-radius: 16rpx; + transition: background-color 0.2s ease; + min-height: 80rpx; +} + +.detail-item:last-child { + margin-bottom: 0; +} + +.detail-label { + font-size: 26rpx; + color: #888; + display: block; + margin-bottom: 12rpx; + font-weight: 500; + letter-spacing: 0.5rpx; +} + +.detail-value { + font-size: 30rpx; + color: #333; + line-height: 1.8; + display: block; + word-break: break-all; + width: 100%; + box-sizing: border-box; + overflow-wrap: break-word; +} + +/* 卖家信息区域 */ +.seller-section { + width: 100%; + box-sizing: border-box; + background: #fff; + padding: 40rpx 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.04); + border-radius: 0; +} + +.seller-card { + width: 100%; + box-sizing: border-box; + display: flex; + align-items: center; + padding: 30rpx; + background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%); + border-radius: 24rpx; + box-shadow: 0 4rpx 16rpx rgba(66, 133, 244, 0.1); + transition: transform 0.2s ease, box-shadow 0.2s ease; + min-height: 140rpx; +} + +.seller-card:active { + transform: translateY(-2rpx); + box-shadow: 0 6rpx 20rpx rgba(66, 133, 244, 0.15); +} + +.seller-avatar { + width: 100rpx; + height: 100rpx; + border-radius: 50%; + margin-right: 24rpx; + border: 3rpx solid #fff; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); +} + +.seller-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 10rpx; + overflow: hidden; +} + +.seller-name { + font-size: 30rpx; + font-weight: 600; + color: #1a1a1a; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + margin-bottom: 4rpx; +} + +.seller-sno { + font-size: 24rpx; + color: #666; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + margin-bottom: 4rpx; +} + +.seller-time { + font-size: 24rpx; + color: #999; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +} + +.contact-btn { + flex-shrink: 0; + padding: 14rpx 28rpx; + background: linear-gradient(135deg, #4285F4 0%, #667eea 100%); + color: #fff; + border-radius: 50rpx; + font-size: 26rpx; + font-weight: 500; + box-shadow: 0 4rpx 12rpx rgba(66, 133, 244, 0.3); + transition: transform 0.2s ease, box-shadow 0.2s ease; + white-space: nowrap; +} + +.contact-btn:active { + transform: scale(0.95); + box-shadow: 0 2rpx 8rpx rgba(66, 133, 244, 0.25); +} + +/* 交易地点板块 */ +.location-section { + width: 100%; + box-sizing: border-box; + background: #fff; + padding: 40rpx 30rpx; + margin-bottom: 20rpx; + box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.04); +} + +.location-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20rpx; + background: #fafafa; + border-radius: 16rpx; + margin-bottom: 20rpx; +} + +.location-label { + font-size: 26rpx; + color: #888; +} + +.location-value { + font-size: 30rpx; + color: #333; +} + +.location-distance { + font-size: 30rpx; + color: #1565C0; + font-weight: 600; +} + +.location-actions { + display: flex; + gap: 16rpx; + margin: 10rpx 0 20rpx; +} + +.loc-btn { + padding: 18rpx 28rpx; + border-radius: 50rpx; + font-size: 26rpx; + border: 2rpx solid #4285F4; + color: #4285F4; + background: #fff; +} + +.loc-btn.primary { + background: linear-gradient(135deg, #4285F4 0%, #667eea 100%); + color: #fff; + border: none; +} + +.location-tips { + font-size: 24rpx; + color: #666; +} + +/* 底部操作栏 */ +.bottom-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to top, #fff 0%, #fafafa 100%); + padding: 24rpx 30rpx; + padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08); + z-index: 100; + backdrop-filter: blur(20rpx); + height: calc(136rpx + env(safe-area-inset-bottom)); + box-sizing: border-box; +} + +.bar-left { + display: flex; + gap: 24rpx; +} + +.icon-btn { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 88rpx; + height: 88rpx; + font-size: 44rpx; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-radius: 50%; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08); + transition: transform 0.2s ease; +} + +.icon-btn:active { + transform: scale(0.9); +} + +.badge { + position: absolute; + top: -4rpx; + right: -4rpx; + background: linear-gradient(135deg, #FF6B6B 0%, #FF4444 100%); + color: #fff; + font-size: 20rpx; + font-weight: 600; + padding: 4rpx 10rpx; + border-radius: 20rpx; + min-width: 32rpx; + text-align: center; + line-height: 1.2; + box-shadow: 0 2rpx 8rpx rgba(255, 68, 68, 0.4); + border: 2rpx solid #fff; +} + +.bar-right { + display: flex; + gap: 16rpx; + flex: 1; + justify-content: flex-end; +} + +.cart-btn, +.buy-btn { + padding: 20rpx 32rpx; + border-radius: 50rpx; + font-size: 28rpx; + font-weight: 600; + border: none; + flex: 1; + max-width: 240rpx; + transition: all 0.3s ease; + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); + white-space: nowrap; +} + +.cart-btn { + background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%); + color: #4285F4; + border: 2rpx solid #4285F4; + box-shadow: 0 4rpx 12rpx rgba(66, 133, 244, 0.2); +} + +.cart-btn:active { + transform: scale(0.95); + box-shadow: 0 2rpx 8rpx rgba(66, 133, 244, 0.15); +} + +.cart-btn.added { + background: linear-gradient(135deg, #f0f0f0 0%, #e0e0e0 100%); + color: #999; + border-color: #d0d0d0; + box-shadow: none; +} + +.buy-btn { + background: linear-gradient(135deg, #4285F4 0%, #667eea 100%); + color: #fff; + box-shadow: 0 4rpx 16rpx rgba(66, 133, 244, 0.4); +} + +.buy-btn:active { + transform: scale(0.95); + box-shadow: 0 2rpx 10rpx rgba(66, 133, 244, 0.3); +} + +.buy-btn[disabled], +.cart-btn[disabled] { + background: linear-gradient(135deg, #e0e0e0 0%, #d0d0d0 100%); + color: #bbb; + border-color: #d0d0d0; + box-shadow: none; + opacity: 0.6; +} + +/* 加载状态 */ +.loading-container { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background: linear-gradient(to bottom, #f8f9fa 0%, #f5f5f5 100%); +} + +.loading-content { + text-align: center; + padding: 40rpx; +} + +.loading-text { + font-size: 28rpx; + color: #999; + margin-top: 20rpx; + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} diff --git a/src5/code/miniprogram/pages/profile-edit/profile-edit.js b/src5/code/miniprogram/pages/profile-edit/profile-edit.js new file mode 100644 index 0000000..3ee34b7 --- /dev/null +++ b/src5/code/miniprogram/pages/profile-edit/profile-edit.js @@ -0,0 +1,383 @@ +// pages/profile-edit/profile-edit.js +Page({ + /** + * 页面的初始数据 + */ + data: { + userInfo: { + nickName: '', + avatar: '', + phone: '', + major: '', + grade: '', + dorm: '' + }, + avatarTemp: '', + grades: ['大一', '大二', '大三', '大四', '研究生', '博士生'], + gradeIndex: 0 + }, + + /** + * 生命周期函数--监听页面加载 + */ + onLoad(options) { + this.loadUserInfo(); + }, + + /** + * 确保有openid + */ + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getOpenId' + } + }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { + console.error('获取openid失败:', err); + } + } + return openid; + }, + + /** + * 加载用户信息 + */ + async loadUserInfo() { + try { + // 先从本地存储获取登录时保存的用户信息 + const localUserInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = localUserInfo._id; // 获取登录时保存的用户ID + + const db = wx.cloud.database(); + let userData = null; + + // 优先使用登录时保存的用户ID来查询数据库 + if (loggedInUserId) { + try { + const userResult = await db.collection('T_user') + .doc(loggedInUserId) + .get(); + + if (userResult.data) { + userData = userResult.data; + } + } catch (err) { + console.error('通过用户ID获取用户信息失败:', err); + // 如果使用_id查询失败,尝试使用openid查询(向后兼容) + const openid = await this.ensureOpenId(); + if (openid) { + try { + const userResult = await db.collection('T_user') + .where({ + _openid: openid + }) + .get(); + + if (userResult.data && userResult.data.length > 0) { + userData = userResult.data[0]; + } + } catch (err2) { + console.error('使用openid查询用户信息也失败:', err2); + } + } + } + } else { + // 如果没有登录的用户ID,尝试使用openid(向后兼容) + const openid = await this.ensureOpenId(); + if (openid) { + try { + const userResult = await db.collection('T_user') + .where({ + _openid: openid + }) + .get(); + + if (userResult.data && userResult.data.length > 0) { + userData = userResult.data[0]; + } + } catch (err) { + console.error('从云数据库获取用户信息失败:', err); + } + } + } + + // 如果从数据库获取到数据,使用数据库数据 + if (userData) { + const gradeIndex = this.data.grades.findIndex(g => g === userData.年级) >= 0 + ? this.data.grades.findIndex(g => g === userData.年级) + : 0; + + this.setData({ + userInfo: { + nickName: userData.sname || userData.nickName || '', + avatar: userData.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U', + phone: userData.phone || '', + major: userData.major || '', + grade: userData.年级 || '', + dorm: userData.sushe || '' + }, + gradeIndex: gradeIndex + }); + } else if (localUserInfo.sname || localUserInfo.nickName) { + // 如果数据库中没有数据,从本地存储获取 + this.setData({ + userInfo: { + nickName: localUserInfo.sname || localUserInfo.nickName || '', + avatar: localUserInfo.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U', + phone: localUserInfo.phone || '', + major: localUserInfo.major || '', + grade: localUserInfo.grade || localUserInfo.年级 || '', + dorm: localUserInfo.sushe || localUserInfo.dorm || '' + } + }); + } + } catch (err) { + console.error('加载用户信息失败:', err); + const localUserInfo = wx.getStorageSync('userInfo') || {}; + this.setData({ + userInfo: { + nickName: localUserInfo.sname || localUserInfo.nickName || '', + avatar: localUserInfo.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U', + phone: localUserInfo.phone || '', + major: localUserInfo.major || '', + grade: localUserInfo.grade || localUserInfo.年级 || '', + dorm: localUserInfo.sushe || localUserInfo.dorm || '' + } + }); + } + }, + + /** + * 昵称输入 + */ + onNickNameInput(e) { + this.setData({ + 'userInfo.nickName': e.detail.value + }); + }, + + /** + * 手机号输入 + */ + onPhoneInput(e) { + this.setData({ + 'userInfo.phone': e.detail.value + }); + }, + + /** + * 专业输入 + */ + onMajorInput(e) { + this.setData({ + 'userInfo.major': e.detail.value + }); + }, + + /** + * 宿舍输入 + */ + onDormInput(e) { + this.setData({ + 'userInfo.dorm': e.detail.value + }); + }, + + /** + * 选择年级 + */ + onGradeChange(e) { + const index = e.detail.value; + this.setData({ + gradeIndex: index, + 'userInfo.grade': this.data.grades[index] + }); + }, + + /** + * 选择头像 + */ + chooseAvatar() { + wx.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: (res) => { + const tempFilePath = res.tempFilePaths[0]; + this.setData({ + avatarTemp: tempFilePath + }); + // 上传头像 + this.uploadAvatar(tempFilePath); + } + }); + }, + + /** + * 上传头像 + */ + async uploadAvatar(filePath) { + wx.showLoading({ + title: '上传中...' + }); + + try { + const cloudPath = `avatars/${Date.now()}-${Math.random().toString(36).substr(2, 9)}.jpg`; + const uploadResult = await wx.cloud.uploadFile({ + cloudPath: cloudPath, + filePath: filePath + }); + + this.setData({ + 'userInfo.avatar': uploadResult.fileID, + avatarTemp: uploadResult.fileID + }); + + wx.hideLoading(); + wx.showToast({ + title: '头像上传成功', + icon: 'success' + }); + } catch (err) { + console.error('上传头像失败:', err); + wx.hideLoading(); + wx.showToast({ + title: '上传失败', + icon: 'none' + }); + } + }, + + /** + * 保存个人信息 + */ + async saveProfile() { + const { userInfo } = this.data; + + // 验证必填项 + if (!userInfo.nickName || !userInfo.nickName.trim()) { + wx.showToast({ + title: '请输入昵称', + icon: 'none' + }); + return; + } + + if (!userInfo.phone || !userInfo.phone.trim()) { + wx.showToast({ + title: '请输入手机号', + icon: 'none' + }); + return; + } + + // 验证手机号格式 + const phoneReg = /^1[3-9]\d{9}$/; + if (!phoneReg.test(userInfo.phone)) { + wx.showToast({ + title: '请输入正确的手机号', + icon: 'none' + }); + return; + } + + wx.showLoading({ + title: '保存中...' + }); + + try { + const db = wx.cloud.database(); + const localUserInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = localUserInfo._id; // 获取登录时保存的用户ID + + if (!loggedInUserId) { + // 如果没有登录的用户ID,尝试使用openid(向后兼容) + const openid = await this.ensureOpenId(); + if (!openid) { + wx.showToast({ + title: '请先登录', + icon: 'none' + }); + return; + } + } + + // 更新数据库 + const updateData = { + sname: userInfo.nickName, + phone: userInfo.phone, + major: userInfo.major, + 年级: userInfo.grade, + sushe: userInfo.dorm, + updateTime: new Date() + }; + + if (userInfo.avatar && userInfo.avatar.startsWith('cloud://')) { + updateData.avatar = userInfo.avatar; + } + + // 优先使用登录的用户ID来更新 + if (loggedInUserId) { + await db.collection('T_user') + .doc(loggedInUserId) + .update({ + data: updateData + }); + } else { + // 如果没有用户ID,使用openid(向后兼容) + const openid = await this.ensureOpenId(); + if (openid) { + await db.collection('T_user') + .where({ + _openid: openid + }) + .update({ + data: updateData + }); + } + } + + // 更新本地存储 + const updatedUserInfo = { + ...wx.getStorageSync('userInfo') || {}, + nickName: userInfo.nickName, + avatar: userInfo.avatar, + phone: userInfo.phone, + major: userInfo.major, + grade: userInfo.grade, + dorm: userInfo.dorm, + sname: userInfo.nickName, + sushe: userInfo.dorm, + 年级: userInfo.grade + }; + wx.setStorageSync('userInfo', updatedUserInfo); + + wx.hideLoading(); + wx.showToast({ + title: '保存成功', + icon: 'success' + }); + + setTimeout(() => { + wx.navigateBack(); + }, 1500); + } catch (err) { + console.error('保存个人信息失败:', err); + wx.hideLoading(); + wx.showToast({ + title: '保存失败', + icon: 'none' + }); + } + } +}); + diff --git a/src5/code/miniprogram/pages/profile-edit/profile-edit.json b/src5/code/miniprogram/pages/profile-edit/profile-edit.json new file mode 100644 index 0000000..f1cb411 --- /dev/null +++ b/src5/code/miniprogram/pages/profile-edit/profile-edit.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "个人信息" +} + diff --git a/src5/code/miniprogram/pages/profile-edit/profile-edit.wxml b/src5/code/miniprogram/pages/profile-edit/profile-edit.wxml new file mode 100644 index 0000000..05a7891 --- /dev/null +++ b/src5/code/miniprogram/pages/profile-edit/profile-edit.wxml @@ -0,0 +1,54 @@ + + + + + + 头像 + + + + + + + + + 昵称 + + + + + + 手机号 + + + + + + 专业 + + + + + + 年级 + + + {{userInfo.grade}} + 请选择年级 + + + + + + + 宿舍 + + + + + + + + + + diff --git a/src5/code/miniprogram/pages/profile-edit/profile-edit.wxss b/src5/code/miniprogram/pages/profile-edit/profile-edit.wxss new file mode 100644 index 0000000..6cdede9 --- /dev/null +++ b/src5/code/miniprogram/pages/profile-edit/profile-edit.wxss @@ -0,0 +1,97 @@ +/* pages/profile-edit/profile-edit.wxss */ +.page-container { + min-height: 100vh; + background-color: #f5f5f5; + padding-bottom: 100rpx; +} + +.form-container { + background: white; + margin: 20rpx; + border-radius: 20rpx; + overflow: hidden; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); +} + +.form-item { + display: flex; + align-items: center; + padding: 30rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.form-item:last-child { + border-bottom: none; +} + +.form-label { + width: 150rpx; + font-size: 28rpx; + color: #333; +} + +.form-input { + flex: 1; + font-size: 28rpx; + color: #333; +} + +.avatar-section { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; +} + +.avatar { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + border: 2rpx solid #e0e0e0; +} + +.change-avatar-btn { + padding: 10rpx 20rpx; + background: #667eea; + color: white; + border-radius: 8rpx; + font-size: 24rpx; + border: none; + margin-left: 20rpx; +} + +.change-avatar-btn::after { + border: none; +} + +.picker-view { + flex: 1; + font-size: 28rpx; + color: #333; +} + +.placeholder { + color: #999; +} + +.button-section { + padding: 30rpx; +} + +.save-btn { + width: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 50rpx; + padding: 30rpx; + font-size: 32rpx; + font-weight: bold; + box-shadow: 0 8rpx 25rpx rgba(102, 126, 234, 0.3); +} + +.save-btn:active { + transform: translateY(2rpx); + box-shadow: 0 4rpx 15rpx rgba(102, 126, 234, 0.3); +} + diff --git a/src5/code/miniprogram/pages/profile/profile.js b/src5/code/miniprogram/pages/profile/profile.js new file mode 100644 index 0000000..1b74424 --- /dev/null +++ b/src5/code/miniprogram/pages/profile/profile.js @@ -0,0 +1,772 @@ +// pages/profile/profile.js +Page({ + + /** + * 页面的初始数据 + */ + data: { + userInfo: {}, + userStats: { + products: 0, + wanted: 0, + orders: 0, + favorites: 0 + }, + notificationEnabled: true, + hasOrderAlert: false, + // 图表数据 + monthlyPosted: [], + monthlyPurchased: [] + }, + + /** + * 生命周期函数--监听页面加载 + */ + async onLoad(options) { + try { + await this.ensureOpenId(); + await this.loadUserInfo(); + await this.loadUserStats(); + await this.loadNotificationSetting(); + } catch (err) { + console.error('页面加载失败:', err); + // 即使加载失败,也尝试显示基本页面 + const userInfo = wx.getStorageSync('userInfo') || {}; + this.setData({ + userInfo: userInfo + }); + } + }, + + /** + * 生命周期函数--监听页面显示 + */ + async onShow() { + try { + // 页面显示时强制重新加载所有数据,确保使用最新的登录信息 + // 先清空可能的旧数据缓存 + await this.ensureOpenId(); + // 强制重新加载用户信息和统计数据 + await this.loadUserInfo(); + await this.loadUserStats(); + await this.loadNotificationSetting(); + await this.checkOrderAlerts(); + await this.loadMonthlyCharts(); + try { if (this.getTabBar && this.getTabBar()) { this.getTabBar().setSelected(4); } } catch (e) {} + } catch (err) { + console.error('页面显示失败:', err); + // 即使加载失败,也尝试显示基本页面 + const userInfo = wx.getStorageSync('userInfo') || {}; + this.setData({ + userInfo: userInfo + }); + } + }, + + /** + * 加载消息通知设置 + */ + async loadNotificationSetting() { + try { + // 先从本地存储获取 + const localSetting = wx.getStorageSync('notificationEnabled'); + if (localSetting !== undefined && localSetting !== null) { + this.setData({ + notificationEnabled: localSetting + }); + } + + // 从数据库获取 + const db = wx.cloud.database(); + const openid = await this.ensureOpenId(); + + if (openid) { + const userResult = await db.collection('T_user') + .where({ + _openid: openid + }) + .get(); + + if (userResult.data && userResult.data.length > 0) { + const userData = userResult.data[0]; + if (userData.notificationEnabled !== undefined && userData.notificationEnabled !== null) { + this.setData({ + notificationEnabled: userData.notificationEnabled + }); + wx.setStorageSync('notificationEnabled', userData.notificationEnabled); + } + } + } + } catch (err) { + console.error('加载通知设置失败:', err); + } + }, + + /** + * 确保有openid(如果没有则从云函数获取) + */ + async ensureOpenId() { + let openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getOpenId' + } + }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { + console.error('获取openid失败:', err); + } + } + return openid; + }, + + /** + * 加载用户信息 + */ + async loadUserInfo() { + try { + // 强制从本地存储获取最新的登录用户ID(登录时会更新) + const localUserInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = localUserInfo._id; // 获取登录时保存的用户ID + + let userInfo = {}; + + // 优先使用登录时保存的用户ID来查询数据库 + if (loggedInUserId) { + try { + const db = wx.cloud.database(); + const userResult = await db.collection('T_user') + .doc(loggedInUserId) + .get(); + + if (userResult.data) { + const userData = userResult.data; + userInfo = { + _id: userData._id, + id: userData.sno || userData._id || '', + nickName: userData.sname || userData.nickName || '未设置昵称', + avatar: userData.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U', + level: userData.level || '普通会员', + phone: userData.phone || '', + major: userData.major || '', + sno: userData.sno, + sushe: userData.sushe, + grade: userData.年级 + }; + // 强制更新本地存储 + wx.setStorageSync('userInfo', userInfo); + } else { + // 如果数据库中没有找到,使用本地存储的数据 + userInfo = localUserInfo; + } + } catch (err) { + console.error('从云数据库获取用户信息失败:', err); + // 如果使用_id查询失败,尝试使用openid查询(向后兼容) + const openid = await this.ensureOpenId(); + if (openid) { + try { + const db = wx.cloud.database(); + const userResult = await db.collection('T_user') + .where({ + _openid: openid + }) + .get(); + + if (userResult.data && userResult.data.length > 0) { + const userData = userResult.data[0]; + userInfo = { + _id: userData._id, + id: userData.sno || userData._id || '', + nickName: userData.sname || userData.nickName || '未设置昵称', + avatar: userData.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U', + level: userData.level || '普通会员', + phone: userData.phone || '', + major: userData.major || '', + sno: userData.sno, + sushe: userData.sushe, + grade: userData.年级 + }; + // 更新本地存储 + wx.setStorageSync('userInfo', userInfo); + } else { + userInfo = localUserInfo; + } + } catch (err2) { + console.error('使用openid查询用户信息也失败:', err2); + userInfo = localUserInfo; + } + } else { + userInfo = localUserInfo; + } + } + } else { + // 如果没有登录的用户ID,尝试使用openid(向后兼容) + const openid = await this.ensureOpenId(); + if (openid) { + try { + const db = wx.cloud.database(); + const userResult = await db.collection('T_user') + .where({ + _openid: openid + }) + .get(); + + if (userResult.data && userResult.data.length > 0) { + const userData = userResult.data[0]; + userInfo = { + _id: userData._id, + id: userData.sno || userData._id || '', + nickName: userData.sname || userData.nickName || '未设置昵称', + avatar: userData.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U', + level: userData.level || '普通会员', + phone: userData.phone || '', + major: userData.major || '', + sno: userData.sno, + sushe: userData.sushe, + grade: userData.年级 + }; + // 更新本地存储 + wx.setStorageSync('userInfo', userInfo); + } else { + userInfo = localUserInfo; + } + } catch (err) { + console.error('从云数据库获取用户信息失败:', err); + userInfo = localUserInfo; + } + } else { + userInfo = localUserInfo; + } + } + + // 强制更新页面数据 + this.setData({ + userInfo: userInfo + }); + } catch (err) { + console.error('加载用户信息失败:', err); + const userInfo = wx.getStorageSync('userInfo') || {}; + this.setData({ + userInfo: userInfo + }); + } + }, + + /** + * 加载用户统计数据 + */ + async loadUserStats() { + try { + const db = wx.cloud.database(); + let openid = null; + + // 强制从本地存储获取最新的登录用户ID(登录时会更新) + const userInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = userInfo._id; + + if (loggedInUserId) { + try { + // 使用登录的用户ID查询用户信息,获取其_openid + const userResult = await db.collection('T_user') + .doc(loggedInUserId) + .get(); + + if (userResult.data && userResult.data._openid) { + openid = userResult.data._openid; + } + } catch (err) { + console.error('通过用户ID获取openid失败:', err); + } + } + + // 如果无法通过用户ID获取openid,尝试从缓存获取 + if (!openid) { + openid = await this.ensureOpenId(); + } + + if (!openid) { + // 如果没有openid,使用默认值 + this.setData({ + userStats: { + products: 0, + wanted: 0, + orders: 0, + favorites: 0 + } + }); + // 同时清空本地缓存 + wx.removeStorageSync('userStats'); + return; + } + + // 并行查询各项统计数据 + const _ = db.command; + + // 优先使用sellerUserId查询商品数量(最准确) + let productsCountPromise; + + if (loggedInUserId) { + // 优先使用sellerUserId查询 + productsCountPromise = (async () => { + try { + return await db.collection('T_product').where({ + sellerUserId: loggedInUserId + }).count(); + } catch (err) { + // 如果失败,回退到使用sellerOpenId和_openid(向后兼容) + try { + return await db.collection('T_product').where( + _.or([ + { sellerOpenId: openid }, + { _openid: openid } + ]) + ).count(); + } catch (err2) { + return { total: 0 }; + } + } + })(); + } else { + // 如果没有登录的用户ID,使用sellerOpenId和_openid + productsCountPromise = db.collection('T_product').where( + _.or([ + { sellerOpenId: openid }, + { _openid: openid } + ]) + ).count().catch(() => ({ total: 0 })); + } + + const [productsResult, wantedResult, ordersResult, favoritesResult] = await Promise.all([ + // 我的商品数量(优先使用sellerUserId,否则使用sellerOpenId和_openid) + productsCountPromise, + + // 我的求购数量(优先使用userId,否则使用_openid) + (async () => { + try { + if (loggedInUserId) { + return await db.collection('T_want').where({ + userId: loggedInUserId, + status: 'active' + }).count(); + } else { + return await db.collection('T_want').where({ + _openid: openid, + status: 'active' + }).count(); + } + } catch (err) { + return { total: 0 }; + } + })(), + + // 我的订单数量(优先使用buyerUserId,否则使用buyerOpenId和_openid) + (async () => { + try { + if (loggedInUserId) { + return await db.collection('T_order').where({ + buyerUserId: loggedInUserId + }).count(); + } else { + return await db.collection('T_order').where( + _.or([ + { buyerOpenId: openid }, + { _openid: openid } + ]) + ).count(); + } + } catch (err) { + return { total: 0 }; + } + })(), + + // 我的收藏数量 + (async () => { + try { + if (loggedInUserId) { + return await db.collection('T_favorites').where({ userId: loggedInUserId }).count(); + } else { + return await db.collection('T_favorites').where({ _openid: openid }).count(); + } + } catch (err) { + return { total: 0 }; + } + })() + ]); + + const stats = { + products: productsResult.total || 0, + wanted: wantedResult.total || 0, + orders: ordersResult.total || 0, + favorites: favoritesResult.total || 0 + }; + + // 更新本地存储 + wx.setStorageSync('userStats', stats); + + this.setData({ + userStats: stats + }); + } catch (err) { + console.error('加载用户统计数据失败:', err); + // 失败时使用本地存储或默认值 + const stats = wx.getStorageSync('userStats') || { + products: 0, + wanted: 0, + orders: 0, + favorites: 0 + }; + this.setData({ + userStats: stats + }); + } + }, + + /** + * 月度图表:发布与购买数量 + */ + async loadMonthlyCharts() { + try { + const db = wx.cloud.database(); + const _ = db.command; + const userInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = userInfo._id; + const openid = await this.ensureOpenId(); + + // 最近12个月标签 + const now = new Date(); + const months = []; + for (let i = 11; i >= 0; i--) { + const d = new Date(now.getFullYear(), now.getMonth() - i, 1); + const label = `${d.getFullYear()}-${('0' + (d.getMonth()+1)).slice(-2)}`; + months.push({ y: d.getFullYear(), m: d.getMonth()+1, label }); + } + + // 拉取我的发布商品(按 createTime) + let postedQuery; + if (loggedInUserId) { + postedQuery = db.collection('T_product').where({ sellerUserId: loggedInUserId }); + } else { + postedQuery = db.collection('T_product').where(_.or([{ sellerOpenId: openid }, { _openid: openid }])); + } + const postedRes = await postedQuery.orderBy('createTime', 'asc').get(); + const posted = (postedRes.data || []).filter(p => !!p.createTime); + + // 拉取我的购买订单(按 createTime) + let purchaseQuery; + if (loggedInUserId) { + purchaseQuery = db.collection('T_order').where({ buyerUserId: loggedInUserId }); + } else { + purchaseQuery = db.collection('T_order').where(_.or([{ buyerOpenId: openid }, { _openid: openid }])); + } + const orderRes = await purchaseQuery.orderBy('createTime', 'asc').get(); + const purchased = (orderRes.data || []).filter(o => !!o.createTime); + + // 统计每月数量 + const countByMonth = (list) => { + const map = new Map(months.map(m => [m.label, 0])); + list.forEach(item => { + const d = new Date(item.createTime); + const label = `${d.getFullYear()}-${('0' + (d.getMonth()+1)).slice(-2)}`; + if (map.has(label)) map.set(label, (map.get(label) || 0) + 1); + }); + return months.map(m => ({ month: m.label, count: map.get(m.label) || 0 })); + }; + const postedMonthly = countByMonth(posted); + const purchasedMonthly = countByMonth(purchased); + + // 归一化到百分比用于图表高度/位置 + const maxPosted = Math.max(1, ...postedMonthly.map(x => x.count)); + const maxPurchased = Math.max(1, ...purchasedMonthly.map(x => x.count)); + const postedDisplay = postedMonthly.map(x => ({ month: x.month, count: x.count, percentage: Math.round(100 * x.count / maxPosted) })); + const purchasedDisplay = purchasedMonthly.map((x, idx) => ({ month: x.month, sales: x.count, percentage: Math.round(100 * x.count / maxPurchased), position: Math.round(100 * idx / (purchasedMonthly.length - 1 || 1)) })); + + this.setData({ monthlyPosted: postedDisplay, monthlyPurchased: purchasedDisplay }); + } catch (err) { + console.error('加载月度图表失败:', err); + this.setData({ monthlyPosted: [], monthlyPurchased: [] }); + } + }, + + /** + * 我的商品点击事件 + */ + onMyProducts() { + wx.navigateTo({ + url: '/pages/myProducts/myProducts' + }); + }, + + /** + * 我的求购点击事件 + */ + onMyWanted() { + wx.navigateTo({ + url: '/pages/wanted-list/wanted-list?filter=my' + }); + }, + + /** + * 我的订单点击事件 + */ + onMyOrders() { + // 进入订单页后清除本地变更标记 + wx.removeStorageSync('orderChanged'); + wx.navigateTo({ + url: '/pages/orders/orders' + }); + }, + + /** + * 我的收藏点击事件 + */ + onMyFavorites() { + wx.navigateTo({ + url: '/pages/favorites/favorites' + }); + }, + + /** + * 个人信息编辑 + */ + onProfileEdit() { + wx.navigateTo({ + url: '/pages/profile-edit/profile-edit' + }); + }, + + /** + * 安全设置 + */ + onSecurity() { + wx.navigateTo({ + url: '/pages/security/security' + }); + }, + + /** + * 收货地址管理 + */ + onAddress() { + wx.navigateTo({ + url: '/pages/address/address' + }); + }, + + /** + * 意见反馈 + */ + onFeedback() { + wx.navigateTo({ + url: '/pages/feedback/feedback' + }); + }, + + /** + * 消息通知开关 + */ + async onNotificationChange(e) { + const enabled = e.detail.value; + this.setData({ + notificationEnabled: enabled + }); + + try { + const db = wx.cloud.database(); + const openid = await this.ensureOpenId(); + + if (openid) { + // 更新数据库中的通知设置 + await db.collection('T_user') + .where({ + _openid: openid + }) + .update({ + data: { + notificationEnabled: enabled, + updateTime: new Date() + } + }); + } + + // 更新本地存储 + wx.setStorageSync('notificationEnabled', enabled); + + wx.showToast({ + title: enabled ? '通知已开启' : '通知已关闭', + icon: 'success' + }); + } catch (err) { + console.error('保存通知设置失败:', err); + wx.showToast({ + title: enabled ? '通知已开启' : '通知已关闭', + icon: 'success' + }); + } + }, + + /** + * 隐私设置 + */ + onPrivacy() { + wx.navigateTo({ + url: '/pages/privacy/privacy' + }); + }, + + /** + * 消息通知设置(用于菜单项点击,实际开关在switch中) + */ + onNotification() { + // 这个方法用于菜单项点击,但实际功能由switch组件处理 + // 可以在这里添加跳转到详细通知设置页面的逻辑 + }, + + /** + * 关于我们 + */ + onAbout() { + wx.showModal({ + title: '关于SContact', + content: '版本: 1.0.0\n开发者: SContact团队\n联系方式: support@scontact.com', + showCancel: false, + confirmText: '知道了' + }); + }, + + /** + * 退出登录 + */ + onLogout() { + wx.showModal({ + title: '确认退出', + content: '确定要退出登录吗?', + success: (res) => { + if (res.confirm) { + // 清除登录信息 + wx.removeStorageSync('userInfo'); + wx.removeStorageSync('token'); + wx.removeStorageSync('userStats'); + wx.removeStorageSync('openid'); + wx.removeStorageSync('notificationEnabled'); + + // 显示退出成功提示 + wx.showToast({ + title: '退出成功', + icon: 'success', + duration: 1500 + }); + + // 延迟跳转到登录页面 + setTimeout(() => { + wx.redirectTo({ + url: '/pages/index/index' + }); + }, 1500); + } + } + }); + }, + + /** + * 用户下拉刷新 + */ + async onPullDownRefresh() { + console.log('下拉刷新个人中心'); + + // 重新加载用户信息 + await this.loadUserInfo(); + await this.loadUserStats(); + await this.loadNotificationSetting(); + + // 模拟刷新延迟 + setTimeout(() => { + wx.stopPullDownRefresh(); + }, 300); + }, + + /** + * 页面上拉触底事件的处理函数 + */ + onReachBottom() { + console.log('上拉加载更多'); + + // 可以在这里加载更多数据 + // 页面不再弹出“加载中”,直接静默处理或忽略 + }, + /** + * 检查订单红点提醒:未读通知或待处理订单 + */ + async checkOrderAlerts() { + try { + const db = wx.cloud.database(); + const userInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = userInfo._id; + const openid = await this.ensureOpenId(); + const _ = db.command; + + let hasUnreadNotify = false; + let hasPendingOrders = false; + + // 卖家未读通知(如:商品被购买) + if (loggedInUserId) { + try { + const notifyRes = await db.collection('T_notify') + .where({ sellerUserId: loggedInUserId, status: 'unread' }) + .get(); + hasUnreadNotify = !!(notifyRes.data && notifyRes.data.length > 0); + } catch (e) { /* 忽略错误 */ } + } else if (openid) { + try { + const notifyRes = await db.collection('T_notify') + .where(_.and([ + _.or([{ sellerOpenId: openid }, { _openid: openid }]), + { status: 'unread' } + ])) + .get(); + hasUnreadNotify = !!(notifyRes.data && notifyRes.data.length > 0); + } catch (e) { /* 忽略错误 */ } + } + + // 买家/卖家待处理订单(待付款、待确认付款、待发货、待收货) + const pendingStatuses = ['待付款', '待确认付款', '待发货', '待收货']; + try { + let orderQuery; + if (loggedInUserId) { + orderQuery = db.collection('T_order').where( + _.or([ + { buyerUserId: loggedInUserId }, + { sellerUserId: loggedInUserId } + ]) + ).where({ status: _.in(pendingStatuses) }); + } else if (openid) { + orderQuery = db.collection('T_order').where( + _.or([ + { buyerOpenId: openid }, + { sellerOpenId: openid }, + { _openid: openid } + ]) + ).where({ status: _.in(pendingStatuses) }); + } + if (orderQuery) { + const orderRes = await orderQuery.get(); + hasPendingOrders = !!(orderRes.data && orderRes.data.length > 0); + } + } catch (e) { /* 忽略错误 */ } + + // 本地变更标记(订单状态刚被更新) + const localChanged = !!wx.getStorageSync('orderChanged'); + + this.setData({ + hasOrderAlert: hasUnreadNotify || hasPendingOrders || localChanged + }); + } catch (err) { + console.error('检查订单提醒失败:', err); + } + } +}) \ No newline at end of file diff --git a/src5/code/miniprogram/pages/profile/profile.json b/src5/code/miniprogram/pages/profile/profile.json new file mode 100644 index 0000000..28d8c27 --- /dev/null +++ b/src5/code/miniprogram/pages/profile/profile.json @@ -0,0 +1,9 @@ +{ + "navigationBarTitleText": "个人中心", + "navigationBarBackgroundColor": "#4f8bff", + "navigationBarTextStyle": "white", + "backgroundColor": "#f0f7ff", + "backgroundTextStyle": "light", + "enablePullDownRefresh": false, + "navigationStyle": "default" +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/profile/profile.wxml b/src5/code/miniprogram/pages/profile/profile.wxml new file mode 100644 index 0000000..f40abfc --- /dev/null +++ b/src5/code/miniprogram/pages/profile/profile.wxml @@ -0,0 +1,121 @@ + + + + + + + + + + + {{userStats.products || 0}} + 我的商品 + + + {{userStats.wanted || 0}} + 我的求购 + + + {{userStats.orders || 0}} + 我的订单 + + + + + + + + + 交易管理 + + + 👜 + 我的商品 + > + + + 📝 + 我的求购 + > + + + 📦 + 我的订单 + > + + + + ❤️ + 我的收藏 + > + + + + + + 账户设置 + + + 👤 + 个人信息 + > + + + + + + + + + + + 发布趋势(近12个月) + + + + + + + {{item.count}} + + {{item.month}} + + + + + + + + 购买趋势(近12个月) + + + + + + + + + + + + + {{item.month}} + + + {{item.sales}} + + + + + + + + + + + + \ No newline at end of file diff --git a/src5/code/miniprogram/pages/profile/profile.wxss b/src5/code/miniprogram/pages/profile/profile.wxss new file mode 100644 index 0000000..f5df0c4 --- /dev/null +++ b/src5/code/miniprogram/pages/profile/profile.wxss @@ -0,0 +1,224 @@ +/* pages/profile/profile.wxss */ + +page { + background-color: #f0f7ff; + height: 100%; +} + +.page-container { + min-height: 100vh; + background: linear-gradient(180deg, #f0f7ff 0%, #eef5ff 100%); + padding-bottom: 100rpx; +} + +/* 顶部用户信息区域 */ +.user-header { + background: linear-gradient(135deg, #5ba8ff 0%, #7ec8ff 100%); + padding: 40rpx 30rpx; + padding-top: 10rpx; + color: white; + margin-top: 0; + position: relative; +} + +.user-avatar-section { + display: flex; + align-items: center; + margin-bottom: 40rpx; +} + +.user-avatar { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + border: 4rpx solid rgba(255, 255, 255, 0.3); + margin-right: 30rpx; +} + +.user-info { + flex: 1; +} + +.user-name { + font-size: 36rpx; + font-weight: bold; + display: block; + margin-bottom: 10rpx; +} + +.user-id, .user-level { + font-size: 24rpx; + opacity: 0.9; + display: block; + margin-bottom: 5rpx; +} + +.user-stats { + display: flex; + justify-content: space-around; + text-align: center; +} + +.stat-item { + padding: 20rpx 0; +} + +.stat-number { + font-size: 32rpx; + font-weight: bold; + display: block; + margin-bottom: 10rpx; +} + +.stat-label { + font-size: 24rpx; + opacity: 0.9; +} + +/* 功能菜单区域 */ +.menu-section { + padding: 30rpx; +} + +.menu-group { + background: #ffffff; + border-radius: 20rpx; + margin-bottom: 30rpx; + overflow: hidden; + box-shadow: 0 8rpx 24rpx rgba(91, 168, 255, 0.16); +} + +.group-title { + display: block; + padding: 30rpx 30rpx 20rpx; + font-size: 28rpx; + font-weight: 800; + color: #2a4a7a; + border-bottom: 1rpx solid #eef2f7; +} + +.menu-list { + padding: 0; +} + +.menu-item { + display: flex; + align-items: center; + padding: 30rpx; + border-bottom: 1rpx solid #f8f8f8; + transition: background-color 0.3s; +} + +.menu-item:active { + background-color: #f8f8f8; +} + +.menu-item:last-child { + border-bottom: none; +} + +.menu-icon { + width: 40rpx; + height: 40rpx; + margin-right: 20rpx; +} + +.menu-text { + flex: 1; + font-size: 28rpx; + color: #3d4d66; +} + +.menu-emoji { + font-size: 40rpx; + margin-right: 20rpx; + line-height: 1; +} + +.menu-arrow { + font-size: 24rpx; + color: #999; +} + +.menu-switch { + margin-left: auto; +} + +/* 红点提醒样式 */ +.red-dot { + position: absolute; + top: 6rpx; + right: 6rpx; + width: 16rpx; + height: 16rpx; + background-color: #ff3b30; + border-radius: 50%; + box-shadow: 0 2rpx 6rpx rgba(255, 59, 48, 0.4); +} + +.red-dot.menu { + right: 30rpx; + top: 30rpx; +} + +/* 底部操作按钮 */ +.action-section { + padding: 30rpx; +} + +.logout-btn { + width: 100%; + background: linear-gradient(135deg, #5ba8ff 0%, #4f8bff 100%); + color: white; + border: none; + border-radius: 50rpx; + padding: 30rpx; + font-size: 32rpx; + font-weight: bold; + box-shadow: 0 8rpx 25rpx rgba(79, 139, 255, 0.25); +} + +.logout-btn:active { + transform: translateY(2rpx); + box-shadow: 0 4rpx 15rpx rgba(79, 139, 255, 0.25); +} + +/* 响应式调整 */ +@media (max-width: 750rpx) { + .user-header { + padding: 30rpx 20rpx; + } + + .user-avatar { + width: 100rpx; + height: 100rpx; + } + + .menu-section { + padding: 20rpx; + } + + .menu-item { + padding: 25rpx; + } +} +.chart-container { padding: 20rpx 30rpx; } +.bar-chart { width: 100%; } +.chart-bars { display: flex; justify-content: space-around; align-items: flex-end; height: 300rpx; padding: 20rpx 0; } +.bar-item { flex: 1; display: flex; flex-direction: column; align-items: center; height: 260rpx; } +.bar-wrapper { width: 100%; height: 220rpx; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; position: relative; } +.bar { width: 60%; background: linear-gradient(180deg, #5ba8ff 0%, #4f8bff 100%); border-radius: 8rpx 8rpx 0 0; min-height: 4rpx; transition: all 0.3s ease; } +.bar-value { position: absolute; top: -40rpx; font-size: 22rpx; color: #5e7ea6; font-weight: 600; } +.bar-label { font-size: 22rpx; color: #5e7ea6; margin-top: 15rpx; } + +.line-chart { width: 100%; height: 300rpx; position: relative; } +.chart-area { width: 100%; height: 100%; position: relative; } +.chart-grid { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; justify-content: space-between; } +.grid-line { height: 1rpx; background: #e0e0e0; } +.chart-line { position: absolute; top: 0; left: 0; right: 0; bottom: 0; } +.line-path { position: absolute; left: 0; right: 0; bottom: 0; height: 100%; } +.line-point { position: absolute; width: 12rpx; height: 12rpx; background: #4f8bff; border-radius: 50%; box-shadow: 0 4rpx 12rpx rgba(79, 139, 255, 0.3); } +.chart-labels { position: absolute; left: 0; right: 0; bottom: -36rpx; display: flex; justify-content: space-between; } +.label-item { font-size: 20rpx; color: #5e7ea6; } +.chart-values { position: absolute; left: 0; right: 0; top: 8rpx; display: flex; justify-content: space-between; } +.value-item { font-size: 20rpx; color: #5e7ea6; } \ No newline at end of file diff --git a/src5/code/miniprogram/pages/publish/publish.js b/src5/code/miniprogram/pages/publish/publish.js new file mode 100644 index 0000000..e5c4f60 --- /dev/null +++ b/src5/code/miniprogram/pages/publish/publish.js @@ -0,0 +1,1053 @@ +// pages/publish/publish.js +Page({ + /** + * 页面的初始数据 + */ + data: { + // 页面来源标识 + fromAIPricing: false, + // 是否展示价格信息(仅AI定价或编辑模式且有数据时显示) + showPriceInfo: false, + isEditMode: false, // 是否为编辑模式 + editProductId: '', // 编辑的商品ID + + // 商品基本信息 + productImage: '', + productName: '', + productCategory: '', + productDescription: '', + categories: ['电子产品', '图书文具', '服装鞋帽', '家居用品', '运动户外', '美妆个护', '其他'], + categoryIndex: 0, + + // 价格信息 + originalPrice: '', + suggestedPrice: '', + priceRange: '', + salePrice: '', + + // 商品状况 + conditionLevel: '', + aiScore: '', + + // AI分析报告 + analysisReport: '', + + // 发布设置 + contactInfo: '', + transactionMethods: ['面交', '快递', '自提'], + // 兼容WXML中使用的 tradeMethods 命名 + tradeMethods: ['面交', '快递', '自提'], + selectedMethods: [], + tradeMethodIndex: 0, // 默认选择第一个交易方式 + + // 发布状态 + isPublishing: false, + + // 发布建议与推荐类别 + recommendLoading: false, + recommendMessage: '', + recommendedCategories: [], + categoryGuides: {}, + + // 交易地点(仅地图选点) + tradeLocation: { latitude: null, longitude: null, landmarkName: '' }, + tradeAddress: '' + }, + + /** + * 生命周期函数--监听页面加载 + */ + async onLoad(options) { + console.log('发布商品页面加载,接收参数:', options); + + // 检查是否为编辑模式 + if (options.mode === 'edit' && options.id) { + // 编辑模式:加载商品数据 + this.setData({ + isEditMode: true, + editProductId: options.id + }); + await this.loadProductData(options.id); + return; + } + + // 从页面参数中获取数据 + if (options && Object.keys(options).length > 0) { + // 解码URL参数(小程序路由参数会自动编码,需要手动解码) + const decodeParam = (value) => { + if (!value) return ''; + try { + return decodeURIComponent(value); + } catch (e) { + console.warn('参数解码失败:', value, e); + return value; + } + }; + + // 解码所有参数 + const decodedOptions = {}; + Object.keys(options).forEach(key => { + decodedOptions[key] = decodeParam(options[key]); + }); + + console.log('解码后的参数:', decodedOptions); + + // 从AI定价页面跳转,带完整参数 + const receivedCategory = decodedOptions.productCategory || ''; + + // 查找类别在列表中的索引 + let categoryIndex = 0; + if (receivedCategory) { + const index = this.data.categories.indexOf(receivedCategory); + if (index >= 0) { + categoryIndex = index; + } + } + + this.setData({ + fromAIPricing: true, + productImage: decodedOptions.imagePath || '', + productName: decodedOptions.productName || '', + productCategory: receivedCategory, // 保存类别名称,但用户需要自己选择 + productDescription: decodedOptions.productDescription || '', + originalPrice: decodedOptions.originalPrice || '', + suggestedPrice: decodedOptions.suggestedPrice || '', + priceRange: decodedOptions.priceRange || '', + conditionLevel: decodedOptions.conditionLevel || '', + aiScore: decodedOptions.aiScore || '', + analysisReport: decodedOptions.analysisReport || '', + categoryIndex: categoryIndex // 设置类别选择器的索引 + }); + + // 根据AI定价携带的数据,决定是否显示价格信息 + this.updatePriceInfoVisibility(); + + // 建议售价仅用于参考,不自动填充“商品售价”,需用户自行输入 + + // 显示从AI定价跳转的提示 + wx.showToast({ + title: 'AI分析结果已加载', + icon: 'success', + duration: 1500 + }); + + // 交易地点需用户在地图上选点设置 + } else { + // 从主界面直接进入,显示空表单 + this.setData({ + fromAIPricing: false, + showPriceInfo: false, + productName: '', + productCategory: '', + productDescription: '', + salePrice: '', + categoryIndex: 0 + }); + + // 显示直接发布的提示 + wx.showToast({ + title: '请填写商品信息', + icon: 'none', + duration: 1500 + }); + + // 交易地点需用户在地图上选点设置 + } + + // 加载基于分类供需的发布建议(卖家视角) + this.loadPublishRecommendations('product'); + }, + + /** + * 加载发布建议(基于分类供需对比) + */ + async loadPublishRecommendations(forType) { + try { + this.setData({ recommendLoading: true }); + const res = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getPublishRecommendations', + forType + } + }); + const result = res.result || {}; + if (result.success && result.data) { + const { recommended = [], message = '', categoryGuides = {} } = result.data; + this.setData({ + recommendedCategories: recommended, + recommendMessage: message, + categoryGuides + }); + // 应用类别指引到当前选择 + const cat = this.data.productCategory || this.data.categories[this.data.categoryIndex]; + if (cat) { + this.applyGuideForCategory(cat); + } + } else { + console.warn('获取发布建议失败:', result.error || res); + } + } catch (e) { + console.error('调用发布建议云函数失败:', e); + } finally { + this.setData({ recommendLoading: false }); + } + }, + + /** + * 加载商品数据(编辑模式) + */ + async loadProductData(productId) { + wx.showLoading({ + title: '加载中...', + mask: true + }); + + try { + const db = wx.cloud.database(); + const productDoc = await db.collection('T_product').doc(productId).get(); + + if (productDoc.data) { + const product = productDoc.data; + + // 查找类别索引 + let categoryIndex = 0; + if (product.productCategory) { + const index = this.data.categories.indexOf(product.productCategory); + if (index >= 0) { + categoryIndex = index; + } + } + + // 查找交易方式索引 + let tradeMethodIndex = 0; + if (product.transactionMethod) { + const index = this.data.transactionMethods.indexOf(product.transactionMethod); + if (index >= 0) { + tradeMethodIndex = index; + } + } + + this.setData({ + productImage: product.productImage || '', + productName: product.productName || '', + productCategory: product.productCategory || '', + productDescription: product.productDescription || '', + categoryIndex: categoryIndex, + originalPrice: product.originalPrice ? product.originalPrice.toString() : '', + suggestedPrice: product.suggestedPrice ? product.suggestedPrice.toString() : '', + priceRange: product.priceRange || '', + salePrice: product.salePrice ? product.salePrice.toString() : '', + conditionLevel: product.conditionLevel || '', + aiScore: product.aiScore || '', + analysisReport: product.analysisReport || '', + contactInfo: product.contactInfo || '', + tradeMethodIndex: tradeMethodIndex + }); + + // 编辑模式载入交易地点 + this.setData({ + tradeLocation: { + latitude: product.tradeLocationLat || null, + longitude: product.tradeLocationLng || null, + landmarkName: product.tradeLandmarkName || '' + } + }); + + // 交易地点编辑模式已载入,如需变更请使用地图选点 + + // 编辑模式:若已有建议价格/报告,显示价格信息 + this.updatePriceInfoVisibility(); + + wx.hideLoading(); + wx.showToast({ + title: '商品信息已加载', + icon: 'success', + duration: 1500 + }); + } else { + wx.hideLoading(); + wx.showToast({ + title: '商品不存在', + icon: 'none' + }); + setTimeout(() => { + wx.navigateBack(); + }, 1500); + } + } catch (err) { + console.error('加载商品数据失败:', err); + wx.hideLoading(); + wx.showToast({ + title: '加载失败', + icon: 'none' + }); + setTimeout(() => { + wx.navigateBack(); + }, 1500); + } + }, + + /** + * 生命周期函数--监听页面显示 + */ + onShow() { + console.log('发布商品页面显示'); + }, + + /** + * 商品名称输入事件 + */ + onProductNameInput(e) { + this.setData({ + productName: e.detail.value + }); + }, + + /** + * 商品类别选择事件 + */ + onCategoryChange(e) { + const index = e.detail.value; + this.setData({ + categoryIndex: index, + productCategory: this.data.categories[index] + }); + // 根据类别自动预填价格区间与交易方式 + this.applyGuideForCategory(this.data.categories[index]); + }, + + /** + * 选择推荐类别快速填充 + */ + onSelectRecommendedCategory(e) { + const category = e.currentTarget.dataset.category; + const idx = this.data.categories.indexOf(category); + if (idx >= 0) { + this.setData({ + categoryIndex: idx, + productCategory: category + }); + wx.showToast({ title: `已选择:${category}`, icon: 'none' }); + // 应用类别指引 + this.applyGuideForCategory(category); + } else { + wx.showToast({ title: '该类别不在当前列表', icon: 'none' }); + } + }, + + /** + * 根据类别指引预填常用字段(价格区间、交易方式) + */ + applyGuideForCategory(category) { + const guide = this.data.categoryGuides && this.data.categoryGuides[category]; + if (!guide) return; + const range = guide.typicalPriceRange || ''; + const commonTrade = guide.commonTrade || '面交'; + const idx = this.data.transactionMethods.indexOf(commonTrade); + const next = {}; + if (range) next.priceRange = range; + if (idx >= 0) next.tradeMethodIndex = idx; + if ((!this.data.suggestedPrice || this.data.suggestedPrice === '') && guide.avgPrice > 0) { + next.suggestedPrice = guide.avgPrice.toString(); + if (!this.data.salePrice || this.data.salePrice === '') { + next.salePrice = guide.avgPrice.toString(); + } + } + this.setData(next); + // 类别指引不触发价格信息展示(仅AI定价或编辑模式下展示) + this.updatePriceInfoVisibility(); + }, + + /** + * 商品描述输入事件 + */ + onDescriptionInput(e) { + this.setData({ + productDescription: e.detail.value + }); + }, + + /** + * 上传商品图片 + */ + onUploadImage() { + wx.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: (res) => { + const tempFilePaths = res.tempFilePaths; + if (tempFilePaths.length > 0) { + this.setData({ + productImage: tempFilePaths[0] + }); + + wx.showToast({ + title: '图片上传成功', + icon: 'success' + }); + } + }, + fail: (error) => { + console.error('选择图片失败:', error); + wx.showToast({ + title: '图片选择失败', + icon: 'none' + }); + } + }); + }, + + /** + * 售价输入 + */ + onSalePriceInput(e) { + const value = e.detail.value; + this.setData({ + salePrice: value + }); + }, + + /** + * 联系方式输入 + */ + onContactInput(e) { + const value = e.detail.value; + this.setData({ + contactInfo: value + }); + }, + + /** + * 交易方式选择 + */ + onTradeMethodChange(e) { + const index = parseInt(e.detail.value); + this.setData({ + tradeMethodIndex: index + }); + }, + + /** + * 更新价格信息显示逻辑 + */ + updatePriceInfoVisibility() { + const { fromAIPricing, isEditMode, suggestedPrice, priceRange, originalPrice, analysisReport } = this.data; + const hasAIData = !!(suggestedPrice || priceRange || analysisReport || originalPrice); + const show = (fromAIPricing && hasAIData) || (isEditMode && hasAIData); + if (this.data.showPriceInfo !== show) { + this.setData({ showPriceInfo: show }); + } + }, + + /** + * 跳转到AI定价页面 + */ + goToAIPricing() { + wx.navigateTo({ + url: '/pages/pricing/pricing' + }); + }, + + + + + + + + + + /** + * 在地图上选点,确保交易地点坐标精准(GCJ-02) + */ + onChooseLocation() { + try { + const base = (this.data.tradeLocation && typeof this.data.tradeLocation.latitude === 'number' && typeof this.data.tradeLocation.longitude === 'number') + ? this.data.tradeLocation + : null; + const openChooser = (lat, lng) => { + wx.chooseLocation({ + latitude: typeof lat === 'number' ? lat : undefined, + longitude: typeof lng === 'number' ? lng : undefined, + success: (res) => { + if (typeof res.latitude === 'number' && typeof res.longitude === 'number') { + const landmarkName = res.name || '地图选点'; + this.setData({ + tradeLocation: { latitude: res.latitude, longitude: res.longitude, landmarkName } + }); + this.resolvePublishTradeAddress(); + wx.showToast({ title: '已设置地图选点', icon: 'success' }); + } else { + wx.showToast({ title: '选点失败', icon: 'none' }); + } + }, + fail: (err) => { + console.warn('地图选点失败:', err); + wx.showToast({ title: '未选择地点', icon: 'none' }); + } + }); + }; + + if (base) { + openChooser(base.latitude, base.longitude); + } else { + wx.getLocation({ + type: 'gcj02', + isHighAccuracy: true, + highAccuracyExpireTime: 10000, + success: (loc) => { + openChooser(loc.latitude, loc.longitude); + }, + fail: (err) => { + console.warn('获取当前位置失败,打开选点工具默认视图:', err); + openChooser(undefined, undefined); + } + }); + } + } catch (e) { + console.warn('调用地图选点异常:', e); + } + }, + + + + async resolvePublishTradeAddress() { + const loc = this.data.tradeLocation; + if (!loc || typeof loc.latitude !== 'number' || typeof loc.longitude !== 'number') return; + try { + const { reverseGeocode } = require('../../utils/geocoder.js'); + const addr = await reverseGeocode(loc); + this.setData({ tradeAddress: addr }); + } catch (e) { + console.warn('解析交易地址失败:', e); + } + }, + + /** + * 验证表单数据 + */ + validateForm() { + const { + fromAIPricing, + productName, + productCategory, + productDescription, + salePrice, + contactInfo, + selectedMethods + } = this.data; + + // 验证商品名称 + if (!productName || productName.trim() === '') { + wx.showToast({ + title: '请输入商品名称', + icon: 'none' + }); + return false; + } + + // 验证商品类别 + if (!productCategory || productCategory.trim() === '') { + wx.showToast({ + title: '请选择商品类别', + icon: 'none' + }); + return false; + } + + // 验证商品描述 + if (!productDescription || productDescription.trim() === '') { + wx.showToast({ + title: '请输入商品描述', + icon: 'none' + }); + return false; + } + + // 验证售价 + if (!salePrice || isNaN(parseFloat(salePrice)) || parseFloat(salePrice) <= 0) { + wx.showToast({ + title: '请输入有效的售价', + icon: 'none' + }); + return false; + } + + // 验证联系方式 + if (!contactInfo || contactInfo.trim() === '') { + wx.showToast({ + title: '请输入联系方式', + icon: 'none' + }); + return false; + } + + // 验证商品图片(无论哪种模式都需要图片) + if (!this.data.productImage || this.data.productImage.trim() === '') { + wx.showToast({ + title: '请上传商品图片', + icon: 'none' + }); + return false; + } + // 面交/自提必须使用地图选点,确保有坐标 + try { + const methods = this.data.tradeMethods || this.data.transactionMethods || ['面交','快递','自提']; + const idx = typeof this.data.tradeMethodIndex === 'number' ? this.data.tradeMethodIndex : 0; + const method = methods[idx]; + if (method === '面交' || method === '自提') { + const tl = this.data.tradeLocation || {}; + const hasCoords = typeof tl.latitude === 'number' && typeof tl.longitude === 'number'; + if (!hasCoords) { + wx.showToast({ title: '请在地图上选点设置交易地点', icon: 'none' }); + return false; + } + } + } catch (_) {} + + return true; + }, + + /** + * 验证发布数据(与validateForm一致) + */ + validatePublishData() { + return this.validateForm(); + }, + + /** + * 发布前确保交易地点: + * - 若已有坐标,直接通过 + * - 若仅有地标名,尝试匹配校园地标或正向地理编码 + * - 若都没有但有自动定位,采用自动定位 + * - 仍无则提示用户设置地点并返回 false + */ + async ensureTradeLocationBeforePublish() { + try { + const methods = this.data.tradeMethods || this.data.transactionMethods || ['面交','快递','自提']; + const idx = typeof this.data.tradeMethodIndex === 'number' ? this.data.tradeMethodIndex : 0; + const method = methods[idx]; + if (method !== '面交' && method !== '自提') return true; + + const tl = this.data.tradeLocation || {}; + if (typeof tl.latitude === 'number' && typeof tl.longitude === 'number') { + return true; + } + + // 仅有地标名时尝试匹配校园地标 + if (tl.landmarkName && tl.landmarkName.trim() !== '') { + try { + const { campusLandmarks } = require('../../utils/campusMap.js'); + const match = campusLandmarks && campusLandmarks.find(l => String(tl.landmarkName).includes(l.name)); + if (match && typeof match.latitude === 'number' && typeof match.longitude === 'number') { + this.setData({ tradeLocation: { latitude: match.latitude, longitude: match.longitude, landmarkName: tl.landmarkName } }); + await this.resolvePublishTradeAddress(); + return true; + } + } catch (_) {} + } + + // 仍无可用地点 + wx.showModal({ + title: '请设置交易地点', + content: '面交/自提需要交易地点。请在地图上选点设置交易地点。', + showCancel: false, + confirmText: '知道了' + }); + return false; + } catch (e) { + console.warn('ensureTradeLocationBeforePublish 异常:', e); + return false; + } + }, + + /** + * 发布商品 + */ + onPublish() { + // 防止重复提交:如果正在发布中,直接返回 + if (this.data.isPublishing) { + console.log('正在发布中,忽略重复点击'); + return; + } + + // 验证数据 + if (!this.validateForm()) { + return; + } + // 先确保交易地点(面交/自提) + this.ensureTradeLocationBeforePublish() + .then(async (ok) => { + if (!ok) return; + // 设置发布状态(立即设置,防止重复点击) + this.setData({ isPublishing: true }); + // 显示发布中提示 + wx.showLoading({ title: '发布中...', mask: true }); + // 先上传图片到云存储(如果需要) + this.uploadImageAndPublish(); + }) + .catch((err) => { + console.warn('发布前地点校验异常:', err); + wx.showToast({ title: '请稍后重试', icon: 'none' }); + }); + }, + + /** + * 上传图片并发布商品 + */ + uploadImageAndPublish() { + // 双重检查:防止并发调用 + if (!this.data.isPublishing) { + console.warn('发布状态异常,取消操作'); + return; + } + + const { productImage, fromAIPricing } = this.data; + + console.log('准备上传图片,图片路径:', productImage); + console.log('是否从AI定价页面:', fromAIPricing); + + // 如果有图片且不是云存储路径(cloud://开头),需要上传 + if (productImage && !productImage.startsWith('cloud://')) { + // 图片是临时路径,需要上传到云存储 + const cloudPath = `products/${Date.now()}_${Math.random().toString(36).substr(2, 9)}.jpg`; + + console.log('上传图片到云存储,路径:', cloudPath); + + wx.cloud.uploadFile({ + cloudPath: cloudPath, + filePath: productImage, + success: (uploadRes) => { + console.log('图片上传成功:', uploadRes); + // 图片上传成功后,保存商品信息 + this.saveProductToDatabase(uploadRes.fileID); + }, + fail: (err) => { + console.error('图片上传失败:', err); + wx.hideLoading(); + wx.showToast({ + title: '图片上传失败: ' + (err.errMsg || '未知错误'), + icon: 'none', + duration: 3000 + }); + // 重置发布状态 + this.setData({ + isPublishing: false + }); + } + }); + } else { + // 图片已经是云存储路径或没有图片,直接保存商品信息 + const imageFileID = productImage || ''; + console.log('图片已是云存储路径或为空,直接保存,fileID:', imageFileID); + this.saveProductToDatabase(imageFileID); + } + }, + + /** + * 保存商品信息到数据库 + */ + async saveProductToDatabase(imageFileID) { + // 双重检查:防止并发调用 + if (!this.data.isPublishing) { + console.warn('发布状态异常,取消保存操作'); + return; + } + + const db = wx.cloud.database(); + const { + productName, + productCategory, + productDescription, + originalPrice, + suggestedPrice, + priceRange, + salePrice, + conditionLevel, + aiScore, + analysisReport, + contactInfo, + transactionMethods, + tradeMethodIndex + } = this.data; + + // 获取当前用户的 openid(优先使用登录的用户ID获取) + let openid = null; + const userInfo = wx.getStorageSync('userInfo') || {}; + const loggedInUserId = userInfo._id; + + if (loggedInUserId) { + try { + // 使用登录的用户ID查询用户信息,获取其_openid + const userResult = await db.collection('T_user') + .doc(loggedInUserId) + .get(); + + if (userResult.data && userResult.data._openid) { + openid = userResult.data._openid; + } + } catch (err) { + console.error('通过用户ID获取openid失败:', err); + } + } + + // 如果无法通过用户ID获取openid,尝试从缓存获取或重新获取 + if (!openid) { + openid = wx.getStorageSync('openid'); + if (!openid) { + try { + const result = await wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { + type: 'getOpenId' + } + }); + if (result.result && result.result.openid) { + openid = result.result.openid; + wx.setStorageSync('openid', openid); + } + } catch (err) { + console.error('获取openid失败:', err); + } + } + } + + // 构建商品数据 + const productData = { + // 基本信息 + productName: productName, + productCategory: productCategory, + productDescription: productDescription, + productImage: imageFileID, + + // 价格信息 + originalPrice: parseFloat(originalPrice) || 0, + suggestedPrice: parseFloat(suggestedPrice) || 0, + salePrice: parseFloat(salePrice) || 0, + priceRange: priceRange || '', + + // 商品状况 + conditionLevel: conditionLevel || '', + aiScore: aiScore || '', + analysisReport: analysisReport || '', + + // 发布信息 + contactInfo: contactInfo, + transactionMethod: transactionMethods[tradeMethodIndex] || '面交', + + // 交易地点:保存为公共地标与坐标(不含房间号) + tradeLandmarkName: (this.data.tradeLocation && this.data.tradeLocation.landmarkName) || '', + tradeLocationLat: (this.data.tradeLocation && this.data.tradeLocation.latitude) || null, + tradeLocationLng: (this.data.tradeLocation && this.data.tradeLocation.longitude) || null, + tradeAddress: this.data.tradeAddress || '', + + // 更新时间 + updateTime: new Date() + }; + + + + // 如果是编辑模式,不更新创建时间和统计信息 + if (!this.data.isEditMode) { + // 新增模式:添加初始状态信息 + productData.status = '在售'; + productData.viewCount = 0; + productData.likeCount = 0; + productData.createTime = new Date(); + productData.sellerOpenId = openid || 'unknown'; + productData.sellerAppId = wx.getAccountInfoSync().miniProgram.appId || ''; + // 添加sellerUserId字段,直接保存登录用户的_id,用于精确区分发布者 + productData.sellerUserId = loggedInUserId || ''; + } + // 编辑模式:只更新可编辑的字段,保持原有状态和统计数据 + + console.log('准备保存商品数据:', productData); + console.log('商品名称:', productName); + console.log('商品描述:', productDescription); + console.log('商品类别:', productCategory); + console.log('成色:', conditionLevel); + console.log('价格范围:', priceRange); + console.log('sellerOpenId:', openid); + + // 检查是否有URL编码的字符(如果还有编码字符,说明解码失败) + const hasEncodedChars = (str) => { + if (!str) return false; + return /%[0-9A-F]{2}/i.test(str); + }; + + if (hasEncodedChars(productName) || hasEncodedChars(productDescription) || hasEncodedChars(conditionLevel)) { + console.warn('检测到可能未解码的URL编码字符!'); + console.warn('productName:', productName); + console.warn('productDescription:', productDescription); + console.warn('conditionLevel:', conditionLevel); + } + + // 保存到数据库 + if (this.data.isEditMode && this.data.editProductId) { + // 编辑模式:更新商品 + db.collection('T_product').doc(this.data.editProductId).update({ + data: productData, + success: (res) => { + console.log('商品更新成功:', res); + + // 成功后同步交易地标到 T_campus_landmarks,并写入当前商品ID到 productIds(不影响用户流程) + try { + const name = productData.tradeLandmarkName || (this.data.tradeLocation && this.data.tradeLocation.landmarkName) || productData.tradeAddress || ''; + const lat = Number(productData.tradeLocationLat); + const lng = Number(productData.tradeLocationLng); + const selling = String(productData.status || '').trim() === '在售'; + if (name && Number.isFinite(lat) && Number.isFinite(lng)) { + wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { type: 'upsertCampusLandmark', name, latitude: lat, longitude: lng, address: productData.tradeAddress, source: 'product', productId: this.data.editProductId, selling, productCategory: productData.productCategory, thumbUrl: productData.productImage } + }).then(r => { + console.log('地标同步完成(编辑):', r); + }).catch(e => { + console.warn('地标同步失败(编辑):', e); + }); + } + } catch (e) { console.warn('地标同步异常(编辑):', e); } + + wx.hideLoading(); + this.setData({ + isPublishing: false + }); + + wx.showToast({ + title: '更新成功', + icon: 'success', + duration: 2000, + mask: true + }); + + setTimeout(() => { + wx.navigateBack(); + }, 2000); + }, + fail: (err) => { + console.error('商品更新失败:', err); + + wx.hideLoading(); + this.setData({ + isPublishing: false + }); + + wx.showModal({ + title: '更新失败', + content: '商品更新失败,请重试。错误信息:' + (err.errMsg || '未知错误'), + showCancel: false, + confirmText: '知道了' + }); + } + }); + } else { + // 新增模式:添加商品 + db.collection('T_product').add({ + data: productData, + success: (res) => { + console.log('商品发布成功:', res); + + // 发布成功后同步交易地标到 T_campus_landmarks,并写入新商品ID到 productIds(不影响用户流程) + try { + const name = productData.tradeLandmarkName || (this.data.tradeLocation && this.data.tradeLocation.landmarkName) || productData.tradeAddress || ''; + const lat = Number(productData.tradeLocationLat); + const lng = Number(productData.tradeLocationLng); + const selling = String(productData.status || '').trim() === '在售'; + if (name && Number.isFinite(lat) && Number.isFinite(lng)) { + wx.cloud.callFunction({ + name: 'quickstartFunctions', + data: { type: 'upsertCampusLandmark', name, latitude: lat, longitude: lng, address: productData.tradeAddress, source: 'product', productId: res._id, selling, productCategory: productData.productCategory, thumbUrl: productData.productImage } + }).then(r => { + console.log('地标同步完成(发布):', r); + }).catch(e => { + console.warn('地标同步失败(发布):', e); + }); + } + } catch (e) { console.warn('地标同步异常(发布):', e); } + + // 先隐藏loading + wx.hideLoading(); + + // 立即重置发布状态,防止重复提交 + this.setData({ + isPublishing: false + }); + + // 显示发布成功提示 + wx.showToast({ + title: '发布成功', + icon: 'success', + duration: 2000, + mask: true + }); + + // 等待提示显示完整后再跳转到首页 + setTimeout(() => { + wx.reLaunch({ + url: '/pages/main/main', + success: () => { + console.log('成功跳转到首页'); + }, + fail: (err) => { + console.error('跳转失败:', err); + // 如果reLaunch失败,尝试使用navigateBack + wx.navigateBack({ + delta: 999 // 返回首页(如果有多个页面) + }); + } + }); + }, 2000); + }, + fail: (err) => { + console.error('商品保存失败:', err); + + // 先隐藏loading + wx.hideLoading(); + + // 重置发布状态 + this.setData({ + isPublishing: false + }); + + wx.showModal({ + title: '发布失败', + content: '商品保存失败,请重试。错误信息:' + (err.errMsg || '未知错误'), + showCancel: false, + confirmText: '知道了' + }); + } + }); + } + }, + + /** + * 返回修改 + */ + onBack() { + wx.navigateBack({ + delta: 1 + }); + }, + + /** + * 分享功能 + */ + onShareAppMessage() { + return { + title: '我在二手市场发布了商品:' + this.data.productName, + path: '/pages/main/main', + imageUrl: this.data.productImage + }; + }, + + /** + * 用户点击右上角分享 + */ + onShareTimeline() { + return { + title: '我在二手市场发布了商品:' + this.data.productName, + imageUrl: this.data.productImage + }; + } +}) \ No newline at end of file diff --git a/src5/code/miniprogram/pages/publish/publish.json b/src5/code/miniprogram/pages/publish/publish.json new file mode 100644 index 0000000..cae7b71 --- /dev/null +++ b/src5/code/miniprogram/pages/publish/publish.json @@ -0,0 +1,6 @@ +{ + "usingComponents": {}, + "navigationBarTitleText": "发布商品", + "navigationBarBackgroundColor": "#4285F4", + "navigationBarTextStyle": "white" +} \ No newline at end of file diff --git a/src5/code/miniprogram/pages/publish/publish.wxml b/src5/code/miniprogram/pages/publish/publish.wxml new file mode 100644 index 0000000..ca34afb --- /dev/null +++ b/src5/code/miniprogram/pages/publish/publish.wxml @@ -0,0 +1,227 @@ + + + + + + + + 商品图片 + + + + + + 暂无图片 + + + + + + + + 商品信息 + + + + 商品名称 + + + + 商品类别 + + + {{categories[categoryIndex] || '请选择商品类别'}} + + + + + + 商品描述 +