yzb 2 months ago
parent 55fa75c6bf
commit cfaeb6d9c5

1
src

@ -1 +0,0 @@
Subproject commit 344d90be6efcf811527ce49769debd97cf626e4d

@ -0,0 +1,14 @@
# Windows
[Dd]esktop.ini
Thumbs.db
$RECYCLE.BIN/
# macOS
.DS_Store
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
# Node.js
node_modules/

@ -0,0 +1,87 @@
# 校园二手交易小程序
## 1. 系统简介
本项目是一个功能完善的校园二手交易微信小程序,旨在为大学校园内的学生和教职工提供一个安全、便捷、高效的二手物品交易平台。系统不仅包含了完整的商品交易流程,还融入了丰富的社交和实用功能,构建了一个活跃的校园社区生态。
### 主要功能模块
- **商品模块**:用户可以轻松发布、编辑、下架自己的二手商品,支持图文描述、价格、分类、交易地点等信息。
- **交易流程**:实现了从创建订单、买家支付(模拟)、卖家确认、买家收货到交易完成的全闭环流程。
- **求购广场**:用户可以发布求购信息,寻找自己需要的物品,其他用户可以响应求购。
- **智能定价**:支持用户上传二手商品,生成闲置商品破损程度分析和定价建议
- **实时聊天**:买卖双方可以进行一对一的实时沟通,支持文字和图片消息。
- **校园地图模式**:以地图为载体,直观展示校园内各个交易地点的商品信息,方便用户发现附近的宝贝。
- **智能推荐**:根据用户的浏览和收藏行为,个性化推荐可能感兴趣的商品。
### 技术栈
- **前端**:微信小程序原生开发 (WXML, WXSS, JavaScript)
- **后端**:微信小程序云开发
- **数据库**:微信云开发数据库
- **核心服务**:微信云函数、云存储
---
## 2. 环境配置与部署指南
请严格按照以下步骤进行配置,以确保项目能成功运行。
### 步骤 1准备工作
1. **安装微信开发者工具**:前往 [微信开放平台](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html) 下载并安装最新版的微信开发者工具。
2. **注册小程序账号**:拥有一个自己的小程序 AppID。个人或企业主体均可。
### 步骤 2导入项目
1. 打开微信开发者工具,点击“导入项目”。
2. **项目目录**:选择本项目的根目录 `ruangong1`
3. **AppID**:填写您自己的小程序 AppID。
4. **项目名称**:自定义即可。
5. 点击“导入”。
### 步骤 3开通并配置云开发环境
1. 在开发者工具的顶部工具栏中,点击“云开发”按钮,打开云开发控制台。
2. 按照提示**开通云开发**,系统会自动为您创建一个云开发环境。
3. **记住您的环境 ID**,它通常是一串类似 `your-env-id-xxxxxxxx` 的字符串。
4. 回到开发者工具的编辑器界面,打开文件 `miniprogram/app.js`
5. 找到以下代码块(大约在第 6 行):
```javascript
this.globalData = {
env: '' // <--- ID
};
```
6. 将您刚刚获取的**环境 ID** 填入 `env` 字段的引号中。
### 步骤 4部署云函数
1. 在开发者工具的左侧文件树中,找到 `cloudfunctions/quickstartFunctions` 目录。
2. 右键点击该目录,选择“**上传并部署:云端安装依赖**”。
3. 等待几分钟,直到开发者工具的控制台提示部署成功。
### 步骤 5创建数据库集合
这是**非常关键**的一步。项目需要以下数据库集合来存储数据,您必须手动创建它们。
1. 打开云开发控制台,切换到“数据库”标签页。
2. 点击“**+**”号按钮,选择“创建集合”。
3. 依次创建以下所有集合(**集合名称必须完全一致**
- `T_user`
- `T_product`
- `T_want`
- `T_order`
- `T_favorites`
- `T_campus_landmarks`
- `T_chat`
- `T_message`
- `T_notify`
- `T_user_behavior`
4. **设置权限**:为了方便开发和测试,您可以暂时将所有集合的权限设置为“**所有用户可读,仅创建者可读写**”。上线前请根据实际业务需求调整为更严格的权限规则。
### 步骤 6运行项目
完成以上所有步骤后,点击开发者工具顶部的“编译”按钮。如果一切顺利,您应该可以在模拟器中看到小程序的启动界面。
至此,项目已成功在您的开发环境中运行起来。祝您使用愉快!

@ -0,0 +1,14 @@
{
"permissions": {
"openapi": [
"wxacode.get"
]
},
"timeout": 300,
"envVariables": {
"QQMAP_KEY": "QBJBZ-E433N-DYYFX-SDC5E-EUB3V-MJBOE",
"QQMAP_KEYS": "7YTBZ-DTQHW-RPURE-YWKGQ-UTODK-RZFVP",
"QQMAP_REFERER": "https://servicewechat.com",
"QQMAP_SK": ""
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,17 @@
{
"name": "quickstartFunctions",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@coze/api": "^1.3.7",
"axios": "^1.13.1",
"form-data": "^4.0.4",
"wx-server-sdk": "~2.4.0"
}
}

@ -0,0 +1,58 @@
// 测试AI定价功能的脚本
// 用于本地测试云函数逻辑
const cloud = require("wx-server-sdk");
cloud.init({
env: "cloud1-7gmt1869f0703287" // 使用你的环境ID
});
// 导入云函数
const analyzeProductPrice = require('./index.js').analyzeProductPrice;
async function testAnalyzeProductPrice() {
console.log('========== 开始测试AI定价功能 ==========');
// 测试用例1: 使用fileID需要先上传一个图片到云存储
const testEvent1 = {
type: 'analyzeProductPrice',
fileID: 'cloud://cloud1-7gmt1869f0703287.636c-cloud1-7gmt1869f0703287-1316160869/pricing/test.jpg',
originalPrice: 100
};
// 测试用例2: 使用imageUrl使用一个公开的图片URL
const testEvent2 = {
type: 'analyzeProductPrice',
imageUrl: 'https://pic1.zhimg.com/v2-24d60d3e9a24ce8d1f3ee0d9f05f929c_b.jpg',
originalPrice: 100
};
try {
console.log('\n测试用例2: 使用imageUrl测试');
console.log('测试参数:', JSON.stringify(testEvent2, null, 2));
const result = await analyzeProductPrice(testEvent2);
console.log('\n========== 测试结果 ==========');
console.log(JSON.stringify(result, null, 2));
if (result.success) {
console.log('\n✅ 测试成功!');
console.log('商品名称:', result.data.productName);
console.log('建议价格:', result.data.suggestedPrice);
console.log('商品成色:', result.data.conditionLevel);
console.log('AI评分:', result.data.aiScore);
} else {
console.log('\n❌ 测试失败!');
console.log('错误信息:', result.error);
console.log('错误详情:', result.details);
}
} catch (error) {
console.error('\n========== 测试异常 ==========');
console.error('错误信息:', error.message);
console.error('错误堆栈:', error.stack);
}
}
// 运行测试
testAnalyzeProductPrice();

@ -0,0 +1,28 @@
// 测试云函数AI定价功能
async function testCloudFunction() {
try {
console.log('开始测试云函数AI定价功能...');
// 模拟云函数调用 - 使用用户提供的具体图片URL
const event = {
type: 'analyzeProductPrice',
imageUrl: 'https://pic1.zhimg.com/v2-24d60d3e9a24ce8d1f3ee0d9f05f929c_b.jpg',
originalPrice: 100
};
// 导入云函数
const cloudFunction = require('./index.js');
console.log('调用云函数analyzeProductPrice...');
const result = await cloudFunction.main(event, {});
console.log('云函数调用结果:', JSON.stringify(result, null, 2));
} catch (error) {
console.error('❌ 测试过程中发生错误:', error.message);
console.error('错误堆栈:', error.stack);
}
}
// 运行测试
testCloudFunction();

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

@ -0,0 +1,21 @@
// app.js
App({
onLaunch: function () {
this.globalData = {
// env 参数说明:
// env 参数决定接下来小程序发起的云开发调用wx.cloud.xxx会默认请求到哪个云环境的资源
// 此处请填入环境 ID, 环境 ID 可打开云控制台查看
// 如不填则使用默认环境(第一个创建的环境)
env: "cloud1-7gmt1869f0703287"
};
if (!wx.cloud) {
console.error("请使用 2.2.3 或以上的基础库以使用云能力");
} else {
wx.cloud.init({
env: this.globalData.env,
traceUser: true,
});
}
},
});

@ -0,0 +1,77 @@
{
"pages": [
"pages/index/index",
"pages/main/main",
"pages/market/market",
"pages/cart/cart",
"pages/messages/messages",
"pages/profile/profile",
"pages/recommend-list/recommend-list",
"pages/chat/chat"
],
"subpackages": [
{ "root": "pages/register", "pages": ["register"] },
{ "root": "pages/interests", "pages": ["interests"] },
{ "root": "pages/myProducts", "pages": ["myProducts"] },
{ "root": "pages/orders", "pages": ["orders"] },
{ "root": "pages/wanted-list", "pages": ["wanted-list"] },
{ "root": "pages/favorites", "pages": ["favorites"] },
{ "root": "pages/profile-edit", "pages": ["profile-edit"] },
{ "root": "pages/security", "pages": ["security"] },
{ "root": "pages/address", "pages": ["address"] },
{ "root": "pages/feedback", "pages": ["feedback"] },
{ "root": "pages/privacy", "pages": ["privacy"] },
{ "root": "pages/buy", "pages": ["buy"] },
{ "root": "pages/purchase", "pages": ["purchase"] },
{ "root": "pages/admin-login", "pages": ["admin-login"], "independent": true },
{ "root": "pages/admin-dashboard", "pages": ["admin-dashboard"], "independent": true },
{ "root": "pages/admin-products", "pages": ["admin-products"], "independent": true },
{ "root": "pages/admin-users", "pages": ["admin-users"], "independent": true },
{ "root": "pages/admin-user-edit", "pages": ["admin-user-edit"], "independent": true },
{ "root": "pages/product-detail", "pages": ["product-detail"] },
{ "root": "pages/publish", "pages": ["publish"] },
{ "root": "pages/pricing", "pages": ["pricing"] }
],
"window": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "校园二手交易",
"navigationBarBackgroundColor": "#4f8bff",
"backgroundColor": "#f0f7ff"
},
"tabBar": {
"custom": true,
"color": "#999999",
"selectedColor": "#4f8bff",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{ "pagePath": "pages/main/main", "text": "首页" },
{ "pagePath": "pages/market/market", "text": "藏宝图" },
{ "pagePath": "pages/cart/cart", "text": "购物车" },
{ "pagePath": "pages/messages/messages", "text": "消息" },
{ "pagePath": "pages/profile/profile", "text": "我的" }
]
},
"requiredPrivateInfos": [
"chooseAddress",
"chooseLocation",
"choosePoi",
"getLocation",
"onLocationChange",
"startLocationUpdate",
"startLocationUpdateBackground"
],
"requiredBackgroundModes": [
"location"
],
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
},
"style": "v2",
"componentFramework": "glass-easel",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}

@ -0,0 +1,52 @@
/**app.wxss**/
/* 全局样式重置 */
page {
height: 100%;
background-color: #f5f5f5;
}
/* 容器样式 */
/**app.wxss**/
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 200rpx 0;
box-sizing: border-box;
}
/* 通用文本样式 */
text {
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun, sans-serif;
}
/* 通用按钮样式 */
button {
border-radius: 8rpx;
font-size: 32rpx;
}
button::after {
border: none;
}
/* 通用输入框样式 */
input {
font-size: 32rpx;
color: #333;
}
/* 通用图片样式 */
image {
display: block;
}
/* 通用滚动条样式 */
::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}

@ -0,0 +1,28 @@
Component({
/**
* 页面的初始数据
*/
data: {
showTip: false,
},
properties: {
showTipProps: Boolean,
title:String,
content:String
},
observers: {
showTipProps: function(showTipProps) {
this.setData({
showTip: showTipProps
});
}
},
methods: {
onClose(){
this.setData({
showTip: !this.data.showTip
});
},
}
});

@ -0,0 +1,4 @@
{
"usingComponents": {},
"component": true
}

@ -0,0 +1,10 @@
<!--miniprogram/components/cloudTipModal/index.wxml-->
<!-- wx:if="{{showUploadTip}}" -->
<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="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>
</view>

@ -0,0 +1,60 @@
.install_tip_back {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0,0,0,0.4);
z-index: 1;
}
.install_tip_close{
position:absolute;
right: 10rpx;
top: 10rpx;
width: 32px;
height: 32px;
/* background-color: red; */
}
.install_tip_detail {
position: fixed;
background-color: white;
right: 0;
bottom: 0;
left: 0;
border-radius: 40rpx 40rpx 0 0;
padding: 50rpx 50rpx 100rpx 50rpx;
z-index: 9;
}
.install_tip_detail_title {
font-weight: 400;
font-size: 40rpx;
text-align: center;
}
.install_tip_detail_tip {
font-size: 25rpx;
color: rgba(0,0,0,0.4);
margin-top: 20rpx;
text-align: left;
}
.install_tip_detail_buttons {
padding-top: 50rpx;
display: flex;
}
.install_tip_detail_button {
color: #07C160;
font-weight: 500;
background-color: rgba(0,0,0,0.1);
width: 40%;
text-align: center;
/* height: 90rpx; */
/* line-height: 90rpx; */
border-radius: 10rpx;
margin: 0 auto;
}
.install_tip_detail_button_primary {
background-color: #07C160;
color: #fff;
}

@ -0,0 +1,23 @@
Component({
data: {
selected: 0,
tabs: [
{ pagePath: '/pages/main/main' },
{ pagePath: '/pages/market/market' },
{ pagePath: '/pages/cart/cart' },
{ pagePath: '/pages/messages/messages' },
{ pagePath: '/pages/profile/profile' }
]
},
methods: {
setSelected(index) {
this.setData({ selected: index });
},
onTabTap(e) {
const idx = Number(e.currentTarget.dataset.index);
const tab = this.data.tabs[idx];
if (!tab) return;
wx.switchTab({ url: tab.pagePath });
}
}
});

@ -0,0 +1,22 @@
<view class="tab-bar">
<view class="tab-item {{selected === 0 ? 'active' : ''}}" data-index="0" bindtap="onTabTap">
<text class="tab-icon-emoji">🏠</text>
<text class="tab-text">首页</text>
</view>
<view class="tab-item {{selected === 1 ? 'active' : ''}}" data-index="1" bindtap="onTabTap">
<text class="tab-icon-emoji">🗺️</text>
<text class="tab-text">藏宝图</text>
</view>
<view class="tab-item {{selected === 2 ? 'active' : ''}}" data-index="2" bindtap="onTabTap">
<text class="tab-icon-emoji">🛒</text>
<text class="tab-text">购物车</text>
</view>
<view class="tab-item {{selected === 3 ? 'active' : ''}}" data-index="3" bindtap="onTabTap">
<text class="tab-icon-emoji">📨</text>
<text class="tab-text">消息</text>
</view>
<view class="tab-item {{selected === 4 ? 'active' : ''}}" data-index="4" bindtap="onTabTap">
<text class="tab-icon-emoji">👤</text>
<text class="tab-text">我的</text>
</view>
</view>

