@ -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,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": ""
|
||||
}
|
||||
}
|
||||
@ -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,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();
|
||||
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 294 KiB |
@ -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,3 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
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,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,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "商品管理",
|
||||
"navigationBarBackgroundColor": "#667eea",
|
||||
"navigationBarTextStyle": "white",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
@ -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,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,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,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,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": "意见反馈"
|
||||
}
|
||||
|
||||