yzb 5 months ago
parent 5185e1f95d
commit 0df741bfae

@ -1,12 +1,87 @@
# 云开发 quickstart
# 校园二手交易小程序
这是云开发的快速启动指引,其中演示了如何上手使用云开发的三大基础能力:
## 1. 系统简介
- 数据库:一个既可在小程序前端操作,也能在云函数中读写的 JSON 文档型数据库
- 文件存储:在小程序前端直接上传/下载云端文件,在云开发控制台可视化管理
- 云函数:在云端运行的代码,微信私有协议天然鉴权,开发者只需编写业务逻辑代码
本项目是一个功能完善的校园二手交易微信小程序,旨在为大学校园内的学生和教职工提供一个安全、便捷、高效的二手物品交易平台。系统不仅包含了完整的商品交易流程,还融入了丰富的社交和实用功能,构建了一个活跃的校园社区生态。
## 参考文档
### 主要功能模块
- [云开发文档](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html)
- **商品模块**:用户可以轻松发布、编辑、下架自己的二手商品,支持图文描述、价格、分类、交易地点等信息。
- **交易流程**:实现了从创建订单、买家支付(模拟)、卖家确认、买家收货到交易完成的全闭环流程。
- **求购广场**:用户可以发布求购信息,寻找自己需要的物品,其他用户可以响应求购。
- **智能定价**:支持用户上传二手商品,生成闲置商品破损程度分析和定价建议
- **实时聊天**:买卖双方可以进行一对一的实时沟通,支持文字和图片消息。
- **校园地图模式**:以地图为载体,直观展示校园内各个交易地点的商品信息,方便用户发现附近的宝贝。
- **智能推荐**:根据用户的浏览和收藏行为,个性化推荐可能感兴趣的商品。
### 技术栈
- **前端**:微信小程序原生开发 (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运行项目
完成以上所有步骤后,点击开发者工具顶部的“编译”按钮。如果一切顺利,您应该可以在模拟器中看到小程序的启动界面。
至此,项目已成功在您的开发环境中运行起来。祝您使用愉快!

@ -5,5 +5,10 @@
]
},
"timeout": 300,
"envVariables": {}
}
"envVariables": {
"QQMAP_KEY": "QBJBZ-E433N-DYYFX-SDC5E-EUB3V-MJBOE",
"QQMAP_KEYS": "7YTBZ-DTQHW-RPURE-YWKGQ-UTODK-RZFVP",
"QQMAP_REFERER": "https://servicewechat.com",
"QQMAP_SK": ""
}
}

File diff suppressed because it is too large Load Diff