@ -0,0 +1,22 @@
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
background: #ffffff;
border-top: 1rpx solid #e0e0e0;
padding: 15rpx 0;
z-index: 1000;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
}
.tab-item.active .tab-text { color: #4f8bff; }
.tab-icon-emoji { font-size: 44rpx; margin-bottom: 8rpx; line-height: 1; }
.tab-text { font-size: 20rpx; }

@ -0,0 +1,6 @@
const envList = [];
const isMac = false;
module.exports = {
envList,
isMac
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@ -0,0 +1,200 @@
// pages/address/address.js
Page({
/**
* 页面的初始数据
*/
data: {
addressList: [],
defaultAddressId: null
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadAddressList();
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
this.loadAddressList();
},
/**
* 确保有openid
*/
async ensureOpenId() {
let openid = wx.getStorageSync('openid');
if (!openid) {
try {
const result = await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'getOpenId'
}
});
if (result.result && result.result.openid) {
openid = result.result.openid;
wx.setStorageSync('openid', openid);
}
} catch (err) {
console.error('获取openid失败:', err);
}
}
return openid;
},
/**
* 加载地址列表
*/
async loadAddressList() {
try {
const db = wx.cloud.database();
const openid = await this.ensureOpenId();
if (!openid) {
wx.showToast({
title: '请先登录',
icon: 'none'
});
this.setData({
addressList: []
});
return;
}
const result = await db.collection('T_address')
.where({
_openid: openid
})
.orderBy('isDefault', 'desc')
.orderBy('createTime', 'desc')
.get();
let defaultAddressId = null;
if (result.data && result.data.length > 0) {
const defaultAddr = result.data.find(addr => addr.isDefault);
if (defaultAddr) {
defaultAddressId = defaultAddr._id;
}
}
this.setData({
addressList: result.data || [],
defaultAddressId: defaultAddressId
});
} catch (err) {
console.error('加载地址列表失败:', err);
wx.showToast({
title: '加载失败',
icon: 'none'
});
}
},
/**
* 添加地址
*/
onAddAddress() {
wx.navigateTo({
url: '/pages/address-edit/address-edit'
});
},
/**
* 编辑地址
*/
onEditAddress(e) {
const addressId = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/address-edit/address-edit?id=${addressId}`
});
},
/**
* 删除地址
*/
async onDeleteAddress(e) {
const addressId = e.currentTarget.dataset.id;
wx.showModal({
title: '确认删除',
content: '确定要删除该地址吗?',
success: async (res) => {
if (res.confirm) {
try {
const db = wx.cloud.database();
await db.collection('T_address').doc(addressId).remove();
wx.showToast({
title: '删除成功',
icon: 'success'
});
this.loadAddressList();
} catch (err) {
console.error('删除地址失败:', err);
wx.showToast({
title: '删除失败',
icon: 'none'
});
}
}
}
});
},
/**
* 设置默认地址
*/
async onSetDefault(e) {
const addressId = e.currentTarget.dataset.id;
try {
const db = wx.cloud.database();
const openid = await this.ensureOpenId();
if (!openid) {
wx.showToast({
title: '请先登录',
icon: 'none'
});
return;
}
// 先将所有地址设为非默认
await db.collection('T_address')
.where({
_openid: openid
})
.update({
data: {
isDefault: false
}
});
// 设置当前地址为默认
await db.collection('T_address').doc(addressId).update({
data: {
isDefault: true
}
});
wx.showToast({
title: '设置成功',
icon: 'success'
});
this.loadAddressList();
} catch (err) {
console.error('设置默认地址失败:', err);
wx.showToast({
title: '设置失败',
icon: 'none'
});
}
}
});

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "收货地址",
"enablePullDownRefresh": true
}

@ -0,0 +1,37 @@
<!--pages/address/address.wxml-->
<view class="page-container">
<!-- 地址列表 -->
<view class="address-list" wx:if="{{addressList.length > 0}}">
<view class="address-item" wx:for="{{addressList}}" wx:key="_id">
<view class="address-info" bindtap="onEditAddress" data-id="{{item._id}}">
<view class="address-header">
<text class="name">{{item.name}}</text>
<text class="phone">{{item.phone}}</text>
<text class="default-tag" wx:if="{{item.isDefault}}">默认</text>
</view>
<view class="address-detail">{{item.province}}{{item.city}}{{item.district}}{{item.detail}}</view>
</view>
<view class="address-actions">
<view class="action-btn" bindtap="onSetDefault" data-id="{{item._id}}">
<text wx:if="{{!item.isDefault}}">设为默认</text>
<text wx:else class="default-text">默认地址</text>
</view>
<view class="action-btn edit" bindtap="onEditAddress" data-id="{{item._id}}">编辑</view>
<view class="action-btn delete" bindtap="onDeleteAddress" data-id="{{item._id}}">删除</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-container" wx:else>
<image class="empty-icon" src="https://via.placeholder.com/200x200/E0E0E0/999999?text=地址为空" mode="aspectFit"></image>
<text class="empty-text">暂无收货地址</text>
<text class="empty-tip">快去添加一个地址吧~</text>
</view>
<!-- 添加地址按钮 -->
<view class="add-button-section">
<button class="add-btn" bindtap="onAddAddress">+ 添加新地址</button>
</view>
</view>

@ -0,0 +1,144 @@
/* pages/address/address.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.address-list {
padding: 20rpx;
}
.address-item {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.address-info {
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
margin-bottom: 20rpx;
}
.address-header {
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.name {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-right: 20rpx;
}
.phone {
font-size: 28rpx;
color: #666;
margin-right: 20rpx;
}
.default-tag {
font-size: 22rpx;
color: #ff6b6b;
background: #ffe5e5;
padding: 4rpx 12rpx;
border-radius: 4rpx;
}
.address-detail {
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
.address-actions {
display: flex;
justify-content: flex-end;
gap: 20rpx;
}
.action-btn {
font-size: 26rpx;
color: #667eea;
padding: 10rpx 20rpx;
border-radius: 8rpx;
border: 1rpx solid #667eea;
}
.action-btn:active {
opacity: 0.7;
}
.action-btn.edit {
color: #07c160;
border-color: #07c160;
}
.action-btn.delete {
color: #ff6b6b;
border-color: #ff6b6b;
}
.default-text {
color: #999;
border-color: #e0e0e0;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 30rpx;
}
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-bottom: 10rpx;
}
.empty-tip {
font-size: 24rpx;
color: #ccc;
}
.add-button-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 30rpx;
background: white;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.add-btn {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 50rpx;
padding: 30rpx;
font-size: 32rpx;
font-weight: bold;
box-shadow: 0 8rpx 25rpx rgba(102, 126, 234, 0.3);
}
.add-btn:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 15rpx rgba(102, 126, 234, 0.3);
}

@ -0,0 +1,404 @@
// pages/admin-dashboard/admin-dashboard.js
const { QQMAP_KEY, QQMAP_REFERER } = require('../../utils/config.js');
Page({
data: {
adminInfo: {},
stats: {
totalProducts: 0,
totalUsers: 0,
totalSales: 0,
totalOrders: 0
},
monthlyProducts: [],
monthlySales: [],
categoryStats: [],
loading: true,
refreshing: false,
// 供需缺口 Top N 展示与推送(不分校区)
topNOptions: [3,5,8],
topNIndex: 1, // 默认5
topGaps: [],
gapsLoading: false,
pushInProgress: false,
pushResult: null,
// 求购关键词词云
keywordsLoading: false,
wantedKeywords: [],
keywordsDisplay: []
,
// 地标同步工具
syncDryRun: true,
syncLimit: 300,
syncLandmarksLoading: false,
syncResult: null
},
onLoad() {
// 检查管理员登录状态
const adminInfo = wx.getStorageSync('adminInfo');
if (!adminInfo) {
wx.redirectTo({
url: '/pages/admin-login/admin-login'
});
return;
}
this.setData({
adminInfo: adminInfo
});
this.loadDashboardData();
// 加载供需缺口TopN
this.loadSupplyDemandGaps();
// 加载求购关键词词云
this.loadWantedKeywords();
},
/** 切换地标同步 dryRun */
onSyncDryRunToggle(e) {
this.setData({ syncDryRun: !!e.detail.value });
},
/** 设置地标同步 limit */
onSyncLimitInput(e) {
const v = parseInt(e.detail.value, 10);
if (Number.isFinite(v) && v > 0) {
this.setData({ syncLimit: v });
}
},
/** 一键同步地标 */
async onSyncLandmarks() {
if (this.data.syncLandmarksLoading) return;
try {
this.setData({ syncLandmarksLoading: true, syncResult: null });
// 先确保集合存在
try {
await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'createCampusLandmarksCollection' } });
} catch (_) { /* 如果已存在或创建失败,继续执行同步 */ }
const res = await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: { type: 'syncProductLandmarks', dryRun: this.data.syncDryRun, limit: this.data.syncLimit, key: QQMAP_KEY, referer: QQMAP_REFERER }
});
const result = res.result || {};
// 兼容展示字段
result.candidateCount = result.totalCandidates || result.candidateCount || 0;
result.upserted = (result.created || 0) + (result.updated || 0);
this.setData({ syncResult: result });
if (result.success) {
const candidates = result.totalCandidates || result.candidateCount || 0;
const upserted = (result.created || 0) + (result.updated || 0);
const msg = this.data.syncDryRun
? `试运行:候选 ${candidates}`
: `已同步 ${upserted} 条地标`;
wx.showToast({ title: msg, icon: 'success' });
} else {
wx.showToast({ title: result.error || '同步失败', icon: 'none' });
}
} catch (err) {
console.error('同步地标失败:', err);
wx.showToast({ title: '同步失败', icon: 'none' });
} finally {
this.setData({ syncLandmarksLoading: false });
}
},
/**
* 页面显示时刷新数据
*/
onShow() {
// 每次页面显示时都刷新统计数据,确保数据实时更新
// 检查是否已经加载过数据(避免首次加载时重复请求)
if (this.data.adminInfo && Object.keys(this.data.adminInfo).length > 0 && !this.data.loading) {
// 静默刷新,不显示加载状态
this.loadDashboardData(true);
}
},
/**
* 加载仪表盘数据
*/
async loadDashboardData(refresh = false) {
// 如果不是刷新操作,显示加载状态
if (!refresh) {
this.setData({
loading: true
});
}
try {
// 调用云函数获取统计数据
const res = await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'getAdminStats'
}
});
if (res.result && res.result.success) {
const stats = res.result.data;
// 处理月度商品发布量数据
const monthlyProducts = this.processMonthlyData(stats.monthlyProducts || []);
// 处理月度销售额数据
const monthlySales = this.processSalesData(stats.monthlySales || []);
// 处理分类统计数据
const categoryStats = this.processCategoryData(stats.categoryStats || []);
this.setData({
stats: {
totalProducts: stats.totalProducts || 0,
totalUsers: stats.totalUsers || 0,
totalSales: stats.totalSales || 0,
totalOrders: stats.totalOrders || 0
},
monthlyProducts: monthlyProducts,
monthlySales: monthlySales,
categoryStats: categoryStats,
loading: false,
refreshing: false
});
console.log('统计数据已更新:', {
totalProducts: stats.totalProducts,
totalUsers: stats.totalUsers,
totalSales: stats.totalSales,
totalOrders: stats.totalOrders
});
} else {
throw new Error(res.result?.error || '获取数据失败');
}
} catch (err) {
console.error('加载仪表盘数据失败:', err);
wx.showToast({
title: '加载失败',
icon: 'none'
});
this.setData({
loading: false,
refreshing: false
});
}
},
/**
* 加载供需缺口Top N全校区合并不分校区
*/
async loadSupplyDemandGaps() {
try {
this.setData({ gapsLoading: true });
const res = await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'getPublishRecommendations',
forType: 'admin'
}
});
const result = res.result || {};
if (result.success && result.data) {
const stats = result.data.stats || [];
const n = this.data.topNOptions[this.data.topNIndex] || 5;
const top = stats.slice(0, n);
this.setData({ topGaps: top });
} else {
wx.showToast({ title: '获取供需缺口失败', icon: 'none' });
}
} catch (err) {
console.error('获取供需缺口失败:', err);
wx.showToast({ title: '网络异常', icon: 'none' });
} finally {
this.setData({ gapsLoading: false });
}
},
/** 选择TopN */
onTopNChange(e) {
this.setData({ topNIndex: parseInt(e.detail.value) });
this.loadSupplyDemandGaps();
},
/** 一键推送建议 */
async onPushRecommendation() {
if (this.data.pushInProgress) return;
try {
this.setData({ pushInProgress: true });
const categories = (this.data.topGaps || []).map(item => item.category).filter(Boolean);
if (categories.length === 0) {
wx.showToast({ title: '暂无可推送的类别', icon: 'none' });
return;
}
const res = await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'adminPushRecommendations',
categories,
target: 'all'
}
});
const result = res.result || {};
if (result.success) {
wx.showToast({ title: `已推送${result.pushed || 0}`, icon: 'success' });
} else {
wx.showToast({ title: result.error || '推送失败', icon: 'none' });
}
this.setData({ pushResult: result });
} catch (err) {
console.error('推送建议失败:', err);
wx.showToast({ title: '推送失败', icon: 'none' });
} finally {
this.setData({ pushInProgress: false });
}
},
/** 加载求购关键词词云 */
async loadWantedKeywords() {
try {
this.setData({ keywordsLoading: true });
const res = await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: { type: 'getWantedKeywordCloud', limit: 60 }
});
const result = res.result || {};
if (result.success && result.data && Array.isArray(result.data.keywords)) {
const keywords = result.data.keywords;
const colors = ['#3366FF','#33A852','#FBBC04','#EA4335','#7E57C2','#00ACC1','#F06292','#8D6E63'];
const display = keywords.map((k, idx) => {
const size = Math.round(24 + (k.score || 0) * 22); // 24~46rpx
const opacity = (0.6 + (k.score || 0) * 0.4).toFixed(2); // 0.6~1.0
const color = colors[idx % colors.length];
return {
text: k.text,
weight: k.weight,
style: `font-size: ${size}rpx; color: ${color}; opacity: ${opacity};`
};
});
this.setData({ wantedKeywords: keywords, keywordsDisplay: display });
} else {
this.setData({ wantedKeywords: [], keywordsDisplay: [] });
}
} catch (err) {
console.error('加载求购关键词失败:', err);
wx.showToast({ title: '词云加载失败', icon: 'none' });
} finally {
this.setData({ keywordsLoading: false });
}
},
/**
* 处理月度数据用于柱状图
*/
processMonthlyData(data) {
if (!data || data.length === 0) {
// 返回空数据
const months = ['1月', '2月', '3月', '4月', '5月', '6月'];
return months.map(month => ({
month: month,
count: 0,
percentage: 0
}));
}
const maxCount = Math.max(...data.map(item => item.count), 1);
return data.map(item => ({
month: item.month,
count: item.count,
percentage: (item.count / maxCount) * 100
}));
},
/**
* 处理销售额数据用于折线图
*/
processSalesData(data) {
if (!data || data.length === 0) {
const months = ['1月', '2月', '3月', '4月', '5月', '6月'];
return months.map((month, index) => ({
month: month,
sales: 0,
percentage: 0,
position: (index / (months.length - 1)) * 100
}));
}
const maxSales = Math.max(...data.map(item => item.sales), 1);
const totalItems = data.length;
return data.map((item, index) => ({
month: item.month,
sales: item.sales,
percentage: (item.sales / maxSales) * 100,
position: totalItems > 1 ? (index / (totalItems - 1)) * 100 : 50
}));
},
/**
* 处理分类数据用于饼图
*/
processCategoryData(data) {
if (!data || data.length === 0) {
return [];
}
const total = data.reduce((sum, item) => sum + item.count, 0);
const colors = ['#4285F4', '#34A853', '#FBBC05', '#EA4335', '#9C27B0', '#00BCD4', '#FF9800', '#795548'];
return data.map((item, index) => ({
category: item.category,
count: item.count,
percentage: (item.count / total) * 100,
color: colors[index % colors.length]
}));
},
/**
* 下拉刷新
*/
onRefresh() {
this.setData({
refreshing: true
});
this.loadDashboardData();
},
/**
* 导航到其他页面
*/
onNavigateTo(e) {
const page = e.currentTarget.dataset.page;
const pages = {
products: '/pages/admin-products/admin-products',
users: '/pages/admin-users/admin-users',
orders: '/pages/admin-orders/admin-orders'
};
if (pages[page]) {
wx.navigateTo({
url: pages[page]
});
}
},
/**
* 退出登录
*/
onLogout() {
wx.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
wx.removeStorageSync('adminInfo');
wx.removeStorageSync('adminToken');
wx.redirectTo({
url: '/pages/admin-login/admin-login'
});
}
}
});
}
});

@ -0,0 +1,7 @@
{
"navigationBarTitleText": "管理后台",
"navigationBarBackgroundColor": "#667eea",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true
}

@ -0,0 +1,220 @@
<!--pages/admin-dashboard/admin-dashboard.wxml-->
<view class="page-container">
<!-- 顶部导航栏 -->
<view class="admin-header">
<view class="header-content">
<text class="admin-title">管理后台</text>
<view class="admin-info" bindtap="onLogout">
<text class="admin-name">{{adminInfo.name || '管理员'}}</text>
<text class="logout-btn">退出</text>
</view>
</view>
</view>
<!-- 统计卡片 -->
<scroll-view class="scroll-container" scroll-y="true" refresher-enabled="{{true}}" refresher-triggered="{{refreshing}}" bindrefresherrefresh="onRefresh">
<!-- 数据概览 -->
<view class="stats-overview">
<view class="stat-card" bindtap="onNavigateTo" data-page="products">
<view class="stat-icon products">📦</view>
<view class="stat-content">
<text class="stat-value">{{stats.totalProducts}}</text>
<text class="stat-label">商品总数</text>
</view>
</view>
<view class="stat-card" bindtap="onNavigateTo" data-page="users">
<view class="stat-icon users">👥</view>
<view class="stat-content">
<text class="stat-value">{{stats.totalUsers}}</text>
<text class="stat-label">用户总数</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon sales">💰</view>
<view class="stat-content">
<text class="stat-value">¥{{stats.totalSales}}</text>
<text class="stat-label">总销售额</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon orders">📋</view>
<view class="stat-content">
<text class="stat-value">{{stats.totalOrders}}</text>
<text class="stat-label">订单总数</text>
</view>
</view>
</view>
<!-- 供需缺口 Top N -->
<view class="chart-section">
<view class="section-header">
<text class="section-title">供需缺口 Top {{topNOptions[topNIndex]}}</text>
<text class="section-subtitle">全校区</text>
</view>
<view class="filters-row">
<picker range="{{topNOptions}}" value="{{topNIndex}}" bindchange="onTopNChange" class="filter-picker">
<view class="picker-display">
<text class="picker-text">Top {{topNOptions[topNIndex]}}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<view class="gaps-list">
<view class="gap-item" wx:for="{{topGaps}}" wx:key="category">
<view class="gap-left">
<text class="gap-category">{{item.category}}</text>
<text class="gap-sub">供 {{item.supply}} · 需 {{item.demand}}</text>
</view>
<view class="gap-right">
<text class="gap-value {{item.gap >= 0 ? 'pos' : 'neg'}}">{{item.gap >= 0 ? ('缺口 +' + item.gap) : ('过供 ' + (item.gap))}}</text>
</view>
</view>
<view class="empty-tips" wx:if="{{!gapsLoading && topGaps.length === 0}}">暂无数据</view>
</view>
<view class="push-row">
<button class="push-btn" loading="{{pushInProgress}}" disabled="{{pushInProgress}}" bindtap="onPushRecommendation">一键推送建议</button>
</view>
</view>
<!-- 月度商品发布量统计 -->
<view class="chart-section">
<view class="section-header">
<text class="section-title">月度商品发布量</text>
<text class="section-subtitle">最近6个月</text>
</view>
<view class="chart-container">
<view class="bar-chart">
<view class="chart-bars">
<view class="bar-item" wx:for="{{monthlyProducts}}" 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>
</view>
<!-- 月度销售额统计 -->
<view class="chart-section">
<view class="section-header">
<text class="section-title">月度销售额</text>
<text class="section-subtitle">最近6个月</text>
</view>
<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="{{monthlySales}}" wx:key="month" style="left: {{item.position}}%; bottom: {{item.percentage}}%"></view>
</view>
</view>
<view class="chart-labels">
<text class="label-item" wx:for="{{monthlySales}}" wx:key="month">{{item.month}}</text>
</view>
<view class="chart-values">
<text class="value-item" wx:for="{{monthlySales}}" wx:key="month">¥{{item.sales}}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 商品分类统计 -->
<view class="chart-section">
<view class="section-header">
<text class="section-title">商品分类分布</text>
</view>
<view class="pie-chart-container">
<view class="pie-chart">
<view class="pie-item" wx:for="{{categoryStats}}" wx:key="category">
<view class="pie-segment" style="background: {{item.color}}; width: {{item.percentage}}%"></view>
<view class="pie-label">
<view class="label-color" style="background: {{item.color}}"></view>
<text class="label-text">{{item.category}}: {{item.count}}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 求购关键词词云 -->
<view class="chart-section">
<view class="section-header">
<text class="section-title">求购关键词词云</text>
<text class="section-subtitle">按出现频次加权</text>
</view>
<view wx:if="{{keywordsLoading}}" class="word-cloud-loading">加载词云...</view>
<view wx:else class="word-cloud">
<block wx:for="{{keywordsDisplay}}" wx:key="text">
<text class="word-chip" style="{{item.style}}">{{item.text}}</text>
</block>
<view class="empty-tips" wx:if="{{!keywordsDisplay.length}}">暂无关键词</view>
</view>
</view>
<!-- 数据工具:地标同步 -->
<view class="chart-section">
<view class="section-header">
<text class="section-title">数据工具</text>
<text class="section-subtitle">地标与地图</text>
</view>
<view class="filters-row">
<view class="filter-item">
<text class="filter-label">试运行</text>
<switch checked="{{syncDryRun}}" bindchange="onSyncDryRunToggle"></switch>
</view>
<view class="filter-item">
<text class="filter-label">Limit</text>
<input class="filter-input" type="number" placeholder="300" value="{{syncLimit}}" bindinput="onSyncLimitInput"/>
</view>
</view>
<view class="push-row">
<button class="push-btn" loading="{{syncLandmarksLoading}}" disabled="{{syncLandmarksLoading}}" bindtap="onSyncLandmarks">一键同步地标</button>
</view>
<view class="gaps-list" wx:if="{{syncResult}}">
<view class="gap-item">
<view class="gap-left">
<text class="gap-category">候选</text>
<text class="gap-sub">{{syncResult.candidateCount || 0}}</text>
</view>
<view class="gap-right">
<text class="gap-value pos">{{syncResult.upserted || 0}} 已写入</text>
</view>
</view>
<view class="empty-tips" wx:if="{{!syncResult.success}}">{{syncResult.error || '执行失败'}}</view>
</view>
</view>
<!-- 快捷操作 -->
<view class="quick-actions">
<view class="action-item" bindtap="onNavigateTo" data-page="products">
<text class="action-icon">📦</text>
<text class="action-text">商品管理</text>
</view>
<view class="action-item" bindtap="onNavigateTo" data-page="users">
<text class="action-icon">👥</text>
<text class="action-text">用户管理</text>
</view>
<view class="action-item" bindtap="onNavigateTo" data-page="orders">
<text class="action-icon">📋</text>
<text class="action-text">订单管理</text>
</view>
</view>
</scroll-view>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading}}">
<view class="loading-content">
<text class="loading-text">加载中...</text>
</view>
</view>

@ -0,0 +1,445 @@
/* pages/admin-dashboard/admin-dashboard.wxss */
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
/* 顶部导航栏 */
.admin-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30rpx;
padding-top: calc(30rpx + env(safe-area-inset-top));
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-title {
font-size: 36rpx;
font-weight: 600;
color: white;
}
.admin-info {
display: flex;
align-items: center;
gap: 20rpx;
}
.admin-name {
font-size: 28rpx;
color: white;
}
.logout-btn {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
padding: 8rpx 20rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 20rpx;
}
.scroll-container {
flex: 1;
padding: 20rpx;
}
/* 数据概览 */
.stats-overview {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
margin-bottom: 30rpx;
}
.stat-card {
background: white;
border-radius: 20rpx;
padding: 30rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.stat-icon {
font-size: 60rpx;
margin-right: 20rpx;
}
.stat-content {
flex: 1;
display: flex;
flex-direction: column;
}
.stat-value {
font-size: 36rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #999;
}
/* 图表区域 */
.chart-section {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.section-subtitle {
font-size: 24rpx;
color: #999;
}
.chart-container {
width: 100%;
}
/* 供需缺口 TopN */
.filters-row {
display: flex;
gap: 20rpx;
margin-bottom: 20rpx;
}
.filter-picker {
flex: 1;
}
.gaps-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.gap-item {
display: flex;
justify-content: space-between;
align-items: center;
background: #f8fbff;
border: 1rpx solid #e8ecff;
border-radius: 12rpx;
padding: 18rpx 20rpx;
}
.gap-left {
display: flex;
flex-direction: column;
}
.gap-category {
font-size: 28rpx;
color: #333;
font-weight: 600;
}
.gap-sub {
font-size: 22rpx;
color: #666;
}
.gap-right {
display: flex;
align-items: center;
}
.gap-value {
font-size: 26rpx;
font-weight: 600;
}
.gap-value.pos { color: #EA4335; }
.gap-value.neg { color: #34A853; }
.empty-tips {
font-size: 24rpx;
color: #999;
text-align: center;
padding: 20rpx 0;
}
.push-row { margin-top: 20rpx; }
.push-btn {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
border-radius: 12rpx;
padding: 22rpx;
font-size: 28rpx;
}
/* 柱状图 */
.bar-chart {
width: 100%;
}
.chart-bars {
display: flex;
justify-content: space-around;
align-items: flex-end;
height: 300rpx;
padding: 20rpx 0;
}
.bar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
}
.bar-wrapper {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
position: relative;
}
.bar {
width: 60%;
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
border-radius: 8rpx 8rpx 0 0;
min-height: 20rpx;
transition: all 0.3s ease;
}
.bar-value {
position: absolute;
top: -40rpx;
font-size: 22rpx;
color: #666;
font-weight: 600;
}
.bar-label {
font-size: 22rpx;
color: #999;
margin-top: 15rpx;
}
/* 折线图 */
.line-chart {
width: 100%;
height: 300rpx;
position: relative;
}
.chart-area {
width: 100%;
height: 100%;
position: relative;
}
.chart-grid {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.grid-line {
height: 1rpx;
background: #e0e0e0;
}
.chart-line {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 60rpx;
}
.line-path {
position: relative;
width: 100%;
height: 100%;
}
.line-point {
position: absolute;
width: 16rpx;
height: 16rpx;
background: #667eea;
border-radius: 50%;
border: 3rpx solid white;
box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.3);
transform: translate(-50%, 50%);
}
.chart-labels {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60rpx;
display: flex;
justify-content: space-around;
align-items: center;
}
.label-item {
font-size: 22rpx;
color: #999;
}
.chart-values {
position: absolute;
top: -30rpx;
left: 0;
right: 0;
display: flex;
justify-content: space-around;
}
.value-item {
font-size: 20rpx;
color: #666;
}
/* 饼图 */
.pie-chart-container {
width: 100%;
}
.pie-chart {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.pie-item {
display: flex;
align-items: center;
gap: 20rpx;
}
.pie-segment {
height: 40rpx;
border-radius: 20rpx;
min-width: 20rpx;
}
.pie-label {
flex: 1;
display: flex;
align-items: center;
gap: 15rpx;
}
.label-color {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
}
.label-text {
font-size: 26rpx;
color: #333;
}
/* 快捷操作 */
.quick-actions {
display: flex;
gap: 20rpx;
margin-bottom: 30rpx;
}
.action-item {
flex: 1;
background: white;
border-radius: 20rpx;
padding: 40rpx 20rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.action-icon {
font-size: 60rpx;
margin-bottom: 15rpx;
}
.action-text {
font-size: 26rpx;
color: #333;
}
/* 加载状态 */
.loading-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
text-align: center;
}
.loading-text {
font-size: 28rpx;
color: #666;
}
/* 词云 */
.word-cloud {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
align-items: center;
}
.word-chip {
background: #f2f3f5;
border-radius: 24rpx;
padding: 10rpx 16rpx;
line-height: 1.2;
}
.word-cloud-loading {
font-size: 24rpx;
color: #999;
}

@ -0,0 +1,141 @@
// pages/admin-login/admin-login.js
Page({
data: {
username: '',
password: '',
isLoginDisabled: false
},
onLoad() {
// 检查是否已登录
const adminInfo = wx.getStorageSync('adminInfo');
if (adminInfo) {
wx.redirectTo({
url: '/pages/admin-dashboard/admin-dashboard'
});
}
},
// 账号输入处理
onInputUsername(e) {
this.setData({
username: e.detail.value.trim()
});
this.checkLoginButton();
},
// 密码输入处理
onInputPassword(e) {
this.setData({
password: e.detail.value.trim()
});
this.checkLoginButton();
},
// 检查登录按钮状态
checkLoginButton() {
this.setData({
isLoginDisabled: false
});
},
// 登录处理
onLogin() {
const { username, password } = this.data;
if (!this.validateInput(username, password)) {
return;
}
wx.showLoading({
title: '登录中...',
mask: true
});
// 调用云函数验证管理员身份
wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'adminLogin',
username: username,
password: password
},
success: (res) => {
wx.hideLoading();
if (res.result && res.result.success) {
// 登录成功
const adminInfo = {
_id: res.result.adminId,
username: res.result.username,
name: res.result.name || '管理员',
role: res.result.role || 'admin'
};
wx.setStorageSync('adminInfo', adminInfo);
wx.setStorageSync('adminToken', 'admin_token_' + res.result.adminId);
wx.showToast({
title: '登录成功',
icon: 'success',
duration: 1500
});
setTimeout(() => {
wx.redirectTo({
url: '/pages/admin-dashboard/admin-dashboard'
});
}, 1500);
} else {
wx.showToast({
title: res.result?.error || '账号或密码错误',
icon: 'none'
});
}
},
fail: (err) => {
wx.hideLoading();
console.error('管理员登录失败:', err);
wx.showToast({
title: '网络错误,请重试',
icon: 'none'
});
}
});
},
// 输入验证
validateInput(username, password) {
if (!username) {
wx.showToast({
title: '请输入管理员账号',
icon: 'none'
});
return false;
}
if (!password) {
wx.showToast({
title: '请输入密码',
icon: 'none'
});
return false;
}
if (password.length < 6) {
wx.showToast({
title: '密码长度不能少于6位',
icon: 'none'
});
return false;
}
return true;
},
// 返回用户登录
onBackToUserLogin() {
wx.navigateBack();
}
});

@ -0,0 +1,6 @@
{
"navigationBarTitleText": "管理员登录",
"navigationBarBackgroundColor": "#667eea",
"navigationBarTextStyle": "white"
}

@ -0,0 +1,52 @@
<!--pages/admin-login/admin-login.wxml-->
<view class="page-container">
<view class="login-container">
<!-- 顶部标题 -->
<view class="header">
<text class="admin-icon">👨‍💼</text>
<text class="title">管理员登录</text>
<text class="subtitle">校园二手交易管理后台</text>
</view>
<!-- 登录表单 -->
<view class="login-form">
<view class="form-group">
<text class="label">管理员账号</text>
<input
class="input"
type="text"
placeholder="请输入管理员账号"
bindinput="onInputUsername"
value="{{username}}"
/>
</view>
<view class="form-group">
<text class="label">密码</text>
<input
class="input"
type="password"
placeholder="请输入密码"
bindinput="onInputPassword"
value="{{password}}"
password
/>
</view>
<!-- 登录按钮 -->
<button
class="login-btn {{isLoginDisabled ? 'disabled' : ''}}"
bindtap="onLogin"
disabled="{{isLoginDisabled}}"
>
登录
</button>
<!-- 返回普通登录 -->
<view class="back-link">
<text class="link" bindtap="onBackToUserLogin">返回用户登录</text>
</view>
</view>
</view>
</view>

@ -0,0 +1,97 @@
/* pages/admin-login/admin-login.wxss */
.page-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
}
.login-container {
width: 100%;
max-width: 600rpx;
}
.header {
text-align: center;
margin-bottom: 60rpx;
}
.admin-icon {
font-size: 120rpx;
display: block;
margin-bottom: 30rpx;
}
.title {
display: block;
font-size: 48rpx;
font-weight: 600;
color: white;
margin-bottom: 15rpx;
}
.subtitle {
display: block;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.login-form {
background: white;
border-radius: 30rpx;
padding: 60rpx 40rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
}
.form-group {
margin-bottom: 40rpx;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
font-weight: 500;
}
.input {
width: 100%;
height: 88rpx;
background: #f5f5f5;
border-radius: 15rpx;
padding: 0 25rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
.login-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 15rpx;
font-size: 32rpx;
font-weight: 600;
margin-top: 40rpx;
border: none;
}
.login-btn.disabled {
background: #ccc;
}
.back-link {
text-align: center;
margin-top: 40rpx;
}
.link {
color: #667eea;
font-size: 26rpx;
text-decoration: underline;
}

@ -0,0 +1,220 @@
// pages/admin-products/admin-products.js
Page({
data: {
products: [],
keyword: '',
page: 0,
pageSize: 20,
hasMore: true,
loading: true,
refreshing: false
},
onLoad() {
// 检查管理员登录状态
const adminInfo = wx.getStorageSync('adminInfo');
if (!adminInfo) {
wx.redirectTo({
url: '/pages/admin-login/admin-login'
});
return;
}
this.loadProducts();
},
/**
* 加载商品列表
*/
async loadProducts(refresh = false) {
if (refresh) {
this.setData({
page: 0,
products: [],
hasMore: true
});
}
if (!this.data.hasMore && !refresh) {
return;
}
this.setData({
loading: true
});
try {
const db = wx.cloud.database();
let queryCondition = {};
// 搜索条件
if (this.data.keyword) {
queryCondition.productName = db.RegExp({
regexp: this.data.keyword,
options: 'i'
});
}
const result = await db.collection('T_product')
.where(queryCondition)
.orderBy('createTime', 'desc')
.skip(this.data.page * this.data.pageSize)
.limit(this.data.pageSize)
.get();
const products = result.data.map(item => ({
...item,
timeText: this.formatTime(item.createTime)
}));
this.setData({
products: refresh ? products : [...this.data.products, ...products],
page: this.data.page + 1,
hasMore: result.data.length === this.data.pageSize,
loading: false,
refreshing: false
});
} catch (err) {
console.error('加载商品列表失败:', err);
wx.showToast({
title: '加载失败',
icon: 'none'
});
this.setData({
loading: false,
refreshing: false
});
}
},
/**
* 格式化时间
*/
formatTime(date) {
if (!date) return '';
const d = new Date(date);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({
keyword: e.detail.value
});
},
/**
* 搜索
*/
onSearch() {
this.loadProducts(true);
},
/**
* 下拉刷新
*/
onRefresh() {
this.setData({
refreshing: true
});
this.loadProducts(true);
},
/**
* 加载更多
*/
onLoadMore() {
if (!this.data.loading && this.data.hasMore) {
this.loadProducts();
}
},
/**
* 编辑商品
*/
onEditProduct(e) {
const productId = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/admin-product-edit/admin-product-edit?id=${productId}`
});
},
/**
* 切换商品状态上架/下架
*/
async onToggleStatus(e) {
const productId = e.currentTarget.dataset.id;
const currentStatus = e.currentTarget.dataset.status;
const newStatus = currentStatus === '在售' ? '已下架' : '在售';
wx.showModal({
title: '提示',
content: `确定要${newStatus === '在售' ? '上架' : '下架'}该商品吗?`,
success: async (res) => {
if (res.confirm) {
wx.showLoading({
title: '处理中...',
mask: true
});
try {
const result = await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'adminUpdateProduct',
productId: productId,
data: {
status: newStatus
}
}
});
wx.hideLoading();
if (result.result && result.result.success) {
wx.showToast({
title: '操作成功',
icon: 'success'
});
this.loadProducts(true);
} else {
wx.showToast({
title: result.result?.error || '操作失败',
icon: 'none'
});
}
} catch (err) {
wx.hideLoading();
console.error('操作失败:', err);
wx.showToast({
title: '操作失败',
icon: 'none'
});
}
}
}
});
},
/**
* 添加商品
*/
onAddProduct() {
wx.navigateTo({
url: '/pages/admin-product-edit/admin-product-edit'
});
},
/**
* 页面显示时刷新
*/
onShow() {
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
if (prevPage && prevPage.route === 'pages/admin-product-edit/admin-product-edit') {
this.loadProducts(true);
}
}
});

@ -0,0 +1,6 @@
{
"navigationBarTitleText": "商品管理",
"navigationBarBackgroundColor": "#667eea",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true
}

@ -0,0 +1,54 @@
<!--pages/admin-products/admin-products.wxml-->
<view class="page-container">
<!-- 顶部搜索栏 -->
<view class="search-header">
<view class="search-bar">
<text class="search-icon">🔍</text>
<input class="search-input" placeholder="搜索商品名称..." value="{{keyword}}" bindinput="onSearchInput" bindconfirm="onSearch"/>
</view>
<view class="add-btn" bindtap="onAddProduct">
<text></text>
</view>
</view>
<!-- 商品列表 -->
<scroll-view class="product-list" scroll-y="true" refresher-enabled="{{true}}" refresher-triggered="{{refreshing}}" bindrefresherrefresh="onRefresh" bindscrolltolower="onLoadMore">
<view class="product-item" wx:for="{{products}}" wx:key="_id">
<image class="product-image" src="{{item.productImage ? (Array.isArray(item.productImage) ? item.productImage[0] : item.productImage) : 'https://via.placeholder.com/600x400/cccccc/666666?text=商品图'}}" mode="aspectFill"></image>
<view class="product-info">
<text class="product-name">{{item.productName}}</text>
<text class="product-price">¥{{item.salePrice || item.suggestedPrice || 0}}</text>
<view class="product-meta">
<text class="product-category">{{item.productCategory || '其他'}}</text>
<text class="product-status {{item.status === '在售' ? 'on-sale' : 'off-sale'}}">{{item.status || '未知'}}</text>
</view>
<text class="product-time">发布时间: {{item.timeText}}</text>
</view>
<view class="product-actions">
<view class="action-btn edit" bindtap="onEditProduct" data-id="{{item._id}}">编辑</view>
<view class="action-btn {{item.status === '在售' ? 'offline' : 'online'}}" bindtap="onToggleStatus" data-id="{{item._id}}" data-status="{{item.status}}">
{{item.status === '在售' ? '下架' : '上架'}}
</view>
</view>
</view>
<view class="load-more" wx:if="{{hasMore && !loading}}">
<text>加载中...</text>
</view>
<view class="no-more" wx:if="{{!hasMore && products.length > 0}}">
<text>没有更多了</text>
</view>
<view class="empty-state" wx:if="{{!loading && products.length === 0}}">
<text class="empty-icon">📦</text>
<text class="empty-text">暂无商品</text>
</view>
</scroll-view>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading && products.length === 0}}">
<view class="loading-content">
<text class="loading-text">加载中...</text>
</view>
</view>