@ -1,25 +1,57 @@
{
"pages": [
"pages/index/index",
"pages/logs/logs",
"pages/register/register",
"pages/interests/interests",
"pages/main/main",
"pages/pricing/pricing",
"pages/publish/publish",
"pages/buy/buy",
"pages/purchase/purchase",
"pages/market/market",
"pages/cart/cart",
"pages/messages/messages",
"pages/profile/profile",
"pages/myProducts/myProducts",
"pages/orders/orders",
"pages/product-detail/product-detail",
"pages/wanted-list/wanted-list"
"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": "black",
"navigationBarTextStyle": "white",
"navigationBarTitleText": "校园二手交易",
"navigationBarBackgroundColor": "#f5f5f5",
"backgroundColor": "#f5f5f5"
"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",

@ -3,7 +3,7 @@
<view class="install_tip" wx:if="{{showTip}}">
<view class="install_tip_back"></view>
<view class="install_tip_detail">
<image class="install_tip_close" bind:tap="onClose" src="../../images/icons/close.png"/>
<image class="install_tip_close" bind:tap="onClose" src="https://via.placeholder.com/24x24/000000/ffffff?text=X"/>
<view class="install_tip_detail_title">{{title}}</view>
<view class="install_tip_detail_tip">{{content}}</view>
</view>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 898 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 898 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 898 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 893 KiB

@ -1,4 +1,5 @@
// pages/buy/buy.js
const reco = require('../../utils/recommendation.js');
Page({
/**
* 页面的初始数据
@ -46,6 +47,19 @@ Page({
* 生命周期函数--监听页面加载
*/
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();
},
@ -103,7 +117,17 @@ Page({
* 搜索按钮点击事件
*/
onSearch() {
this.filterProducts();
// 若存在关键词,则刷新并按关键词从后端查询;否则仅本地筛选
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();
}
},
/**
@ -170,6 +194,7 @@ Page({
*/
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}`
});
@ -201,26 +226,15 @@ Page({
return;
}
// 进入加载状态,并推进到下一页,改为真实数据库分页查询
const nextPage = this.data.currentPage + 1;
this.setData({
loading: true
loading: true,
currentPage: nextPage
});
// 模拟网络请求
setTimeout(() => {
const newProducts = this.generateMockProducts(this.data.currentPage + 1, this.data.pageSize);
if (newProducts.length > 0) {
this.setData({
products: [...this.data.products, ...newProducts],
currentPage: this.data.currentPage + 1,
hasMore: newProducts.length === this.data.pageSize
});
this.filterProducts();
}
this.setData({
loading: false
});
}, 500);
// 加载下一页商品
this.loadProducts(false);
},
/**
@ -228,7 +242,8 @@ Page({
*/
loadProducts(refresh = false) {
const db = wx.cloud.database();
const { currentPage, pageSize, selectedCategory, categories } = this.data;
const { currentPage, pageSize, selectedCategory, categories, searchKeyword } = this.data;
const _ = db.command;
// 如果是刷新,重置页码
const page = refresh ? 1 : currentPage;
@ -250,6 +265,18 @@ Page({
});
}
// 关键词搜索(商品名称或描述匹配,忽略大小写)
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') // 按创建时间倒序
@ -263,7 +290,17 @@ Page({
if (res.data && res.data.length > 0) {
// 处理商品数据
this.processProducts(res.data).then((processedProducts) => {
const products = refresh ? processedProducts : [...this.data.products, ...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,
@ -355,7 +392,7 @@ Page({
async processProducts(products) {
const processedProducts = await Promise.all(products.map(async (product) => {
// 获取图片临时URL
let imageUrl = '/images/default-product.png'; // 默认图片
let imageUrl = '/images/仓鼠.png'; // 默认本地图片,避免外网占位图超时
if (product.productImage) {
try {
// 如果是云存储路径获取临时URL
@ -386,7 +423,7 @@ Page({
// 获取卖家信息
const sellerName = product.sellerInfo?.sname || '用户';
const sellerAvatar = product.sellerInfo?.avatar || '/images/default-avatar.png';
const sellerAvatar = product.sellerInfo?.avatar || '/images/更多犬种.png';
// 返回格式化后的商品数据
return {
@ -492,8 +529,8 @@ Page({
condition: '99新',
location: '闵行校区',
publishTime: '2小时前',
image: '/images/iphone13.jpg',
sellerAvatar: '/images/avatar1.jpg',
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'
@ -507,8 +544,8 @@ Page({
condition: '95新',
location: '徐汇校区',
publishTime: '1天前',
image: '/images/math-book.jpg',
sellerAvatar: '/images/avatar2.jpg',
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'
@ -524,8 +561,8 @@ Page({
condition: '85新',
location: '松江校区',
publishTime: '3天前',
image: '/images/nike-shoes.jpg',
sellerAvatar: '/images/avatar3.jpg',
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'
@ -541,8 +578,8 @@ Page({
condition: '98新',
location: '闵行校区',
publishTime: '5小时前',
image: '/images/macbook.jpg',
sellerAvatar: '/images/avatar4.jpg',
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'
@ -558,8 +595,8 @@ Page({
condition: '90新',
location: '徐汇校区',
publishTime: '1天前',
image: '/images/sk2.jpg',
sellerAvatar: '/images/avatar5.jpg',
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'

@ -8,6 +8,8 @@
type="text"
placeholder="搜索商品名称、类别..."
bindinput="onSearchInput"
bindconfirm="onSearch"
confirm-type="search"
value="{{searchKeyword}}"
class="search-field"
/>

@ -18,7 +18,7 @@ Page({
if (token && userInfo) {
// 如果已登录,跳转到主页面
wx.redirectTo({
wx.switchTab({
url: '/pages/main/main'
});
}
@ -127,6 +127,11 @@ Page({
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);
@ -139,7 +144,7 @@ Page({
// 跳转到主页面
setTimeout(() => {
wx.redirectTo({
wx.switchTab({
url: '/pages/main/main'
});
}, 1500);
@ -176,6 +181,13 @@ Page({
});
},
// 管理员登录
onAdminLogin() {
wx.navigateTo({
url: '/pages/admin-login/admin-login'
});
},
// 微信一键登录
onGetPhoneNumber(e) {
if (e.detail.errMsg === 'getPhoneNumber:ok') {
@ -207,7 +219,7 @@ Page({
});
setTimeout(() => {
wx.redirectTo({
wx.switchTab({
url: '/pages/main/main'
});
}, 1500);

@ -52,6 +52,11 @@
<view class="forgot-password">
<text class="link" bindtap="onForgotPassword">忘记密码?</text>
</view>
<!-- 管理员登录入口 -->
<view class="admin-login-link">
<text class="link" bindtap="onAdminLogin">管理员登录</text>
</view>
</view>
<!-- 快速登录选项 -->

@ -151,6 +151,18 @@ page {
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);

@ -1,4 +1,6 @@
// pages/interests/interests.js
const reco = require('../../utils/recommendation.js');
Page({
/**
@ -294,6 +296,7 @@ Page({
const userInfo = wx.getStorageSync('userInfo') || {};
userInfo.interests = interests;
wx.setStorageSync('userInfo', userInfo);
try { reco.recordInterests(interests); } catch (e) {}
resolve(res.result);
} else {
// 即使云函数失败,也保存到本地,确保用户可以继续使用
@ -301,6 +304,7 @@ Page({
userInfo.interests = interests;
wx.setStorageSync('userInfo', userInfo);
console.warn('云函数保存失败,已保存到本地:', res.result?.error);
try { reco.recordInterests(interests); } catch (e) {}
resolve({ success: true, localOnly: true });
}
},
@ -311,6 +315,7 @@ Page({
userInfo.interests = interests;
wx.setStorageSync('userInfo', userInfo);
console.warn('云函数调用失败,已保存到本地');
try { reco.recordInterests(interests); } catch (e) {}
resolve({ success: true, localOnly: true });
}
});
@ -345,12 +350,8 @@ Page({
duration: 2000
});
// 延迟跳转到主页面
setTimeout(() => {
wx.redirectTo({
url: '/pages/main/main'
});
}, 2000);
// 直接切换到首页 Tab
wx.switchTab({ url: '/pages/main/main' });
})
.catch((err) => {
wx.hideLoading();
@ -382,12 +383,8 @@ Page({
duration: 2000
});
// 延迟跳转到主页面
setTimeout(() => {
wx.redirectTo({
url: '/pages/main/main'
});
}, 2000);
// 直接切换到首页 Tab
wx.switchTab({ url: '/pages/main/main' });
})
.catch((err) => {
wx.hideLoading();

@ -1,4 +1,5 @@
// pages/main/main.js
const reco = require('../../utils/recommendation.js');
Page({
/**
@ -8,6 +9,7 @@ Page({
currentTab: 'home',
userInfo: {},
searchText: '',
hasMessageAlert: false,
// 推荐商品数据
recommendProducts: [
@ -15,28 +17,28 @@ Page({
id: 1,
name: '二手iPhone 13',
price: '2999',
image: 'https://via.placeholder.com/280x200/4285F4/ffffff?text=iPhone13',
image: '/images/仓鼠.png',
tag: '电子产品'
},
{
id: 2,
name: 'Java编程思想',
price: '35',
image: 'https://via.placeholder.com/280x200/34A853/ffffff?text=Java',
image: '/images/更多犬种.png',
tag: '二手书'
},
{
id: 3,
name: '耐克运动鞋',
price: '180',
image: 'https://via.placeholder.com/280x200/EA4335/ffffff?text=运动鞋',
image: '/images/边牧.png',
tag: '服装鞋帽'
},
{
id: 4,
name: '戴尔笔记本电脑',
price: '3200',
image: 'https://via.placeholder.com/280x200/FBBC05/ffffff?text=笔记本',
image: '/images/羊.png',
tag: '电子产品'
}
],
@ -78,6 +80,13 @@ Page({
*/
onShow() {
this.loadUserInfo();
this.loadRecommendations();
this.loadUnreadStatus();
try {
if (this.getTabBar && this.getTabBar()) {
this.getTabBar().setSelected(0);
}
} catch (e) {}
},
/**
@ -85,73 +94,177 @@ Page({
*/
loadUserInfo() {
const userInfo = wx.getStorageSync('userInfo') || {};
this.setData({
userInfo: 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
});
// 从云数据库获取推荐商品
wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'getRecommendedProducts',
interests: userInterests,
limit: 8
},
success: (res) => {
wx.hideLoading();
console.log('获取推荐商品响应:', res);
if (res.result && res.result.success) {
const productsData = res.result.data || [];
if (productsData.length > 0) {
// 格式化商品数据
const products = productsData.map(item => ({
id: item.id,
name: item.name,
price: typeof item.price === 'number' ? item.price.toFixed(2) : item.price,
image: item.image,
tag: item.tag || item.category || '其他'
}));
this.setData({
recommendProducts: products
});
console.log('智能推荐商品加载完成,数量:', products.length);
} else {
// 数据库中没有商品,显示友好提示
console.log('数据库中暂无推荐商品');
this.loadEmptyProducts();
}
} else {
console.warn('获取推荐商品失败:', res.result?.error);
this.loadDefaultProducts();
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);
}
},
fail: (err) => {
wx.hideLoading();
console.error('获取推荐商品失败:', err);
// 失败时使用默认商品数据
this.loadDefaultProducts();
} 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);
},
/**
@ -164,7 +277,7 @@ Page({
name: '暂无推荐商品',
price: '0.00',
tag: '其他',
image: 'https://via.placeholder.com/280x200/E0E0E0/999999?text=暂无商品',
image: '/images/更多犬种.png',
isEmpty: true
}
];
@ -184,7 +297,7 @@ Page({
name: '加载失败,请稍后重试',
price: '0.00',
tag: '其他',
image: 'https://via.placeholder.com/280x200/FF6B6B/ffffff?text=加载失败',
image: '/images/仓鼠.png',
isError: true
}
];
@ -209,6 +322,40 @@ Page({
});
},
/**
* 搜索确认选择搜商品或搜求购并跳转
*/
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}`
});
}
}
});
},
/**
* 功能导航点击事件
*/
@ -244,6 +391,12 @@ Page({
*/
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}`
});
@ -417,14 +570,37 @@ Page({
case 'market':
this.loadMarketContent();
break;
case 'cart':
// 跳转到购物车页面
wx.navigateTo({
url: '/pages/cart/cart'
});
// 保持当前tab状态
setTimeout(() => {
this.setData({ currentTab: 'home' });
}, 100);
break;
case 'message':
this.loadMessageContent();
// 跳转到消息列表页面
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;
}
},
@ -440,14 +616,35 @@ Page({
* 加载市场内容
*/
loadMarketContent() {
console.log('加载市场内容');
// 跳转到市场页面(地图/列表)
wx.navigateTo({
url: '/pages/market/market'
});
// 保持首页tab高亮
setTimeout(() => {
this.setData({ currentTab: 'home' });
}, 100);
},
/**
* 加载消息内容
*/
loadMessageContent() {
console.log('加载消息内容');
// 已改为跳转到消息列表页
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 });
}
},
/**
@ -523,5 +720,11 @@ Page({
onReachBottom() {
console.log('上拉触底');
// 不执行任何操作,避免显示加载提示
},
/**
* 更多推荐入口
*/
onViewMoreRecommendations() {
wx.navigateTo({ url: '/pages/recommend-list/recommend-list' });
}
})

@ -1,7 +1,7 @@
{
"usingComponents": {},
"navigationBarTitleText": "校园二手交易",
"navigationBarBackgroundColor": "#4285F4",
"navigationBarBackgroundColor": "#4f8bff",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true
}

@ -3,11 +3,30 @@
<!-- 顶部导航栏 -->
<view class="header">
<view class="search-bar">
<image class="search-icon" src="/images/search.png" mode="aspectFit"></image>
<input class="search-input" placeholder="搜索商品、求购信息" bindinput="onSearchInput" />
<image class="search-icon" src="/images/边牧.png" mode="aspectFit"></image>
<input
class="search-input"
placeholder="搜索商品、求购信息"
value="{{searchText}}"
bindinput="onSearchInput"
bindconfirm="onSearchConfirm"
confirm-type="search"
/>
</view>
<view class="user-info" bindtap="onUserProfile">
<image class="avatar" src="{{userInfo.avatar || '/images/default-avatar.png'}}" mode="aspectFit"></image>
<image class="avatar" src="{{userInfo.avatar || '/images/更多犬种.png'}}" mode="aspectFit"></image>
</view>
</view>
<view class="hero-banner">
<view class="hero-content">
<text class="hero-title"> CAUC市集</text>
<text class="hero-subtitle">发现校园好物,心动立刻带回家</text>
<view class="hero-tags">
<text class="hero-tag">闲置</text>
<text class="hero-tag">限量</text>
<text class="hero-tag">人气</text>
</view>
</view>
</view>
@ -15,14 +34,14 @@
<view class="nav-section">
<!-- 卖家功能 -->
<view class="function-group">
<text class="group-title">我是卖家</text>
<text class="group-title">出闲置</text>
<view class="nav-grid">
<view class="nav-item" bindtap="onNavigateTo" data-page="pricing">
<image class="nav-icon" src="/images/边牧.png" mode="aspectFit"></image>
<view class="nav-item theme-pricing" bindtap="onNavigateTo" data-page="pricing">
<text class="nav-icon-emoji">🤖</text>
<text class="nav-text">AI定价</text>
</view>
<view class="nav-item" bindtap="onNavigateTo" data-page="publish">
<image class="nav-icon" src="/images/仓鼠.png" mode="aspectFit"></image>
<view class="nav-item theme-publish" bindtap="onNavigateTo" data-page="publish">
<text class="nav-icon-emoji"></text>
<text class="nav-text">发布商品</text>
</view>
</view>
@ -30,14 +49,14 @@
<!-- 买家功能 -->
<view class="function-group">
<text class="group-title">我是买家</text>
<text class="group-title">找闲置</text>
<view class="nav-grid">
<view class="nav-item" bindtap="onNavigateTo" data-page="buy">
<image class="nav-icon" src="/images/更多犬种.png" mode="aspectFit"></image>
<view class="nav-item theme-buy" bindtap="onNavigateTo" data-page="buy">
<text class="nav-icon-emoji">🛍️</text>
<text class="nav-text">商品购买</text>
</view>
<view class="nav-item" bindtap="onNavigateTo" data-page="wanted">
<image class="nav-icon" src="/images/羊.png" mode="aspectFit"></image>
<view class="nav-item theme-wanted" bindtap="onNavigateTo" data-page="wanted">
<text class="nav-icon-emoji">📣</text>
<text class="nav-text">求购</text>
</view>
</view>
@ -47,12 +66,18 @@
<!-- 推荐商品区域 -->
<view class="recommend-section">
<view class="section-header">
<text class="section-title">为您推荐</text>
<text class="section-subtitle">基于您的兴趣智能推荐</text>
<view class="header-left">
<text class="section-title">猜您感兴趣</text>
<text class="section-subtitle">基于你的最近浏览与兴趣为你推荐</text>
</view>
<view class="more-btn" bindtap="onViewMoreRecommendations">
<text>更多推荐</text>
<text class="more-arrow"></text>
</view>
</view>
<scroll-view class="recommend-scroll" scroll-x="true">
<view class="recommend-list">
<view class="product-card" wx:for="{{recommendProducts}}" wx:key="id" bindtap="onProductTap" data-id="{{item.id}}">
<view class="product-card" wx:for="{{recommendProducts}}" wx:key="id" bindtap="onProductTap" data-id="{{item.id}}" data-product="{{item}}">
<image class="product-image" src="{{item.image}}" mode="aspectFill"></image>
<view class="product-info">
<text class="product-name">{{item.name}}</text>
@ -69,7 +94,7 @@
<view class="section-header">
<view class="header-left">
<text class="section-title">热门求购</text>
<text class="section-subtitle">大家都在找什么</text>
<text class="section-subtitle">大家都在找什么闲置</text>
</view>
<view class="more-btn" bindtap="onViewMoreWanted">
<text>更多</text>
@ -90,23 +115,5 @@
</view>
</view>
<!-- 底部导航栏 -->
<view class="tab-bar">
<view class="tab-item {{currentTab === 'home' ? 'active' : ''}}" bindtap="onTabChange" data-tab="home">
<image class="tab-icon" src="{{currentTab === 'home' ? '/images/home-active.png' : '/images/home.png'}}"></image>
<text class="tab-text">首页</text>
</view>
<view class="tab-item {{currentTab === 'market' ? 'active' : ''}}" bindtap="onTabChange" data-tab="market">
<image class="tab-icon" src="{{currentTab === 'market' ? '/images/market-active.png' : '/images/market.png'}}"></image>
<text class="tab-text">市场</text>
</view>
<view class="tab-item {{currentTab === 'message' ? 'active' : ''}}" bindtap="onTabChange" data-tab="message">
<image class="tab-icon" src="{{currentTab === 'message' ? '/images/message-active.png' : '/images/message.png'}}"></image>
<text class="tab-text">消息</text>
</view>
<view class="tab-item {{currentTab === 'profile' ? 'active' : ''}}" bindtap="onTabChange" data-tab="profile">
<image class="tab-icon" src="{{currentTab === 'profile' ? '/images/profile-active.png' : '/images/profile.png'}}"></image>
<text class="tab-text">我的</text>
</view>
</view>
<!-- 使用自定义TabBar移除页面内底部导航 -->
</view>

@ -2,7 +2,7 @@
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
background: linear-gradient(180deg, #f0f7ff 0%, #eef5ff 100%);
padding-bottom: 100rpx;
}
@ -11,18 +11,19 @@
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background: linear-gradient(135deg, #4285F4 0%, #34A853 100%);
color: white;
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.9);
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 {
@ -46,16 +47,31 @@
width: 100%;
height: 100%;
border-radius: 50%;
border: 3rpx solid white;
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: white;
background: #ffffff;
margin: 20rpx;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
box-shadow: 0 8rpx 24rpx rgba(91, 168, 255, 0.16);
}
.function-group {
@ -67,13 +83,13 @@
}
.group-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
font-size: 30rpx;
font-weight: 800;
color: #2a4a7a;
margin-bottom: 20rpx;
display: block;
padding-left: 10rpx;
border-left: 6rpx solid #4285F4;
padding-left: 16rpx;
border-left: 10rpx solid #4f8bff;
}
.nav-grid {
@ -86,8 +102,12 @@
display: flex;
flex-direction: column;
align-items: center;
width: 150rpx;
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 {
@ -96,6 +116,27 @@
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;
@ -104,11 +145,11 @@
/* 推荐商品区域 */
.recommend-section {
background: white;
background: #ffffff;
margin: 20rpx;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
box-shadow: 0 8rpx 24rpx rgba(91, 168, 255, 0.16);
}
.section-header {
@ -143,16 +184,16 @@
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
font-size: 34rpx;
font-weight: 800;
color: #2a4a7a;
display: block;
margin-bottom: 10rpx;
}
.section-subtitle {
font-size: 24rpx;
color: #999;
color: #5e7ea6;
}
.recommend-scroll {
@ -167,14 +208,17 @@
display: inline-block;
width: 280rpx;
margin-right: 20rpx;
background: #f8f9fa;
border-radius: 15rpx;
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 {
@ -193,27 +237,28 @@
.product-price {
font-size: 28rpx;
color: #FF6B35;
font-weight: bold;
color: #3b82f6;
font-weight: 800;
display: block;
margin-bottom: 8rpx;
}
.product-tag {
font-size: 20rpx;
color: #666;
background: #e9ecef;
padding: 4rpx 12rpx;
border-radius: 8rpx;
color: #fff;
background: #9ec5ff;
padding: 6rpx 14rpx;
border-radius: 999rpx;
box-shadow: 0 6rpx 16rpx rgba(158, 197, 255, 0.3);
}
/* 热门求购 */
.wanted-section {
background: white;
background: #ffffff;
margin: 20rpx;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
box-shadow: 0 8rpx 24rpx rgba(91, 168, 255, 0.16);
}
.wanted-list {
@ -223,10 +268,11 @@
}
.wanted-item {
background: #f8f9fa;
background: linear-gradient(180deg, #fff 0%, #f6fbff 100%);
padding: 25rpx;
border-radius: 15rpx;
border-left: 6rpx solid #4285F4;
border-radius: 18rpx;
border-left: 8rpx solid #9ec5ff;
box-shadow: 0 8rpx 24rpx rgba(91,168,255,0.12);
}
.wanted-info {
@ -237,17 +283,17 @@
}
.wanted-title {
font-size: 28rpx;
color: #333;
font-weight: bold;
font-size: 30rpx;
color: #2a4a7a;
font-weight: 800;
display: block;
margin-bottom: 10rpx;
}
.wanted-price {
font-size: 24rpx;
color: #FF6B35;
font-weight: 600;
color: #3b82f6;
font-weight: 700;
}
.wanted-time {
@ -269,7 +315,7 @@
left: 0;
right: 0;
display: flex;
background: white;
background: #ffffff;
border-top: 1rpx solid #e0e0e0;
padding: 15rpx 0;
z-index: 1000;
@ -289,6 +335,13 @@
margin-bottom: 8rpx;
}
.tab-icon-emoji {
font-size: 44rpx;
margin-bottom: 8rpx;
display: block;
line-height: 1;
}
.tab-text {
font-size: 20rpx;
color: #999;
@ -297,6 +350,7 @@
.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) {

@ -10,7 +10,7 @@ Page({
selectedCategory: 0,
// 商品状态筛选
statusFilter: ['全部', '在售', '已售出', '已下架'],
statusFilter: ['全部', '在售', '交易中', '已售出', '已下架'],
selectedStatus: 0,
// 排序选项
@ -35,6 +35,12 @@ Page({
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 清除可能缓存的旧openid确保使用登录的用户ID获取正确的openid
const userInfo = wx.getStorageSync('userInfo') || {};
if (userInfo._id) {
// 如果有登录的用户ID清除旧的openid缓存强制重新获取
wx.removeStorageSync('openid');
}
this.loadMyProducts();
},
@ -42,110 +48,259 @@ Page({
* 生命周期函数--监听页面显示
*/
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;
},
/**
* 加载我的商品列表
*/
loadMyProducts() {
async loadMyProducts() {
this.setData({
isLoading: true,
error: ''
});
// 模拟从后端获取数据
setTimeout(() => {
// 模拟商品数据
const mockProducts = [
{
id: 'p1',
name: '二手iPhone 13 128GB 白色',
price: '2999',
originalPrice: '5999',
image: 'https://via.placeholder.com/200x200/4285F4/ffffff?text=iPhone13',
category: '电子产品',
status: 'selling', // selling: 在售, sold: 已售出, off: 已下架
publishTime: '2024-04-15 10:30',
views: 156,
likes: 23,
comments: 8
},
{
id: 'p2',
name: 'Java编程思想 第4版 九成新',
price: '35',
originalPrice: '108',
image: 'https://via.placeholder.com/200x200/34A853/ffffff?text=Java',
category: '图书文具',
status: 'selling',
publishTime: '2024-04-14 16:45',
views: 89,
likes: 12,
comments: 5
},
{
id: 'p3',
name: '耐克运动鞋 Air Max 270',
price: '180',
originalPrice: '899',
image: 'https://via.placeholder.com/200x200/EA4335/ffffff?text=运动鞋',
category: '服装鞋帽',
status: 'sold',
publishTime: '2024-04-12 09:15',
views: 210,
likes: 35,
comments: 12
},
{
id: 'p4',
name: '戴尔笔记本电脑 XPS 13',
price: '3200',
originalPrice: '9999',
image: 'https://via.placeholder.com/200x200/FBBC05/ffffff?text=笔记本',
category: '电子产品',
status: 'selling',
publishTime: '2024-04-10 14:20',
views: 178,
likes: 28,
comments: 9
},
{
id: 'p5',
name: '瑜伽垫 加厚加宽 蓝色',
price: '68',
originalPrice: '158',
image: 'https://via.placeholder.com/200x200/4285F4/ffffff?text=瑜伽垫',
category: '运动户外',
status: 'off',
publishTime: '2024-04-08 11:30',
views: 67,
likes: 9,
comments: 3
},
{
id: 'p6',
name: 'SK-II神仙水 75ml',
price: '380',
originalPrice: '590',
image: 'https://via.placeholder.com/200x200/FF6B9D/ffffff?text=神仙水',
category: '美妆个护',
status: 'selling',
publishTime: '2024-04-05 15:45',
views: 234,
likes: 42,
comments: 18
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({
products: mockProducts,
filteredProducts: mockProducts,
error: '加载失败,请重试',
isLoading: false,
hasMore: false // 模拟没有更多数据
products: [],
filteredProducts: []
});
wx.showToast({
title: '加载失败: ' + (err.errMsg || '未知错误'),
icon: 'none',
duration: 3000
});
}, 1500);
}
},
/**
* 格式化时间
*/
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() + '日';
}
},
/**
@ -198,9 +353,10 @@ Page({
// 按状态筛选
if (this.data.selectedStatus !== 0) {
const statusMap = {
1: 'selling',
2: 'sold',
3: 'off'
1: 'selling', // 在售
2: 'transacting', // 交易中
3: 'sold', // 已售出
4: 'off' // 已下架
};
const status = statusMap[this.data.selectedStatus];
filtered = filtered.filter(item => item.status === status);
@ -208,8 +364,12 @@ Page({
// 排序
switch (this.data.selectedSort) {
case 1: // 发布时间
filtered.sort((a, b) => new Date(b.publishTime) - new Date(a.publishTime));
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));
@ -217,7 +377,7 @@ Page({
case 3: // 价格从高到低
filtered.sort((a, b) => parseFloat(b.price) - parseFloat(a.price));
break;
default: // 默认排序
default: // 默认排序(保持数据库返回的顺序)
// 保持原顺序
break;
}
@ -236,35 +396,68 @@ Page({
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}`
});
},
/**
* 下架商品
*/
onOffShelf(e) {
async onOffShelf(e) {
const productId = e.currentTarget.dataset.id;
wx.showModal({
title: '确认下架',
content: '确定要下架该商品吗?下架后可重新上架。',
success: (res) => {
success: async (res) => {
if (res.confirm) {
// 更新商品状态
const updatedProducts = this.data.products.map(item => {
if (item.id === productId && item.status === 'selling') {
return { ...item, status: 'off' };
}
return item;
});
this.setData({
products: updatedProducts
});
this.filterProducts();
wx.showToast({
title: '商品已下架',
icon: 'success'
});
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'
});
}
}
}
});
@ -273,31 +466,53 @@ Page({
/**
* 重新上架商品
*/
onReShelf(e) {
async onReShelf(e) {
const productId = e.currentTarget.dataset.id;
wx.showModal({
title: '重新上架',
content: '确定要重新上架该商品吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
// 更新商品状态
const updatedProducts = this.data.products.map(item => {
if (item.id === productId && item.status === 'off') {
return { ...item, status: 'selling', publishTime: new Date().toLocaleString('zh-CN') };
}
return item;
});
this.setData({
products: updatedProducts
});
this.filterProducts();
wx.showToast({
title: '商品已重新上架',
icon: 'success'
});
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'
});
}
}
}
});
@ -316,26 +531,34 @@ Page({
/**
* 删除商品
*/
onDeleteProduct(e) {
async onDeleteProduct(e) {
const productId = e.currentTarget.dataset.id;
wx.showModal({
title: '确认删除',
content: '确定要删除该商品吗?删除后无法恢复。',
success: (res) => {
success: async (res) => {
if (res.confirm) {
// 删除商品
const updatedProducts = this.data.products.filter(item => item.id !== productId);
this.setData({
products: updatedProducts
});
this.filterProducts();
wx.showToast({
title: '商品已删除',
icon: 'success'
});
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'
});
}
}
}
});
@ -357,8 +580,8 @@ Page({
/**
* 下拉刷新
*/
onPullDownRefresh() {
this.loadMyProducts();
async onPullDownRefresh() {
await this.loadMyProducts();
wx.stopPullDownRefresh();
},
@ -367,5 +590,14 @@ Page({
*/
onBack() {
wx.navigateBack();
},
/**
* 去发布商品
*/
onPublish() {
wx.navigateTo({
url: '/pages/publish/publish'
});
}
});

@ -47,7 +47,7 @@
<!-- 商品列表 -->
<view class="product-list">
<view wx:if="{{filteredProducts.length === 0}}" class="empty-state">
<image class="empty-image" src="/images/empty.png" mode="aspectFit"></image>
<image class="empty-image" src="https://via.placeholder.com/200x200/eeeeee/999999?text=Empty" mode="aspectFit"></image>
<text class="empty-text">暂无商品</text>
<button class="publish-btn" bindtap="onPublish">去发布商品</button>
</view>
@ -62,6 +62,9 @@
<view class="product-status" wx:if="{{item.status === 'sold'}}">
<text>已售出</text>
</view>
<view class="product-status transacting" wx:if="{{item.status === 'transacting'}}">
<text>交易中</text>
</view>
<view class="product-status off" wx:if="{{item.status === 'off'}}">
<text>已下架</text>
</view>
@ -109,6 +112,12 @@
<button class="action-btn off-btn" bindtap="onOffShelf" data-id="{{item.id}}">下架</button>
<button class="action-btn delete-btn" bindtap="onDeleteProduct" data-id="{{item.id}}">删除</button>
</view>
<!-- 交易中商品的操作 -->
<view wx:elif="{{item.status === 'transacting'}}">
<button class="action-btn view-btn" bindtap="onProductDetail" data-id="{{item.id}}">查看详情</button>
<button class="action-btn on-btn" bindtap="onHandleTransaction" data-id="{{item.id}}">处理交易</button>
</view>
<!-- 已售出商品的操作 -->
<view wx:elif="{{item.status === 'sold'}}">

@ -295,6 +295,10 @@ page {
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);
}

@ -6,6 +6,8 @@ Page({
data: {
// 当前选中的标签
activeTab: 'all',
// 视角buyer买家/ seller卖家
viewRole: 'buyer',
// 订单数据
orders: [],
@ -17,29 +19,45 @@ Page({
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
async onLoad(options) {
// 可以从选项中获取初始标签
if (options.tab) {
this.setData({
activeTab: options.tab
});
}
this.loadOrders();
// 支持通过参数设置初始视角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();
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 页面显示时重新加载订单数据
this.loadOrders();
async onShow() {
// 页面显示时强制重新加载订单数据
// 清除可能缓存的旧openid确保使用登录的用户ID获取正确的openid
const userInfo = wx.getStorageSync('userInfo') || {};
if (userInfo._id) {
// 如果有登录的用户ID清除旧的openid缓存强制重新获取
wx.removeStorageSync('openid');
}
await this.loadOrders();
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
this.loadOrders();
async onPullDownRefresh() {
await this.loadOrders();
},
/**
@ -54,164 +72,288 @@ Page({
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;
},
/**
* 加载订单数据
*/
loadOrders() {
async loadOrders() {
this.setData({ isLoading: true });
// 模拟从后端获取订单数据
setTimeout(() => {
// 模拟不同状态的订单数据
const mockOrders = [
{
id: '1',
orderNumber: '20240501123456',
orderTime: '2024-05-01 14:30:25',
status: 'pending',
statusText: '待付款',
totalCount: 2,
totalPrice: 199.80,
products: [
{
productId: 'p1',
name: '智能手表 运动监测 心率血压多功能防水',
specs: '黑色 标准版',
price: 129.90,
count: 1,
image: '/images/product1.jpg'
},
{
productId: 'p2',
name: '蓝牙耳机 无线降噪 长续航',
specs: '白色',
price: 69.90,
count: 1,
image: '/images/product2.jpg'
}
]
},
{
id: '2',
orderNumber: '20240428987654',
orderTime: '2024-04-28 09:15:42',
status: 'paid',
statusText: '待发货',
totalCount: 1,
totalPrice: 899.00,
products: [
{
productId: 'p3',
name: '平板电脑 10.2英寸 128GB WiFi版',
specs: '深空灰',
price: 899.00,
count: 1,
image: '/images/product3.jpg'
}
]
},
{
id: '3',
orderNumber: '20240420567890',
orderTime: '2024-04-20 16:45:18',
status: 'shipped',
statusText: '待收货',
totalCount: 3,
totalPrice: 258.70,
products: [
{
productId: 'p4',
name: '便携式充电宝 20000mAh 双向快充',
specs: '白色',
price: 79.90,
count: 2,
image: '/images/product4.jpg'
},
{
productId: 'p5',
name: '手机支架 桌面懒人支架 可调节',
specs: '银色',
price: 98.90,
count: 1,
image: '/images/product5.jpg'
}
]
},
{
id: '4',
orderNumber: '20240410135790',
orderTime: '2024-04-10 11:20:36',
status: 'completed',
statusText: '已完成',
totalCount: 1,
totalPrice: 359.00,
products: [
{
productId: 'p6',
name: '无线充电器 15W快充 兼容多设备',
specs: '黑色',
price: 359.00,
count: 1,
image: '/images/product6.jpg'
}
]
},
{
id: '5',
orderNumber: '20240401246800',
orderTime: '2024-04-01 18:50:12',
status: 'cancelled',
statusText: '已取消',
totalCount: 1,
totalPrice: 599.00,
products: [
{
productId: 'p7',
name: '智能音箱 AI语音助手 360°环绕音效',
specs: '红色',
price: 599.00,
count: 1,
image: '/images/product7.jpg'
}
]
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);
}
];
// 根据当前标签筛选订单
let filteredOrders = mockOrders;
}
// 如果无法通过用户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') {
filteredOrders = mockOrders.filter(order => order.status === this.data.activeTab);
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: filteredOrders,
orders: [],
isLoading: false
});
// 停止下拉刷新
wx.stopPullDownRefresh();
}, 1000);
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}`;
},
/**
* 取消订单
*/
cancelOrder(e) {
async cancelOrder(e) {
const orderId = e.currentTarget.dataset.id;
wx.showModal({
title: '取消订单',
content: '确定要取消该订单吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
// 这里可以调用接口取消订单
wx.showToast({
title: '订单已取消',
icon: 'success'
});
// 重新加载订单数据
setTimeout(() => {
this.loadOrders();
}, 500);
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'
});
}
}
}
});
@ -242,81 +384,171 @@ Page({
}
console.log('找到订单信息:', order);
// 2. 模拟调用支付接口获取支付参数实际项目中应调用后端API
setTimeout(() => {
// 模拟支付参数(实际项目中由后端返回)
const payParams = {
timeStamp: '' + Math.floor(Date.now() / 1000),
nonceStr: 'noncestr' + Math.random().toString(36).substr(2, 15),
package: 'prepay_id=wx202405011234567890abcdef1234567890',
signType: 'MD5',
paySign: '8A40C1194B39B20B16238888A8D8DF2E'
};
console.log('准备调用支付接口,参数:', payParams);
// 3. 调用微信支付接口
wx.requestPayment({
...payParams,
success: (res) => {
console.log('支付成功:', res);
wx.hideLoading();
wx.showToast({
title: '支付成功',
icon: 'success'
});
// 4. 重新加载订单数据,更新订单状态
setTimeout(() => {
this.loadOrders();
}, 1000);
// 5. 支付成功后跳转到订单详情页
setTimeout(() => {
this.viewOrderDetail(e);
}, 1500);
},
fail: (err) => {
console.error('支付失败:', err);
wx.hideLoading();
// 判断是否是用户取消支付
if (err.errMsg.indexOf('cancel') >= 0) {
// 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: '支付已取消',
title: '操作失败',
icon: 'none'
});
} else {
// 其他支付失败情况
}
}
}
});
},
/**
* 卖家发货
*/
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: 'error'
title: '操作失败',
icon: 'none'
});
}
}
// 移除重复的complete回调因为success和fail中已经处理了hideLoading
});
}, 1000);
}
});
},
/**
* 确认收货
*/
confirmReceipt(e) {
async confirmReceipt(e) {
const orderId = e.currentTarget.dataset.id;
wx.showModal({
title: '确认收货',
content: '确认已收到商品吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
// 这里可以调用接口确认收货
wx.showToast({
title: '已确认收货',
icon: 'success'
});
// 重新加载订单数据
setTimeout(() => {
this.loadOrders();
}, 500);
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'
});
}
}
}
});

@ -1,5 +1,10 @@
<!--pages/orders/orders.wxml-->
<view class="orders-container">
<!-- 视角切换 -->
<view class="role-tabs">
<view class="role-tab {{viewRole === 'buyer' ? 'active' : ''}}" bindtap="switchRole" data-role="buyer">我的购买</view>
<view class="role-tab {{viewRole === 'seller' ? 'active' : ''}}" bindtap="switchRole" data-role="seller">我的出售</view>
</view>
<!-- 订单状态筛选标签 -->
<view class="order-tabs">
<view class="tab-item {{activeTab === 'all' ? 'active' : ''}}" bindtap="switchTab" data-tab="all">全部</view>
@ -20,7 +25,7 @@
<!-- 空状态 -->
<view wx:elif="{{orders.length === 0 && !isLoading}}" class="empty-container">
<image class="empty-icon" src="/images/empty-orders.png" mode="aspectFit"></image>
<image class="empty-icon" src="https://via.placeholder.com/200x200/E0E0E0/999999?text=暂无订单" mode="aspectFit"></image>
<text class="empty-text">暂无订单</text>
<text class="empty-subtext">去逛逛,发现心仪商品</text>
<button class="go-shopping-btn" bindtap="goShopping">去购物</button>
@ -41,7 +46,7 @@
<!-- 订单商品 -->
<view class="order-products">
<view wx:for="{{item.products}}" wx:for-item="product" wx:key="productId" class="product-item">
<image class="product-image" src="{{product.image || '/images/default-product.png'}}" mode="aspectFit"></image>
<image class="product-image" src="{{product.image || 'https://via.placeholder.com/600x400/cccccc/666666?text=商品图'}}" mode="aspectFit"></image>
<view class="product-info">
<text class="product-name">{{product.name}}</text>
<text class="product-specs">{{product.specs || '规格:标准'}}</text>
@ -67,13 +72,19 @@
</view>
<view class="order-actions">
<button wx:if="{{item.status === 'pending'}}" class="action-btn secondary" bindtap="cancelOrder" data-id="{{item.id}}">
<!-- 卖家视角操作 -->
<block wx:if="{{viewRole==='seller'}}">
<button wx:if="{{item.statusText==='待卖家确认'}}" class="action-btn primary" bindtap="confirmPayment" data-id="{{item.id}}">确认收款</button>
<button wx:if="{{item.statusText==='待发货'}}" class="action-btn primary" bindtap="shipOrder" data-id="{{item.id}}">发货</button>
</block>
<!-- 买家视角操作 -->
<button wx:if="{{viewRole==='buyer' && item.status === 'pending'}}" class="action-btn secondary" bindtap="cancelOrder" data-id="{{item.id}}">
取消订单
</button>
<button wx:if="{{item.status === 'pending'}}" class="action-btn primary" bindtap="payOrder" data-id="{{item.id}}">
<button wx:if="{{viewRole==='buyer' && item.status === 'pending'}}" class="action-btn primary" bindtap="payOrder" data-id="{{item.id}}">
去付款
</button>
<button wx:if="{{item.status === 'shipped'}}" class="action-btn primary" bindtap="confirmReceipt" data-id="{{item.id}}">
<button wx:if="{{viewRole==='buyer' && item.status === 'shipped'}}" class="action-btn primary" bindtap="confirmReceipt" data-id="{{item.id}}">
确认收货
</button>
<button wx:if="{{item.status === 'completed' || item.status === 'cancelled'}}" class="action-btn secondary" bindtap="viewOrderDetail" data-id="{{item.id}}">

@ -4,6 +4,33 @@
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;
@ -12,7 +39,7 @@
padding: 0 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
position: fixed;
top: 0;
top: 80rpx; /* 位于视角切换条下方 */
left: 0;
right: 0;
z-index: 100;
@ -44,7 +71,7 @@
/* 订单列表 */
.order-list {
margin-top: 90rpx;
margin-top: 170rpx; /* 顶部两条固定栏的总高度 */
}
/* 加载状态 */
@ -348,4 +375,20 @@
.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;
}

@ -18,7 +18,7 @@
</view>
</view>
<view class="upload-placeholder" wx:else>
<image class="camera-icon" src="/images/边牧.png" mode="aspectFit"></image>
<image class="camera-icon" src="https://via.placeholder.com/80x80/888888/ffffff?text=IMG" mode="aspectFit"></image>
<text class="placeholder-text">点击上传商品图片</text>
<text class="placeholder-hint">支持 JPG、PNG 格式</text>
</view>
@ -44,11 +44,11 @@
<!-- 功能按钮区域 -->
<view class="button-group">
<button class="camera-btn" bindtap="onTakePhoto">
<image class="btn-icon" src="/images/边牧.png" mode="aspectFit"></image>
<image class="btn-icon" src="https://via.placeholder.com/60x60/34A853/ffffff?text=Cam" mode="aspectFit"></image>
<text>拍照上传</text>
</button>
<button class="ai-pricing-btn" bindtap="onAIPricing" disabled="{{!imagePath || !originalPrice}}">
<image class="btn-icon" src="/images/边牧.png" mode="aspectFit"></image>
<image class="btn-icon" src="https://via.placeholder.com/60x60/FF6B35/ffffff?text=AI" mode="aspectFit"></image>
<text>智能定价</text>
</button>
</view>
@ -128,7 +128,7 @@
<!-- 加载状态 -->
<view class="loading-section" wx:if="{{isAnalyzing}}">
<view class="loading-content">
<image class="loading-icon" src="/images/边牧.png" mode="aspectFit"></image>
<image class="loading-icon" src="https://via.placeholder.com/80x80/4285F4/ffffff?text=AI" mode="aspectFit"></image>
<text class="loading-text">AI正在分析商品图片...</text>
<text class="loading-subtext">请稍候,这可能需要几秒钟</text>
</view>

File diff suppressed because it is too large Load Diff

@ -11,7 +11,7 @@
indicator-dots="{{imageUrls.length > 1}}"
bindchange="onImageChange"
>
<block wx:for="{{imageUrls}}" wx:key="{{index}}">
<block wx:for="{{imageUrls}}" wx:key="index">
<swiper-item>
<view class="image-wrapper" bindtap="onPreviewImage" data-index="{{currentImageIndex}}">
<image
@ -86,6 +86,29 @@
</view>
</view>
<!-- 交易地点信息 -->
<view class="location-section">
<view class="section-title">交易地点</view>
<view class="location-row">
<text class="location-label">约定地点</text>
<text class="location-value">{{product.tradeLandmarkName || '未设置'}}</text>
</view>
<view class="location-row" wx:if="{{tradeLocation}}">
<text class="location-label">真实地址</text>
<text class="location-value">{{tradeAddress || '未解析'}}</text>
</view>
<view class="location-row" wx:if="{{distanceText}}">
<text class="location-label">距你</text>
<text class="location-distance">{{distanceText}}</text>
</view>
<view class="location-actions">
<button class="loc-btn" bindtap="onNavigateToTradeLocation" disabled="{{!tradeLocation}}">导航到交易地点</button>
</view>
<view class="location-tips">
<text>建议在校园公共区域当面验货,注意财物安全。</text>
</view>
</view>
<!-- 卖家信息 -->
<view class="seller-section">
<view class="section-title">卖家信息</view>

@ -361,6 +361,68 @@ page {
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;

@ -12,50 +12,485 @@ Page({
orders: 0,
favorites: 0
},
notificationEnabled: true
notificationEnabled: true,
hasOrderAlert: false,
// 图表数据
monthlyPosted: [],
monthlyPurchased: []
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadUserInfo();
this.loadUserStats();
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
});
}
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
this.loadUserInfo();
this.loadUserStats();
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;
},
/**
* 加载用户信息
*/
loadUserInfo() {
const userInfo = wx.getStorageSync('userInfo') || {};
this.setData({
userInfo: userInfo
});
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
});
}
},
/**
* 加载用户统计数据
*/
loadUserStats() {
// 模拟从后端获取用户统计数据
const stats = wx.getStorageSync('userStats') || {
products: 12,
wanted: 3,
orders: 8,
favorites: 25
};
this.setData({
userStats: stats
});
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: [] });
}
},
/**
@ -71,43 +506,37 @@ Page({
* 我的求购点击事件
*/
onMyWanted() {
wx.showToast({
title: '跳转到我的求购',
icon: 'none'
wx.navigateTo({
url: '/pages/wanted-list/wanted-list?filter=my'
});
// 这里可以跳转到我的求购页面
},
/**
* 我的订单点击事件
*/
onMyOrders() {
// 进入订单页后清除本地变更标记
wx.removeStorageSync('orderChanged');
wx.navigateTo({
url: '/pages/orders/orders'
});
// 这里可以跳转到我的订单页面
},
/**
* 我的收藏点击事件
*/
onMyFavorites() {
wx.showToast({
title: '跳转到我的收藏',
icon: 'none'
wx.navigateTo({
url: '/pages/favorites/favorites'
});
// 这里可以跳转到我的收藏页面
},
/**
* 个人信息编辑
*/
onProfileEdit() {
wx.showModal({
title: '编辑个人信息',
content: '此功能正在开发中',
showCancel: false,
confirmText: '知道了'
wx.navigateTo({
url: '/pages/profile-edit/profile-edit'
});
},
@ -115,11 +544,8 @@ Page({
* 安全设置
*/
onSecurity() {
wx.showModal({
title: '安全设置',
content: '此功能正在开发中',
showCancel: false,
confirmText: '知道了'
wx.navigateTo({
url: '/pages/security/security'
});
},
@ -127,11 +553,8 @@ Page({
* 收货地址管理
*/
onAddress() {
wx.showModal({
title: '收货地址',
content: '此功能正在开发中',
showCancel: false,
confirmText: '知道了'
wx.navigateTo({
url: '/pages/address/address'
});
},
@ -139,40 +562,70 @@ Page({
* 意见反馈
*/
onFeedback() {
wx.showModal({
title: '意见反馈',
content: '此功能正在开发中',
showCancel: false,
confirmText: '知道了'
wx.navigateTo({
url: '/pages/feedback/feedback'
});
},
/**
* 消息通知开关
*/
onNotificationChange(e) {
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.showModal({
title: '隐私设置',
content: '此功能正在开发中',
showCancel: false,
confirmText: '知道了'
wx.navigateTo({
url: '/pages/privacy/privacy'
});
},
/**
* 消息通知设置用于菜单项点击实际开关在switch中
*/
onNotification() {
// 这个方法用于菜单项点击但实际功能由switch组件处理
// 可以在这里添加跳转到详细通知设置页面的逻辑
},
/**
* 关于我们
@ -199,6 +652,8 @@ Page({
wx.removeStorageSync('userInfo');
wx.removeStorageSync('token');
wx.removeStorageSync('userStats');
wx.removeStorageSync('openid');
wx.removeStorageSync('notificationEnabled');
// 显示退出成功提示
wx.showToast({
@ -221,21 +676,18 @@ Page({
/**
* 用户下拉刷新
*/
onPullDownRefresh() {
async onPullDownRefresh() {
console.log('下拉刷新个人中心');
// 重新加载用户信息
this.loadUserInfo();
this.loadUserStats();
await this.loadUserInfo();
await this.loadUserStats();
await this.loadNotificationSetting();
// 模拟刷新延迟
setTimeout(() => {
wx.stopPullDownRefresh();
wx.showToast({
title: '刷新成功',
icon: 'success'
});
}, 1000);
}, 300);
},
/**
@ -245,16 +697,76 @@ Page({
console.log('上拉加载更多');
// 可以在这里加载更多数据
wx.showLoading({
title: '加载中...'
});
setTimeout(() => {
wx.hideLoading();
wx.showToast({
title: '没有更多数据了',
icon: 'none'
// 页面不再弹出“加载中”,直接静默处理或忽略
},
/**
* 检查订单红点提醒未读通知或待处理订单
*/
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
});
}, 1000);
} catch (err) {
console.error('检查订单提醒失败:', err);
}
}
})

@ -1,8 +1,9 @@
{
"navigationBarTitleText": "个人中心",
"navigationBarBackgroundColor": "#667eea",
"navigationBarBackgroundColor": "#4f8bff",
"navigationBarTextStyle": "white",
"backgroundColor": "#f5f5f5",
"backgroundTextStyle": "dark",
"enablePullDownRefresh": true
"backgroundColor": "#f0f7ff",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false,
"navigationStyle": "default"
}

@ -3,7 +3,7 @@
<!-- 顶部用户信息区域 -->
<view class="user-header">
<view class="user-avatar-section">
<image class="user-avatar" src="{{userInfo.avatar || '/images/default-avatar.png'}}" mode="aspectFit"></image>
<image class="user-avatar" src="{{userInfo.avatar || '/images/更多犬种.png'}}" mode="aspectFit"></image>
<view class="user-info">
<text class="user-name">{{userInfo.nickName || '未设置昵称'}}</text>
<text class="user-id">ID: {{userInfo.id || '未设置'}}</text>
@ -19,9 +19,10 @@
<text class="stat-number">{{userStats.wanted || 0}}</text>
<text class="stat-label">我的求购</text>
</view>
<view class="stat-item" bindtap="onMyOrders">
<view class="stat-item" bindtap="onMyOrders" style="position: relative;">
<text class="stat-number">{{userStats.orders || 0}}</text>
<text class="stat-label">我的订单</text>
<view class="red-dot" wx:if="{{hasOrderAlert}}"></view>
</view>
</view>
</view>
@ -32,22 +33,23 @@
<text class="group-title">交易管理</text>
<view class="menu-list">
<view class="menu-item" bindtap="onMyProducts">
<image class="menu-icon" src="/images/product.png" mode="aspectFit"></image>
<text class="menu-emoji">👜</text>
<text class="menu-text">我的商品</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" bindtap="onMyWanted">
<image class="menu-icon" src="/images/wanted.png" mode="aspectFit"></image>
<text class="menu-emoji">📝</text>
<text class="menu-text">我的求购</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" bindtap="onMyOrders">
<image class="menu-icon" src="/images/order.png" mode="aspectFit"></image>
<view class="menu-item" bindtap="onMyOrders" style="position: relative;">
<text class="menu-emoji">📦</text>
<text class="menu-text">我的订单</text>
<text class="menu-arrow">></text>
<view class="red-dot menu" wx:if="{{hasOrderAlert}}"></view>
</view>
<view class="menu-item" bindtap="onMyFavorites">
<image class="menu-icon" src="/images/favorite.png" mode="aspectFit"></image>
<text class="menu-emoji">❤️</text>
<text class="menu-text">我的收藏</text>
<text class="menu-arrow">></text>
</view>
@ -58,47 +60,55 @@
<text class="group-title">账户设置</text>
<view class="menu-list">
<view class="menu-item" bindtap="onProfileEdit">
<image class="menu-icon" src="/images/profile.png" mode="aspectFit"></image>
<text class="menu-emoji">👤</text>
<text class="menu-text">个人信息</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" bindtap="onSecurity">
<image class="menu-icon" src="/images/security.png" mode="aspectFit"></image>
<text class="menu-text">安全设置</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" bindtap="onAddress">
<image class="menu-icon" src="/images/address.png" mode="aspectFit"></image>
<text class="menu-text">收货地址</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" bindtap="onFeedback">
<image class="menu-icon" src="/images/feedback.png" mode="aspectFit"></image>
<text class="menu-text">意见反馈</text>
<text class="menu-arrow">></text>
</view>
</view>
</view>
</view>
<!-- 图表分析 -->
<view class="menu-section">
<view class="menu-group">
<text class="group-title">系统设置</text>
<view class="menu-list">
<view class="menu-item" bindtap="onNotification">
<image class="menu-icon" src="/images/notification.png" mode="aspectFit"></image>
<text class="menu-text">消息通知</text>
<view class="menu-switch">
<switch checked="{{notificationEnabled}}" bindchange="onNotificationChange" />
<text class="group-title">发布趋势近12个月</text>
<view class="chart-container">
<view class="bar-chart">
<view class="chart-bars">
<view class="bar-item" wx:for="{{monthlyPosted}}" wx:key="month">
<view class="bar-wrapper">
<view class="bar" style="height: {{item.percentage}}%"></view>
<text class="bar-value">{{item.count}}</text>
</view>
<text class="bar-label">{{item.month}}</text>
</view>
</view>
</view>
<view class="menu-item" bindtap="onPrivacy">
<image class="menu-icon" src="/images/privacy.png" mode="aspectFit"></image>
<text class="menu-text">隐私设置</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" bindtap="onAbout">
<image class="menu-icon" src="/images/about.png" mode="aspectFit"></image>
<text class="menu-text">关于我们</text>
<text class="menu-arrow">></text>
</view>
</view>
<view class="menu-group">
<text class="group-title">购买趋势近12个月</text>
<view class="chart-container">
<view class="line-chart">
<view class="chart-area">
<view class="chart-grid">
<view class="grid-line" wx:for="{{5}}" wx:key="index"></view>
</view>
<view class="chart-line">
<view class="line-path">
<view class="line-point" wx:for="{{monthlyPurchased}}" wx:key="month" style="left: {{item.position}}%; bottom: {{item.percentage}}%"></view>
</view>
</view>
<view class="chart-labels">
<text class="label-item" wx:for="{{monthlyPurchased}}" wx:key="month">{{item.month}}</text>
</view>
<view class="chart-values">
<text class="value-item" wx:for="{{monthlyPurchased}}" wx:key="month">{{item.sales}}</text>
</view>
</view>
</view>
</view>
</view>

@ -1,16 +1,24 @@
/* pages/profile/profile.wxss */
page {
background-color: #f0f7ff;
height: 100%;
}
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
background: linear-gradient(180deg, #f0f7ff 0%, #eef5ff 100%);
padding-bottom: 100rpx;
}
/* 顶部用户信息区域 */
.user-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #5ba8ff 0%, #7ec8ff 100%);
padding: 40rpx 30rpx;
padding-top: 10rpx;
color: white;
margin-top: 0;
position: relative;
}
.user-avatar-section {
@ -73,20 +81,20 @@
}
.menu-group {
background: white;
background: #ffffff;
border-radius: 20rpx;
margin-bottom: 30rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
box-shadow: 0 8rpx 24rpx rgba(91, 168, 255, 0.16);
}
.group-title {
display: block;
padding: 30rpx 30rpx 20rpx;
font-size: 28rpx;
font-weight: bold;
color: #333;
border-bottom: 1rpx solid #f0f0f0;
font-weight: 800;
color: #2a4a7a;
border-bottom: 1rpx solid #eef2f7;
}
.menu-list {
@ -118,7 +126,13 @@
.menu-text {
flex: 1;
font-size: 28rpx;
color: #333;
color: #3d4d66;
}
.menu-emoji {
font-size: 40rpx;
margin-right: 20rpx;
line-height: 1;
}
.menu-arrow {
@ -130,6 +144,23 @@
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;
@ -137,19 +168,19 @@
.logout-btn {
width: 100%;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 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(255, 107, 107, 0.3);
box-shadow: 0 8rpx 25rpx rgba(79, 139, 255, 0.25);
}
.logout-btn:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 15rpx rgba(255, 107, 107, 0.3);
box-shadow: 0 4rpx 15rpx rgba(79, 139, 255, 0.25);
}
/* 响应式调整 */
@ -170,4 +201,24 @@
.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; }

@ -6,6 +6,10 @@ Page({
data: {
// 页面来源标识
fromAIPricing: false,
// 是否展示价格信息仅AI定价或编辑模式且有数据时显示
showPriceInfo: false,
isEditMode: false, // 是否为编辑模式
editProductId: '', // 编辑的商品ID
// 商品基本信息
productImage: '',
@ -31,19 +35,42 @@ Page({
// 发布设置
contactInfo: '',
transactionMethods: ['面交', '快递', '自提'],
// 兼容WXML中使用的 tradeMethods 命名
tradeMethods: ['面交', '快递', '自提'],
selectedMethods: [],
tradeMethodIndex: 0, // 默认选择第一个交易方式
// 发布状态
isPublishing: false
isPublishing: false,
// 发布建议与推荐类别
recommendLoading: false,
recommendMessage: '',
recommendedCategories: [],
categoryGuides: {},
// 交易地点(仅地图选点)
tradeLocation: { latitude: null, longitude: null, landmarkName: '' },
tradeAddress: ''
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
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参数小程序路由参数会自动编码需要手动解码
@ -91,13 +118,11 @@ Page({
analysisReport: decodedOptions.analysisReport || '',
categoryIndex: categoryIndex // 设置类别选择器的索引
});
// 根据AI定价携带的数据决定是否显示价格信息
this.updatePriceInfoVisibility();
// 如果传递了建议价格,设置为默认售价
if (decodedOptions.suggestedPrice) {
this.setData({
salePrice: decodedOptions.suggestedPrice
});
}
// 建议售价仅用于参考,不自动填充“商品售价”,需用户自行输入
// 显示从AI定价跳转的提示
wx.showToast({
@ -105,10 +130,13 @@ Page({
icon: 'success',
duration: 1500
});
// 交易地点需用户在地图上选点设置
} else {
// 从主界面直接进入,显示空表单
this.setData({
fromAIPricing: false,
showPriceInfo: false,
productName: '',
productCategory: '',
productDescription: '',
@ -122,6 +150,141 @@ Page({
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);
}
},
@ -150,6 +313,50 @@ Page({
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();
},
/**
@ -222,6 +429,101 @@ Page({
});
},
/**
* 更新价格信息显示逻辑
*/
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);
}
},
/**
* 验证表单数据
*/
@ -289,7 +591,21 @@ Page({
});
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;
},
@ -300,6 +616,52 @@ Page({
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;
}
},
/**
* 发布商品
*/
@ -314,20 +676,21 @@ Page({
if (!this.validateForm()) {
return;
}
// 设置发布状态(立即设置,防止重复点击)
this.setData({
isPublishing: true
});
// 显示发布中提示
wx.showLoading({
title: '发布中...',
mask: true
});
// 先上传图片到云存储(如果需要)
this.uploadImageAndPublish();
// 先确保交易地点(面交/自提)
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' });
});
},
/**
@ -385,7 +748,7 @@ Page({
/**
* 保存商品信息到数据库
*/
saveProductToDatabase(imageFileID) {
async saveProductToDatabase(imageFileID) {
// 双重检查:防止并发调用
if (!this.data.isPublishing) {
console.warn('发布状态异常,取消保存操作');
@ -409,6 +772,47 @@ Page({
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 = {
// 基本信息
@ -432,26 +836,39 @@ Page({
contactInfo: contactInfo,
transactionMethod: transactionMethods[tradeMethodIndex] || '面交',
// 状态信息
status: '在售',
viewCount: 0,
likeCount: 0,
// 交易地点:保存为公共地标与坐标(不含房间号)
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 || '',
// 时间信息
createTime: new Date(),
updateTime: new Date(),
// 用户信息(暂时简化)
sellerOpenId: 'test_openid', // 暂时使用测试数据
sellerAppId: 'test_appid'
// 更新时间
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) => {
@ -467,63 +884,141 @@ Page({
}
// 保存到数据库
db.collection('T_product').add({
data: productData,
success: (res) => {
console.log('商品发布成功:', res);
// 先隐藏loading
wx.hideLoading();
if (this.data.isEditMode && this.data.editProductId) {
// 编辑模式:更新商品
db.collection('T_product').doc(this.data.editProductId).update({
data: productData,
success: (res) => {
console.log('商品更新成功:', res);
// 立即重置发布状态,防止重复提交
this.setData({
isPublishing: false
});
// 成功后同步交易地标到 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
});
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 // 返回首页(如果有多个页面)
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
});
}, 2000);
},
fail: (err) => {
console.error('商品保存失败:', err);
// 先隐藏loading
wx.hideLoading();
// 重置发布状态
this.setData({
isPublishing: false
});
// 等待提示显示完整后再跳转到首页
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();
wx.showModal({
title: '发布失败',
content: '商品保存失败,请重试。错误信息:' + (err.errMsg || '未知错误'),
showCancel: false,
confirmText: '知道了'
});
}
});
// 重置发布状态
this.setData({
isPublishing: false
});
wx.showModal({
title: '发布失败',
content: '商品保存失败,请重试。错误信息:' + (err.errMsg || '未知错误'),
showCancel: false,
confirmText: '知道了'
});
}
});
}
},
/**

@ -75,12 +75,28 @@
</view>
</view>
<!-- 发布建议(基于分类供需) -->
<view class="section recommend-section" wx:if="{{recommendedCategories.length > 0 || recommendMessage}}">
<view class="section-header">
<text class="section-title">发布建议</text>
</view>
<view class="recommend-content">
<text class="recommend-message">{{recommendMessage || '暂无建议'}}</text>
<view class="recommend-chips">
<view class="recommend-chip" wx:for="{{recommendedCategories}}" wx:key="cat" bindtap="onSelectRecommendedCategory" data-category="{{item}}">
<text class="chip-text">{{item}}</text>
</view>
</view>
</view>
</view>
<!-- 价格信息 -->
<view class="section">
<view class="section-header">
<text class="section-title">价格信息</text>
</view>
<view class="price-group">
<!-- 当来自AI定价或已有建议数据时显示价格信息 -->
<view class="price-group" wx:if="{{showPriceInfo}}">
<view class="price-item">
<text class="price-label">原价</text>
<text class="price-value original-price">¥{{originalPrice}}</text>
@ -94,6 +110,11 @@
<text class="price-value range-price">{{priceRange}}</text>
</view>
</view>
<!-- 未选择AI定价时显示引导与按钮 -->
<view class="price-empty" wx:else>
<text class="empty-tip">未选择AI定价点击进行智能定价</text>
<button type="primary" size="mini" bindtap="goToAIPricing" class="ai-pricing-btn">去AI定价</button>
</view>
</view>
<!-- 商品状况 -->
@ -171,6 +192,26 @@
</view>
</view>
<!-- 交易地点 -->
<view class="section">
<view class="section-header">
<text class="section-title">交易地点</text>
</view>
<view class="location-settings">
<view class="setting-item">
<text class="setting-label">地图选点</text>
<button size="mini" type="default" bindtap="onChooseLocation">在地图上选点</button>
</view>
<view class="selected-location" wx:if="{{tradeLocation && tradeLocation.latitude}}">
<text class="loc-name">已选:{{tradeLocation.landmarkName || '地图选点'}}</text>
<text class="loc-addr">{{tradeAddress || '正在解析地址中…'}}</text>
</view>
<view class="privacy-note">
<text>为保护隐私,仅显示楼/区域级别,不含房间号</text>
</view>
</view>
</view>
<!-- 发布按钮 -->
<view class="publish-actions">
<button
@ -179,7 +220,7 @@
disabled="{{isPublishing}}"
loading="{{isPublishing}}"
>
{{isPublishing ? '发布中...' : '确认发布'}}
{{isPublishing ? (isEditMode ? '保存中...' : '发布中...') : (isEditMode ? '保存修改' : '确认发布')}}
</button>
<button class="back-btn" bindtap="onBack" disabled="{{isPublishing}}">返回修改</button>
</view>

@ -152,6 +152,41 @@
color: #ccc;
}
/* 发布建议样式 */
.recommend-section {
background: #f8fbff;
}
.recommend-content {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.recommend-message {
font-size: 26rpx;
color: #335;
line-height: 1.6;
}
.recommend-chips {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.recommend-chip {
padding: 12rpx 22rpx;
background: #eef4ff;
border: 1rpx solid #dbe7ff;
border-radius: 999rpx;
}
.chip-text {
font-size: 24rpx;
color: #3b6efb;
}
/* 价格信息 */
.price-group {
display: flex;
@ -159,6 +194,18 @@
gap: 15rpx;
}
/* 未选择AI定价时的引导样式 */
.price-empty {
display: flex;
flex-direction: column;
gap: 16rpx;
color: #999;
}
.ai-pricing-btn {
align-self: flex-start;
}
.price-item {
display: flex;
justify-content: space-between;
@ -312,6 +359,45 @@
color: #ccc;
}
/* 交易地点 */
.location-settings {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.selected-location {
background: #f8f9fa;
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 16rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.loc-name { font-size: 26rpx; color: #333; }
.loc-addr { font-size: 24rpx; color: #666; }
.location-auto {
display: flex;
justify-content: space-between;
align-items: center;
background: #f8f9fa;
border: 1rpx dashed #e0e0e0;
border-radius: 8rpx;
padding: 12rpx 16rpx;
}
.location-tip {
font-size: 26rpx;
color: #333;
}
.privacy-note {
font-size: 22rpx;
color: #999;
}
/* 发布按钮 */
.publish-actions {
margin-top: 40rpx;

@ -30,7 +30,13 @@ Page({
tradeMethods: ['面交', '快递', '均可'],
// 发布状态
isPublishing: false
isPublishing: false,
// 发布建议与推荐类别(买家视角)
recommendLoading: false,
recommendMessage: '',
recommendedCategories: [],
categoryGuides: {}
},
/**
@ -62,6 +68,44 @@ Page({
} catch (err) {
console.error('云开发初始化失败:', err);
}
// 加载基于分类供需的发布建议(买家发布求购)
this.loadPublishRecommendations('wanted');
},
/**
* 加载发布建议基于分类供需对比
*/
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.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 });
}
},
/**
@ -80,6 +124,39 @@ Page({
this.setData({
categoryIndex: parseInt(e.detail.value)
});
const cat = this.data.categories[parseInt(e.detail.value)];
this.applyGuideForCategory(cat);
},
/**
* 点击推荐类别快速选择
*/
onSelectRecommendedCategory(e) {
const category = e.currentTarget.dataset.category;
const idx = this.data.categories.indexOf(category);
if (idx >= 0) {
this.setData({ categoryIndex: idx });
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 commonTrade = guide.commonTrade || '面交';
const idx = this.data.tradeMethods.indexOf(commonTrade);
const next = {};
if (idx >= 0) next.tradeMethodIndex = idx;
if ((!this.data.expectedPrice || this.data.expectedPrice === '') && guide.avgPrice > 0) {
next.expectedPrice = guide.avgPrice.toString();
}
this.setData(next);
},
/**
@ -295,16 +372,30 @@ Page({
try {
const db = wx.cloud.database();
db.collection('T_want').add({
data: purchaseData,
success: (res) => {
console.log('求购发布成功数据库ID:', res._id);
wx.hideLoading();
// 发布成功提示
wx.showToast({
title: '发布成功!',
icon: 'success',
// 获取登录用户ID
const userInfo = wx.getStorageSync('userInfo') || {};
const loggedInUserId = userInfo._id;
// 添加userId字段用于精确区分发布者
if (loggedInUserId) {
purchaseData.userId = loggedInUserId;
}
db.collection('T_want').add({
data: purchaseData,
success: (res) => {
console.log('求购发布成功数据库ID:', res._id);
wx.hideLoading();
try {
const reco = require('../../utils/recommendation.js');
const cat = this.data.categories[this.data.categoryIndex];
reco.recordPublishWanted(cat, this.data.productName, this.data.purchaseDescription);
} catch (e) {}
// 发布成功提示
wx.showToast({
title: '发布成功!',
icon: 'success',
duration: 2000,
mask: true
});

@ -33,6 +33,15 @@
</view>
</picker>
</view>
<!-- 发布建议(基于分类供需) -->
<view class="recommend-section" wx:if="{{recommendedCategories.length > 0 || recommendMessage}}">
<text class="recommend-message">{{recommendMessage || '暂无建议'}}</text>
<view class="recommend-chips">
<view class="recommend-chip" wx:for="{{recommendedCategories}}" wx:key="cat" bindtap="onSelectRecommendedCategory" data-category="{{item}}">
<text class="chip-text">{{item}}</text>
</view>
</view>
</view>
<view class="info-item">
<text class="info-label">求购描述</text>
<textarea

@ -287,6 +287,40 @@
color: #667eea;
}
/* 发布建议样式 */
.recommend-section {
margin-top: 10rpx;
background: #f8fbff;
border-radius: 12rpx;
padding: 20rpx;
border: 1rpx solid #e6efff;
}
.recommend-message {
font-size: 26rpx;
color: #335;
line-height: 1.6;
margin-bottom: 16rpx;
}
.recommend-chips {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.recommend-chip {
padding: 12rpx 22rpx;
background: #eef4ff;
border: 1rpx solid #dbe7ff;
border-radius: 999rpx;
}
.chip-text {
font-size: 24rpx;
color: #3b6efb;
}
/* 响应式调整 */
@media (max-width: 375px) {
.purchase-container {

@ -14,6 +14,25 @@ Page({
major: '',
grades: ['大一', '大二', '大三', '大四', '研究生', '博士生'],
gradeIndex: 0,
// 中国民航大学的学院列表
departments: ['计算机科学与技术学院', '电子信息与自动化学院', '空中交通管理学院', '飞行技术学院', '航空工程学院', '安全科学与工程学院', '经济与管理学院', '理学院', '人文社会科学学院', '外国语学院', '法学院', '马克思主义学院', '机场学院', '交通科学与工程学院', '其他'],
departmentIndex: 0,
// 各学院的专业列表(简化版)
majors: [
// 计算机科学与技术学院
['计算机科学与技术', '信息安全', '物联网工程', '人工智能'],
// 电子信息与自动化学院
['电子信息工程', '通信工程', '自动化', '电气工程及其自动化'],
// 空中交通管理学院
['交通运输', '交通工程'],
// 飞行技术学院
['飞行技术'],
// 航空工程学院
['飞行器动力工程', '飞行器制造工程', '机械电子工程'],
// 其他学院的专业(简化)
['安全工程', '信息管理与信息系统', '工商管理', '应用数学', '英语', '法学', '公共事业管理', '物流管理', '土木工程']
],
majorIndex: 0,
dormBuilding: '',
dormRoom: '',
@ -26,6 +45,10 @@ Page({
onLoad() {
// 页面加载时初始化
this.setData({
department: this.data.departments[0],
major: this.data.majors[0][0]
});
this.checkRegisterButton();
},
@ -74,20 +97,32 @@ Page({
this.checkRegisterButton();
},
// 学院输入处理
onInputDepartment(e) {
const department = e.detail.value.trim();
// 学院选择处理
onDepartmentChange(e) {
const index = parseInt(e.detail.value);
console.log('选择的学院索引:', index);
console.log('选择的学院:', this.data.departments[index]);
console.log('对应的专业列表:', this.data.majors[index]);
this.setData({
department: department
departmentIndex: index,
majorIndex: 0,
department: this.data.departments[index],
major: this.data.majors[index] ? this.data.majors[index][0] : this.data.majors[0][0]
});
this.checkRegisterButton();
},
// 专业输入处理
onInputMajor(e) {
const major = e.detail.value.trim();
// 专业选择处理
onMajorChange(e) {
const index = parseInt(e.detail.value);
// 根据当前选择的学院获取对应的专业列表
const departmentMajors = this.data.majors[this.data.departmentIndex] || this.data.majors[0];
this.setData({
major: major
majorIndex: index,
// 设置专业名称
major: departmentMajors[index]
});
this.checkRegisterButton();
},
@ -138,8 +173,6 @@ Page({
phone,
password,
confirmPassword,
department,
major,
gradeIndex,
agreed
} = this.data;
@ -147,8 +180,8 @@ Page({
// 基本验证(只检查必填字段)
const isBasicValid = studentId && name && phone && password && confirmPassword;
// 学校信息验证(只检查必填字段
const isSchoolValid = department && major && gradeIndex >= 0;
// 学校信息验证(年级必填,学院和专业可选
const isSchoolValid = gradeIndex >= 0;
// 协议同意
const isAgreed = agreed;

@ -83,25 +83,33 @@
</view>
<view class="form-group">
<text class="label">学院</text>
<input
class="input"
type="text"
placeholder="请输入学院名称"
bindinput="onInputDepartment"
value="{{department}}"
/>
<text class="label">学院(可选)</text>
<!-- 学院选择器 -->
<picker
class="picker"
range="{{departments}}"
bindchange="onDepartmentChange"
value="{{departmentIndex}}"
>
<view class="picker-value">
{{departments[departmentIndex] || '请选择学院'}}
</view>
</picker>
</view>
<view class="form-group">
<text class="label">专业</text>
<input
class="input"
type="text"
placeholder="请输入专业名称"
bindinput="onInputMajor"
value="{{major}}"
/>
<text class="label">专业(可选)</text>
<!-- 专业选择器 -->
<picker
class="picker"
range="{{majors[departmentIndex]}}"
bindchange="onMajorChange"
value="{{majorIndex}}"
>
<view class="picker-value">
{{majors[departmentIndex] && majors[departmentIndex][majorIndex] ? majors[departmentIndex][majorIndex] : '请选择专业'}}
</view>
</picker>
</view>
<view class="form-group">

@ -108,8 +108,10 @@ page {
}
/* 选择器样式 */
/* 确保选择器样式正确 */
.picker {
width: 100%;
height: 88rpx;
}
.picker-value {

@ -11,15 +11,22 @@ Page({
hasMore: true,
pageSize: 20,
currentPage: 0,
selectedWantedId: null // 用于详情页
selectedWantedId: null, // 用于详情页
filterMy: false // 是否只显示我的求购
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
async onLoad(options) {
console.log('求购列表页面加载', options);
// 判断是否只显示我的求购
const filterMy = options.filter === 'my';
this.setData({
filterMy: filterMy
});
// 如果传入了id说明是从热门求购点击进入的先加载该条目的详情
if (options.id) {
this.setData({
@ -27,7 +34,47 @@ Page({
});
}
this.loadWantedList();
// 清除可能缓存的旧openid确保使用登录的用户ID获取正确的openid
const userInfo = wx.getStorageSync('userInfo') || {};
if (userInfo._id) {
// 如果有登录的用户ID清除旧的openid缓存强制重新获取
wx.removeStorageSync('openid');
}
// 如果带有搜索关键词,预填并优先按关键词加载
if (options && options.q) {
try {
const keyword = decodeURIComponent(options.q);
this.setData({ searchText: keyword });
} catch (e) {
this.setData({ searchText: options.q });
}
}
await this.loadWantedList();
},
/**
* 确保有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;
},
/**
@ -66,9 +113,61 @@ Page({
const _ = db.command;
// 构建查询条件
let query = db.collection('T_want').where({
let queryCondition = {
status: 'active'
});
};
// 如果只显示我的求购优先使用userId字段筛选最准确
if (this.data.filterMy) {
const userInfo = wx.getStorageSync('userInfo') || {};
const loggedInUserId = userInfo._id;
if (loggedInUserId) {
// 优先使用userId字段查询最准确
queryCondition.userId = loggedInUserId;
console.log('使用userId查询求购:', loggedInUserId);
} else {
// 如果没有登录的用户ID使用_openid向后兼容
let openid = null;
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) {
queryCondition._openid = openid;
} else {
// 如果没有openid清空列表
this.setData({
wantedList: [],
loading: false,
refreshing: false,
hasMore: false
});
wx.showToast({
title: '请先登录',
icon: 'none'
});
return;
}
}
}
let query = db.collection('T_want').where(queryCondition);
// 如果有搜索关键词,获取更多数据以便筛选;否则正常分页
const limit = this.data.searchText.trim() ? 100 : (this.data.pageSize * 2);
@ -278,17 +377,17 @@ Page({
cancelText: '关闭',
success: (res) => {
if (res.confirm) {
// 复制联系方式
const contactInfo = wanted.contactPhone || wanted.contactWechat || '';
if (contactInfo) {
wx.setClipboardData({
data: contactInfo,
success: () => {
wx.showToast({
title: '已复制联系方式',
icon: 'success'
});
}
const toUserId = wanted.userId || '';
const toOpenId = wanted._openid || '';
const toName = wanted.contactName || '求购用户';
if (toUserId || toOpenId) {
wx.navigateTo({
url: `/pages/chat/chat?toUserId=${toUserId}&toOpenId=${toOpenId}&toName=${encodeURIComponent(toName)}`
});
} else {
wx.showToast({
title: '无法获取联系人信息',
icon: 'none'
});
}
}
@ -326,7 +425,14 @@ Page({
*/
onShow() {
// 如果从其他页面返回,刷新列表
if (this.data.wantedList.length > 0) {
// 清除可能缓存的旧openid确保使用登录的用户ID获取正确的openid
const userInfo = wx.getStorageSync('userInfo') || {};
if (userInfo._id) {
// 如果有登录的用户ID清除旧的openid缓存强制重新获取
wx.removeStorageSync('openid');
}
if (this.data.wantedList.length > 0 || this.data.filterMy) {
this.setData({
currentPage: 0,
wantedList: [],

@ -3,7 +3,7 @@
<!-- 搜索栏 -->
<view class="search-section">
<view class="search-bar">
<image class="search-icon" src="/images/search.png" mode="aspectFit"></image>
<image class="search-icon" src="/images/边牧.png" mode="aspectFit"></image>
<input
class="search-input"
placeholder="搜索求购信息"

@ -17,7 +17,7 @@
"uglifyFileName": false,
"checkInvalidKey": true,
"checkSiteMap": true,
"uploadWithSourceMap": true,
"uploadWithSourceMap": false,
"compileHotReLoad": false,
"useMultiFrameRuntime": true,
"useApiHook": true,
@ -46,7 +46,7 @@
},
"appid": "wx0f9e605dfd3db597",
"projectname": "quickstart-wx-cloud",
"libVersion": "2.20.1",
"libVersion": "3.10.3",
"cloudfunctionTemplateRoot": "cloudfunctionTemplate/",
"condition": {
"search": {
@ -74,7 +74,16 @@
"compileType": "miniprogram",
"srcMiniprogramRoot": "miniprogram/",
"packOptions": {
"ignore": [],
"ignore": [
{
"value": "image",
"type": "folder"
},
{
"value": "image/**",
"type": "glob"
}
],
"include": []
},
"editorSetting": {

@ -7,15 +7,15 @@
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,
"useApiHostProcess": true,
"useApiHook": false,
"useApiHostProcess": false,
"showShadowRootInWxmlPanel": true,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false,
"bigPackageSizeSupport": true,
"useIsolateContext": true
},
"condition": {},

@ -1,134 +0,0 @@
# AI智能定价功能排查指南
## 已修复的问题
### 1. ✅ 云函数超时配置
- **问题**: 云函数默认超时时间只有3秒AI分析需要更长时间
- **修复**: 在 `config.json` 中添加了 `"timeout": 300`5分钟
### 2. ✅ 错误处理改进
- 添加了更详细的错误日志
- 改进了小程序端的错误提示
## 排查步骤
### 步骤1: 检查云函数部署
1. 打开微信开发者工具
2. 右键点击 `cloudfunctions/quickstartFunctions` 文件夹
3. 选择"上传并部署:云端安装依赖"
4. 等待部署完成
### 步骤2: 检查依赖安装
`cloudfunctions/quickstartFunctions` 目录下执行:
```bash
npm install
```
确保安装了以下依赖:
- axios
- form-data
- wx-server-sdk
### 步骤3: 查看云函数日志
1. 打开微信开发者工具
2. 点击"云开发"按钮
3. 进入"云函数" -> "日志"
4. 查看最新的调用日志,查找错误信息
### 步骤4: 测试云函数
在小程序中测试时,查看控制台输出:
- 图片上传是否成功
- 云函数调用参数是否正确
- 返回的错误信息
## 常见错误及解决方案
### 错误1: "云函数调用失败"
**可能原因**:
1. 云函数未部署
2. 云函数名称不匹配
3. 网络问题
**解决方案**:
1. 重新部署云函数
2. 检查 `wx.cloud.callFunction` 中的 `name` 是否为 `'quickstartFunctions'`
3. 检查网络连接
### 错误2: "下载图片文件失败"
**可能原因**:
1. fileID无效
2. 云存储权限问题
3. 文件不存在
**解决方案**:
1. 检查图片是否成功上传到云存储
2. 检查云存储权限配置
3. 查看云函数日志中的详细错误信息
### 错误3: "上传图片到Coze失败"
**可能原因**:
1. Coze API Token无效
2. 网络问题
3. 图片格式不支持
**解决方案**:
1. 检查API Token是否正确
2. 检查网络连接
3. 确认图片格式为JPG/PNG
### 错误4: "调用Coze工作流失败"
**可能原因**:
1. workflow_id错误
2. API Token无效
3. 超时虽然已设置为5分钟
**解决方案**:
1. 检查workflow_id是否正确
2. 检查API Token
3. 查看云函数日志中的详细错误
### 错误5: 超时错误
**可能原因**:
1. AI分析时间过长
2. 网络延迟
**解决方案**:
1. 已设置超时为5分钟
2. 如果仍超时,可能需要优化图片大小或使用更小的图片
## 调试技巧
### 1. 查看小程序控制台日志
在小程序开发者工具中:
- 打开"调试器" -> "Console"
- 查看所有console.log输出
### 2. 查看云函数日志
在云开发控制台:
- 进入"云函数" -> "日志"
- 查看详细的执行日志和错误堆栈
### 3. 测试用例
可以使用 `test-analyze.js` 文件进行本地测试:
```bash
cd cloudfunctions/quickstartFunctions
node test-analyze.js
```
## 检查清单
- [ ] 云函数已部署
- [ ] 依赖已安装npm install
- [ ] config.json 中配置了超时时间
- [ ] API Token正确
- [ ] workflow_id正确
- [ ] 图片上传成功
- [ ] 网络连接正常
## 需要帮助?
如果仍然无法解决问题,请提供:
1. 云函数日志中的错误信息
2. 小程序控制台的错误信息
3. 具体的错误堆栈信息

@ -1,62 +0,0 @@
# 云函数超时问题解决方案
## 问题描述
云函数在3秒后超时虽然已经在 `config.json` 中设置了 `"timeout": 300`,但配置可能没有生效。
## 解决方案
### 方法1: 在云开发控制台设置超时时间(推荐)
1. 打开微信开发者工具
2. 点击"云开发"按钮
3. 进入"云函数"页面
4. 找到 `quickstartFunctions` 云函数
5. 点击"配置"或"设置"
6. 找到"超时时间"设置
7. 设置为 **300秒5分钟** 或更长时间
8. 保存配置
### 方法2: 确保config.json配置正确
检查 `cloudfunctions/quickstartFunctions/config.json` 文件:
```json
{
"permissions": {
"openapi": [
"wxacode.get"
]
},
"timeout": 300,
"envVariables": {}
}
```
### 方法3: 重新部署云函数
1. 右键点击 `cloudfunctions/quickstartFunctions` 文件夹
2. 选择"上传并部署:云端安装依赖"
3. 等待部署完成
### 方法4: 使用异步调用(如果超时时间无法修改)
如果无法修改超时时间,可以考虑:
1. 将AI分析改为异步任务
2. 先返回任务ID给小程序
3. 小程序轮询查询结果
## 当前状态
根据日志,参数格式已经正确:
- ✅ image参数格式正确
- ✅ input参数格式正确数字类型
- ✅ 请求数据格式符合curl示例
问题只是超时时间配置没有生效。
## 下一步
1. **优先使用方法1**:在云开发控制台手动设置超时时间
2. 设置完成后,重新测试
3. 如果仍然超时,可能需要联系微信云开发技术支持
Loading…
Cancel
Save