@ -0,0 +1,206 @@
/* pages/admin-products/admin-products.wxss */
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.search-header {
background: white;
padding: 20rpx 30rpx;
display: flex;
align-items: center;
gap: 20rpx;
border-bottom: 1rpx solid #e0e0e0;
}
.search-bar {
flex: 1;
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 50rpx;
padding: 15rpx 25rpx;
}
.search-icon {
font-size: 28rpx;
margin-right: 15rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
}
.add-btn {
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 40rpx;
}
.product-list {
flex: 1;
padding: 20rpx;
}
.product-item {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.product-image {
width: 160rpx;
height: 160rpx;
border-radius: 15rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
}
.product-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-price {
font-size: 32rpx;
font-weight: 600;
color: #EA4335;
margin-bottom: 15rpx;
}
.product-meta {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 10rpx;
}
.product-category {
font-size: 24rpx;
color: #999;
background: #f5f5f5;
padding: 5rpx 15rpx;
border-radius: 10rpx;
}
.product-status {
font-size: 24rpx;
padding: 5rpx 15rpx;
border-radius: 10rpx;
}
.product-status.on-sale {
background: #E8F5E9;
color: #4CAF50;
}
.product-status.off-sale {
background: #FFEBEE;
color: #F44336;
}
.product-time {
font-size: 22rpx;
color: #999;
}
.product-actions {
display: flex;
flex-direction: column;
gap: 15rpx;
margin-left: 20rpx;
}
.action-btn {
padding: 12rpx 24rpx;
border-radius: 10rpx;
font-size: 24rpx;
text-align: center;
min-width: 100rpx;
}
.action-btn.edit {
background: #E3F2FD;
color: #2196F3;
}
.action-btn.online {
background: #E8F5E9;
color: #4CAF50;
}
.action-btn.offline {
background: #FFEBEE;
color: #F44336;
}
.load-more,
.no-more {
text-align: center;
padding: 30rpx 0;
color: #999;
font-size: 24rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
}
.empty-text {
font-size: 32rpx;
color: #666;
}
.loading-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
text-align: center;
}
.loading-text {
font-size: 28rpx;
color: #666;
}

@ -0,0 +1,306 @@
// pages/admin-user-edit/admin-user-edit.js
Page({
data: {
userId: '',
userInfo: {
sno: '',
sname: '',
phone: '',
major: '',
grade: '',
sushe: '',
avatar: ''
},
newPassword: '',
grades: [
{ label: '大一', value: '大一' },
{ label: '大二', value: '大二' },
{ label: '大三', value: '大三' },
{ label: '大四', value: '大四' },
{ label: '研究生', value: '研究生' },
{ label: '博士生', value: '博士生' }
],
gradeIndex: 0,
loading: true,
submitting: false,
canSubmit: false
},
onLoad(options) {
// 检查管理员登录状态
const adminInfo = wx.getStorageSync('adminInfo');
if (!adminInfo) {
wx.redirectTo({
url: '/pages/admin-login/admin-login'
});
return;
}
const userId = options.id;
if (userId) {
this.setData({
userId: userId
});
this.loadUserInfo(userId);
} else {
// 新建用户(可选功能)
this.setData({
loading: false
});
}
},
/**
* 加载用户信息
*/
async loadUserInfo(userId) {
try {
const db = wx.cloud.database();
const result = await db.collection('T_user').doc(userId).get();
if (result.data) {
const userInfo = result.data;
// 找到年级索引
const gradeValue = userInfo.年级 || userInfo.grade || '';
const gradeIndex = this.data.grades.findIndex(grade => grade.value === gradeValue);
this.setData({
userInfo: {
sno: userInfo.sno || '',
sname: userInfo.sname || '',
phone: userInfo.phone || '',
major: userInfo.major || '',
grade: gradeValue,
sushe: userInfo.sushe || '',
avatar: userInfo.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U'
},
gradeIndex: gradeIndex >= 0 ? gradeIndex : 0,
loading: false
});
// 检查是否可以提交
this.checkCanSubmit();
} else {
throw new Error('用户不存在');
}
} catch (err) {
console.error('加载用户信息失败:', err);
wx.showToast({
title: '加载失败',
icon: 'none'
});
setTimeout(() => {
wx.navigateBack();
}, 1500);
}
},
/**
* 检查是否可以提交
*/
checkCanSubmit() {
const { userInfo } = this.data;
const canSubmit = !!(userInfo.sno && userInfo.sname && userInfo.phone);
this.setData({
canSubmit: canSubmit
});
},
/**
* 输入处理
*/
onInputSno(e) {
this.setData({
'userInfo.sno': e.detail.value
});
this.checkCanSubmit();
},
onInputSname(e) {
this.setData({
'userInfo.sname': e.detail.value
});
this.checkCanSubmit();
},
onInputPhone(e) {
this.setData({
'userInfo.phone': e.detail.value
});
this.checkCanSubmit();
},
onInputMajor(e) {
this.setData({
'userInfo.major': e.detail.value
});
},
onInputSushe(e) {
this.setData({
'userInfo.sushe': e.detail.value
});
},
onInputPassword(e) {
this.setData({
newPassword: e.detail.value
});
},
/**
* 年级选择
*/
onGradeChange(e) {
const index = parseInt(e.detail.value);
this.setData({
gradeIndex: index,
'userInfo.grade': this.data.grades[index].value
});
},
/**
* 选择头像
*/
onChooseAvatar() {
wx.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
wx.showLoading({
title: '上传中...',
mask: true
});
try {
const filePath = res.tempFilePaths[0];
const cloudPath = `avatars/${Date.now()}-${Math.random().toString(36).substr(2, 9)}.jpg`;
const uploadResult = await wx.cloud.uploadFile({
cloudPath: cloudPath,
filePath: filePath
});
this.setData({
'userInfo.avatar': uploadResult.fileID
});
wx.hideLoading();
} catch (err) {
console.error('上传头像失败:', err);
wx.hideLoading();
wx.showToast({
title: '上传失败',
icon: 'none'
});
}
}
});
},
/**
* 提交修改
*/
async onSubmit() {
if (!this.data.canSubmit) {
wx.showToast({
title: '请填写必填项',
icon: 'none'
});
return;
}
// 验证手机号格式
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(this.data.userInfo.phone)) {
wx.showToast({
title: '请输入正确的手机号',
icon: 'none'
});
return;
}
// 如果设置了新密码,验证密码长度
if (this.data.newPassword && this.data.newPassword.length < 6) {
wx.showToast({
title: '密码长度不能少于6位',
icon: 'none'
});
return;
}
this.setData({
submitting: true
});
try {
// 构建更新数据
const updateData = {
sno: this.data.userInfo.sno.trim(),
sname: this.data.userInfo.sname.trim(),
phone: this.data.userInfo.phone.trim(),
major: this.data.userInfo.major.trim() || '',
年级: this.data.userInfo.grade || '', // 保存到数据库时使用中文字段名
sushe: this.data.userInfo.sushe.trim() || '',
avatar: this.data.userInfo.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U',
updateTime: new Date()
};
// 如果设置了新密码,添加到更新数据中
if (this.data.newPassword) {
updateData.password = this.data.newPassword.trim();
}
// 调用云函数更新用户信息
wx.showLoading({
title: '保存中...',
mask: true
});
const result = await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'adminUpdateUser',
userId: this.data.userId,
data: updateData
}
});
wx.hideLoading();
console.log('更新用户信息结果:', result);
if (result.result && result.result.success) {
wx.showToast({
title: '保存成功',
icon: 'success',
duration: 2000
});
setTimeout(() => {
wx.navigateBack();
}, 2000);
} else {
const errorMsg = result.result?.error || result.errMsg || '保存失败';
console.error('保存失败:', errorMsg);
wx.showToast({
title: errorMsg,
icon: 'none',
duration: 3000
});
}
} catch (err) {
console.error('保存用户信息失败:', err);
wx.showToast({
title: err.message || '保存失败',
icon: 'none'
});
} finally {
this.setData({
submitting: false
});
}
}
});

@ -0,0 +1,6 @@
{
"navigationBarTitleText": "编辑用户",
"navigationBarBackgroundColor": "#667eea",
"navigationBarTextStyle": "white"
}

@ -0,0 +1,123 @@
<!--pages/admin-user-edit/admin-user-edit.wxml-->
<view class="page-container">
<scroll-view class="scroll-container" scroll-y="true">
<view class="form-container">
<!-- 用户头像 -->
<view class="form-section">
<text class="section-label">用户头像</text>
<view class="avatar-section">
<image class="avatar" src="{{userInfo.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U'}}" mode="aspectFill"></image>
<view class="avatar-upload" bindtap="onChooseAvatar">
<text>更换头像</text>
</view>
</view>
</view>
<!-- 基本信息 -->
<view class="form-section">
<text class="section-title">基本信息</text>
<view class="form-group">
<text class="label">学号</text>
<input
class="input"
type="text"
placeholder="请输入学号"
value="{{userInfo.sno}}"
bindinput="onInputSno"
disabled="{{!!userId}}"
/>
</view>
<view class="form-group">
<text class="label">姓名</text>
<input
class="input"
type="text"
placeholder="请输入姓名"
value="{{userInfo.sname}}"
bindinput="onInputSname"
/>
</view>
<view class="form-group">
<text class="label">手机号</text>
<input
class="input"
type="number"
placeholder="请输入手机号"
value="{{userInfo.phone}}"
bindinput="onInputPhone"
/>
</view>
<view class="form-group">
<text class="label">专业</text>
<input
class="input"
type="text"
placeholder="请输入专业"
value="{{userInfo.major}}"
bindinput="onInputMajor"
/>
</view>
<view class="form-group">
<text class="label">年级</text>
<picker mode="selector" range="{{grades}}" range-key="label" value="{{gradeIndex}}" bindchange="onGradeChange">
<view class="picker-input">
<text class="{{userInfo.grade ? '' : 'placeholder'}}">{{userInfo.grade || '请选择年级'}}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<view class="form-group">
<text class="label">宿舍</text>
<input
class="input"
type="text"
placeholder="请输入宿舍1号楼101"
value="{{userInfo.sushe}}"
bindinput="onInputSushe"
/>
</view>
</view>
<!-- 密码设置(可选) -->
<view class="form-section">
<text class="section-title">密码设置(可选)</text>
<view class="form-group">
<text class="label">新密码</text>
<input
class="input"
type="password"
placeholder="留空则不修改密码"
value="{{newPassword}}"
bindinput="onInputPassword"
password
/>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-section">
<button
class="submit-btn {{canSubmit ? 'active' : ''}}"
bindtap="onSubmit"
disabled="{{!canSubmit || submitting}}"
>
{{submitting ? '保存中...' : '保存修改'}}
</button>
</view>
</view>
</scroll-view>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading}}">
<view class="loading-content">
<text class="loading-text">加载中...</text>
</view>
</view>

@ -0,0 +1,175 @@
/* pages/admin-user-edit/admin-user-edit.wxss */
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.scroll-container {
flex: 1;
}
.form-container {
padding: 30rpx;
padding-bottom: 100rpx;
}
.form-section {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 30rpx;
}
.section-label {
font-size: 28rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 20rpx;
}
/* 头像部分 */
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
}
.avatar {
width: 200rpx;
height: 200rpx;
border-radius: 50%;
margin-bottom: 30rpx;
border: 4rpx solid #f0f0f0;
}
.avatar-upload {
padding: 15rpx 40rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 50rpx;
font-size: 26rpx;
}
/* 表单组 */
.form-group {
margin-bottom: 30rpx;
}
.form-group:last-child {
margin-bottom: 0;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
font-weight: 500;
}
.input {
width: 100%;
height: 88rpx;
background: #f5f5f5;
border-radius: 15rpx;
padding: 0 25rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
.input[disabled] {
background: #e0e0e0;
color: #999;
}
/* 选择器 */
.picker-input {
width: 100%;
height: 88rpx;
background: #f5f5f5;
border-radius: 15rpx;
padding: 0 25rpx;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 28rpx;
color: #333;
}
.picker-input .placeholder {
color: #999;
}
.picker-arrow {
font-size: 32rpx;
color: #999;
}
/* 提交按钮 */
.submit-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #e0e0e0;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.submit-btn {
width: 100%;
height: 88rpx;
background: #ccc;
color: white;
border-radius: 50rpx;
font-size: 32rpx;
font-weight: 600;
border: none;
transition: all 0.3s ease;
}
.submit-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.submit-btn[disabled] {
opacity: 0.6;
}
/* 加载状态 */
.loading-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
text-align: center;
}
.loading-text {
font-size: 28rpx;
color: #666;
}

@ -0,0 +1,150 @@
// pages/admin-users/admin-users.js
Page({
data: {
users: [],
keyword: '',
page: 0,
pageSize: 20,
hasMore: true,
loading: true,
refreshing: false
},
onLoad() {
// 检查管理员登录状态
const adminInfo = wx.getStorageSync('adminInfo');
if (!adminInfo) {
wx.redirectTo({
url: '/pages/admin-login/admin-login'
});
return;
}
this.loadUsers();
},
/**
* 加载用户列表
*/
async loadUsers(refresh = false) {
if (refresh) {
this.setData({
page: 0,
users: [],
hasMore: true
});
}
if (!this.data.hasMore && !refresh) {
return;
}
this.setData({
loading: true
});
try {
const result = await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'adminGetUsers',
page: this.data.page,
pageSize: this.data.pageSize,
keyword: this.data.keyword
}
});
if (result.result && result.result.success) {
const users = result.result.data.users.map(item => ({
...item,
timeText: this.formatTime(item.createTime)
}));
this.setData({
users: refresh ? users : [...this.data.users, ...users],
page: this.data.page + 1,
hasMore: users.length === this.data.pageSize,
loading: false,
refreshing: false
});
} else {
throw new Error(result.result?.error || '获取用户列表失败');
}
} catch (err) {
console.error('加载用户列表失败:', err);
wx.showToast({
title: '加载失败',
icon: 'none'
});
this.setData({
loading: false,
refreshing: false
});
}
},
/**
* 格式化时间
*/
formatTime(date) {
if (!date) return '';
const d = new Date(date);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({
keyword: e.detail.value
});
},
/**
* 搜索
*/
onSearch() {
this.loadUsers(true);
},
/**
* 下拉刷新
*/
onRefresh() {
this.setData({
refreshing: true
});
this.loadUsers(true);
},
/**
* 加载更多
*/
onLoadMore() {
if (!this.data.loading && this.data.hasMore) {
this.loadUsers();
}
},
/**
* 编辑用户
*/
onEditUser(e) {
const userId = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/admin-user-edit/admin-user-edit?id=${userId}`
});
},
/**
* 页面显示时刷新
*/
onShow() {
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
if (prevPage && (prevPage.route === 'pages/admin-user-edit/admin-user-edit')) {
this.loadUsers(true);
}
}
});

@ -0,0 +1,6 @@
{
"navigationBarTitleText": "用户管理",
"navigationBarBackgroundColor": "#667eea",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true
}

@ -0,0 +1,46 @@
<!--pages/admin-users/admin-users.wxml-->
<view class="page-container">
<!-- 顶部搜索栏 -->
<view class="search-header">
<view class="search-bar">
<text class="search-icon">🔍</text>
<input class="search-input" placeholder="搜索学号、姓名、手机号..." value="{{keyword}}" bindinput="onSearchInput" bindconfirm="onSearch"/>
</view>
</view>
<!-- 用户列表 -->
<scroll-view class="user-list" scroll-y="true" refresher-enabled="{{true}}" refresher-triggered="{{refreshing}}" bindrefresherrefresh="onRefresh" bindscrolltolower="onLoadMore">
<view class="user-item" wx:for="{{users}}" wx:key="_id">
<image class="user-avatar" src="{{item.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U'}}" mode="aspectFill"></image>
<view class="user-info">
<text class="user-name">{{item.sname || '未设置'}}</text>
<text class="user-sno">学号: {{item.sno || '未设置'}}</text>
<text class="user-phone">手机: {{item.phone || '未设置'}}</text>
<text class="user-major">专业: {{item.major || '未设置'}}</text>
<text class="user-time">注册时间: {{item.timeText}}</text>
</view>
<view class="user-actions">
<view class="action-btn edit" bindtap="onEditUser" data-id="{{item._id}}">编辑</view>
</view>
</view>
<view class="load-more" wx:if="{{hasMore && !loading}}">
<text>加载中...</text>
</view>
<view class="no-more" wx:if="{{!hasMore && users.length > 0}}">
<text>没有更多了</text>
</view>
<view class="empty-state" wx:if="{{!loading && users.length === 0}}">
<text class="empty-icon">👥</text>
<text class="empty-text">暂无用户</text>
</view>
</scroll-view>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading && users.length === 0}}">
<view class="loading-content">
<text class="loading-text">加载中...</text>
</view>
</view>

@ -0,0 +1,145 @@
/* pages/admin-users/admin-users.wxss */
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.search-header {
background: white;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #e0e0e0;
}
.search-bar {
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 50rpx;
padding: 15rpx 25rpx;
}
.search-icon {
font-size: 28rpx;
margin-right: 15rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
}
.user-list {
flex: 1;
padding: 20rpx;
}
.user-item {
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.user-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
margin-right: 20rpx;
flex-shrink: 0;
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
}
.user-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
}
.user-sno,
.user-phone,
.user-major {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
}
.user-time {
font-size: 22rpx;
color: #999;
margin-top: 10rpx;
}
.user-actions {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 20rpx;
}
.action-btn {
padding: 15rpx 30rpx;
background: #E3F2FD;
color: #2196F3;
border-radius: 10rpx;
font-size: 26rpx;
text-align: center;
min-width: 100rpx;
}
.load-more,
.no-more {
text-align: center;
padding: 30rpx 0;
color: #999;
font-size: 24rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
}
.empty-text {
font-size: 32rpx;
color: #666;
}
.loading-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
text-align: center;
}
.loading-text {
font-size: 28rpx;
color: #666;
}

@ -0,0 +1,610 @@
// pages/buy/buy.js
const reco = require('../../utils/recommendation.js');
Page({
/**
* 页面的初始数据
*/
data: {
// 搜索相关
searchKeyword: '',
// 筛选条件
categories: ['全部分类', '电子产品', '图书文具', '服装鞋帽', '运动户外', '美妆个护', '家居生活', '其他'],
selectedCategory: 0,
categoryNavItems: [
{ id: 0, name: '全部' },
{ id: 1, name: '电子产品' },
{ id: 2, name: '图书文具' },
{ id: 3, name: '服装鞋帽' },
{ id: 4, name: '运动户外' },
{ id: 5, name: '美妆个护' },
{ id: 6, name: '家居生活' },
{ id: 7, name: '其他' }
],
selectedCategoryIndex: 0,
priceRanges: ['价格范围', '0-50元', '50-100元', '100-200元', '200-500元', '500元以上'],
selectedPriceRange: 0,
sortOptions: ['最新发布', '价格从低到高', '价格从高到低', '评分最高'],
selectedSort: 0,
// 商品列表
products: [],
filteredProducts: [],
// 分页相关
currentPage: 1,
pageSize: 10,
hasMore: true,
// 页面状态
refreshing: false,
loading: false
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
if (options && options.q) {
try {
const keyword = decodeURIComponent(options.q);
this.setData({
searchKeyword: keyword
});
} catch (e) {
// 保底:直接使用传入值
this.setData({
searchKeyword: options.q
});
}
}
this.loadProducts();
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 页面显示时刷新数据,确保显示最新发布的商品
this.loadProducts(true);
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
this.onRefresh();
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
this.loadMore();
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
return {
title: '校园二手交易 - 发现好物',
path: '/pages/buy/buy'
}
},
/**
* 搜索输入事件
*/
onSearchInput(e) {
this.setData({
searchKeyword: e.detail.value
});
// 实时搜索,不需要点击搜索按钮
this.filterProducts();
},
/**
* 搜索按钮点击事件
*/
onSearch() {
// 若存在关键词,则刷新并按关键词从后端查询;否则仅本地筛选
const kw = (this.data.searchKeyword || '').trim();
if (kw) {
try {
const reco = require('../../utils/recommendation.js');
reco.recordSearch(kw, 'product');
} catch (e) {}
this.loadProducts(true);
} else {
this.filterProducts();
}
},
/**
* 清空搜索
*/
onClearSearch() {
this.setData({
searchKeyword: ''
});
this.filterProducts();
},
/**
* 分类导航点击事件
*/
onCategoryNavTap(e) {
const index = parseInt(e.currentTarget.dataset.index);
this.setData({
selectedCategoryIndex: index,
selectedCategory: index
});
this.filterProducts();
},
/**
* 分类选择事件
*/
onCategoryChange(e) {
const index = e.detail.value;
this.setData({
selectedCategory: index,
selectedCategoryIndex: index
});
this.filterProducts();
},
/**
* 价格范围选择事件
*/
onPriceRangeChange(e) {
this.setData({
selectedPriceRange: e.detail.value
});
this.filterProducts();
},
/**
* 排序方式选择事件
*/
onSortChange() {
wx.showActionSheet({
itemList: this.data.sortOptions,
success: (res) => {
this.setData({
selectedSort: res.tapIndex
});
this.sortProducts();
}
});
},
/**
* 商品点击事件
*/
onProductTap(e) {
const product = e.currentTarget.dataset.product;
try { reco.recordClick(product); } catch (e) {}
wx.navigateTo({
url: `/pages/product-detail/product-detail?id=${product.id}`
});
},
/**
* 下拉刷新
*/
onRefresh() {
this.setData({
refreshing: true,
currentPage: 1,
hasMore: true
});
// 重新加载数据
this.loadProducts(true);
this.setData({
refreshing: false
});
wx.stopPullDownRefresh();
},
/**
* 加载更多
*/
loadMore() {
if (this.data.loading || !this.data.hasMore) {
return;
}
// 进入加载状态,并推进到下一页,改为真实数据库分页查询
const nextPage = this.data.currentPage + 1;
this.setData({
loading: true,
currentPage: nextPage
});
// 加载下一页商品
this.loadProducts(false);
},
/**
* 加载商品数据
*/
loadProducts(refresh = false) {
const db = wx.cloud.database();
const { currentPage, pageSize, selectedCategory, categories, searchKeyword } = this.data;
const _ = db.command;
// 如果是刷新,重置页码
const page = refresh ? 1 : currentPage;
this.setData({
loading: true
});
// 构建查询条件
let query = db.collection('T_product').where({
status: '在售' // 只显示在售商品
});
// 分类筛选
if (selectedCategory > 0 && categories[selectedCategory]) {
const category = categories[selectedCategory];
query = query.where({
productCategory: category
});
}
// 关键词搜索(商品名称或描述匹配,忽略大小写)
const kw = (searchKeyword || '').trim();
if (kw) {
const re = db.RegExp({ regexp: kw, options: 'i' });
query = query.where(
_.or([
{ productName: re },
{ productDescription: re }
])
);
}
// 分页查询
query = query
.orderBy('createTime', 'desc') // 按创建时间倒序
.skip((page - 1) * pageSize)
.limit(pageSize);
query.get({
success: (res) => {
console.log('查询商品成功:', res);
if (res.data && res.data.length > 0) {
// 处理商品数据
this.processProducts(res.data).then((processedProducts) => {
// 合并分页结果并按 id 去重,避免 wx:key 重复
const merged = refresh ? processedProducts : [...this.data.products, ...processedProducts];
const seen = new Set();
const products = [];
for (const p of merged) {
const key = p.id || p._id || p._originalData?._id;
if (!key) continue;
if (seen.has(key)) continue;
seen.add(key);
products.push(p);
}
this.setData({
products: products,
filteredProducts: products,
currentPage: page,
hasMore: res.data.length === pageSize,
loading: false
});
// 应用筛选和排序
this.filterProducts();
});
} else {
// 没有数据
this.setData({
hasMore: false,
loading: false
});
if (refresh) {
this.setData({
products: [],
filteredProducts: []
});
}
}
},
fail: (err) => {
console.error('查询商品失败:', err);
wx.showToast({
title: '加载失败,请重试',
icon: 'none',
duration: 2000
});
this.setData({
loading: false
});
}
});
},
/**
* 筛选商品
*/
filterProducts() {
let filtered = this.data.products;
// 关键词搜索
if (this.data.searchKeyword) {
const keyword = this.data.searchKeyword.toLowerCase();
filtered = filtered.filter(product =>
product.name.toLowerCase().includes(keyword) ||
product.description.toLowerCase().includes(keyword) ||
product.category.includes(keyword)
);
}
// 分类筛选
if (this.data.selectedCategory > 0) {
const category = this.data.categories[this.data.selectedCategory];
filtered = filtered.filter(product => product.category === category);
}
// 价格范围筛选
if (this.data.selectedPriceRange > 0) {
const priceRange = this.data.priceRanges[this.data.selectedPriceRange];
filtered = filtered.filter(product => {
const price = parseFloat(product.price);
switch (priceRange) {
case '0-50元': return price <= 50;
case '50-100元': return price > 50 && price <= 100;
case '100-200元': return price > 100 && price <= 200;
case '200-500元': return price > 200 && price <= 500;
case '500元以上': return price > 500;
default: return true;
}
});
}
this.setData({
filteredProducts: filtered
});
this.sortProducts();
},
/**
* 处理商品数据转换格式并获取图片URL
*/
async processProducts(products) {
const processedProducts = await Promise.all(products.map(async (product) => {
// 获取图片临时URL
let imageUrl = '/images/仓鼠.png'; // 默认本地图片,避免外网占位图超时
if (product.productImage) {
try {
// 如果是云存储路径获取临时URL
if (product.productImage.startsWith('cloud://')) {
const tempFileURL = await wx.cloud.getTempFileURL({
fileList: [product.productImage]
});
if (tempFileURL.fileList && tempFileURL.fileList.length > 0) {
imageUrl = tempFileURL.fileList[0].tempFileURL;
}
} else {
// 如果是网络URL直接使用
imageUrl = product.productImage;
}
} catch (err) {
console.error('获取图片URL失败:', err);
}
}
// 计算折扣
let discount = null;
if (product.originalPrice > 0 && product.salePrice > 0) {
discount = (product.salePrice / product.originalPrice * 10).toFixed(1);
}
// 格式化时间
const publishTime = this.formatTime(product.createTime);
// 获取卖家信息
const sellerName = product.sellerInfo?.sname || '用户';
const sellerAvatar = product.sellerInfo?.avatar || '/images/更多犬种.png';
// 返回格式化后的商品数据
return {
id: product._id,
name: product.productName || '未命名商品',
description: product.productDescription || '',
price: product.salePrice || product.suggestedPrice || 0,
originalPrice: product.originalPrice || null,
discount: discount,
category: product.productCategory || '其他',
condition: product.conditionLevel || '未知',
location: '校园', // 可以根据需要添加location字段
publishTime: publishTime,
image: imageUrl,
sellerAvatar: sellerAvatar,
sellerName: sellerName,
sellerRating: '4.8', // 可以根据需要添加评分系统
status: product.status === '在售' ? 'selling' : 'sold',
// 保留原始数据字段,方便后续使用
_originalData: product
};
}));
return processedProducts;
},
/**
* 格式化时间显示
*/
formatTime(date) {
if (!date) return '';
const now = new Date();
const createTime = date instanceof Date ? date : new Date(date);
const diff = now - createTime;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (seconds < 60) {
return '刚刚';
} else if (minutes < 60) {
return `${minutes}分钟前`;
} else if (hours < 24) {
return `${hours}小时前`;
} else if (days < 7) {
return `${days}天前`;
} else {
// 超过7天显示具体日期
const month = createTime.getMonth() + 1;
const day = createTime.getDate();
return `${month}${day}`;
}
},
/**
* 排序商品
*/
sortProducts() {
let sorted = [...this.data.filteredProducts];
switch (this.data.selectedSort) {
case 0: // 最新发布
sorted.sort((a, b) => {
const timeA = a._originalData?.createTime || new Date(0);
const timeB = b._originalData?.createTime || new Date(0);
return new Date(timeB) - new Date(timeA);
});
break;
case 1: // 价格从低到高
sorted.sort((a, b) => parseFloat(a.price || 0) - parseFloat(b.price || 0));
break;
case 2: // 价格从高到低
sorted.sort((a, b) => parseFloat(b.price || 0) - parseFloat(a.price || 0));
break;
case 3: // 评分最高(暂时使用固定评分)
sorted.sort((a, b) => parseFloat(b.sellerRating || 0) - parseFloat(a.sellerRating || 0));
break;
}
this.setData({
filteredProducts: sorted
});
},
/**
* 生成模拟商品数据
*/
generateMockProducts(page, pageSize) {
if (page > 3) return []; // 模拟只有3页数据
const mockProducts = [
{
id: 1,
name: 'iPhone 13 Pro Max',
description: '99新256GB国行在保无划痕无磕碰',
price: '5999',
originalPrice: '8999',
discount: '6.7',
category: '电子产品',
condition: '99新',
location: '闵行校区',
publishTime: '2小时前',
image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=400&q=60',
sellerAvatar: 'https://via.placeholder.com/100x100/CCCCCC/444444?text=U',
sellerName: '张同学',
sellerRating: '4.8',
status: 'selling'
},
{
id: 2,
name: '高等数学教材',
description: '同济大学第七版,几乎全新,有少量笔记',
price: '25',
category: '图书文具',
condition: '95新',
location: '徐汇校区',
publishTime: '1天前',
image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=400&q=60',
sellerAvatar: 'https://via.placeholder.com/100x100/CCCCCC/444444?text=U',
sellerName: '李同学',
sellerRating: '4.9',
status: 'selling'
},
{
id: 3,
name: 'Nike Air Force 1',
description: '白色经典款42码穿过几次保养很好',
price: '299',
originalPrice: '799',
discount: '3.7',
category: '运动户外',
condition: '85新',
location: '松江校区',
publishTime: '3天前',
image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=400&q=60',
sellerAvatar: 'https://via.placeholder.com/100x100/CCCCCC/444444?text=U',
sellerName: '王同学',
sellerRating: '4.7',
status: 'selling'
},
{
id: 4,
name: 'MacBook Pro 2021',
description: 'M1芯片16GB内存512GB硬盘性能强劲',
price: '8999',
originalPrice: '12999',
discount: '6.9',
category: '电子产品',
condition: '98新',
location: '闵行校区',
publishTime: '5小时前',
image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=400&q=60',
sellerAvatar: 'https://via.placeholder.com/100x100/CCCCCC/444444?text=U',
sellerName: '赵同学',
sellerRating: '4.9',
status: 'selling'
},
{
id: 5,
name: 'SK-II神仙水',
description: '230ml正品保证还剩大半瓶个人原因转让',
price: '499',
originalPrice: '1540',
discount: '3.2',
category: '美妆个护',
condition: '90新',
location: '徐汇校区',
publishTime: '1天前',
image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=400&q=60',
sellerAvatar: 'https://via.placeholder.com/100x100/CCCCCC/444444?text=U',
sellerName: '陈同学',
sellerRating: '4.8',
status: 'selling'
}
];
// 根据页码返回数据
const startIndex = (page - 1) * pageSize;
return mockProducts.slice(startIndex, startIndex + pageSize);
}
})

@ -0,0 +1,8 @@
{
"usingComponents": {},
"navigationBarTitleText": "商品购买",
"navigationBarBackgroundColor": "#4285F4",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}

@ -0,0 +1,155 @@
<!-- 商品购买页面 -->
<view class="container">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input">
<icon type="search" size="30" class="search-icon"></icon>
<input
type="text"
placeholder="搜索商品名称、类别..."
bindinput="onSearchInput"
bindconfirm="onSearch"
confirm-type="search"
value="{{searchKeyword}}"
class="search-field"
/>
<view class="search-cancel" bindtap="onClearSearch" wx:if="{{searchKeyword}}">
<text>取消</text>
</view>
<button class="search-btn" bindtap="onSearch">搜索</button>
</view>
</view>
<!-- 筛选条件 -->
<view class="filter-bar">
<picker
range="{{categories}}"
value="{{selectedCategory}}"
bindchange="onCategoryChange"
class="filter-item"
>
<view class="filter-display">
<text>{{categories[selectedCategory] || '全部分类'}}</text>
<icon type="arrow-down" size="12"></icon>
</view>
</picker>
<picker
range="{{priceRanges}}"
value="{{selectedPriceRange}}"
bindchange="onPriceRangeChange"
class="filter-item"
>
<view class="filter-display">
<text>{{priceRanges[selectedPriceRange] || '价格范围'}}</text>
<icon type="arrow-down" size="12"></icon>
</view>
</picker>
<view class="filter-item" bindtap="onSortChange">
<view class="filter-display">
<text>{{sortOptions[selectedSort]}}</text>
<icon type="arrow-down" size="12"></icon>
</view>
</view>
</view>
<!-- 商品列表 -->
<scroll-view
class="product-list"
scroll-y
bindscrolltolower="onReachBottom"
refresher-enabled="true"
bindrefresherrefresh="onRefresh"
refresher-triggered="{{refreshing}}"
>
<!-- 商品网格容器 -->
<view class="product-grid">
<!-- 商品卡片 -->
<view
class="product-card"
wx:for="{{filteredProducts}}"
wx:key="id"
bindtap="onProductTap"
data-product="{{item}}"
>
<!-- 商品图片 -->
<view class="product-image-container">
<image
src="{{item.image}}"
mode="aspectFill"
class="product-image"
/>
<view class="product-status" wx:if="{{item.status === 'sold'}}">
<text>已售出</text>
</view>
</view>
<!-- 商品信息 -->
<view class="product-info">
<!-- 卖家信息 -->
<view class="seller-info">
<image
src="{{item.sellerAvatar}}"
mode="aspectFill"
class="seller-avatar"
/>
<text class="seller-name">{{item.sellerName}}</text>
<view class="seller-rating" wx:if="{{item.sellerRating}}">
<text class="rating-text">{{item.sellerRating}}</text>
<icon type="star" size="12" class="star-icon"></icon>
</view>
</view>
<!-- 商品标题 -->
<view class="product-title">
<text class="title-text">{{item.name}}</text>
</view>
<!-- 商品描述 -->
<view class="product-description">
<text class="description-text">{{item.description}}</text>
</view>
<!-- 价格信息 -->
<view class="price-info">
<text class="current-price">¥{{item.price}}</text>
<text class="original-price" wx:if="{{item.originalPrice}}">¥{{item.originalPrice}}</text>
<view class="discount-badge" wx:if="{{item.discount}}">
<text>{{item.discount}}折</text>
</view>
</view>
<!-- 商品标签 -->
<view class="product-tags">
<text class="tag category-tag">{{item.category}}</text>
<text class="tag condition-tag">{{item.condition}}</text>
<text class="tag location-tag">{{item.location}}</text>
</view>
<!-- 发布时间 -->
<view class="publish-time">
<text class="time-text">{{item.publishTime}}</text>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" wx:if="{{hasMore}}">
<text>加载中...</text>
</view>
<!-- 没有更多数据 -->
<view class="no-more" wx:if="{{!hasMore && filteredProducts.length > 0}}">
<text>没有更多商品了</text>
</view>
<!-- 空状态 -->
<view class="empty-state" wx:if="{{filteredProducts.length === 0 && !loading}}">
<view class="empty-icon">📦</view>
<text class="empty-text">暂无商品</text>
<text class="empty-subtext">换个筛选条件试试</text>
</view>
</scroll-view>
</view>

@ -0,0 +1,348 @@
/* 商品购买页面样式 */
.container {
height: 100vh;
background-color: #f5f5f5;
padding-top: 0;
}
/* 搜索栏样式 */
.search-bar {
padding: 0;
background-color: #fff;
border-bottom: 1rpx solid #e0e0e0;
}
.search-input {
display: flex;
align-items: center;
background-color: #f8f9fa;
border-radius: 0;
padding: 20rpx;
height: 100rpx;
}
.search-icon {
margin-right: 20rpx;
color: #999;
}
.search-field {
flex: 1;
font-size: 32rpx;
color: #333;
height: 60rpx;
line-height: 60rpx;
}
.search-cancel {
padding: 10rpx 20rpx;
background-color: #e9ecef;
border-radius: 30rpx;
font-size: 28rpx;
color: #666;
margin-right: 20rpx;
height: 60rpx;
line-height: 40rpx;
}
.search-btn {
background-color: #007bff;
color: #fff;
border: none;
border-radius: 30rpx;
padding: 10rpx 30rpx;
font-size: 28rpx;
line-height: 1;
margin-left: 20rpx;
height: 60rpx;
min-width: 120rpx;
}
/* 分类导航 */
.category-nav {
display: flex;
white-space: nowrap;
padding: 24rpx 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.category-item {
padding: 12rpx 32rpx;
margin-right: 20rpx;
border-radius: 40rpx;
background-color: #f8f9fa;
border: 1rpx solid #e9ecef;
font-size: 28rpx;
color: #6c757d;
transition: all 0.3s ease;
flex-shrink: 0;
}
.category-item.active {
background-color: #007bff;
border-color: #007bff;
color: #fff;
}
.category-text {
font-size: 28rpx;
font-weight: 500;
}
/* 筛选栏样式 */
.filter-bar {
display: flex;
background-color: #fff;
border-bottom: 1rpx solid #e0e0e0;
padding: 20rpx;
}
.filter-item {
flex: 1;
text-align: center;
font-size: 26rpx;
color: #666;
}
.filter-display {
display: flex;
align-items: center;
justify-content: center;
}
/* 商品列表样式 */
.product-list {
height: calc(100vh - 180rpx);
}
/* 商品网格容器 */
.product-grid {
display: flex;
flex-wrap: wrap;
padding: 10rpx;
gap: 10rpx;
box-sizing: border-box;
}
/* 商品卡片样式 */
.product-card {
width: calc(50% - 8rpx);
background-color: #fff;
border-radius: 8rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 10rpx;
box-sizing: border-box;
flex-shrink: 0;
}
.product-image-container {
position: relative;
height: 200rpx;
}
.product-image {
width: 100%;
height: 100%;
}
.product-status {
position: absolute;
top: 10rpx;
right: 10rpx;
background-color: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 4rpx 8rpx;
border-radius: 10rpx;
font-size: 18rpx;
}
/* 商品信息样式 */
.product-info {
padding: 15rpx;
}
/* 卖家信息样式 */
.seller-info {
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.seller-avatar {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
margin-right: 8rpx;
}
.seller-name {
font-size: 22rpx;
color: #666;
margin-right: 8rpx;
}
.seller-rating {
display: flex;
align-items: center;
background-color: #fff8e1;
padding: 2rpx 6rpx;
border-radius: 8rpx;
}
.rating-text {
font-size: 18rpx;
color: #ff9800;
margin-right: 3rpx;
}
.star-icon {
color: #ff9800;
font-size: 16rpx;
}
/* 商品标题样式 */
.product-title {
margin-bottom: 8rpx;
}
.title-text {
font-size: 26rpx;
font-weight: bold;
color: #333;
line-height: 1.3;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
/* 商品描述样式 */
.product-description {
margin-bottom: 10rpx;
}
.description-text {
font-size: 22rpx;
color: #666;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
/* 价格信息样式 */
.price-info {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.current-price {
font-size: 28rpx;
font-weight: bold;
color: #ff4444;
margin-right: 8rpx;
}
.original-price {
font-size: 20rpx;
color: #999;
text-decoration: line-through;
margin-right: 8rpx;
}
.discount-badge {
background-color: #ff4444;
color: #fff;
padding: 2rpx 6rpx;
border-radius: 6rpx;
font-size: 18rpx;
}
/* 商品标签样式 */
.product-tags {
display: flex;
flex-wrap: wrap;
margin-bottom: 8rpx;
}
.tag {
font-size: 18rpx;
padding: 2rpx 6rpx;
border-radius: 6rpx;
margin-right: 6rpx;
margin-bottom: 4rpx;
}
.category-tag {
background-color: #e3f2fd;
color: #1976d2;
}
.condition-tag {
background-color: #f3e5f5;
color: #7b1fa2;
}
.location-tag {
background-color: #e8f5e8;
color: #388e3c;
}
/* 发布时间样式 */
.publish-time {
font-size: 18rpx;
color: #999;
}
/* 加载更多样式 */
.load-more, .no-more {
text-align: center;
padding: 40rpx;
font-size: 26rpx;
color: #999;
}
/* 空状态样式 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 40rpx;
}
.empty-image {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
}
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 15rpx;
}
.empty-subtext {
font-size: 26rpx;
color: #999;
}
/* 响应式调整 */
@media (max-width: 750rpx) {
.product-card {
margin: 15rpx;
}
.product-info {
padding: 20rpx;
}
.product-image-container {
height: 250rpx;
}
}

@ -0,0 +1,279 @@
// pages/cart/cart.js
const reco = require('../../utils/recommendation.js');
Page({
data: {
items: [],
totalPrice: '0.00'
},
onShow() {
this.loadCart();
try {
if (this.getTabBar && this.getTabBar()) {
this.getTabBar().setSelected(2);
}
} catch (e) {}
},
loadCart() {
try {
const cart = wx.getStorageSync('cart') || [];
const items = (cart || []).map(it => ({
id: it.id,
name: it.name || '商品',
price: Number(it.price || 0),
image: it.image || '/images/仓鼠.png',
category: it.category || '其他',
sellerName: it.sellerName || '用户',
count: Number(it.count || 1)
}));
this.setData({ items });
this.updateTotal();
} catch (e) {
console.error('加载购物车失败:', e);
}
},
updateTotal() {
const sum = this.data.items.reduce((s, it) => s + (Number(it.price) || 0) * (Number(it.count) || 1), 0);
this.setData({ totalPrice: sum.toFixed(2) });
},
incQty(e) {
const idx = e.currentTarget.dataset.index;
const items = [...this.data.items];
items[idx].count = Math.min(99, (items[idx].count || 1) + 1);
this.setData({ items });
wx.setStorageSync('cart', items);
this.updateTotal();
},
decQty(e) {
const idx = e.currentTarget.dataset.index;
const items = [...this.data.items];
items[idx].count = Math.max(1, (items[idx].count || 1) - 1);
this.setData({ items });
wx.setStorageSync('cart', items);
this.updateTotal();
},
removeItem(e) {
const idx = e.currentTarget.dataset.index;
const items = [...this.data.items];
items.splice(idx, 1);
this.setData({ items });
wx.setStorageSync('cart', items);
this.updateTotal();
},
clearCart() {
wx.showModal({
title: '清空购物车',
content: '确定要清空所有商品吗?',
success: (res) => {
if (res.confirm) {
wx.setStorageSync('cart', []);
this.setData({ items: [], totalPrice: '0.00' });
}
}
});
},
onItemTap(e) {
try {
const id = e.currentTarget.dataset.id;
const p = (this.data.items || []).find(x => x.id === id);
if (p) { try { reco.recordClick({ _id: p.id, productName: p.name, productCategory: p.category }); } catch (_) {} }
if (!id) return;
wx.navigateTo({ url: `/pages/product-detail/product-detail?id=${id}` });
} catch (err) { /* 忽略 */ }
},
goShopping() {
wx.switchTab({ url: '/pages/main/main' });
},
async ensureOpenId() {
let openid = wx.getStorageSync('openid');
if (!openid) {
try {
const result = await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'getOpenId' } });
if (result.result && result.result.openid) {
openid = result.result.openid;
wx.setStorageSync('openid', openid);
}
} catch (err) { console.error('获取openid失败:', err); }
}
return openid;
},
async checkout() {
const items = this.data.items;
if (!items || items.length === 0) {
wx.showToast({ title: '购物车为空', icon: 'none' });
return;
}
wx.showLoading({ title: '创建订单...', mask: true });
try {
const db = wx.cloud.database();
const userInfo = wx.getStorageSync('userInfo') || {};
const buyerUserId = userInfo._id || '';
let buyerOpenId = '';
if (buyerUserId) {
try {
const userRes = await db.collection('T_user').doc(buyerUserId).get();
buyerOpenId = userRes.data && userRes.data._openid ? userRes.data._openid : '';
} catch (err) { console.warn('通过用户ID获取openid失败:', err); }
}
if (!buyerOpenId) buyerOpenId = await this.ensureOpenId();
if (!buyerOpenId) {
wx.hideLoading();
wx.showToast({ title: '请先登录', icon: 'none' });
return;
}
// 拉取商品详情,确认在售,并获取卖家信息
const productDocs = await Promise.all(items.map(async it => {
try {
const res = await db.collection('T_product').doc(it.id).get();
return res.data || null;
} catch (e) { return null; }
}));
// 过滤不可购买的商品
const valid = [];
const removedIds = [];
for (let i = 0; i < items.length; i++) {
const it = items[i];
const doc = productDocs[i];
if (!doc || doc.status !== '在售') {
removedIds.push(it.id);
} else {
valid.push({ it, doc });
}
}
if (removedIds.length > 0) {
wx.showToast({ title: `部分商品不可购买,已移除`, icon: 'none' });
}
if (valid.length === 0) {
wx.hideLoading();
// 移除不可购买商品
const still = items.filter(x => !removedIds.includes(x.id));
wx.setStorageSync('cart', still);
this.setData({ items: still });
this.updateTotal();
return;
}
// 按卖家分组生成订单
const groups = new Map();
valid.forEach(({ it, doc }) => {
const key = doc.sellerUserId || doc.sellerOpenId || ('unknown-' + (doc._id || it.id));
if (!groups.has(key)) groups.set(key, { sellerUserId: doc.sellerUserId || '', sellerOpenId: doc.sellerOpenId || '', sellerName: (doc.sellerInfo && doc.sellerInfo.name) || '用户', sellerPhone: (doc.sellerInfo && doc.sellerInfo.phone) || (doc.contactInfo || ''), items: [] });
groups.get(key).items.push({ it, doc });
});
const createdOrderIds = [];
for (const [key, group] of groups.entries()) {
const products = group.items.map(({ it, doc }) => ({
productId: doc._id,
name: doc.productName || it.name,
price: Number(doc.salePrice || doc.suggestedPrice || it.price || 0),
count: Number(it.count || 1),
image: Array.isArray(doc.productImage) ? (doc.productImage[0] || it.image) : (doc.productImage || it.image),
specs: doc.productCategory || it.category || '标准'
}));
const totalPrice = products.reduce((s, p) => s + (p.price || 0) * (p.count || 1), 0);
const orderData = {
orderNumber: `ORD${Date.now()}${Math.random().toString(36).substr(2, 5).toUpperCase()}`,
status: '待付款',
buyerOpenId: buyerOpenId,
buyerUserId: buyerUserId,
sellerOpenId: group.sellerOpenId,
sellerUserId: group.sellerUserId,
sellerName: group.sellerName,
sellerPhone: group.sellerPhone,
products,
totalPrice,
totalCount: products.reduce((s, p) => s + (Number(p.count) || 0), 0),
transactionMethod: '面交',
createTime: new Date(),
updateTime: new Date()
};
const orderRes = await db.collection('T_order').add({ data: orderData });
createdOrderIds.push(orderRes._id);
// 将本组商品状态更新为交易中
await Promise.all(group.items.map(({ doc }) => db.collection('T_product').doc(doc._id).update({ data: { status: '交易中', updateTime: new Date() } })));
// 写入卖家通知
try {
await db.collection('T_notify').add({ data: {
productId: group.items[0].doc._id,
productName: group.items[0].doc.productName || '商品',
orderId: orderRes._id || '',
sellerUserId: group.sellerUserId || '',
sellerOpenId: group.sellerOpenId || '',
type: 'purchase',
content: '有新的购物车订单,请及时处理',
status: 'unread',
createTime: new Date(),
updateTime: new Date()
} });
} catch (e) {
// 忽略通知错误
}
// 在聊天中通知该卖家:买家已下单(购物车结算)
try {
if (buyerOpenId && group.sellerOpenId) {
const sessionKey = [buyerOpenId, group.sellerOpenId].sort().join('|');
const sessionKeyUser = (buyerUserId && group.sellerUserId) ? [buyerUserId, group.sellerUserId].sort().join('|') : '';
await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'sendChatMessage',
contentType: 'text',
content: `[系统] 买家已下单(购物车):订单号 ${orderData.orderNumber},合计 ¥${orderData.totalPrice},件数 ${orderData.totalCount}`,
toUserId: group.sellerUserId || '',
toOpenId: group.sellerOpenId || '',
productId: group.items[0].doc._id || '',
orderId: orderRes._id || '',
isSystem: true
}
});
}
} catch (e) {
console.warn('写入购物车下单系统消息失败:', e);
}
}
// 结算成功:从购物车移除已下单的商品
const orderedIds = valid.map(v => v.it.id);
const remaining = items.filter(x => !orderedIds.includes(x.id));
wx.setStorageSync('cart', remaining);
this.setData({ items: remaining });
this.updateTotal();
wx.hideLoading();
wx.showModal({
title: '下单成功',
content: '订单已创建,请前往订单页查看并支付',
showCancel: false,
confirmText: '查看订单',
success: () => {
wx.navigateTo({ url: '/pages/orders/orders?tab=pending' });
}
});
} catch (err) {
console.error('购物车结算失败:', err);
wx.hideLoading();
wx.showToast({ title: '结算失败', icon: 'none' });
}
}
});

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "购物车",
"usingComponents": {}
}

@ -0,0 +1,41 @@
<view class="cart-page">
<view class="cart-header">
<text class="title">购物车</text>
<text class="count" wx:if="{{items.length > 0}}">共 {{items.length}} 件</text>
</view>
<view wx:if="{{items.length === 0}}" class="empty">
<text>购物车空空如也,去逛逛吧~</text>
<button class="go-btn" bindtap="goShopping">去购物</button>
</view>
<scroll-view wx:if="{{items.length > 0}}" scroll-y="true" class="list">
<view class="item" wx:for="{{items}}" wx:key="id" data-index="{{index}}" bindtap="onItemTap" data-id="{{item.id}}">
<image class="thumb" src="{{item.image}}" mode="aspectFill" />
<view class="info">
<text class="name">{{item.name}}</text>
<text class="category">{{item.category}}</text>
<view class="price-line">
<text class="price">¥{{item.price}}</text>
<view class="qty">
<button class="minus" catchtap="decQty" data-index="{{index}}">-</button>
<text class="num">{{item.count}}</text>
<button class="plus" catchtap="incQty" data-index="{{index}}">+</button>
</view>
</view>
<button class="remove" catchtap="removeItem" data-index="{{index}}">移除</button>
</view>
</view>
</scroll-view>
<view wx:if="{{items.length > 0}}" class="bottom-bar">
<view class="total">
<text>合计:</text>
<text class="total-price">¥{{totalPrice}}</text>
</view>
<view class="actions">
<button class="clear-btn" bindtap="clearCart">清空</button>
<button class="checkout-btn" bindtap="checkout">去结算</button>
</view>
</view>
</view>

@ -0,0 +1,23 @@
.cart-page { padding: 20rpx; }
.cart-header { display:flex; justify-content:space-between; align-items:center; margin-bottom: 20rpx; }
.title { font-size: 32rpx; font-weight: bold; }
.count { font-size: 24rpx; color:#888; }
.empty { text-align:center; color:#666; padding:60rpx 0; }
.go-btn { margin-top: 20rpx; background:#4285F4; color:#fff; border-radius: 12rpx; }
.list { max-height: calc(100vh - 240rpx); }
.item { display:flex; background:#fff; border-radius: 16rpx; overflow:hidden; box-shadow:0 6rpx 18rpx rgba(0,0,0,0.06); margin-bottom: 20rpx; }
.thumb { width: 240rpx; height: 200rpx; }
.info { flex:1; padding: 16rpx; }
.name { font-size: 28rpx; color:#333; display:block; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.category { font-size: 22rpx; color:#999; margin-top: 6rpx; }
.price-line { display:flex; justify-content:space-between; align-items:center; margin-top: 12rpx; }
.price { font-size: 30rpx; color:#FF6B35; font-weight:bold; }
.qty { display:flex; align-items:center; }
.minus, .plus { width: 52rpx; height: 52rpx; border-radius: 8rpx; background:#f2f2f2; }
.num { width: 60rpx; text-align:center; font-size: 26rpx; }
.remove { margin-top: 12rpx; background:#ffe5e5; color:#d33; border-radius: 10rpx; }
.bottom-bar { position: fixed; left:0; right:0; bottom:0; background:#fff; box-shadow:0 -6rpx 18rpx rgba(0,0,0,0.06); padding: 16rpx 20rpx; display:flex; justify-content:space-between; align-items:center; }
.total { font-size: 26rpx; }
.total-price { font-size: 32rpx; color:#FF6B35; font-weight:bold; }
.clear-btn { background:#eee; border-radius: 12rpx; margin-right: 12rpx; }
.checkout-btn { background:#34A853; color:#fff; border-radius: 12rpx; }

@ -0,0 +1,258 @@
// pages/chat/chat.js
Page({
data: {
peer: { name: '', avatar: '', openId: '', userId: '' },
myOpenId: '',
myAvatar: '',
myUserId: '',
productId: '',
sessionKey: '',
sessionKeyUser: '',
messages: [],
textInput: '',
sending: false,
uploading: false,
scrollInto: '',
watcher: null
},
async onLoad(options) {
// 参数toUserId(推荐), toOpenId(兼容), toName, productId
const toOpenId = options.toOpenId || '';
const toName = decodeURIComponent(options.toName || '');
const productId = options.productId || '';
const toUserId = options.toUserId || '';
const myOpenId = await this.ensureOpenId();
const myAvatar = (wx.getStorageSync('userInfo') || {}).avatar || '';
const myUserId = (wx.getStorageSync('userInfo') || {})._id || '';
const sessionKey = [myOpenId, toOpenId].sort().join('|');
const sessionKeyUser = (toUserId && myUserId) ? [myUserId, toUserId].sort().join('|') : '';
this.setData({
peer: { name: toName || '对话', avatar: '', openId: toOpenId, userId: toUserId },
myOpenId,
myAvatar,
myUserId,
productId,
sessionKey,
sessionKeyUser
});
await this.loadPeerInfo();
await this.loadMessages();
this.startWatch();
},
onUnload() {
try { if (this.data.watcher) this.data.watcher.close(); } catch(e){}
},
goBack() { wx.navigateBack({ delta: 1 }); },
async ensureOpenId() {
let openid = wx.getStorageSync('openid');
if (!openid) {
try {
const result = await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'getOpenId' } });
if (result.result && result.result.openid) {
openid = result.result.openid;
wx.setStorageSync('openid', openid);
}
} catch (err) { console.error('获取openid失败:', err); }
}
return openid;
},
async loadPeerInfo() {
try {
const db = wx.cloud.database();
let u = null;
if (this.data.peer.userId) {
const byId = await db.collection('T_user').doc(this.data.peer.userId).get();
u = byId.data || null;
}
if (!u && this.data.peer.openId) {
const byOpen = await db.collection('T_user').where({ _openid: this.data.peer.openId }).limit(1).get();
u = (byOpen.data && byOpen.data[0]) || null;
}
if (u) {
const name = u.sname || u.nickName || this.data.peer.name || '对话';
const avatar = u.avatar || this.data.peer.avatar || '';
const openId = u._openid || this.data.peer.openId || '';
const userId = u._id || this.data.peer.userId || '';
this.setData({ peer: { ...this.data.peer, name, avatar, openId, userId } });
}
} catch(e){}
},
async loadMessages() {
try {
const db = wx.cloud.database();
const res = await db.collection('T_message').where({ toUserId: this.data.peer.userId, fromUserId: this.data.myUserId }).limit(1).get();
const doc = (res.data && res.data[0]) || null;
if (!doc) {
await db.collection('T_message').add({ data: { toUserId: this.data.peer.userId, fromUserId: this.data.myUserId, message: [] } });
this.setData({ messages: [] });
return;
}
const list = (doc.message || []).map(m => ({ ...m, _id: m._id || `${m.timestamp}`, timeText: this.formatTime(m.timestamp) }));
this.setData({ messages: list });
this.scrollToBottom();
} catch(err) { console.error('加载消息失败:', err); }
},
startWatch() {
try {
const db = wx.cloud.database();
const watcher = db.collection('T_message').where({ toUserId: this.data.peer.userId, fromUserId: this.data.myUserId })
.watch({
onChange: snapshot => {
const doc = (snapshot.docs && snapshot.docs[0]) || null;
const list = doc ? (doc.message || []).map(m => ({ ...m, _id: m._id || `${m.timestamp}`, timeText: this.formatTime(m.timestamp) })) : [];
this.setData({ messages: list });
this.scrollToBottom();
},
onError: err => { this.startPolling(); }
});
this.setData({ watcher });
} catch(e) {
this.startPolling();
}
},
startPolling() {
if (this._pollTimer) clearInterval(this._pollTimer);
this._pollTimer = setInterval(()=>this.loadMessages(), 3000);
},
onTextInput(e) { this.setData({ textInput: e.detail.value }); },
async sendText() {
const content = (this.data.textInput || '').trim();
if (!content) return;
this.setData({ sending: true });
try {
await this._sendMessage({ contentType: 'text', content });
this.setData({ textInput: '' });
} catch(err) {
wx.showToast({ title: '发送失败', icon: 'none' });
} finally {
this.setData({ sending: false });
}
},
async chooseImage() {
this.setData({ uploading: true });
try {
const pick = await wx.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album','camera'] });
const filePath = pick.tempFilePaths[0];
const cloudPath = `chat-images/${Date.now()}-${Math.random().toString(36).slice(2)}.jpg`;
const up = await wx.cloud.uploadFile({ cloudPath, filePath });
// 可直接用 fileID也尝试获取临时URL用于展示
let imageUrl = up.fileID;
try {
const tmp = await wx.cloud.getTempFileURL({ fileList: [up.fileID] });
if (tmp.fileList && tmp.fileList[0] && tmp.fileList[0].tempFileURL) imageUrl = tmp.fileList[0].tempFileURL;
} catch(e){}
await this._sendMessage({ contentType: 'image', content: up.fileID, imageUrl });
} catch(err) {
wx.showToast({ title: '选择图片失败', icon: 'none' });
} finally { this.setData({ uploading: false }); }
},
async _sendMessage({ contentType, content, imageUrl }) {
try {
const db = wx.cloud.database();
const _ = db.command;
const msg = {
fromUserId: this.data.myUserId,
fromOpenId: this.data.myOpenId,
contentType,
content,
imageUrl: imageUrl || '',
timestamp: Date.now()
};
const a = { toUserId: this.data.peer.userId, fromUserId: this.data.myUserId };
const b = { toUserId: this.data.myUserId, fromUserId: this.data.peer.userId };
const chkA = await db.collection('T_message').where(a).limit(1).get();
if (!(chkA.data && chkA.data[0])) { await db.collection('T_message').add({ data: { toUserId: a.toUserId, fromUserId: a.fromUserId, message: [] } }); }
try { await db.collection('T_message').where(a).update({ data: { message: _.push(msg) } }); }
catch(e){
if (String(e.errMsg||'').includes('does not exist')||e.errCode===-502005){ await wx.cloud.callFunction({ name:'quickstartFunctions', data:{ type:'createMessageCollection' } }); await db.collection('T_message').where(a).update({ data: { message: _.push(msg) } }); }
else { throw e; }
}
const chkB = await db.collection('T_message').where(b).limit(1).get();
if (!(chkB.data && chkB.data[0])) { await db.collection('T_message').add({ data: { toUserId: b.toUserId, fromUserId: b.fromUserId, message: [] } }); }
try { await db.collection('T_message').where(b).update({ data: { message: _.push(msg) } }); }
catch(e){
if (String(e.errMsg||'').includes('does not exist')||e.errCode===-502005){ await wx.cloud.callFunction({ name:'quickstartFunctions', data:{ type:'createMessageCollection' } }); await db.collection('T_message').where(b).update({ data: { message: _.push(msg) } }); }
}
const messages = [...this.data.messages, { ...msg, _id: `local-${msg.timestamp}`, timeText: this.formatTime(msg.timestamp) }];
this.setData({ messages });
this.scrollToBottom();
} catch (err) {
console.error('发送消息失败:', err);
throw err;
}
},
async loadMore() {
try {
const oldest = this.data.messages[0]?.timestamp || 0;
if (!oldest) return;
const res = await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'getChatMessages',
toUserId: this.data.peer.userId || '',
toOpenId: this.data.peer.openId || '',
before: oldest,
limit: 50
}
});
const ret = res.result || {};
const list = (ret.data || []).map(m => ({ ...m, timeText: this.formatTime(m.timestamp) }));
const merged = [...list, ...this.data.messages];
this.setData({ messages: merged });
} catch (e) {}
},
previewImage(e) {
const url = e.currentTarget.dataset.url;
wx.previewImage({ urls: [url], current: url });
},
async onMessageLongPress(e) {
const id = e.currentTarget.dataset.id;
const mine = !!e.currentTarget.dataset.mine;
if (!mine) return;
try {
wx.showActionSheet({ itemList: ['撤回'], success: async (res) => {
if (res.tapIndex === 0) {
try {
const ret = await wx.cloud.callFunction({ name: 'quickstartFunctions', data: { type: 'revokeChatMessage', id } });
if (ret.result && ret.result.success) {
await this.loadMessages();
} else {
wx.showToast({ title: '撤回失败', icon: 'none' });
}
} catch (err) { wx.showToast({ title: '撤回失败', icon: 'none' }); }
}
}});
} catch (e) {}
},
scrollToBottom() {
const last = this.data.messages[this.data.messages.length - 1];
if (last) this.setData({ scrollInto: `msg-${last._id || 'bottom-anchor'}` });
else this.setData({ scrollInto: 'bottom-anchor' });
},
formatTime(ts) {
const d = new Date(ts);
const pad = n => (n<10?'0'+n:n);
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
});

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "消息",
"usingComponents": {}
}

@ -0,0 +1,57 @@
<view class="chat-page">
<view class="chat-topbar">
<view class="back" bindtap="goBack">返回</view>
<text class="title">聊天</text>
<view class="more"></view>
</view>
<!-- 消息列表 -->
<scroll-view class="messages" scroll-y="true" scroll-with-animation="true" scroll-into-view="{{scrollInto}}">
<view class="load-more" bindtap="loadMore">加载更多</view>
<view class="sys-info">
<image class="avatar" src="{{peer.avatar || 'https://via.placeholder.com/80x80/cccccc/ffffff?text=U'}}" mode="aspectFill" />
<view class="peer-info">
<text class="name">{{peer.name || '对话'}}</text>
<text class="tip">与对方实时沟通,支持文字和图片</text>
</view>
</view>
<block wx:for="{{messages}}" wx:key="_id">
<view class="time-line">{{item.timeText}}</view>
<block wx:if="{{item.isRevoked || item.contentType === 'revoke' || item.isSystem || item.contentType === 'system' || item.contentType === 'error'}}">
<view class="sys-row">
<view class="sys-tip {{item.contentType === 'error' ? 'error' : (item.contentType === 'revoke' ? 'revoked' : '')}}">
<text>{{item.contentType === 'revoke' ? '对方撤回一条消息' : (item.content || '[系统消息]')}}</text>
</view>
</view>
</block>
<block wx:else>
<view class="msg {{item.fromUserId ? (item.fromUserId === myUserId ? 'mine' : 'other') : (item.fromOpenId === myOpenId ? 'mine' : 'other')}}" id="msg-{{item._id}}" bindlongpress="onMessageLongPress" data-id="{{item._id}}" data-mine="{{item.fromUserId ? (item.fromUserId === myUserId) : (item.fromOpenId === myOpenId)}}">
<image class="msg-avatar" src="{{item.fromUserId ? (item.fromUserId === myUserId ? myAvatar : peer.avatar) : (item.fromOpenId === myOpenId ? myAvatar : peer.avatar)}}" mode="aspectFill" />
<view class="bubble">
<block wx:if="{{item.contentType === 'text'}}">
<text class="text">{{item.content}}</text>
</block>
<block wx:elif="{{item.contentType === 'image'}}">
<image class="image" src="{{item.imageUrl || item.content}}" mode="aspectFill" bindtap="previewImage" data-url="{{item.imageUrl || item.content}}" />
</block>
</view>
</view>
</block>
</block>
<view id="bottom-anchor"></view>
</scroll-view>
<!-- 底部输入栏 -->
<view class="draft-row" wx:if="{{textInput}}">
<view class="draft-tip">{{textInput}}</view>
</view>
<view class="input-bar">
<view class="input-top">
<textarea class="text-input" placeholder="输入消息" value="{{textInput}}" show-confirm-bar="false" bindinput="onTextInput" />
</view>
<view class="input-bottom">
<button class="circle-btn left-btn" bindtap="chooseImage" loading="{{uploading}}" disabled="{{uploading}}">🖼️</button>
<button class="circle-btn send-circle right-btn" bindtap="sendText" disabled="{{sending}}">发送</button>
</view>
</view>
</view>

@ -0,0 +1,34 @@
.chat-page { display:flex; flex-direction:column; height:100vh; background:#f5f5f5; }
.chat-topbar { display:flex; align-items:center; justify-content:center; position:sticky; top:0; z-index:10; height:88rpx; background:linear-gradient(90deg,#7B1FA2,#E91E63); color:#fff; box-shadow:0 6rpx 12rpx rgba(0,0,0,0.1); }
.chat-topbar .back { position:absolute; left:20rpx; font-size:28rpx; opacity:0.9; }
.chat-topbar .title { font-size:32rpx; font-weight:700; }
.avatar { width:64rpx; height:64rpx; border-radius:50%; margin-right:16rpx; }
.sys-info { display:flex; align-items:center; background:#fff; padding:20rpx; box-shadow:0 4rpx 12rpx rgba(0,0,0,0.05); border-radius:16rpx; margin:20rpx 0; }
.peer-info .name { font-size:28rpx; color:#333; font-weight:600; }
.peer-info .tip { font-size:22rpx; color:#999; }
.messages { flex:1; padding:20rpx; }
.load-more { text-align:center; padding:10rpx; color:#666; }
.time-line { text-align:center; color:#999; font-size:22rpx; margin:10rpx 0; }
.sys-row { display:flex; justify-content:center; margin:12rpx 0; }
.sys-tip { max-width:80%; background:#e0e0e0; color:#333; border-radius:20rpx; padding:10rpx 16rpx; font-size:24rpx; box-shadow:0 2rpx 6rpx rgba(0,0,0,0.05); }
.sys-tip.revoked { background:#eee; color:#666; }
.sys-tip.error { background:#ffe9e9; color:#d93025; }
.msg { display:flex; align-items:flex-end; margin-bottom:20rpx; }
.msg.other { flex-direction:row; }
.msg.mine { flex-direction:row-reverse; }
.msg-avatar { width:48rpx; height:48rpx; border-radius:50%; margin:0 10rpx; }
.bubble { max-width:60%; border-radius:16rpx; padding:12rpx 16rpx; box-shadow:0 2rpx 8rpx rgba(0,0,0,0.08); background:#fff; }
.msg.mine .bubble { background:#2ecc71; color:#fff; }
.text { font-size:26rpx; color:inherit; }
.image { width:360rpx; height:240rpx; border-radius:12rpx; }
.time { font-size:20rpx; color:#999; margin:0 8rpx; }
.input-bar { display:flex; flex-direction:column; gap:12rpx; background:#fff; padding:12rpx 16rpx; border-top:1rpx solid #eee; }
.input-top { display:flex; }
.input-bottom { display:flex; align-items:center; justify-content:space-between; }
.circle-btn { width:48rpx; height:48rpx; border-radius:50%; border:2rpx solid #333; background:#fff; display:flex; align-items:center; justify-content:center; font-size:22rpx; }
.left-btn { margin-right:0; }
.right-btn { margin-left:0; }
.text-input { flex:1; width:100%; background:#f7f7f7; border-radius:20rpx; padding:16rpx 20rpx; font-size:30rpx; min-height:64rpx; height:64rpx; }
.send-circle { border-color:#00c853; color:#00c853; }
.draft-row { display:flex; justify-content:center; background:#f5f5f5; }
.draft-tip { max-width:90%; background:#e0e0e0; color:#333; border-radius:20rpx; padding:12rpx 18rpx; font-size:26rpx; margin:12rpx; }

@ -0,0 +1,380 @@
// pages/favorites/favorites.js
Page({
/**
* 页面的初始数据
*/
data: {
favorites: [],
loading: false,
isEmpty: false
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 清除可能缓存的旧openid确保使用登录的用户ID获取正确的openid
const userInfo = wx.getStorageSync('userInfo') || {};
if (userInfo._id) {
// 如果有登录的用户ID清除旧的openid缓存强制重新获取
wx.removeStorageSync('openid');
}
this.loadFavorites();
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 清除可能缓存的旧openid确保使用登录的用户ID获取正确的openid
const userInfo = wx.getStorageSync('userInfo') || {};
if (userInfo._id) {
// 如果有登录的用户ID清除旧的openid缓存强制重新获取
wx.removeStorageSync('openid');
}
this.loadFavorites();
},
/**
* 确保有openid
*/
async ensureOpenId() {
let openid = wx.getStorageSync('openid');
if (!openid) {
try {
const result = await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'getOpenId'
}
});
if (result.result && result.result.openid) {
openid = result.result.openid;
wx.setStorageSync('openid', openid);
}
} catch (err) {
console.error('获取openid失败:', err);
}
}
return openid;
},
/**
* 加载收藏列表
*/
async loadFavorites() {
this.setData({
loading: true,
isEmpty: false
});
try {
const db = wx.cloud.database();
let openid = null;
// 优先使用登录时保存的用户ID来获取openid
const userInfo = wx.getStorageSync('userInfo') || {};
const loggedInUserId = userInfo._id;
if (loggedInUserId) {
try {
// 使用登录的用户ID查询用户信息获取其_openid
const userResult = await db.collection('T_user')
.doc(loggedInUserId)
.get();
if (userResult.data && userResult.data._openid) {
openid = userResult.data._openid;
}
} catch (err) {
console.error('通过用户ID获取openid失败:', err);
}
}
// 如果无法通过用户ID获取openid尝试从缓存获取
if (!openid) {
openid = await this.ensureOpenId();
}
if (!openid) {
wx.showToast({
title: '请先登录',
icon: 'none'
});
this.setData({
loading: false,
isEmpty: true,
favorites: []
});
return;
}
// 查询收藏列表优先按用户ID其次按openid
let result;
try {
if (loggedInUserId) {
result = await db.collection('T_favorites')
.where({ userId: loggedInUserId })
.orderBy('createTime', 'desc')
.get();
} else {
result = await db.collection('T_favorites')
.where({ _openid: openid })
.orderBy('createTime', 'desc')
.get();
}
} catch (collectionErr) {
// 如果集合不存在,显示空列表而不是错误
console.warn('收藏集合不存在或查询失败:', collectionErr);
if (collectionErr.errMsg && collectionErr.errMsg.includes('not exist')) {
// 集合不存在,显示空列表
this.setData({
favorites: [],
loading: false,
isEmpty: true
});
return;
}
// 其他错误,重新抛出
throw collectionErr;
}
if (result && result.data && result.data.length > 0) {
// 获取收藏的商品ID列表过滤掉空值
const productIds = result.data
.map(item => item.productId)
.filter(id => id && id.trim && id.trim() !== '');
let favorites = [];
// 只有当有有效的商品ID时才查询商品详情
if (productIds.length > 0) {
try {
// 查询商品详情
let productsResult;
try {
productsResult = await db.collection('T_product')
.where({
_id: db.command.in(productIds)
})
.get();
} catch (productQueryErr) {
// 如果商品查询失败,使用收藏记录中的信息
console.warn('查询商品详情失败:', productQueryErr);
productsResult = { data: [] };
}
// 将收藏信息与商品信息合并
favorites = result.data.map(favorite => {
// 如果收藏记录中的productId无效直接返回null
if (!favorite.productId || (favorite.productId.trim && favorite.productId.trim() === '')) {
return null;
}
const product = productsResult.data && productsResult.data.find(p => p._id === favorite.productId);
// 如果商品不存在返回null会被过滤掉
if (!product) {
return null;
}
// 处理商品图片
let productImage = 'https://via.placeholder.com/600x400/cccccc/ffffff?text=Product';
if (product.productImage) {
if (Array.isArray(product.productImage) && product.productImage.length > 0) {
productImage = product.productImage[0];
} else if (typeof product.productImage === 'string') {
productImage = product.productImage;
}
}
return {
_id: favorite._id,
productId: favorite.productId,
product: {
id: product._id,
name: product.productName || '商品名称',
price: product.salePrice || product.suggestedPrice || product.originalPrice || 0,
image: productImage,
category: product.productCategory || '其他',
status: product.status || '在售'
}
};
}).filter(item => item !== null); // 过滤掉null值
} catch (productErr) {
console.error('查询商品详情失败:', productErr);
// 如果查询商品失败,使用收藏记录中的信息(如果有)
favorites = result.data.map(favorite => {
if (!favorite.productId) {
return null;
}
return {
_id: favorite._id,
productId: favorite.productId,
product: {
id: favorite.productId,
name: favorite.productName || '商品名称',
price: favorite.productPrice || 0,
image: favorite.productImage || 'https://via.placeholder.com/600x400/cccccc/ffffff?text=Product',
category: favorite.productCategory || '其他',
status: '未知'
}
};
}).filter(item => item !== null);
}
} else {
// 如果没有有效的商品ID显示空列表
favorites = [];
}
this.setData({
favorites: favorites,
loading: false,
isEmpty: favorites.length === 0
});
} else {
this.setData({
favorites: [],
loading: false,
isEmpty: true
});
}
} catch (err) {
console.error('加载收藏列表失败:', err);
console.error('错误详情:', JSON.stringify(err, null, 2));
// 如果错误是集合不存在,静默处理,显示空列表
if (err.errMsg && (err.errMsg.includes('not exist') || err.errMsg.includes('Db or Table not exist'))) {
console.log('收藏集合不存在,显示空列表');
this.setData({
loading: false,
isEmpty: true,
favorites: []
});
return;
}
let errorMsg = '加载失败';
if (err.errMsg) {
if (err.errMsg.includes('network') || err.errMsg.includes('Failed to fetch')) {
errorMsg = '网络连接失败';
} else if (err.errMsg.includes('permission')) {
errorMsg = '权限不足';
} else {
// 不显示详细错误信息给用户
errorMsg = '加载失败,请稍后重试';
}
}
// 对于非集合不存在的错误,显示提示
wx.showToast({
title: errorMsg,
icon: 'none',
duration: 2000
});
this.setData({
loading: false,
isEmpty: true,
favorites: []
});
}
},
/**
* 查看商品详情
*/
onProductTap(e) {
const productId = e.currentTarget.dataset.id;
if (productId) {
wx.navigateTo({
url: `/pages/product-detail/product-detail?id=${productId}`,
fail: (err) => {
console.error('跳转失败:', err);
wx.showToast({
title: '商品不存在',
icon: 'none'
});
}
});
} else {
wx.showToast({
title: '商品ID无效',
icon: 'none'
});
}
},
/**
* 取消收藏
*/
async onRemoveFavorite(e) {
const favoriteId = e.currentTarget.dataset.id;
const productName = e.currentTarget.dataset.name || '该商品';
if (!favoriteId) {
wx.showToast({
title: '操作失败',
icon: 'none'
});
return;
}
wx.showModal({
title: '取消收藏',
content: `确定要取消收藏"${productName}"吗?`,
success: async (res) => {
if (res.confirm) {
try {
const db = wx.cloud.database();
await db.collection('T_favorites').doc(favoriteId).remove();
try {
const reco = require('../../utils/recommendation.js');
const item = (this.data.favorites || []).find(x => x._id === favoriteId);
const product = item && item.product ? item.product : { name: productName, category: item && item.product ? item.product.category : '其他' };
reco.recordUnfavorite(product);
} catch (e) {}
wx.showToast({
title: '已取消收藏',
icon: 'success'
});
// 重新加载收藏列表
await this.loadFavorites();
} catch (err) {
console.error('取消收藏失败:', err);
console.error('错误详情:', JSON.stringify(err, null, 2));
let errorMsg = '操作失败';
if (err.errMsg) {
if (err.errMsg.includes('network') || err.errMsg.includes('Failed to fetch')) {
errorMsg = '网络连接失败';
} else if (err.errMsg.includes('permission')) {
errorMsg = '权限不足';
} else {
errorMsg = '操作失败:' + err.errMsg;
}
}
wx.showToast({
title: errorMsg,
icon: 'none'
});
}
}
}
});
},
/**
* 下拉刷新
*/
async onPullDownRefresh() {
await this.loadFavorites();
wx.stopPullDownRefresh();
}
});

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "我的收藏",
"enablePullDownRefresh": true
}

@ -0,0 +1,37 @@
<!--pages/favorites/favorites.wxml-->
<view class="page-container">
<!-- 加载中 -->
<view wx:if="{{loading}}" class="loading-container">
<text>加载中...</text>
</view>
<!-- 空状态 -->
<view wx:elif="{{isEmpty}}" class="empty-container">
<image class="empty-icon" src="https://via.placeholder.com/200x200/eeeeee/999999?text=Empty" mode="aspectFit"></image>
<text class="empty-text">暂无收藏</text>
<text class="empty-tip">快去收藏喜欢的商品吧~</text>
</view>
<!-- 收藏列表 -->
<view wx:else class="favorites-list">
<view class="favorite-item" wx:for="{{favorites}}" wx:key="_id" bindtap="onProductTap" data-id="{{item.product.id}}">
<image class="product-image" src="{{item.product.image}}" mode="aspectFill"></image>
<view class="product-info">
<text class="product-name">{{item.product.name}}</text>
<view class="product-meta">
<text class="product-category">{{item.product.category}}</text>
<text class="product-price">¥{{item.product.price}}</text>
</view>
<view class="product-status">
<text class="status-text" wx:if="{{item.product.status === '在售'}}">在售</text>
<text class="status-text sold" wx:elif="{{item.product.status === '已售出'}}">已售出</text>
<text class="status-text off" wx:else>已下架</text>
</view>
</view>
<view class="remove-btn" catchtap="onRemoveFavorite" data-id="{{item._id}}" data-name="{{item.product.name}}">
<text>取消收藏</text>
</view>
</view>
</view>
</view>

@ -0,0 +1,129 @@
/* pages/favorites/favorites.wxss */
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
}
.loading-container,
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 30rpx;
}
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-bottom: 10rpx;
}
.empty-tip {
font-size: 24rpx;
color: #ccc;
}
.favorites-list {
padding: 20rpx;
}
.favorite-item {
display: flex;
align-items: center;
background: white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.product-image {
width: 160rpx;
height: 160rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-name {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
}
.product-category {
font-size: 24rpx;
color: #999;
background: #f0f0f0;
padding: 4rpx 12rpx;
border-radius: 4rpx;
}
.product-price {
font-size: 32rpx;
color: #ff6b6b;
font-weight: bold;
}
.product-status {
margin-top: 10rpx;
}
.status-text {
font-size: 24rpx;
color: #07c160;
background: #e8f5e9;
padding: 4rpx 12rpx;
border-radius: 4rpx;
}
.status-text.sold {
color: #999;
background: #f5f5f5;
}
.status-text.off {
color: #ff9800;
background: #fff3e0;
}
.remove-btn {
margin-left: 20rpx;
padding: 10rpx 20rpx;
background: #ff6b6b;
color: white;
border-radius: 8rpx;
font-size: 24rpx;
}
.remove-btn:active {
opacity: 0.7;
}

@ -0,0 +1,214 @@
// pages/feedback/feedback.js
Page({
/**
* 页面的初始数据
*/
data: {
feedbackType: 'suggestion',
types: [
{ value: 'suggestion', label: '意见反馈' },
{ value: 'bug', label: '问题反馈' },
{ value: 'feature', label: '功能建议' },
{ value: 'other', label: '其他' }
],
content: '',
contact: '',
images: []
},
/**
* 选择反馈类型
*/
onTypeChange(e) {
this.setData({
feedbackType: e.currentTarget.dataset.type
});
},
/**
* 内容输入
*/
onContentInput(e) {
this.setData({
content: e.detail.value
});
},
/**
* 联系方式输入
*/
onContactInput(e) {
this.setData({
contact: e.detail.value
});
},
/**
* 选择图片
*/
chooseImage() {
wx.chooseImage({
count: 3,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths;
this.uploadImages(tempFilePaths);
}
});
},
/**
* 上传图片
*/
async uploadImages(filePaths) {
wx.showLoading({
title: '上传中...'
});
try {
const uploadPromises = filePaths.map(filePath => {
const cloudPath = `feedback/${Date.now()}-${Math.random().toString(36).substr(2, 9)}.jpg`;
return wx.cloud.uploadFile({
cloudPath: cloudPath,
filePath: filePath
});
});
const uploadResults = await Promise.all(uploadPromises);
const imageUrls = uploadResults.map(result => result.fileID);
this.setData({
images: [...this.data.images, ...imageUrls]
});
wx.hideLoading();
wx.showToast({
title: '上传成功',
icon: 'success'
});
} catch (err) {
console.error('上传图片失败:', err);
wx.hideLoading();
wx.showToast({
title: '上传失败',
icon: 'none'
});
}
},
/**
* 删除图片
*/
onDeleteImage(e) {
const index = e.currentTarget.dataset.index;
const images = this.data.images.filter((_, i) => i !== index);
this.setData({
images: images
});
},
/**
* 确保有openid
*/
async ensureOpenId() {
let openid = wx.getStorageSync('openid');
if (!openid) {
try {
const result = await wx.cloud.callFunction({
name: 'quickstartFunctions',
data: {
type: 'getOpenId'
}
});
if (result.result && result.result.openid) {
openid = result.result.openid;
wx.setStorageSync('openid', openid);
}
} catch (err) {
console.error('获取openid失败:', err);
}
}
return openid;
},
/**
* 提交反馈
*/
async submitFeedback() {
const { feedbackType, content, contact, images } = this.data;
// 验证必填项
if (!content || !content.trim()) {
wx.showToast({
title: '请输入反馈内容',
icon: 'none'
});
return;
}
if (content.trim().length < 10) {
wx.showToast({
title: '反馈内容至少10个字',
icon: 'none'
});
return;
}
wx.showLoading({
title: '提交中...'
});
try {
const db = wx.cloud.database();
const openid = await this.ensureOpenId();
if (!openid) {
wx.hideLoading();
wx.showToast({
title: '请先登录',
icon: 'none'
});
return;
}
await db.collection('T_feedback').add({
data: {
_openid: openid,
type: feedbackType,
content: content.trim(),
contact: contact.trim() || '',
images: images,
status: 'pending', // pending: 待处理, processing: 处理中, resolved: 已解决
createTime: new Date(),
updateTime: new Date()
}
});
wx.hideLoading();
wx.showToast({
title: '提交成功',
icon: 'success'
});
// 清空表单
this.setData({
content: '',
contact: '',
images: []
});
setTimeout(() => {
wx.navigateBack();
}, 1500);
} catch (err) {
console.error('提交反馈失败:', err);
wx.hideLoading();
wx.showToast({
title: '提交失败',
icon: 'none'
});
}
}
});

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "意见反馈"
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save