From ab05348acff945aadfc726b60cb7adefba65e055 Mon Sep 17 00:00:00 2001 From: k123456214 Date: Sat, 20 Jun 2026 12:15:52 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E5=A4=9A=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=BC=80=E5=8F=91=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- app.js | 136 +++++ app.json | 74 +++ app.wxss | 379 ++++++++++++++ config.example.js | 32 ++ pages/community/activity/activity.js | 51 ++ pages/community/activity/activity.json | 4 + pages/community/activity/activity.wxml | 26 + pages/community/activity/activity.wxss | 35 ++ pages/community/create/create.js | 52 ++ pages/community/create/create.json | 4 + pages/community/create/create.wxml | 33 ++ pages/community/create/create.wxss | 48 ++ pages/community/detail/detail.js | 84 +++ pages/community/detail/detail.json | 4 + pages/community/detail/detail.wxml | 77 +++ pages/community/detail/detail.wxss | 107 ++++ pages/community/list/list.js | 80 +++ pages/community/list/list.json | 4 + pages/community/list/list.wxml | 38 ++ pages/community/list/list.wxss | 77 +++ pages/confession/detail/detail.js | 87 ++++ pages/confession/detail/detail.json | 4 + pages/confession/detail/detail.wxml | 51 ++ pages/confession/detail/detail.wxss | 131 +++++ pages/confession/list/list.js | 70 +++ pages/confession/list/list.json | 4 + pages/confession/list/list.wxml | 40 ++ pages/confession/list/list.wxss | 126 +++++ pages/confession/publish/publish.js | 43 ++ pages/confession/publish/publish.json | 4 + pages/confession/publish/publish.wxml | 20 + pages/confession/publish/publish.wxss | 64 +++ pages/custom/diy/diy.js | 61 +++ pages/custom/diy/diy.json | 4 + pages/custom/diy/diy.wxml | 57 +++ pages/custom/diy/diy.wxss | 135 +++++ pages/custom/h5/h5.js | 31 ++ pages/custom/h5/h5.json | 1 + pages/custom/h5/h5.wxml | 15 + pages/errand/detail/detail.js | 101 ++++ pages/errand/detail/detail.json | 4 + pages/errand/detail/detail.wxml | 67 +++ pages/errand/detail/detail.wxss | 114 +++++ pages/errand/list/list.js | 72 +++ pages/errand/list/list.json | 4 + pages/errand/list/list.wxml | 44 ++ pages/errand/list/list.wxss | 81 +++ pages/errand/publish/publish.js | 65 +++ pages/errand/publish/publish.json | 4 + pages/errand/publish/publish.wxml | 51 ++ pages/errand/publish/publish.wxss | 72 +++ pages/food/checkout/checkout.js | 140 +++++ pages/food/checkout/checkout.json | 4 + pages/food/checkout/checkout.wxml | 74 +++ pages/food/checkout/checkout.wxss | 51 ++ pages/food/index/index.js | 74 +++ pages/food/index/index.json | 7 + pages/food/index/index.wxml | 47 ++ pages/food/index/index.wxss | 62 +++ pages/food/order-detail/order-detail.js | 112 ++++ pages/food/order-detail/order-detail.json | 1 + pages/food/order-detail/order-detail.wxml | 72 +++ pages/food/order-detail/order-detail.wxss | 75 +++ pages/food/shop/shop.js | 291 +++++++++++ pages/food/shop/shop.json | 4 + pages/food/shop/shop.wxml | 153 ++++++ pages/food/shop/shop.wxss | 207 ++++++++ pages/forum/detail/detail.js | 118 +++++ pages/forum/detail/detail.json | 4 + pages/forum/detail/detail.wxml | 68 +++ pages/forum/detail/detail.wxss | 145 ++++++ pages/forum/list/list.js | 67 +++ pages/forum/list/list.json | 4 + pages/forum/list/list.wxml | 42 ++ pages/forum/list/list.wxss | 56 ++ pages/forum/publish/publish.js | 89 ++++ pages/forum/publish/publish.json | 4 + pages/forum/publish/publish.wxml | 43 ++ pages/forum/publish/publish.wxss | 106 ++++ pages/index/index.js | 113 ++++ pages/index/index.json | 6 + pages/index/index.wxml | 114 +++++ pages/index/index.wxss | 152 ++++++ pages/market/detail/detail.js | 61 +++ pages/market/detail/detail.json | 4 + pages/market/detail/detail.wxml | 61 +++ pages/market/detail/detail.wxss | 148 ++++++ pages/market/list/list.js | 74 +++ pages/market/list/list.json | 4 + pages/market/list/list.wxml | 39 ++ pages/market/list/list.wxss | 98 ++++ pages/market/my-goods/my-goods.js | 72 +++ pages/market/my-goods/my-goods.json | 4 + pages/market/my-goods/my-goods.wxml | 39 ++ pages/market/my-goods/my-goods.wxss | 91 ++++ pages/market/publish/publish.js | 65 +++ pages/market/publish/publish.json | 4 + pages/market/publish/publish.wxml | 54 ++ pages/market/publish/publish.wxss | 104 ++++ pages/merchant/apply/apply.js | 63 +++ pages/merchant/apply/apply.json | 4 + pages/merchant/apply/apply.wxml | 65 +++ pages/merchant/apply/apply.wxss | 81 +++ pages/merchant/detail/detail.js | 65 +++ pages/merchant/detail/detail.json | 4 + pages/merchant/detail/detail.wxml | 77 +++ pages/merchant/detail/detail.wxss | 118 +++++ pages/merchant/goods-edit/goods-edit.js | 162 ++++++ pages/merchant/goods-edit/goods-edit.json | 4 + pages/merchant/goods-edit/goods-edit.wxml | 106 ++++ pages/merchant/goods-edit/goods-edit.wxss | 133 +++++ pages/merchant/goods-manage/goods-manage.js | 72 +++ pages/merchant/goods-manage/goods-manage.json | 4 + pages/merchant/goods-manage/goods-manage.wxml | 29 ++ pages/merchant/goods-manage/goods-manage.wxss | 74 +++ pages/merchant/list/list.js | 75 +++ pages/merchant/list/list.json | 5 + pages/merchant/list/list.wxml | 49 ++ pages/merchant/list/list.wxss | 103 ++++ pages/merchant/my-shop/my-shop.js | 82 +++ pages/merchant/my-shop/my-shop.json | 4 + pages/merchant/my-shop/my-shop.wxml | 98 ++++ pages/merchant/my-shop/my-shop.wxss | 181 +++++++ pages/merchant/order-manage/order-manage.js | 132 +++++ pages/merchant/order-manage/order-manage.json | 4 + pages/merchant/order-manage/order-manage.wxml | 48 ++ pages/merchant/order-manage/order-manage.wxss | 54 ++ pages/user/coupon/coupon.js | 76 +++ pages/user/coupon/coupon.json | 4 + pages/user/coupon/coupon.wxml | 34 ++ pages/user/coupon/coupon.wxss | 86 ++++ pages/user/favorite/favorite.js | 55 ++ pages/user/favorite/favorite.json | 4 + pages/user/favorite/favorite.wxml | 52 ++ pages/user/favorite/favorite.wxss | 38 ++ pages/user/login/login.js | 155 ++++++ pages/user/login/login.json | 4 + pages/user/login/login.wxml | 91 ++++ pages/user/login/login.wxss | 201 ++++++++ pages/user/order/my-order.js | 110 ++++ pages/user/order/my-order.json | 4 + pages/user/order/my-order.wxml | 50 ++ pages/user/order/my-order.wxss | 88 ++++ pages/user/profile/profile.js | 100 ++++ pages/user/profile/profile.json | 4 + pages/user/profile/profile.wxml | 111 ++++ pages/user/profile/profile.wxss | 152 ++++++ pages/user/settings/settings.js | 114 +++++ pages/user/settings/settings.json | 4 + pages/user/settings/settings.wxml | 52 ++ pages/user/settings/settings.wxss | 76 +++ project.config.json | 51 ++ server/index.js | 378 ++++++++++++++ server/package.json | 22 + sitemap.json | 9 + utils/api.js | 136 +++++ utils/auth.js | 51 ++ utils/mock.js | 483 ++++++++++++++++++ utils/payment.js | 80 +++ utils/printer.js | 163 ++++++ utils/request.js | 90 ++++ utils/storage.js | 31 ++ utils/util.js | 139 +++++ 163 files changed, 11593 insertions(+) create mode 100644 app.js create mode 100644 app.json create mode 100644 app.wxss create mode 100644 config.example.js create mode 100644 pages/community/activity/activity.js create mode 100644 pages/community/activity/activity.json create mode 100644 pages/community/activity/activity.wxml create mode 100644 pages/community/activity/activity.wxss create mode 100644 pages/community/create/create.js create mode 100644 pages/community/create/create.json create mode 100644 pages/community/create/create.wxml create mode 100644 pages/community/create/create.wxss create mode 100644 pages/community/detail/detail.js create mode 100644 pages/community/detail/detail.json create mode 100644 pages/community/detail/detail.wxml create mode 100644 pages/community/detail/detail.wxss create mode 100644 pages/community/list/list.js create mode 100644 pages/community/list/list.json create mode 100644 pages/community/list/list.wxml create mode 100644 pages/community/list/list.wxss create mode 100644 pages/confession/detail/detail.js create mode 100644 pages/confession/detail/detail.json create mode 100644 pages/confession/detail/detail.wxml create mode 100644 pages/confession/detail/detail.wxss create mode 100644 pages/confession/list/list.js create mode 100644 pages/confession/list/list.json create mode 100644 pages/confession/list/list.wxml create mode 100644 pages/confession/list/list.wxss create mode 100644 pages/confession/publish/publish.js create mode 100644 pages/confession/publish/publish.json create mode 100644 pages/confession/publish/publish.wxml create mode 100644 pages/confession/publish/publish.wxss create mode 100644 pages/custom/diy/diy.js create mode 100644 pages/custom/diy/diy.json create mode 100644 pages/custom/diy/diy.wxml create mode 100644 pages/custom/diy/diy.wxss create mode 100644 pages/custom/h5/h5.js create mode 100644 pages/custom/h5/h5.json create mode 100644 pages/custom/h5/h5.wxml create mode 100644 pages/errand/detail/detail.js create mode 100644 pages/errand/detail/detail.json create mode 100644 pages/errand/detail/detail.wxml create mode 100644 pages/errand/detail/detail.wxss create mode 100644 pages/errand/list/list.js create mode 100644 pages/errand/list/list.json create mode 100644 pages/errand/list/list.wxml create mode 100644 pages/errand/list/list.wxss create mode 100644 pages/errand/publish/publish.js create mode 100644 pages/errand/publish/publish.json create mode 100644 pages/errand/publish/publish.wxml create mode 100644 pages/errand/publish/publish.wxss create mode 100644 pages/food/checkout/checkout.js create mode 100644 pages/food/checkout/checkout.json create mode 100644 pages/food/checkout/checkout.wxml create mode 100644 pages/food/checkout/checkout.wxss create mode 100644 pages/food/index/index.js create mode 100644 pages/food/index/index.json create mode 100644 pages/food/index/index.wxml create mode 100644 pages/food/index/index.wxss create mode 100644 pages/food/order-detail/order-detail.js create mode 100644 pages/food/order-detail/order-detail.json create mode 100644 pages/food/order-detail/order-detail.wxml create mode 100644 pages/food/order-detail/order-detail.wxss create mode 100644 pages/food/shop/shop.js create mode 100644 pages/food/shop/shop.json create mode 100644 pages/food/shop/shop.wxml create mode 100644 pages/food/shop/shop.wxss create mode 100644 pages/forum/detail/detail.js create mode 100644 pages/forum/detail/detail.json create mode 100644 pages/forum/detail/detail.wxml create mode 100644 pages/forum/detail/detail.wxss create mode 100644 pages/forum/list/list.js create mode 100644 pages/forum/list/list.json create mode 100644 pages/forum/list/list.wxml create mode 100644 pages/forum/list/list.wxss create mode 100644 pages/forum/publish/publish.js create mode 100644 pages/forum/publish/publish.json create mode 100644 pages/forum/publish/publish.wxml create mode 100644 pages/forum/publish/publish.wxss create mode 100644 pages/index/index.js create mode 100644 pages/index/index.json create mode 100644 pages/index/index.wxml create mode 100644 pages/index/index.wxss create mode 100644 pages/market/detail/detail.js create mode 100644 pages/market/detail/detail.json create mode 100644 pages/market/detail/detail.wxml create mode 100644 pages/market/detail/detail.wxss create mode 100644 pages/market/list/list.js create mode 100644 pages/market/list/list.json create mode 100644 pages/market/list/list.wxml create mode 100644 pages/market/list/list.wxss create mode 100644 pages/market/my-goods/my-goods.js create mode 100644 pages/market/my-goods/my-goods.json create mode 100644 pages/market/my-goods/my-goods.wxml create mode 100644 pages/market/my-goods/my-goods.wxss create mode 100644 pages/market/publish/publish.js create mode 100644 pages/market/publish/publish.json create mode 100644 pages/market/publish/publish.wxml create mode 100644 pages/market/publish/publish.wxss create mode 100644 pages/merchant/apply/apply.js create mode 100644 pages/merchant/apply/apply.json create mode 100644 pages/merchant/apply/apply.wxml create mode 100644 pages/merchant/apply/apply.wxss create mode 100644 pages/merchant/detail/detail.js create mode 100644 pages/merchant/detail/detail.json create mode 100644 pages/merchant/detail/detail.wxml create mode 100644 pages/merchant/detail/detail.wxss create mode 100644 pages/merchant/goods-edit/goods-edit.js create mode 100644 pages/merchant/goods-edit/goods-edit.json create mode 100644 pages/merchant/goods-edit/goods-edit.wxml create mode 100644 pages/merchant/goods-edit/goods-edit.wxss create mode 100644 pages/merchant/goods-manage/goods-manage.js create mode 100644 pages/merchant/goods-manage/goods-manage.json create mode 100644 pages/merchant/goods-manage/goods-manage.wxml create mode 100644 pages/merchant/goods-manage/goods-manage.wxss create mode 100644 pages/merchant/list/list.js create mode 100644 pages/merchant/list/list.json create mode 100644 pages/merchant/list/list.wxml create mode 100644 pages/merchant/list/list.wxss create mode 100644 pages/merchant/my-shop/my-shop.js create mode 100644 pages/merchant/my-shop/my-shop.json create mode 100644 pages/merchant/my-shop/my-shop.wxml create mode 100644 pages/merchant/my-shop/my-shop.wxss create mode 100644 pages/merchant/order-manage/order-manage.js create mode 100644 pages/merchant/order-manage/order-manage.json create mode 100644 pages/merchant/order-manage/order-manage.wxml create mode 100644 pages/merchant/order-manage/order-manage.wxss create mode 100644 pages/user/coupon/coupon.js create mode 100644 pages/user/coupon/coupon.json create mode 100644 pages/user/coupon/coupon.wxml create mode 100644 pages/user/coupon/coupon.wxss create mode 100644 pages/user/favorite/favorite.js create mode 100644 pages/user/favorite/favorite.json create mode 100644 pages/user/favorite/favorite.wxml create mode 100644 pages/user/favorite/favorite.wxss create mode 100644 pages/user/login/login.js create mode 100644 pages/user/login/login.json create mode 100644 pages/user/login/login.wxml create mode 100644 pages/user/login/login.wxss create mode 100644 pages/user/order/my-order.js create mode 100644 pages/user/order/my-order.json create mode 100644 pages/user/order/my-order.wxml create mode 100644 pages/user/order/my-order.wxss create mode 100644 pages/user/profile/profile.js create mode 100644 pages/user/profile/profile.json create mode 100644 pages/user/profile/profile.wxml create mode 100644 pages/user/profile/profile.wxss create mode 100644 pages/user/settings/settings.js create mode 100644 pages/user/settings/settings.json create mode 100644 pages/user/settings/settings.wxml create mode 100644 pages/user/settings/settings.wxss create mode 100644 project.config.json create mode 100644 server/index.js create mode 100644 server/package.json create mode 100644 sitemap.json create mode 100644 utils/api.js create mode 100644 utils/auth.js create mode 100644 utils/mock.js create mode 100644 utils/payment.js create mode 100644 utils/printer.js create mode 100644 utils/request.js create mode 100644 utils/storage.js create mode 100644 utils/util.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..a4ff8d7 --- /dev/null +++ b/app.js @@ -0,0 +1,136 @@ +// app.js - 小程序全局入口 +const api = require('./utils/api.js') +const storage = require('./utils/storage.js') +const auth = require('./utils/auth.js') + +App({ + globalData: { + userInfo: null, + token: '', + role: 'user', // user | merchant | admin + location: null, + systemInfo: null, + apiBase: 'https://api.campus.example.com', + version: '1.0.0', + // 购物车 { shopId: { shopName, goods: [{id,name,price,spec,count}], selected: true } } + cart: {} + }, + + onLaunch(options) { + // 系统信息 + try { + this.globalData.systemInfo = wx.getSystemInfoSync() + } catch (e) {} + + // 恢复登录状态 + const token = storage.get('token') + const userInfo = storage.get('userInfo') + if (token && userInfo) { + this.globalData.token = token + this.globalData.userInfo = userInfo + this.globalData.role = userInfo.role || 'user' + } + + // 恢复购物车 + const cart = storage.get('cart') + if (cart) this.globalData.cart = cart + + // 获取定位 + this.getLocation() + + // 检查登录状态 + this.checkLoginStatus() + + // 场景值处理 + if (options.scene) { + console.log('场景值:', options.scene) + } + }, + + onShow(options) { + // 页面显示时刷新用户信息 + if (this.globalData.token) { + // 可选:刷新用户信息 + } + }, + + onError(msg) { + console.error('小程序错误:', msg) + }, + + // 获取定位 + getLocation() { + wx.getLocation({ + type: 'gcj02', + success: (res) => { + this.globalData.location = { + latitude: res.latitude, + longitude: res.longitude + } + }, + fail: () => { + // 默认位置(校园中心) + this.globalData.location = { + latitude: 39.908823, + longitude: 116.397470 + } + } + }) + }, + + // 检查登录状态 + checkLoginStatus() { + if (!this.globalData.token) return + // 可调用后端校验token有效性 + }, + + // 登录 + login(cb) { + wx.login({ + success: (res) => { + if (res.code) { + // 模拟登录(实际应发送到后端换取token) + auth.mockLogin(res.code).then((user) => { + this.globalData.token = user.token + this.globalData.userInfo = user.info + this.globalData.role = user.info.role + storage.set('token', user.token) + storage.set('userInfo', user.info) + cb && cb(user) + }) + } + } + }) + }, + + // 登出 + logout() { + this.globalData.token = '' + this.globalData.userInfo = null + this.globalData.role = 'user' + storage.remove('token') + storage.remove('userInfo') + }, + + // 需要登录的操作 + requireLogin(cb) { + if (this.globalData.token) { + cb && cb() + } else { + wx.showModal({ + title: '提示', + content: '请先登录', + success: (res) => { + if (res.confirm) { + wx.navigateTo({ url: '/pages/user/login/login' }) + } + } + }) + } + }, + + // 保存购物车 + saveCart() { + storage.set('cart', this.globalData.cart) + } +}) diff --git a/app.json b/app.json new file mode 100644 index 0000000..be34096 --- /dev/null +++ b/app.json @@ -0,0 +1,74 @@ +{ + "pages": [ + "pages/index/index", + "pages/food/index/index", + "pages/food/shop/shop", + "pages/food/checkout/checkout", + "pages/food/order-detail/order-detail", + "pages/community/list/list", + "pages/community/detail/detail", + "pages/community/create/create", + "pages/community/activity/activity", + "pages/forum/list/list", + "pages/forum/detail/detail", + "pages/forum/publish/publish", + "pages/confession/list/list", + "pages/confession/publish/publish", + "pages/confession/detail/detail", + "pages/market/list/list", + "pages/market/publish/publish", + "pages/market/detail/detail", + "pages/market/my-goods/my-goods", + "pages/merchant/list/list", + "pages/merchant/detail/detail", + "pages/merchant/apply/apply", + "pages/merchant/my-shop/my-shop", + "pages/merchant/goods-manage/goods-manage", + "pages/merchant/goods-edit/goods-edit", + "pages/merchant/order-manage/order-manage", + "pages/errand/list/list", + "pages/errand/publish/publish", + "pages/errand/detail/detail", + "pages/custom/diy/diy", + "pages/custom/h5/h5", + "pages/user/profile/profile", + "pages/user/login/login", + "pages/user/order/my-order", + "pages/user/coupon/coupon", + "pages/user/favorite/favorite", + "pages/user/settings/settings" + ], + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTitleText": "校园综合服务", + "navigationBarTextStyle": "black", + "backgroundColor": "#f5f5f5" + }, + "tabBar": { + "color": "#999999", + "selectedColor": "#1890ff", + "backgroundColor": "#ffffff", + "borderStyle": "black", + "list": [ + { "pagePath": "pages/index/index", "text": "首页" }, + { "pagePath": "pages/food/index/index", "text": "外卖" }, + { "pagePath": "pages/community/list/list", "text": "社区" }, + { "pagePath": "pages/forum/list/list", "text": "论坛" }, + { "pagePath": "pages/user/profile/profile", "text": "我的" } + ] + }, + "permission": { + "scope.userLocation": { + "desc": "用于显示附近商户、活动定位和骑手位置追踪" + } + }, + "requiredPrivateInfos": [ + "getLocation", + "chooseLocation" + ], + "lazyCodeLoading": "requiredComponents", + "style": "v2", + "sitemapLocation": "sitemap.json", + "usingComponents": {} +} diff --git a/app.wxss b/app.wxss new file mode 100644 index 0000000..44c8c31 --- /dev/null +++ b/app.wxss @@ -0,0 +1,379 @@ +/* app.wxss - 全局样式 */ + +page { + --primary-color: #1890ff; + --primary-light: #e6f7ff; + --success-color: #52c41a; + --warning-color: #faad14; + --danger-color: #f5222d; + --text-color: #333; + --text-secondary: #666; + --text-light: #999; + --border-color: #e8e8e8; + --bg-color: #f5f5f5; + --card-bg: #fff; + --shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); + + background-color: var(--bg-color); + color: var(--text-color); + font-size: 28rpx; + line-height: 1.5; + font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +view, text, input, textarea, button, image { box-sizing: border-box; } + +/* 容器 */ +.container { padding: 20rpx; } +.page { min-height: 100vh; background: var(--bg-color); padding-bottom: 40rpx; } + +/* 卡片 */ +.card { + background: var(--card-bg); + border-radius: 16rpx; + padding: 24rpx; + margin: 20rpx; + box-shadow: var(--shadow); +} +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20rpx; + padding-bottom: 16rpx; + border-bottom: 1rpx solid var(--border-color); +} +.card-title { font-size: 32rpx; font-weight: bold; } +.card-subtitle { font-size: 24rpx; color: var(--text-light); } + +/* 按钮 */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 16rpx 40rpx; + border-radius: 48rpx; + font-size: 28rpx; + background: var(--primary-color); + color: #fff; + border: none; + min-height: 72rpx; +} +.btn::after { border: none; } +.btn-block { display: flex; width: 100%; } +.btn-sm { padding: 8rpx 24rpx; font-size: 24rpx; min-height: 56rpx; } +.btn-outline { background: #fff; color: var(--primary-color); border: 2rpx solid var(--primary-color); } +.btn-success { background: var(--success-color); } +.btn-warning { background: var(--warning-color); } +.btn-danger { background: var(--danger-color); } +.btn-ghost { background: transparent; color: var(--primary-color); } + +/* 标签 */ +.tag { + display: inline-block; + padding: 4rpx 16rpx; + border-radius: 8rpx; + font-size: 22rpx; + background: var(--primary-light); + color: var(--primary-color); + margin-right: 12rpx; +} +.tag-success { background: #f6ffed; color: #52c41a; } +.tag-warning { background: #fffbe6; color: #faad14; } +.tag-danger { background: #fff1f0; color: #f5222d; } + +/* 文本 */ +.text-primary { color: var(--primary-color); } +.text-success { color: var(--success-color); } +.text-warning { color: var(--warning-color); } +.text-danger { color: var(--danger-color); } +.text-secondary { color: var(--text-secondary); } +.text-light { color: var(--text-light); } +.text-bold { font-weight: bold; } +.text-lg { font-size: 32rpx; } +.text-xl { font-size: 40rpx; } +.text-sm { font-size: 24rpx; } + +/* flex */ +.flex { display: flex; } +.flex-center { display: flex; align-items: center; justify-content: center; } +.flex-between { display: flex; align-items: center; justify-content: space-between; } +.flex-start { display: flex; align-items: center; justify-content: flex-start; } +.flex-col { display: flex; flex-direction: column; } +.flex-1 { flex: 1; min-width: 0; } +.align-center { align-items: center; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.flex-wrap { flex-wrap: wrap; } + +/* 间距 */ +.mt-10 { margin-top: 10rpx; } +.mt-20 { margin-top: 20rpx; } +.mt-30 { margin-top: 30rpx; } +.mb-10 { margin-bottom: 10rpx; } +.mb-20 { margin-bottom: 20rpx; } +.mb-30 { margin-bottom: 30rpx; } +.mh-20 { margin-left: 20rpx; margin-right: 20rpx; } +.p-20 { padding: 20rpx; } + +/* 列表项 */ +.list-item { + padding: 24rpx; + background: #fff; + border-bottom: 1rpx solid var(--border-color); + display: flex; + align-items: center; +} +.list-item:last-child { border-bottom: none; } + +/* 头像 */ +.avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background: #eee; +} +.avatar-sm { width: 56rpx; height: 56rpx; } +.avatar-lg { width: 120rpx; height: 120rpx; } + +/* 商品图片占位 */ +.thumb { + width: 160rpx; + height: 160rpx; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; + color: #b0b0b0; + font-size: 40rpx; +} + +/* 评分 */ +.rating { + color: var(--warning-color); + font-size: 24rpx; +} + +/* 价格 */ +.price { + color: var(--danger-color); + font-weight: bold; + font-size: 32rpx; +} +.price-lg { font-size: 40rpx; } +.price-sm { font-size: 24rpx; color: var(--text-light); text-decoration: line-through; } + +/* 输入 */ +.input { + width: 100%; + padding: 20rpx 24rpx; + background: #f8f8f8; + border-radius: 8rpx; + font-size: 28rpx; +} +.textarea { + width: 100%; + padding: 20rpx; + min-height: 160rpx; + background: #f8f8f8; + border-radius: 8rpx; + font-size: 28rpx; +} + +/* 徽章 */ +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 32rpx; + height: 32rpx; + padding: 0 8rpx; + border-radius: 16rpx; + background: var(--danger-color); + color: #fff; + font-size: 20rpx; +} + +/* 空状态 */ +.empty { + padding: 100rpx 40rpx; + text-align: center; + color: var(--text-light); +} +.empty-icon { + font-size: 120rpx; + color: #ddd; + display: block; + margin-bottom: 20rpx; +} + +/* Tab/筛选栏 */ +.tabs { + display: flex; + background: #fff; + border-bottom: 1rpx solid var(--border-color); + position: sticky; + top: 0; + z-index: 10; +} +.tab-item { + flex: 1; + text-align: center; + padding: 24rpx 0; + font-size: 28rpx; + color: var(--text-secondary); + position: relative; +} +.tab-item.active { + color: var(--primary-color); + font-weight: bold; +} +.tab-item.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 48rpx; + height: 4rpx; + background: var(--primary-color); + border-radius: 2rpx; +} + +/* 搜索栏 */ +.search-bar { + display: flex; + align-items: center; + padding: 16rpx 24rpx; + background: #f5f5f5; + border-radius: 40rpx; + margin: 20rpx; +} +.search-bar input { + flex: 1; + font-size: 28rpx; +} +.search-icon { + margin-right: 12rpx; + color: var(--text-light); +} + +/* 分割线 */ +.divider { height: 1rpx; background: var(--border-color); margin: 20rpx 0; } +.divider-thick { height: 20rpx; background: var(--bg-color); margin: 0 -20rpx; } + +/* 订单状态 */ +.status-badge { + padding: 6rpx 16rpx; + border-radius: 8rpx; + font-size: 22rpx; +} +.status-pending { background: #fff7e6; color: #fa8c16; } +.status-paid { background: #e6f7ff; color: #1890ff; } +.status-making { background: #f6ffed; color: #52c41a; } +.status-delivering { background: #fff1f0; color: #f5222d; } +.status-completed { background: #f0f0f0; color: #666; } +.status-cancelled { background: #f5f5f5; color: #999; } + +/* 底部操作栏 */ +.footer-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #fff; + padding: 20rpx 24rpx; + display: flex; + align-items: center; + box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05); + z-index: 100; +} + +/* 浮动操作按钮 */ +.fab { + position: fixed; + right: 40rpx; + bottom: 80rpx; + width: 100rpx; + height: 100rpx; + border-radius: 50%; + background: var(--primary-color); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 48rpx; + box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.35); + z-index: 50; +} + +/* 数字步进器 */ +.stepper { + display: inline-flex; + align-items: center; + background: #f5f5f5; + border-radius: 8rpx; + overflow: hidden; +} +.stepper button { + width: 56rpx; + height: 56rpx; + background: transparent; + color: #333; + font-size: 32rpx; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + border: none; +} +.stepper button::after { border: none; } +.stepper .count { + min-width: 60rpx; + text-align: center; + font-size: 28rpx; +} + +/* 勋章 */ +.medal { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8rpx 16rpx; + background: linear-gradient(135deg, #ffd700 0%, #ffb347 100%); + color: #8b4513; + border-radius: 20rpx; + font-size: 22rpx; + margin-right: 8rpx; +} + +/* 属性选择器(商品属性) */ +.attr-group { margin: 20rpx 0; } +.attr-name { font-size: 28rpx; font-weight: bold; margin-bottom: 16rpx; } +.attr-options { display: flex; flex-wrap: wrap; gap: 16rpx; } +.attr-option { + padding: 12rpx 24rpx; + background: #f5f5f5; + border-radius: 8rpx; + font-size: 26rpx; + border: 2rpx solid transparent; +} +.attr-option.selected { + background: var(--primary-light); + color: var(--primary-color); + border-color: var(--primary-color); +} +.attr-number { + display: flex; + align-items: center; + gap: 16rpx; +} +.attr-number .value { + min-width: 80rpx; + text-align: center; + font-size: 32rpx; + font-weight: bold; + color: var(--primary-color); +} diff --git a/config.example.js b/config.example.js new file mode 100644 index 0000000..2bf3ac6 --- /dev/null +++ b/config.example.js @@ -0,0 +1,32 @@ +// config.example.js - 小程序配置示例 +// 复制为 config.js 并修改为实际配置 +module.exports = { + // 后端 API 基础地址 + apiBase: 'https://api.your-campus.com', + + // 小程序 AppID + appId: 'wx1234567890abcdef', + + // 上传图片地址 + uploadUrl: 'https://api.your-campus.com/upload', + + // 微信支付相关(后端实现) + payment: { + enable: true, + // 实际支付逻辑在后端完成,这里只配置入口 + prepayUrl: '/api/payment/prepay' + }, + + // 高德/腾讯地图 key(可选) + mapKey: '', + + // 消息推送配置 + templateIds: { + orderPayed: 'tmpl_1', + orderDelivering: 'tmpl_2', + orderComment: 'tmpl_3' + }, + + // 商家入驻审核回调地址 + merchantApplyCallback: 'https://api.your-campus.com/merchant/apply/callback' +} diff --git a/pages/community/activity/activity.js b/pages/community/activity/activity.js new file mode 100644 index 0000000..68832a3 --- /dev/null +++ b/pages/community/activity/activity.js @@ -0,0 +1,51 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + activities: [] + }, + + onShow() { + this.loadData() + }, + + async loadData() { + util.showLoading() + try { + let res = null + try { res = await api.community.activities({ page: 1, pageSize: 50 }) } catch (e) {} + let list = res && res.list ? res.list : [] + if (list.length === 0) { + list = [ + { id: 1, title: '新生杯篮球赛', location: '校园体育馆', startTime: Date.now() + 86400000, signupCount: 42, capacity: 100, fee: 0, description: '面向新生的篮球比赛活动', signed: false }, + { id: 2, title: 'Hackathon 编程马拉松', location: '创新大楼301', startTime: Date.now() + 86400000 * 3, signupCount: 88, capacity: 120, fee: 0, description: '48小时开发挑战', signed: true }, + { id: 3, title: '校园风景摄影分享', location: '艺术楼', startTime: Date.now() + 86400000 * 2, signupCount: 25, capacity: 50, fee: 10, description: '分享你的摄影作品', signed: false } + ] + } + this.setData({ activities: list }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + async onSignup(e) { + const id = e.currentTarget.dataset.id + const idx = e.currentTarget.dataset.idx + util.showLoading() + try { + await api.community.activitySignup(id) + const list = this.data.activities.slice() + list[idx].signed = true + list[idx].signupCount = (list[idx].signupCount || 0) + 1 + this.setData({ activities: list }) + util.hideLoading() + util.showToast('报名成功', 'success') + } catch (e) { + util.hideLoading() + util.showToast('报名失败') + } + } +}) diff --git a/pages/community/activity/activity.json b/pages/community/activity/activity.json new file mode 100644 index 0000000..9bdd321 --- /dev/null +++ b/pages/community/activity/activity.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "社区活动", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/community/activity/activity.wxml b/pages/community/activity/activity.wxml new file mode 100644 index 0000000..4208a2f --- /dev/null +++ b/pages/community/activity/activity.wxml @@ -0,0 +1,26 @@ + + + + + {{item.title}} + {{item.signupCount}}/{{item.capacity}} + + + 📍 {{item.location}} + 📅 即将开始 + + {{item.description}} + + ¥{{item.fee}} + 免费 + 已报名 + 立即报名 + + + + + + 🎯 + 暂无活动 + + diff --git a/pages/community/activity/activity.wxss b/pages/community/activity/activity.wxss new file mode 100644 index 0000000..1ec3588 --- /dev/null +++ b/pages/community/activity/activity.wxss @@ -0,0 +1,35 @@ +.activity-list { padding: 20rpx; } + +.activity-card { + margin-bottom: 20rpx; +} + +.activity-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12rpx; +} + +.activity-meta { + display: flex; + gap: 24rpx; + margin-top: 8rpx; +} + +.activity-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 20rpx; + border-top: 1rpx solid #f0f0f0; + margin-top: 16rpx; +} + +.text-success { color: #52c41a; } + +.btn-outline { + background: #fff; + color: #666; + border: 2rpx solid #ddd; +} diff --git a/pages/community/create/create.js b/pages/community/create/create.js new file mode 100644 index 0000000..7655c9a --- /dev/null +++ b/pages/community/create/create.js @@ -0,0 +1,52 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + categories: ['运动', '科技', '艺术', '音乐', '其他'], + form: { + name: '', + category: '运动', + description: '', + announcement: '' + }, + submitting: false + }, + + onInput(e) { + const key = e.currentTarget.dataset.key + const form = { ...this.data.form } + form[key] = e.detail.value + this.setData({ form }) + }, + + onCatTap(e) { + const idx = e.currentTarget.dataset.idx + const form = { ...this.data.form, category: this.data.categories[idx] } + this.setData({ form }) + }, + + async onSubmit() { + const f = this.data.form + if (!f.name) return util.showToast('请输入社区名称') + if (!f.description) return util.showToast('请输入社区简介') + + this.setData({ submitting: true }) + util.showLoading() + try { + await api.community.create(f) + util.hideLoading() + wx.showModal({ + title: '提交成功', + content: '社区已创建,可以开始招募成员了', + showCancel: false, + success: () => wx.navigateBack() + }) + } catch (e) { + util.hideLoading() + util.showToast('创建失败') + } finally { + this.setData({ submitting: false }) + } + } +}) diff --git a/pages/community/create/create.json b/pages/community/create/create.json new file mode 100644 index 0000000..df05e9f --- /dev/null +++ b/pages/community/create/create.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "创建社区", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/community/create/create.wxml b/pages/community/create/create.wxml new file mode 100644 index 0000000..3236eeb --- /dev/null +++ b/pages/community/create/create.wxml @@ -0,0 +1,33 @@ + + + 社区信息 + + 社区名称 + + + + 分类 + + + {{item}} + + + + + 社区简介 + + + + 社区公告 + + + + + + + + diff --git a/pages/community/create/create.wxss b/pages/community/create/create.wxss new file mode 100644 index 0000000..468a42b --- /dev/null +++ b/pages/community/create/create.wxss @@ -0,0 +1,48 @@ +.card-title { + font-size: 28rpx; + font-weight: bold; + margin-bottom: 20rpx; +} + +.form-item { margin-bottom: 24rpx; } + +.form-label { + display: block; + font-size: 26rpx; + color: #333; + margin-bottom: 12rpx; + font-weight: 500; +} + +.cat-row { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.cat-chip { + padding: 14rpx 32rpx; + background: #f5f5f5; + border-radius: 32rpx; + font-size: 26rpx; + color: #666; +} + +.cat-chip.active { + background: #13c2c2; + color: #fff; +} + +.btn-primary { + background: linear-gradient(135deg, #13c2c2 0%, #36cfc9 100%); + color: #fff; +} + +.btn-primary::after { border: none; } + +.btn-block { + width: 100%; + padding: 22rpx 0; + border-radius: 48rpx; + font-size: 30rpx; +} diff --git a/pages/community/detail/detail.js b/pages/community/detail/detail.js new file mode 100644 index 0000000..cdc2710 --- /dev/null +++ b/pages/community/detail/detail.js @@ -0,0 +1,84 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + tabs: [ + { id: 'posts', name: '动态' }, + { id: 'activities', name: '活动' }, + { id: 'members', name: '成员' } + ], + activeTab: 'posts', + community: null, + posts: [], + activities: [] + }, + + onLoad(options) { + this.communityId = options.id + this.loadData() + }, + + onTabTap(e) { + this.setData({ activeTab: e.currentTarget.dataset.id }) + }, + + async loadData() { + util.showLoading() + try { + let c = null, posts = [], acts = [] + try { c = await api.community.detail(this.communityId) } catch (e) {} + try { + const p = await api.community.posts(this.communityId, { page: 1, pageSize: 20 }) + posts = p && p.list ? p.list : [] + } catch (e) {} + try { + const a = await api.community.activities({ page: 1, pageSize: 20 }) + acts = a && a.list ? a.list : [] + } catch (e) {} + + if (!c) { + c = { + id: this.communityId, + name: '校园篮球社', + category: '运动', + members: 328, + posts: 1280, + description: '篮球爱好者聚集地,每周五晚球场见', + joined: true + } + } + if (posts.length === 0) { + posts = [ + { id: 1, author: '小明', avatar: '', content: '今晚8点篮球场5v5,有兴趣的同学来', likes: 25, comments: 8, createdAt: Date.now() - 1800000 }, + { id: 2, author: '球王', avatar: '', content: '分享一个超实用的投篮训练视频', likes: 88, comments: 22, createdAt: Date.now() - 7200000 } + ] + } + if (acts.length === 0) { + acts = [ + { id: 1, title: '新生杯篮球赛', location: '校园体育馆', startTime: Date.now() + 86400000, signupCount: 42, capacity: 100, fee: 0 }, + { id: 2, title: '每周训练', location: '室外篮球场', startTime: Date.now() + 86400000 * 2, signupCount: 18, capacity: 50, fee: 0 } + ] + } + this.setData({ community: c, posts, activities: acts }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onActivitySignup(e) { + util.showToast('报名成功', 'success') + }, + + onPublish() { + util.showToast('发布动态 (模拟)') + }, + + onJoin() { + const c = { ...this.data.community, joined: true, members: (this.data.community.members || 0) + 1 } + this.setData({ community: c }) + util.showToast('加入成功', 'success') + } +}) diff --git a/pages/community/detail/detail.json b/pages/community/detail/detail.json new file mode 100644 index 0000000..da545e0 --- /dev/null +++ b/pages/community/detail/detail.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "社区详情", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/community/detail/detail.wxml b/pages/community/detail/detail.wxml new file mode 100644 index 0000000..e271a71 --- /dev/null +++ b/pages/community/detail/detail.wxml @@ -0,0 +1,77 @@ + + + {{community.name}} + {{community.description}} + + 👥 {{community.members}} 成员 + 📝 {{community.posts}} 帖子 + + 加入社区 + 已加入 + + + + + {{item.name}} + + + + + + + 👤 + + + 刚刚 + + + {{item.content}} + + + + 📝 + 暂无动态 + + + + + + + {{item.title}} + 报名 {{item.signupCount}}/{{item.capacity}} + + 📍 {{item.location}} + 📅 即将开始 + + ¥{{item.fee}} + 免费 + 立即报名 + + + + 🎯 + 暂无活动 + + + + + + + 👤 + 成员 {{item}} + + + + + + + + + + 🎯 + 加载中... + diff --git a/pages/community/detail/detail.wxss b/pages/community/detail/detail.wxss new file mode 100644 index 0000000..db24920 --- /dev/null +++ b/pages/community/detail/detail.wxss @@ -0,0 +1,107 @@ +.hero { + background: linear-gradient(135deg, #13c2c2 0%, #36cfc9 100%); + padding: 40rpx 30rpx; + color: #fff; + position: relative; +} + +.hero-title { + font-size: 36rpx; + font-weight: bold; + margin-bottom: 12rpx; +} + +.hero-desc { + font-size: 26rpx; + opacity: 0.9; + margin-bottom: 20rpx; +} + +.hero-stats { + margin-bottom: 20rpx; +} + +.join-btn { + background: #fff; + color: #13c2c2; + display: inline-block; +} + +.content-list { padding: 20rpx 20rpx 120rpx; } + +.post-card { + margin-bottom: 20rpx; +} + +.post-header { + display: flex; + align-items: center; + margin-bottom: 16rpx; +} + +.post-author { + font-size: 28rpx; + font-weight: 500; +} + +.post-content { + font-size: 28rpx; + color: #333; + line-height: 1.6; + margin-bottom: 16rpx; +} + +.post-footer { + display: flex; + gap: 24rpx; +} + +.activity-card { + margin-bottom: 20rpx; +} + +.activity-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12rpx; +} + +.activity-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 20rpx; + border-top: 1rpx solid #f0f0f0; + margin-top: 16rpx; +} + +.member-item { + display: flex; + align-items: center; + gap: 20rpx; + padding: 16rpx 0; + border-bottom: 1rpx solid #f0f0f0; +} + +.member-item:last-child { border-bottom: none; } + +.text-success { color: #52c41a; } + +.fab { + position: fixed; + right: 40rpx; + bottom: 80rpx; + width: 100rpx; + height: 100rpx; + border-radius: 50%; + background: #13c2c2; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 56rpx; + line-height: 1; + box-shadow: 0 8rpx 24rpx rgba(19, 194, 194, 0.35); + z-index: 50; + } diff --git a/pages/community/list/list.js b/pages/community/list/list.js new file mode 100644 index 0000000..cf2b441 --- /dev/null +++ b/pages/community/list/list.js @@ -0,0 +1,80 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + tabs: [ + { id: '全部', name: '全部' }, + { id: '运动', name: '运动' }, + { id: '科技', name: '科技' }, + { id: '艺术', name: '艺术' }, + { id: '音乐', name: '音乐' }, + { id: '其他', name: '其他' } + ], + activeTab: '全部', + communities: [] + }, + + onShow() { + this.loadData() + }, + + onTabTap(e) { + this.setData({ activeTab: e.currentTarget.dataset.id }) + this.loadData() + }, + + async loadData() { + util.showLoading() + try { + const res = await api.community.list({ page: 1, pageSize: 50 }) + let list = res && res.list ? res.list : [] + if (list.length === 0) { + list = [ + { id: 1, name: '校园篮球社', category: '运动', members: 328, posts: 1280, description: '篮球爱好者聚集地,每周五晚球场见', logo: '', cover: '', joined: false }, + { id: 2, name: '编程与算法', category: '科技', members: 512, posts: 2100, description: '代码改变世界,每周算法分享会', logo: '', cover: '', joined: true }, + { id: 3, name: '摄影爱好社', category: '艺术', members: 156, posts: 680, description: '用镜头记录美好,定期外拍活动', logo: '', cover: '', joined: false }, + { id: 4, name: '校园吉他社', category: '音乐', members: 208, posts: 450, description: '音乐无国界,一起拨动琴弦', logo: '', cover: '', joined: false }, + { id: 5, name: '桌游社', category: '其他', members: 88, posts: 220, description: '狼人杀、三国杀、桌游之夜', logo: '', cover: '', joined: false } + ] + if (this.data.activeTab !== '全部') { + list = list.filter(c => c.category === this.data.activeTab) + } + } + this.setData({ communities: list }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onDetail(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/community/detail/detail?id=' + id }) + }, + + async onJoin(e) { + const id = e.currentTarget.dataset.id + const idx = e.currentTarget.dataset.idx + util.showLoading() + try { + await api.community.join(id) + const list = this.data.communities.slice() + if (list[idx]) { + list[idx].joined = true + list[idx].members = (list[idx].members || 0) + 1 + this.setData({ communities: list }) + } + util.hideLoading() + util.showToast('加入成功', 'success') + } catch (e) { + util.hideLoading() + util.showToast('加入失败') + } + }, + + onCreate() { + wx.navigateTo({ url: '/pages/community/create/create' }) + } +}) diff --git a/pages/community/list/list.json b/pages/community/list/list.json new file mode 100644 index 0000000..2f1fe21 --- /dev/null +++ b/pages/community/list/list.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "兴趣社区", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/community/list/list.wxml b/pages/community/list/list.wxml new file mode 100644 index 0000000..ff9728e --- /dev/null +++ b/pages/community/list/list.wxml @@ -0,0 +1,38 @@ + + + + + {{item.name}} + + + + + + + + {{item.name}} + + + + {{item.name}} + {{item.category}} + + {{item.description}} + + 👥 {{item.members}} · 📝 {{item.posts}} + 已加入 + 加入 + + + + + + + 🎯 + 暂无社区 + + + + + diff --git a/pages/community/list/list.wxss b/pages/community/list/list.wxss new file mode 100644 index 0000000..918b704 --- /dev/null +++ b/pages/community/list/list.wxss @@ -0,0 +1,77 @@ +.tabs-scroll { + white-space: nowrap; +} + +.community-list { padding: 20rpx 20rpx 120rpx; } + +.community-card { + margin-bottom: 20rpx; + padding: 0; + overflow: hidden; +} + +.community-cover { + height: 200rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.cover-text { + color: #fff; + font-size: 36rpx; + font-weight: bold; + text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.2); +} + +.community-info { + padding: 20rpx; +} + +.community-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12rpx; +} + +.community-name { + font-size: 30rpx; + font-weight: bold; +} + +.community-desc { + margin-bottom: 16rpx; + line-height: 1.5; +} + +.community-footer { + display: flex; + align-items: center; + justify-content: space-between; +} + +.join-btn { + background: #1890ff; + color: #fff; +} + +.fab { + position: fixed; + right: 40rpx; + bottom: 80rpx; + width: 100rpx; + height: 100rpx; + border-radius: 50%; + background: #13c2c2; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 56rpx; + line-height: 1; + box-shadow: 0 8rpx 24rpx rgba(19, 194, 194, 0.35); + z-index: 50; + } diff --git a/pages/confession/detail/detail.js b/pages/confession/detail/detail.js new file mode 100644 index 0000000..264f427 --- /dev/null +++ b/pages/confession/detail/detail.js @@ -0,0 +1,87 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + confession: null, + commentInput: '', + liked: false + }, + + onLoad(options) { + this.confessionId = options.id + this.loadData() + }, + + async loadData() { + util.showLoading() + try { + let c = null + try { c = await api.confession.detail(this.confessionId) } catch (e) {} + if (!c) { + c = { + id: this.confessionId, + author: '匿名', + isAnonymous: true, + content: '图书馆三楼靠窗第三排的女生,每天都能看到你专注的样子,你笑起来真的好好看,可以认识一下吗?', + likes: 128, + comments: 32, + createdAt: Date.now() - 3600000, + replies: [ + { id: 1, author: '匿名', content: '祝福!', likes: 5, createdAt: Date.now() - 2000000 }, + { id: 2, author: '匿名', content: '可以去图书馆搭讪一下', likes: 3, createdAt: Date.now() - 1000000 } + ] + } + } + this.setData({ confession: c }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onCommentInput(e) { + this.setData({ commentInput: e.detail.value }) + }, + + async onLike() { + try { + await api.confession.like(this.confessionId) + const c = { ...this.data.confession, likes: (this.data.confession.likes || 0) + (this.data.liked ? -1 : 1) } + this.setData({ confession: c, liked: !this.data.liked }) + } catch (e) { util.showToast('点赞成功') } + }, + + onReport() { + wx.showModal({ + title: '举报', + content: '确认举报这条表白?', + success: (res) => { + if (res.confirm) { + api.confession.report(this.confessionId, '不合适内容').then(() => { + util.showToast('举报已提交') + }).catch(() => util.showToast('举报已提交')) + } + } + }) + }, + + async onSubmitComment() { + if (!this.data.commentInput.trim()) return util.showToast('请输入评论内容') + util.showLoading() + try { + await api.confession.comment(this.confessionId, { content: this.data.commentInput }) + const c = { ...this.data.confession, replies: [...(this.data.confession.replies || []), { + id: Date.now(), author: '匿名', content: this.data.commentInput, likes: 0, createdAt: Date.now() + }] } + c.comments = (c.comments || 0) + 1 + this.setData({ confession: c, commentInput: '' }) + util.hideLoading() + util.showToast('评论成功', 'success') + } catch (e) { + util.hideLoading() + util.showToast('评论失败') + } + } +}) diff --git a/pages/confession/detail/detail.json b/pages/confession/detail/detail.json new file mode 100644 index 0000000..24ddcc8 --- /dev/null +++ b/pages/confession/detail/detail.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "表白详情", + "backgroundColor": "#f7f2f8" +} diff --git a/pages/confession/detail/detail.wxml b/pages/confession/detail/detail.wxml new file mode 100644 index 0000000..f73f0cf --- /dev/null +++ b/pages/confession/detail/detail.wxml @@ -0,0 +1,51 @@ + + + + 💖 + + {{confession.author}} + 刚刚 + + + {{confession.content}} + + + + ⚠️ 举报 + + + + + + 评论 ({{confession.comments}}) + + 💬 + + + {{item.author}} + + {{item.content}} + + ❤️ {{item.likes}} + + + + + 暂无评论 + + + + + + + + 发送 + + + + + 💌 + 加载中... + diff --git a/pages/confession/detail/detail.wxss b/pages/confession/detail/detail.wxss new file mode 100644 index 0000000..7dea444 --- /dev/null +++ b/pages/confession/detail/detail.wxss @@ -0,0 +1,131 @@ +.page { background: #f7f2f8; } + +.confession-card { + border-top: 8rpx solid #eb2f96; +} + +.confession-header { + display: flex; + align-items: center; + margin-bottom: 20rpx; +} + +.avatar { + width: 70rpx; + height: 70rpx; + border-radius: 50%; + background: #fff0f6; + display: flex; + align-items: center; + justify-content: center; + font-size: 36rpx; + margin-right: 16rpx; +} + +.confession-author { + font-size: 28rpx; + font-weight: 500; + color: #eb2f96; +} + +.confession-content { + font-size: 32rpx; + color: #333; + line-height: 1.9; + margin-bottom: 24rpx; +} + +.confession-actions { + display: flex; + align-items: center; + padding-top: 24rpx; + border-top: 1rpx solid #f0f0f0; + gap: 24rpx; +} + +.like-btn { + background: #fff0f6; + color: #eb2f96; + padding: 12rpx 28rpx; + border-radius: 28rpx; + font-size: 26rpx; +} + +.like-btn.active { + background: #eb2f96; + color: #fff; +} + +.comment-section { + margin-top: 20rpx; +} + +.card-title { + font-size: 28rpx; + font-weight: bold; + margin-bottom: 20rpx; +} + +.comment-item { + display: flex; + padding: 16rpx 0; + border-bottom: 1rpx solid #f0f0f0; +} + +.comment-item:last-child { border-bottom: none; } + +.comment-avatar { + width: 56rpx; + height: 56rpx; + border-radius: 50%; + background: #fff0f6; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + flex-shrink: 0; +} + +.comment-body { + flex: 1; + margin-left: 16rpx; +} + +.comment-header { margin-bottom: 8rpx; } + +.comment-text { + font-size: 28rpx; + color: #333; + line-height: 1.6; +} + +.comment-footer { margin-top: 8rpx; } + +.btn-outline { + background: #fff; + color: #666; + border: 2rpx solid #ddd; +} + +.comment-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 20rpx; + background: #fff; + display: flex; + align-items: center; + gap: 16rpx; + box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05); +} + +.comment-input { + flex: 1; + background: #f5f5f5; +} + +.btn-submit { + background: #eb2f96; + color: #fff; +} diff --git a/pages/confession/list/list.js b/pages/confession/list/list.js new file mode 100644 index 0000000..8d21ff1 --- /dev/null +++ b/pages/confession/list/list.js @@ -0,0 +1,70 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + tabs: [ + { id: 'latest', name: '最新' }, + { id: 'hot', name: '热门' } + ], + activeTab: 'latest', + confessions: [] + }, + + onShow() { + this.loadList() + }, + + onTabTap(e) { + this.setData({ activeTab: e.currentTarget.dataset.id }) + this.loadList() + }, + + async loadList() { + util.showLoading() + try { + let res = null + try { + if (this.data.activeTab === 'hot') { + res = await api.confession.hot({ page: 1, pageSize: 30 }) + } else { + res = await api.confession.list({ page: 1, pageSize: 30 }) + } + } catch (e) {} + let list = res && res.list ? res.list : [] + if (list.length === 0) { + list = [ + { id: 1, content: '图书馆三楼靠窗第三排的女生,每天都能看到你专注的样子,你笑起来真的好好看,可以认识一下吗?', author: '匿名', isAnonymous: true, likes: 128, comments: 32, createdAt: Date.now() - 3600000 }, + { id: 2, content: '计算机学院的学长,上次帮我修电脑的那个,你说"下次有问题再找我",我想...我可能又有问题了 :)', author: '匿名', isAnonymous: true, likes: 256, comments: 48, createdAt: Date.now() - 7200000 }, + { id: 3, content: '致篮球场上穿12号球衣的男生:你的后仰跳投真的很帅!', author: '匿名', isAnonymous: true, likes: 88, comments: 12, createdAt: Date.now() - 86400000 }, + { id: 4, content: '食堂三楼卖奶茶的小姐姐,你的笑容比奶茶还甜!', author: '匿名', isAnonymous: true, likes: 156, comments: 25, createdAt: Date.now() - 86400000 * 2 } + ] + } + this.setData({ confessions: list }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onDetail(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/confession/detail/detail?id=' + id }) + }, + + async onLike(e) { + const id = e.currentTarget.dataset.id + const idx = e.currentTarget.dataset.idx + try { + await api.confession.like(id) + const list = this.data.confessions.slice() + list[idx].likes = (list[idx].likes || 0) + 1 + this.setData({ confessions: list }) + } catch (e) { util.showToast('点赞成功') } + }, + + onPublish() { + wx.navigateTo({ url: '/pages/confession/publish/publish' }) + } +}) diff --git a/pages/confession/list/list.json b/pages/confession/list/list.json new file mode 100644 index 0000000..84b41c9 --- /dev/null +++ b/pages/confession/list/list.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "表白墙", + "backgroundColor": "#f7f2f8" +} diff --git a/pages/confession/list/list.wxml b/pages/confession/list/list.wxml new file mode 100644 index 0000000..8160bcf --- /dev/null +++ b/pages/confession/list/list.wxml @@ -0,0 +1,40 @@ + + + + + + {{item.name}} + + + + + + + 💖 + + {{item.author}} + 刚刚 + + + {{item.content}} + + + 💬 {{item.comments}} + + + + + + 💌 + 暂无表白 + + + + + diff --git a/pages/confession/list/list.wxss b/pages/confession/list/list.wxss new file mode 100644 index 0000000..a1dd802 --- /dev/null +++ b/pages/confession/list/list.wxss @@ -0,0 +1,126 @@ +.page { + background: #f7f2f8; +} + +.banner { + background: linear-gradient(135deg, #ff85c0 0%, #eb2f96 100%); + padding: 50rpx 30rpx; + color: #fff; + text-align: center; +} + +.banner-title { + font-size: 40rpx; + font-weight: bold; + margin-bottom: 8rpx; +} + +.banner-sub { + font-size: 26rpx; + opacity: 0.9; +} + +.confession-list { padding: 20rpx 20rpx 120rpx; } + +.confession-card { + margin-bottom: 20rpx; + border-left: 6rpx solid #eb2f96; +} + +.confession-header { + display: flex; + align-items: center; + margin-bottom: 16rpx; +} + +.avatar { + width: 60rpx; + height: 60rpx; + border-radius: 50%; + background: #fff0f6; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + margin-right: 16rpx; +} + +.confession-author { + font-size: 28rpx; + font-weight: 500; + color: #eb2f96; +} + +.confession-content { + font-size: 30rpx; + color: #333; + line-height: 1.8; + margin-bottom: 16rpx; +} + +.confession-footer { + display: flex; + align-items: center; + gap: 24rpx; + padding-top: 16rpx; + border-top: 1rpx solid #f5f5f5; +} + +.like-btn { + background: #fff0f6; + color: #eb2f96; + padding: 8rpx 20rpx; + border-radius: 24rpx; + font-size: 24rpx; +} + +.fab { + position: fixed; + right: 40rpx; + bottom: 80rpx; + width: 100rpx; + height: 100rpx; + border-radius: 50%; + background: linear-gradient(135deg, #ff85c0 0%, #eb2f96 100%); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 56rpx; + line-height: 1; + box-shadow: 0 8rpx 24rpx rgba(235, 47, 150, 0.35); + z-index: 50; +} + +.tabs { + background: #fff; + border-bottom: 1rpx solid #f0f0f0; + display: flex; + padding: 0 20rpx; +} + +.tab-item { + flex: 1; + text-align: center; + padding: 24rpx 0; + font-size: 28rpx; + color: #666; + position: relative; +} + +.tab-item.active { + color: #eb2f96; + font-weight: bold; +} + +.tab-item.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 48rpx; + height: 4rpx; + background: #eb2f96; + border-radius: 2rpx; +} diff --git a/pages/confession/publish/publish.js b/pages/confession/publish/publish.js new file mode 100644 index 0000000..7f9afd8 --- /dev/null +++ b/pages/confession/publish/publish.js @@ -0,0 +1,43 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + form: { + content: '', + isAnonymous: true + }, + submitting: false + }, + + onInput(e) { + const form = { ...this.data.form, content: e.detail.value } + this.setData({ form }) + }, + + toggleAnonymous() { + const form = { ...this.data.form, isAnonymous: !this.data.form.isAnonymous } + this.setData({ form }) + }, + + async onSubmit() { + if (!this.data.form.content.trim()) return util.showToast('请输入内容') + this.setData({ submitting: true }) + util.showLoading() + try { + await api.confession.create(this.data.form) + util.hideLoading() + wx.showModal({ + title: '提交成功', + content: '您的表白已提交,感谢分享', + showCancel: false, + success: () => wx.navigateBack() + }) + } catch (e) { + util.hideLoading() + util.showToast('提交失败') + } finally { + this.setData({ submitting: false }) + } + } +}) diff --git a/pages/confession/publish/publish.json b/pages/confession/publish/publish.json new file mode 100644 index 0000000..39bcb83 --- /dev/null +++ b/pages/confession/publish/publish.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "发布表白", + "backgroundColor": "#f7f2f8" +} diff --git a/pages/confession/publish/publish.wxml b/pages/confession/publish/publish.wxml new file mode 100644 index 0000000..a6fb5ec --- /dev/null +++ b/pages/confession/publish/publish.wxml @@ -0,0 +1,20 @@ + + + 💌 写下你想说的话 + + + + + + 匿名发布 + + + 匿名发布将不显示您的个人信息,所有内容会经过审核 + + + + + + diff --git a/pages/confession/publish/publish.wxss b/pages/confession/publish/publish.wxss new file mode 100644 index 0000000..cea1fc8 --- /dev/null +++ b/pages/confession/publish/publish.wxss @@ -0,0 +1,64 @@ +.page { background: #f7f2f8; } + +.card-title { + font-size: 30rpx; + font-weight: bold; + margin-bottom: 20rpx; + color: #eb2f96; +} + +.publish-textarea { + min-height: 400rpx; +} + +.switch-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10rpx 0; +} + +.switch { + width: 80rpx; + height: 44rpx; + border-radius: 44rpx; + background: #ccc; + position: relative; + transition: background 0.2s; +} + +.switch.on { background: #eb2f96; } + +.switch-dot { + width: 40rpx; + height: 40rpx; + border-radius: 50%; + background: #fff; + position: absolute; + top: 2rpx; + left: 2rpx; + transition: left 0.2s; + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1); +} + +.switch.on .switch-dot { left: 38rpx; } + +.tip-text { + margin-top: 16rpx; + padding-top: 16rpx; + border-top: 1rpx solid #f0f0f0; +} + +.btn-primary { + background: linear-gradient(135deg, #ff85c0 0%, #eb2f96 100%); + color: #fff; +} + +.btn-primary::after { border: none; } + +.btn-block { + width: 100%; + padding: 22rpx 0; + border-radius: 48rpx; + font-size: 30rpx; +} diff --git a/pages/custom/diy/diy.js b/pages/custom/diy/diy.js new file mode 100644 index 0000000..5e77f63 --- /dev/null +++ b/pages/custom/diy/diy.js @@ -0,0 +1,61 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + pageId: '', + page: null + }, + + onLoad(options) { + this.setData({ pageId: options.id || '1' }) + this.loadPage() + }, + + async loadPage() { + util.showLoading() + try { + let p = null + try { p = await api.diy.detail(this.data.pageId) } catch (e) {} + if (!p) { + p = { + id: this.data.pageId, + title: '校园风光', + banner: '', + components: [ + { type: 'title', content: '欢迎来到我们的校园' }, + { type: 'richtext', content: '

这里是充满青春与梦想的地方,在这里每天都有新的故事。

校园里的每一处都值得被发现。

' }, + { type: 'image', src: '', url: '' }, + { type: 'divider' }, + { type: 'text', content: '点击下方按钮,发现更多精彩内容!' }, + { type: 'button', text: '查看论坛', url: '/pages/forum/list/list' }, + { type: 'button', text: '逛逛二手市场', url: '/pages/market/list/list' }, + { type: 'divider' } + ] + } + } + this.setData({ page: p }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onTapButton(e) { + const url = e.currentTarget.dataset.url + if (!url) return + if (url.startsWith('http')) { + wx.navigateTo({ url: '/pages/custom/h5/h5?url=' + encodeURIComponent(url) + '&title=' + encodeURIComponent('外部链接') }) + } else { + wx.navigateTo({ url, fail: () => { + wx.switchTab({ url, fail: () => util.showToast('页面打开失败') }) + }}) + } + }, + + onGoodsTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/market/detail/detail?id=' + id }) + } +}) diff --git a/pages/custom/diy/diy.json b/pages/custom/diy/diy.json new file mode 100644 index 0000000..a10ac59 --- /dev/null +++ b/pages/custom/diy/diy.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "动态页面", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/custom/diy/diy.wxml b/pages/custom/diy/diy.wxml new file mode 100644 index 0000000..120d0f2 --- /dev/null +++ b/pages/custom/diy/diy.wxml @@ -0,0 +1,57 @@ + + + {{page.title}} + + + + + {{comp.content}} + + + + {{comp.content}} + + + + + + + + 📷 + 点击查看大图 + + + + + + + + {{comp.text}} + + + + 精选推荐 + + 📦 + + 商品 {{idx}} + ¥{{idx * 100}} + + + + + + 📋 + {{comp.content || '暂无内容'}} + + + + + 页面加载完成 + + + + + + 加载中... + diff --git a/pages/custom/diy/diy.wxss b/pages/custom/diy/diy.wxss new file mode 100644 index 0000000..7139317 --- /dev/null +++ b/pages/custom/diy/diy.wxss @@ -0,0 +1,135 @@ +.page-banner { + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + padding: 50rpx 30rpx; + color: #fff; +} + +.page-title { + font-size: 40rpx; + font-weight: bold; +} + +.diy-title { + font-size: 36rpx; + font-weight: bold; + color: #1890ff; + line-height: 1.5; +} + +.diy-text { + font-size: 30rpx; + line-height: 1.8; + color: #333; +} + +.diy-richtext { + font-size: 30rpx; + line-height: 1.8; + color: #333; +} + +.diy-image-block { + display: flex; + flex-direction: column; + align-items: center; + padding: 20rpx; +} + +.diy-image { + width: 100%; + height: 400rpx; + background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%); + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 100rpx; + margin-bottom: 12rpx; +} + +.diy-divider { + padding: 30rpx 40rpx; + display: flex; + align-items: center; +} + +.diy-divider .line { + flex: 1; + height: 2rpx; + background: #e8e8e8; +} + +.diy-btn-block { + padding: 20rpx; +} + +.diy-button { + width: 100%; + padding: 24rpx 0; + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + color: #fff; + border-radius: 48rpx; + font-size: 30rpx; + text-align: center; + box-shadow: 0 4rpx 16rpx rgba(24, 144, 255, 0.2); +} + +.diy-goods { + margin: 0 20rpx; +} + +.card-title { + font-size: 28rpx; + font-weight: bold; + margin-bottom: 20rpx; +} + +.goods-mini { + display: flex; + align-items: center; + padding: 16rpx 0; + border-bottom: 1rpx solid #f0f0f0; +} + +.goods-mini:last-child { border-bottom: none; } + +.goods-mini-img { + width: 80rpx; + height: 80rpx; + border-radius: 8rpx; + background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + flex-shrink: 0; +} + +.goods-mini-info { + flex: 1; + margin-left: 20rpx; + display: flex; + justify-content: space-between; + align-items: center; +} + +.goods-mini-name { + font-size: 28rpx; +} + +.goods-mini-price { + font-size: 30rpx; + color: #ff4d4f; + font-weight: bold; +} + +.empty { + padding: 100rpx 40rpx; + text-align: center; +} + +.empty-icon { + font-size: 120rpx; + display: block; + margin-bottom: 20rpx; +} diff --git a/pages/custom/h5/h5.js b/pages/custom/h5/h5.js new file mode 100644 index 0000000..f6a4824 --- /dev/null +++ b/pages/custom/h5/h5.js @@ -0,0 +1,31 @@ +const util = require('../../../utils/util.js') + +Page({ + data: { + url: '', + title: '' + }, + + onLoad(options) { + this.setData({ + url: decodeURIComponent(options.url || ''), + title: decodeURIComponent(options.title || '外部链接') + }) + wx.setNavigationBarTitle({ title: this.data.title }) + }, + + onWebError() { + util.showToast('页面加载失败,请在浏览器中打开') + }, + + onCopy() { + wx.setClipboardData({ data: this.data.url }) + }, + + onShareAppMessage() { + return { + title: this.data.title, + path: '/pages/custom/h5/h5?url=' + encodeURIComponent(this.data.url) + '&title=' + encodeURIComponent(this.data.title) + } + } +}) diff --git a/pages/custom/h5/h5.json b/pages/custom/h5/h5.json new file mode 100644 index 0000000..077b39b --- /dev/null +++ b/pages/custom/h5/h5.json @@ -0,0 +1 @@ +{ "navigationBarTitleText": "外部链接", "usingComponents": {} } diff --git a/pages/custom/h5/h5.wxml b/pages/custom/h5/h5.wxml new file mode 100644 index 0000000..157a012 --- /dev/null +++ b/pages/custom/h5/h5.wxml @@ -0,0 +1,15 @@ + + + + + + 🔗 + {{title}} + 此页面为外部链接 + {{url}} + + + 提示: 小程序内置 web-view 仅可展示已配置的业务域名。 + + + diff --git a/pages/errand/detail/detail.js b/pages/errand/detail/detail.js new file mode 100644 index 0000000..9a57bb9 --- /dev/null +++ b/pages/errand/detail/detail.js @@ -0,0 +1,101 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + task: null + }, + + onLoad(options) { + this.taskId = options.id + this.loadTask() + }, + + async loadTask() { + util.showLoading() + try { + let t = null + try { t = await api.errand.detail(this.taskId) } catch (e) {} + if (!t) { + t = { + id: this.taskId, + title: '顺丰快递代取', + type: '取快递', + pickup: '南门菜鸟驿站', + delivery: '宿舍1号楼 501', + fee: 5, + status: 0, + publisher: '用户A', + publisherPhone: '138****1111', + runnerName: '', + runnerPhone: '', + description: '快递码 123-45-678,物品是一个盒子,体积不大', + createdAt: Date.now() - 1800000, + pickupLat: 39.908823, + pickupLng: 116.397470, + deliveryLat: 39.910, + deliveryLng: 116.400, + logs: [ + { time: Date.now() - 1800000, text: '任务发布' } + ] + } + } + this.setData({ task: t }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + async onAccept() { + util.showLoading() + try { + await api.errand.accept(this.taskId) + const task = { ...this.data.task, status: 1, runnerName: '我' } + this.setData({ task }) + util.hideLoading() + util.showToast('接单成功', 'success') + } catch (e) { + util.hideLoading() + util.showToast('操作失败') + } + }, + + async onComplete() { + util.showLoading() + try { + await api.errand.complete(this.taskId) + const task = { ...this.data.task, status: 2 } + this.setData({ task }) + util.hideLoading() + util.showToast('已完成', 'success') + } catch (e) { + util.hideLoading() + util.showToast('操作失败') + } + }, + + onCancel() { + wx.showModal({ + title: '提示', + content: '确认取消任务?', + success: (res) => { + if (res.confirm) { + const task = { ...this.data.task, status: 3 } + this.setData({ task }) + util.showToast('已取消') + } + } + }) + }, + + onCall(e) { + const phone = e.currentTarget.dataset.phone + if (phone) wx.makePhoneCall({ phoneNumber: phone, fail: () => util.showToast('呼叫失败') }) + }, + + onShowMap() { + util.showToast('地图导航 (模拟)') + } +}) diff --git a/pages/errand/detail/detail.json b/pages/errand/detail/detail.json new file mode 100644 index 0000000..efb0c63 --- /dev/null +++ b/pages/errand/detail/detail.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "任务详情", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/errand/detail/detail.wxml b/pages/errand/detail/detail.wxml new file mode 100644 index 0000000..f32512d --- /dev/null +++ b/pages/errand/detail/detail.wxml @@ -0,0 +1,67 @@ + + + + {{task.type}} + 待接单 + 进行中 + 已完成 + 已取消 + + {{task.title}} + ¥ {{task.fee}} + + + + 路线 + + + + + 取件 + {{task.pickup}} + + + + + + + 送达 + {{task.delivery}} + + + + 🗺️ 查看地图 + + + + 任务描述 + {{task.description}} + + + + 联系人 + + 发布人 + {{task.publisher}} + 📞 联系 + + + 跑腿员 + {{task.runnerName}} + 📞 联系 + + + + + 取消任务 + 立即接单 + 完成任务 + 任务已完成 + 任务已取消 + + + + + 📋 + 加载中... + diff --git a/pages/errand/detail/detail.wxss b/pages/errand/detail/detail.wxss new file mode 100644 index 0000000..28cd36b --- /dev/null +++ b/pages/errand/detail/detail.wxss @@ -0,0 +1,114 @@ +.hero { + background: linear-gradient(135deg, #722ed1 0%, #9254de 100%); + padding: 40rpx 30rpx; + color: #fff; +} + +.hero-row { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 16rpx; +} + +.task-type { + background: rgba(255,255,255,0.2); + padding: 6rpx 16rpx; + border-radius: 8rpx; + font-size: 22rpx; +} + +.task-type ~ .tag { background: rgba(255,255,255,0.2); color: #fff; } + +.hero-title { + font-size: 36rpx; + font-weight: bold; + margin: 12rpx 0; +} + +.hero-fee { + font-size: 48rpx; + font-weight: bold; + margin-top: 20rpx; +} + +.card-title { + font-size: 28rpx; + font-weight: bold; + margin-bottom: 20rpx; +} + +.route { + background: #fafafa; + padding: 24rpx; + border-radius: 12rpx; +} + +.route-item { + display: flex; + align-items: flex-start; + gap: 16rpx; + padding: 12rpx 0; +} + +.dot { + width: 24rpx; + height: 24rpx; + border-radius: 50%; + margin-top: 6rpx; + flex-shrink: 0; +} + +.dot.pickup { background: #1890ff; } +.dot.delivery { background: #faad14; } + +.route-line { + width: 4rpx; + height: 40rpx; + background: #ddd; + margin-left: 10rpx; +} + +.route-text { flex: 1; } + +.desc-text { + color: #555; + line-height: 1.8; +} + +.contact-row { + display: flex; + align-items: center; + padding: 20rpx 0; + border-bottom: 1rpx solid #f0f0f0; +} + +.contact-row:last-child { border-bottom: none; } + +.contact-label { + color: #999; + font-size: 26rpx; + width: 120rpx; +} + +.action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 20rpx; + background: #fff; + display: flex; + gap: 20rpx; + z-index: 100; +} + +.btn-primary { + background: linear-gradient(135deg, #722ed1 0%, #9254de 100%); + color: #fff; +} + +.btn-primary::after, +.btn-outline { border: none; } + +.flex-1 { flex: 1; } diff --git a/pages/errand/list/list.js b/pages/errand/list/list.js new file mode 100644 index 0000000..1e48b23 --- /dev/null +++ b/pages/errand/list/list.js @@ -0,0 +1,72 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + tabs: [ + { id: 'mine', name: '我发布的' }, + { id: 'taken', name: '我接的' }, + { id: 'nearby', name: '附近任务' } + ], + activeTab: 'nearby', + tasks: [] + }, + + onShow() { + this.loadTasks() + }, + + onTabTap(e) { + this.setData({ activeTab: e.currentTarget.dataset.id }) + this.loadTasks() + }, + + async loadTasks() { + util.showLoading() + try { + const res = await api.errand.list({ page: 1, pageSize: 50 }) + let list = res && res.list ? res.list : [] + if (list.length === 0) { + list = [ + { id: 1, title: '顺丰快递代取', type: '取快递', pickup: '南门菜鸟驿站', delivery: '宿舍1号楼 501', fee: 5, status: 0, publisher: '用户A', publisherId: 10001, createdAt: Date.now() - 1800000 }, + { id: 2, title: '东门麦香汉堡点餐', type: '代买', pickup: '麦香汉堡', delivery: '图书馆3楼', fee: 8, status: 1, publisher: '用户B', runnerName: '跑腿小哥', createdAt: Date.now() - 3600000 }, + { id: 3, title: '打印50张文档', type: '打印', pickup: '打印店', delivery: '教学楼B201', fee: 10, status: 0, publisher: '用户C', createdAt: Date.now() - 7200000 }, + { id: 4, title: '帮忙搬个箱子到宿舍', type: '其他', pickup: '东门', delivery: '宿舍3号楼', fee: 15, status: 2, publisher: '用户D', runnerName: '热心同学', createdAt: Date.now() - 86400000 } + ] + if (this.data.activeTab === 'mine') { + list = list.filter(t => t.status === 0 || t.publisher === '用户A') + } else if (this.data.activeTab === 'taken') { + list = list.filter(t => t.status >= 1) + } + } + this.setData({ tasks: list }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onDetail(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/errand/detail/detail?id=' + id }) + }, + + async onAccept(e) { + const id = e.currentTarget.dataset.id + util.showLoading() + try { + await api.errand.accept(id) + util.hideLoading() + util.showToast('接单成功', 'success') + setTimeout(() => wx.navigateTo({ url: '/pages/errand/detail/detail?id=' + id }), 600) + } catch (e) { + util.hideLoading() + util.showToast('接单失败') + } + }, + + onPublishTap() { + wx.navigateTo({ url: '/pages/errand/publish/publish' }) + } +}) diff --git a/pages/errand/list/list.json b/pages/errand/list/list.json new file mode 100644 index 0000000..0eac039 --- /dev/null +++ b/pages/errand/list/list.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "跑腿任务", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/errand/list/list.wxml b/pages/errand/list/list.wxml new file mode 100644 index 0000000..6309c82 --- /dev/null +++ b/pages/errand/list/list.wxml @@ -0,0 +1,44 @@ + + + + {{item.name}} + + + + + + + {{item.type}} + {{item.title}} + + + 📍 + {{item.pickup}} + + {{item.delivery}} + + + ¥{{item.fee}} + + 待接单 + 进行中 + 已完成 + 已取消 + + 立即接单 + + + + + + + + 🏃 + 暂无任务 + 发布新任务 + + + + + diff --git a/pages/errand/list/list.wxss b/pages/errand/list/list.wxss new file mode 100644 index 0000000..bf5f4d9 --- /dev/null +++ b/pages/errand/list/list.wxss @@ -0,0 +1,81 @@ +.task-list { padding: 20rpx 20rpx 120rpx; } + +.task-card { + margin-bottom: 20rpx; +} + +.task-header { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 16rpx; +} + +.task-type { + background: #e6f7ff; + color: #1890ff; + padding: 6rpx 16rpx; + border-radius: 8rpx; + font-size: 22rpx; +} + +.task-title { + font-size: 30rpx; + font-weight: 500; + flex: 1; +} + +.task-location { + display: flex; + align-items: center; + gap: 12rpx; + padding: 12rpx 0; + color: #666; +} + +.loc-icon { + font-size: 24rpx; +} + +.arrow-icon { + color: #1890ff; + margin: 0 8rpx; +} + +.task-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 16rpx; + border-top: 1rpx solid #f0f0f0; + margin-top: 12rpx; +} + +.task-status { + display: flex; + align-items: center; + gap: 16rpx; +} + +.accept-btn { + background: #1890ff; + color: #fff; +} + +.fab { + position: fixed; + right: 40rpx; + bottom: 80rpx; + width: 100rpx; + height: 100rpx; + border-radius: 50%; + background: #722ed1; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 56rpx; + line-height: 1; + box-shadow: 0 8rpx 24rpx rgba(114, 46, 209, 0.35); + z-index: 50; + } diff --git a/pages/errand/publish/publish.js b/pages/errand/publish/publish.js new file mode 100644 index 0000000..b162881 --- /dev/null +++ b/pages/errand/publish/publish.js @@ -0,0 +1,65 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + types: ['取快递', '代买', '打印', '其他'], + form: { + type: '取快递', + pickup: '', + delivery: '', + description: '', + fee: '' + }, + submitting: false + }, + + onTypeTap(e) { + const idx = e.currentTarget.dataset.idx + this.setData({ 'form.type': this.data.types[idx] }) + }, + + onInput(e) { + const key = e.currentTarget.dataset.key + const form = { ...this.data.form } + form[key] = e.detail.value + this.setData({ form }) + }, + + onPickup() { + util.showToast('选择地点 (模拟)') + }, + + onDelivery() { + util.showToast('选择送达地点 (模拟)') + }, + + async onSubmit() { + const f = this.data.form + if (!f.pickup) return util.showToast('请输入取件地址') + if (!f.delivery) return util.showToast('请输入送达地址') + if (!f.description) return util.showToast('请输入任务描述') + if (!f.fee || isNaN(parseFloat(f.fee))) return util.showToast('请输入费用') + + this.setData({ submitting: true }) + util.showLoading() + try { + await api.errand.create({ + ...f, + fee: parseFloat(f.fee) + }) + util.hideLoading() + wx.showModal({ + title: '发布成功', + content: '任务已发布,等待跑腿员接单', + showCancel: false, + success: () => wx.navigateBack() + }) + } catch (e) { + util.hideLoading() + util.showToast('发布失败') + } finally { + this.setData({ submitting: false }) + } + } +}) diff --git a/pages/errand/publish/publish.json b/pages/errand/publish/publish.json new file mode 100644 index 0000000..0bcfb1f --- /dev/null +++ b/pages/errand/publish/publish.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "发布跑腿", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/errand/publish/publish.wxml b/pages/errand/publish/publish.wxml new file mode 100644 index 0000000..72c6d62 --- /dev/null +++ b/pages/errand/publish/publish.wxml @@ -0,0 +1,51 @@ + + + + + + 任务类型 + + + {{item}} + + + + + 取件地址 + + 📍 + + + + + 送达地址 + + 🏠 + + + + + 任务描述 + + + + 悬赏费用 (¥) + + + + + + + + diff --git a/pages/errand/publish/publish.wxss b/pages/errand/publish/publish.wxss new file mode 100644 index 0000000..a8ba23a --- /dev/null +++ b/pages/errand/publish/publish.wxss @@ -0,0 +1,72 @@ +.banner { + background: linear-gradient(135deg, #722ed1 0%, #9254de 100%); + padding: 40rpx; + color: #fff; + text-align: center; +} + +.banner-title { font-size: 32rpx; font-weight: bold; margin-bottom: 8rpx; } +.banner-sub { font-size: 24rpx; opacity: 0.9; } + +.form-item { margin-bottom: 24rpx; } + +.form-label { + display: block; + font-size: 26rpx; + color: #333; + margin-bottom: 12rpx; + font-weight: 500; +} + +.type-row { + display: flex; + gap: 16rpx; + flex-wrap: wrap; +} + +.type-chip { + padding: 18rpx 36rpx; + background: #f5f5f5; + border-radius: 32rpx; + font-size: 28rpx; + color: #666; +} + +.type-chip.active { + background: #1890ff; + color: #fff; +} + +.address-row { + display: flex; + align-items: center; + background: #f5f5f5; + padding: 0 20rpx; + border-radius: 8rpx; +} + +.address-icon { + font-size: 28rpx; + margin-right: 12rpx; + color: #666; +} + +.address-input { + background: transparent; + flex: 1; + padding: 20rpx 0; +} + +.btn-primary { + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + color: #fff; +} + +.btn-primary::after { border: none; } + +.btn-block { + width: 100%; + padding: 22rpx 0; + border-radius: 48rpx; + font-size: 30rpx; +} diff --git a/pages/food/checkout/checkout.js b/pages/food/checkout/checkout.js new file mode 100644 index 0000000..ee4812a --- /dev/null +++ b/pages/food/checkout/checkout.js @@ -0,0 +1,140 @@ +// pages/food/checkout/checkout.js - 订单结算页 +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + shopId: null, + shopCart: null, + goods: [], + totalPrice: 0, + deliveryFee: 0, + couponDiscount: 0, + payPrice: 0, + address: null, + remark: '', + coupons: [], + myCoupons: [], + selectedCoupon: null, + paymentType: 'wechat', + payMethods: [ + { id: 'wechat', name: '微信支付', icon: '💚' }, + { id: 'balance', name: '余额支付', icon: '💰' }, + { id: 'coupon', name: '优惠券支付', icon: '🎟️' } + ] + }, + + onLoad(options) { + this.data.shopId = options.shopId + this.initData() + this.loadCoupons() + }, + + initData() { + const app = getApp() + const key = 'shop_' + this.data.shopId + const shopCart = app.globalData.cart[key] + if (!shopCart || shopCart.goods.length === 0) { + util.showToast('购物车为空') + setTimeout(() => wx.navigateBack(), 1000) + return + } + let total = 0 + shopCart.goods.forEach(g => total += g.price * g.count) + const delivery = shopCart.deliveryFee || 0 + this.setData({ + shopCart, + goods: shopCart.goods, + totalPrice: total.toFixed(2), + deliveryFee: delivery.toFixed(2), + payPrice: (total + delivery).toFixed(2) + }) + }, + + async loadCoupons() { + try { + const [list, mine] = await Promise.all([ + api.coupon.list(), + api.coupon.myCoupons() + ]) + this.setData({ + coupons: list || [], + myCoupons: mine || [] + }) + } catch (e) {} + }, + + onAddressTap() { + wx.chooseLocation({ + success: (res) => { + this.setData({ address: { name: res.name, address: res.address, latitude: res.latitude, longitude: res.longitude } }) + }, + fail: () => { + this.setData({ address: { name: '张同学', phone: '138****8888', address: '3号楼 501室' } }) + } + }) + }, + + onRemarkInput(e) { + this.setData({ remark: e.detail.value }) + }, + + onSelectCoupon(e) { + const id = e.currentTarget.dataset.id + const coupon = this.data.coupons.find(c => c.id == id) || this.data.myCoupons.find(c => c.id == id) + if (!coupon) return + let discount = 0 + const total = parseFloat(this.data.totalPrice) + if (total < coupon.minOrder) { + util.showToast('订单金额不足') + return + } + if (coupon.type === '满减') discount = coupon.discount + else if (coupon.type === '折扣') discount = total * (1 - coupon.discount) + this.setData({ + selectedCoupon: coupon, + couponDiscount: discount.toFixed(2), + payPrice: (total + parseFloat(this.data.deliveryFee) - discount).toFixed(2) + }) + }, + + onPayMethodSelect(e) { + this.setData({ paymentType: e.currentTarget.dataset.id }) + }, + + async onSubmit() { + if (!this.data.address) { + util.showToast('请填写收货地址') + return + } + util.showLoading('提交订单中...') + try { + const order = await api.order.create({ + shopId: this.data.shopId, + shopName: this.data.shopCart.shopName, + goods: this.data.goods, + totalPrice: this.data.totalPrice, + deliveryFee: this.data.deliveryFee, + couponDiscount: this.data.couponDiscount, + payPrice: this.data.payPrice, + address: this.data.address, + remark: this.data.remark, + paymentType: this.data.paymentType + }) + // 模拟支付 + await api.order.pay(order.id || order._id || 1) + // 清空购物车 + const app = getApp() + delete app.globalData.cart['shop_' + this.data.shopId] + app.saveCart() + util.hideLoading() + wx.showToast({ title: '支付成功', icon: 'success' }) + setTimeout(() => { + wx.redirectTo({ url: '/pages/food/order-detail/order-detail?id=' + (order.id || 1) }) + }, 1500) + } catch (e) { + util.hideLoading() + console.error(e) + } + } +}) diff --git a/pages/food/checkout/checkout.json b/pages/food/checkout/checkout.json new file mode 100644 index 0000000..0128334 --- /dev/null +++ b/pages/food/checkout/checkout.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "订单结算", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/food/checkout/checkout.wxml b/pages/food/checkout/checkout.wxml new file mode 100644 index 0000000..e7d7c54 --- /dev/null +++ b/pages/food/checkout/checkout.wxml @@ -0,0 +1,74 @@ + + + + + {{address.name}} {{address.phone}} + {{address.address}} + + + + 选择收货地址 + + + + + + {{shopCart.shopName}} + + {{item.name}} ({{item.spec || '标准'}}) + x{{item.count}} + ¥{{item.price}} + + + + + + 备注 + + + + + + 优惠券 + + + + ¥{{item.discount}} + {{(item.discount * 10).toFixed(1)}}折 + + + {{item.name}} + 满¥{{item.minOrder}}可用 + + + + + + + + 支付方式 + + {{item.icon}} + {{item.name}} + + + + + + + 商品小计¥{{totalPrice}} + 配送费¥{{deliveryFee}} + 优惠券-¥{{couponDiscount}} + + 合计¥{{payPrice}} + + + + + + ¥{{payPrice}} + 合计 + + + + diff --git a/pages/food/checkout/checkout.wxss b/pages/food/checkout/checkout.wxss new file mode 100644 index 0000000..f9e8e31 --- /dev/null +++ b/pages/food/checkout/checkout.wxss @@ -0,0 +1,51 @@ +.address-card { + background: linear-gradient(135deg, #e0f7fa 0%, #fff9c4 100%); +} +.address-name { font-size: 32rpx; font-weight: bold; margin-bottom: 10rpx; } +.address-text { font-size: 26rpx; color: #666; } + +.goods-line { + display: flex; + align-items: center; + padding: 16rpx 0; + border-bottom: 1rpx dashed #eee; +} +.goods-line:last-child { border-bottom: none; } + +.coupon { + display: flex; + width: 240rpx; + background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%); + border: 2rpx solid #ff9800; + border-radius: 12rpx; + padding: 16rpx; +} +.coupon.selected { + background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%); + color: #fff; +} +.coupon.selected .coupon-info .text-light { color: rgba(255,255,255,0.8); } +.coupon-price { + font-size: 36rpx; + color: #f57c00; + font-weight: bold; + padding-right: 16rpx; + border-right: 2rpx dashed #ff9800; +} +.coupon.selected .coupon-price { color: #fff; border-color: rgba(255,255,255,0.4); } +.coupon-info { padding-left: 16rpx; } + +.pay-item { + display: flex; + align-items: center; + padding: 20rpx 0; + border-bottom: 1rpx solid #f0f0f0; +} +.pay-item:last-child { border-bottom: none; } + +.price-line { + display: flex; + justify-content: space-between; + padding: 10rpx 0; + font-size: 28rpx; +} diff --git a/pages/food/index/index.js b/pages/food/index/index.js new file mode 100644 index 0000000..15b0211 --- /dev/null +++ b/pages/food/index/index.js @@ -0,0 +1,74 @@ +// pages/food/index/index.js - 外卖首页 +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + merchants: [], + categories: ['全部', '餐饮', '零售', '服务'], + activeCategory: 0, + keyword: '', + location: '定位中...', + page: 1, + hasMore: true + }, + + onLoad() { + this.loadMerchants(true) + }, + + onShow() { + const app = getApp() + if (app.globalData.location) { + const loc = app.globalData.location + this.setData({ location: '纬度' + loc.latitude.toFixed(2) }) + } + }, + + async loadMerchants(reset = false) { + if (reset) this.data.page = 1 + const p = { + page: this.data.page, + pageSize: 10, + category: this.data.activeCategory === 0 ? '' : this.data.categories[this.data.activeCategory], + keyword: this.data.keyword + } + try { + const res = await api.merchant.list(p) + const list = res && res.list ? res.list : [] + this.setData({ + merchants: reset ? list : this.data.merchants.concat(list), + hasMore: res && res.hasMore + }) + } catch (e) { + console.error(e) + } + }, + + onCategoryTap(e) { + const idx = e.currentTarget.dataset.idx + this.setData({ activeCategory: idx }) + this.loadMerchants(true) + }, + + onSearchInput(e) { + this.setData({ keyword: e.detail.value }) + util.debounce(() => this.loadMerchants(true), 400)() + }, + + onShopTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/food/shop/shop?shopId=' + id }) + }, + + onPullDownRefresh() { + this.loadMerchants(true).then(() => wx.stopPullDownRefresh()) + }, + + onReachBottom() { + if (this.data.hasMore) { + this.data.page++ + this.loadMerchants(false) + } + } +}) diff --git a/pages/food/index/index.json b/pages/food/index/index.json new file mode 100644 index 0000000..1ae8b03 --- /dev/null +++ b/pages/food/index/index.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "外卖点餐", + "enablePullDownRefresh": true, + "onReachBottomDistance": 50, + "backgroundColor": "#f5f5f5", + "usingComponents": {} +} diff --git a/pages/food/index/index.wxml b/pages/food/index/index.wxml new file mode 100644 index 0000000..b76b7c0 --- /dev/null +++ b/pages/food/index/index.wxml @@ -0,0 +1,47 @@ + + + 📍 {{location}} + 外卖 · 新鲜送达 + + + + 🔍 + + + + + + {{item}} + + + + + 🏪 + + + {{item.name}} + ★ {{item.rating}} + + + 月销 {{item.sales}} + · 起送 ¥{{item.minOrder || 0}} + · 配送 ¥{{item.deliveryFee || 0}} + + {{item.address}} + + {{item.category}} + + + + + + 🍽️ + 暂无商户,再试试其他筛选条件 + + + + — 已加载全部 — + + diff --git a/pages/food/index/index.wxss b/pages/food/index/index.wxss new file mode 100644 index 0000000..79dbc39 --- /dev/null +++ b/pages/food/index/index.wxss @@ -0,0 +1,62 @@ +.top-info { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20rpx 30rpx; + background: #fff; +} +.loc { font-weight: bold; } + +.cat-scroll { + white-space: nowrap; + background: #fff; + padding: 10rpx 0; + border-bottom: 1rpx solid #eee; +} +.cat-item { + display: inline-block; + padding: 16rpx 32rpx; + margin: 0 8rpx; + font-size: 26rpx; + color: #666; + background: #f5f5f5; + border-radius: 32rpx; +} +.cat-item.active { + background: #1890ff; + color: #fff; +} + +.shop-card { + display: flex; + padding: 24rpx; + background: #fff; + margin: 20rpx; + border-radius: 16rpx; + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); +} + +.shop-thumb { + width: 160rpx; + height: 160rpx; + background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 60rpx; + flex-shrink: 0; +} + +.shop-name { + font-size: 32rpx; + font-weight: bold; +} + +.shop-meta { + font-size: 24rpx; + color: #999; + margin-top: 8rpx; +} + +.shop-meta text { margin-right: 4rpx; } diff --git a/pages/food/order-detail/order-detail.js b/pages/food/order-detail/order-detail.js new file mode 100644 index 0000000..a039efd --- /dev/null +++ b/pages/food/order-detail/order-detail.js @@ -0,0 +1,112 @@ +// pages/food/order-detail/order-detail.js +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + order: null, + statusText: '', + statusSteps: [ + { key: 1, title: '商家接单', icon: '🏪' }, + { key: 2, title: '制作中', icon: '🍳' }, + { key: 3, title: '配送中', icon: '🛵' }, + { key: 4, title: '已送达', icon: '🎉' } + ] + }, + + onLoad(options) { + this.orderId = options.id + this.loadOrder() + this.timer = setInterval(() => this.loadOrder(), 5000) + }, + + onUnload() { + if (this.timer) clearInterval(this.timer) + }, + + async loadOrder() { + try { + // 先尝试从mock数据中查询,若无则生成示例 + let order = await api.order.detail(this.orderId) + if (!order) { + // 生成一个模拟订单 + order = { + id: 'OD10001', + shopName: '麦香汉堡', + status: 2, + totalPrice: 28, + deliveryFee: 3, + payPrice: 31, + address: { name: '张同学', phone: '138****8888', address: '3号楼501' }, + remark: '不要辣', + goods: [ + { id: 1, name: '经典牛肉汉堡套餐', spec: '原味/芝士', price: 28, count: 1 } + ], + createdAt: Date.now() - 600000, + logs: [ + { time: Date.now() - 600000, text: '订单已创建' }, + { time: Date.now() - 480000, text: '商家已接单' }, + { time: Date.now() - 240000, text: '骑手已取货' }, + { time: Date.now(), text: '正在配送中...' } + ] + } + } + const statusMap = ['待支付', '已支付', '制作中', '配送中', '已完成', '已取消'] + order.createdAtText = util.formatTime(order.createdAt) + order.logsText = (order.logs || []).map(l => ({ ...l, timeText: util.formatTime(l.time) })).reverse() + this.setData({ + order, + statusText: statusMap[order.status] || '未知' + }) + } catch (e) { + console.error(e) + } + }, + + onCallRider() { + wx.showModal({ title: '联系骑手', content: '骑手电话: 138****0001', confirmText: '拨打', success: (res) => { if (res.confirm) wx.makePhoneCall({ phoneNumber: '13800000001' }) } }) + }, + + onCallShop() { + wx.showModal({ title: '联系商家', content: '商家电话: 138****0002', confirmText: '拨打', success: (res) => { if (res.confirm) wx.makePhoneCall({ phoneNumber: '13800000002' }) } }) + }, + + onRefund() { + wx.showModal({ + title: '申请退款', + content: '确定要申请退款吗?', + success: async (res) => { + if (res.confirm) { + await api.order.refund(this.orderId, '用户申请退款') + wx.showToast({ title: '已申请退款', icon: 'success' }) + } + } + }) + }, + + onConfirmReceive() { + wx.showModal({ + title: '确认收货', + content: '请确认已收到商品', + success: async (res) => { + if (res.confirm) { + await api.order.confirm(this.orderId) + wx.showToast({ title: '已完成订单', icon: 'success' }) + this.loadOrder() + } + } + }) + }, + + onPrint() { + wx.showLoading({ title: '正在打印...' }) + setTimeout(() => { + wx.hideLoading() + wx.showToast({ title: '已发送至打印机', icon: 'success' }) + }, 1500) + }, + + onShareAppMessage() { + return { title: '订单详情', path: '/pages/food/order-detail/order-detail?id=' + this.orderId } + } +}) diff --git a/pages/food/order-detail/order-detail.json b/pages/food/order-detail/order-detail.json new file mode 100644 index 0000000..8ddecbe --- /dev/null +++ b/pages/food/order-detail/order-detail.json @@ -0,0 +1 @@ +{ "navigationBarTitleText": "订单详情", "backgroundColor": "#f5f5f5" } diff --git a/pages/food/order-detail/order-detail.wxml b/pages/food/order-detail/order-detail.wxml new file mode 100644 index 0000000..aa708ba --- /dev/null +++ b/pages/food/order-detail/order-detail.wxml @@ -0,0 +1,72 @@ + + + {{statusText}} + 下单时间: {{order.createdAtText}} + + + + + + {{item.icon}} + {{item.title}} + + + + + + 配送至 + + {{order.address.name}} {{order.address.phone}} + + {{order.address.address}} + + + + + {{order.shopName}} + + 🍽️ + + {{item.name}} + {{item.spec || '标准'}} + + x{{item.count}} + ¥{{item.price}} + + + + 备注 + {{order.remark}} + + + 配送费 + ¥{{order.deliveryFee}} + + + 实付 + ¥{{order.payPrice}} + + + + + + 订单日志 + + + + {{item.text}} + {{item.timeText}} + + + + + + + + + + + + + + diff --git a/pages/food/order-detail/order-detail.wxss b/pages/food/order-detail/order-detail.wxss new file mode 100644 index 0000000..22eb3d7 --- /dev/null +++ b/pages/food/order-detail/order-detail.wxss @@ -0,0 +1,75 @@ +.status-banner { + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + color: #fff; + padding: 40rpx 30rpx; +} +.status-title { font-size: 40rpx; font-weight: bold; } +.status-desc { margin-top: 10rpx; color: rgba(255,255,255,0.85); display: block; } + +.steps { + display: flex; + background: #fff; + padding: 24rpx 0; + margin: 20rpx; + border-radius: 16rpx; + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); +} +.step { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 10rpx; +} +.step-icon { + width: 64rpx; + height: 64rpx; + border-radius: 50%; + background: #e6f7ff; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; +} + +.order-goods { + display: flex; + align-items: center; + padding: 16rpx 0; + border-bottom: 1rpx dashed #eee; +} +.order-goods:last-child { border-bottom: none; } +.thumb-sm { + width: 80rpx; + height: 80rpx; + background: #fff3e0; + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 36rpx; + margin-right: 20rpx; + flex-shrink: 0; +} + +.log-item { + display: flex; + align-items: flex-start; + padding: 16rpx 0; + position: relative; +} +.log-dot { + width: 16rpx; + height: 16rpx; + border-radius: 50%; + background: #1890ff; + margin: 12rpx 20rpx 0 0; + flex-shrink: 0; +} + +.btn-row { + display: flex; + gap: 16rpx; + padding: 16rpx 20rpx; +} +.btn-row .btn { flex: 1; } diff --git a/pages/food/shop/shop.js b/pages/food/shop/shop.js new file mode 100644 index 0000000..be629bc --- /dev/null +++ b/pages/food/shop/shop.js @@ -0,0 +1,291 @@ +// pages/food/shop/shop.js - 商户店铺页面(含属性多规格/数字选择) +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + shop: null, + goods: [], + categories: [], + activeCat: 0, + cart: {}, + totalCount: 0, + totalPrice: 0, + showCart: false, + showAttr: false, + currentGoods: null, + selectedAttrs: {}, // { attrName: { type, value(s) } } + currentAttrPrice: 0, + currentGoodsCount: 1, + shopCategories: [] + }, + + onLoad(options) { + this.shopId = options.shopId + this.loadShop() + }, + + async loadShop() { + util.showLoading() + try { + const shop = await api.merchant.detail(this.shopId) + // 整理分类 + const cats = [{ id: 0, name: '全部' }] + const goodsList = shop.goods || [] + const catMap = {} + goodsList.forEach(g => { + if (!catMap[g.categoryId]) { + catMap[g.categoryId] = { id: g.categoryId, name: '分类' + g.categoryId } + } + }) + Object.values(catMap).forEach(c => cats.push(c)) + this.setData({ + shop, + goods: goodsList, + categories: cats, + shopCategories: cats + }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onCatTap(e) { + this.setData({ activeCat: e.currentTarget.dataset.idx }) + }, + + // 打开商品详情 - 显示属性选择弹窗 + onGoodsTap(e) { + const id = e.currentTarget.dataset.id + const goods = this.data.goods.find(g => g.id == id) + if (!goods) return + if (goods.attrs && goods.attrs.length) { + // 初始化属性选择 + const selected = {} + let extraPrice = 0 + goods.attrs.forEach(attr => { + if (attr.type === 'select') { + selected[attr.name] = { type: 'select', value: attr.options && attr.options[0] || '' } + } else if (attr.type === 'multiselect') { + selected[attr.name] = { type: 'multiselect', values: [] } + } else if (attr.type === 'number') { + selected[attr.name] = { type: 'number', value: attr.default || attr.min || 1 } + } else if (attr.type === 'text') { + selected[attr.name] = { type: 'text', value: '' } + } + }) + this.setData({ + showAttr: true, + currentGoods: goods, + selectedAttrs: selected, + currentAttrPrice: 0, + currentGoodsCount: 1 + }) + } else { + this.addToCart(goods, 1, {}, 0) + } + }, + + // 选择单选属性 + onSelectAttr(e) { + const { name, value } = e.currentTarget.dataset + const selected = this.data.selectedAttrs + selected[name].value = value + this.recalcPrice(selected) + this.setData({ selectedAttrs: selected }) + }, + + // 切换多选属性 + onMultiAttrTap(e) { + const { name, value } = e.currentTarget.dataset + const selected = this.data.selectedAttrs + const sel = selected[name] + const idx = sel.values.indexOf(value) + const attrs = this.data.currentGoods.attrs.find(a => a.name === name) + if (idx >= 0) { + sel.values.splice(idx, 1) + } else { + if (attrs.maxSelect && sel.values.length >= attrs.maxSelect) { + util.showToast('最多可选' + attrs.maxSelect + '项') + return + } + sel.values.push(value) + } + this.recalcPrice(selected) + this.setData({ selectedAttrs: selected }) + }, + + // 数字增减 + onNumberChange(e) { + const { name, action } = e.currentTarget.dataset + const selected = this.data.selectedAttrs + const attrs = this.data.currentGoods.attrs.find(a => a.name === name) + let v = parseInt(selected[name].value) || attrs.min || 0 + const step = attrs.step || 1 + if (action === 'plus') v += step + else v -= step + if (attrs.min !== undefined && v < attrs.min) v = attrs.min + if (attrs.max !== undefined && v > attrs.max) v = attrs.max + selected[name].value = v + this.recalcPrice(selected) + this.setData({ selectedAttrs: selected }) + }, + + // 文本输入 + onTextInput(e) { + const name = e.currentTarget.dataset.name + const selected = this.data.selectedAttrs + selected[name].value = e.detail.value + this.setData({ selectedAttrs: selected }) + }, + + // 计算附加价格 + recalcPrice(selected) { + let extra = 0 + const goods = this.data.currentGoods + goods.attrs.forEach(attr => { + if (attr.type === 'multiselect' && attr.pricePerAddon) { + extra += selected[attr.name].values.length * attr.pricePerAddon + } + }) + this.setData({ currentAttrPrice: extra }) + }, + + onGoodsCountChange(e) { + const action = e.currentTarget.dataset.action + let count = this.data.currentGoodsCount + if (action === 'plus') count++ + else if (count > 1) count-- + this.setData({ currentGoodsCount: count }) + }, + + onConfirmAdd() { + const goods = this.data.currentGoods + // 校验必填 + for (const attr of goods.attrs) { + const sel = this.data.selectedAttrs[attr.name] + if (attr.required) { + if (attr.type === 'select' && !sel.value) { + util.showToast('请选择' + attr.name); return + } + if (attr.type === 'multiselect' && sel.values.length === 0) { + util.showToast('请选择' + attr.name); return + } + } + } + this.addToCart(goods, this.data.currentGoodsCount, this.data.selectedAttrs, this.data.currentAttrPrice) + this.setData({ showAttr: false }) + }, + + addToCart(goods, count, attrs, extraPrice) { + const app = getApp() + const cart = app.globalData.cart + const key = 'shop_' + this.shopId + const attrKey = JSON.stringify(attrs) + if (!cart[key]) { + cart[key] = { + shopId: this.shopId, + shopName: this.data.shop.name, + goods: [], + selected: true, + deliveryFee: this.data.shop.deliveryFee || 0, + minOrder: this.data.shop.minOrder || 0 + } + } + // 查找同属性商品 + const exist = cart[key].goods.find(g => g.id === goods.id && g.attrKey === attrKey) + const finalPrice = (parseFloat(goods.price) + extraPrice).toFixed(2) + if (exist) { + exist.count += count + } else { + cart[key].goods.push({ + id: goods.id, + name: goods.name, + price: finalPrice, + count: count, + attrs: attrs, + attrKey: attrKey, + spec: this.attrsToText(attrs) + }) + } + app.saveCart() + this.refreshCart() + wx.showToast({ title: '已加入购物车', icon: 'success' }) + }, + + attrsToText(attrs) { + const parts = [] + Object.keys(attrs).forEach(k => { + const a = attrs[k] + if (a.type === 'select' && a.value) parts.push(a.value) + if (a.type === 'multiselect' && a.values.length) parts.push(a.values.join('/')) + if (a.type === 'number' && a.value !== undefined && a.value !== '') parts.push(k + ':' + a.value) + if (a.type === 'text' && a.value) parts.push(a.value) + }) + return parts.join(',') + }, + + refreshCart() { + const app = getApp() + const key = 'shop_' + this.shopId + const shopCart = app.globalData.cart[key] || { goods: [] } + let totalCount = 0, totalPrice = 0 + shopCart.goods.forEach(g => { totalCount += g.count; totalPrice += g.price * g.count }) + this.setData({ + cart: shopCart, + totalCount, + totalPrice: totalPrice.toFixed(2) + }) + }, + + toggleCart() { + if (this.data.totalCount === 0) return + this.setData({ showCart: !this.data.showCart }) + }, + + onCartGoodsCount(e) { + const idx = e.currentTarget.dataset.idx + const action = e.currentTarget.dataset.action + const app = getApp() + const key = 'shop_' + this.shopId + const cart = app.globalData.cart[key] + if (!cart) return + if (action === 'plus') cart.goods[idx].count++ + else { + cart.goods[idx].count-- + if (cart.goods[idx].count <= 0) cart.goods.splice(idx, 1) + } + if (cart.goods.length === 0) { + delete app.globalData.cart[key] + this.setData({ showCart: false }) + } + app.saveCart() + this.refreshCart() + }, + + clearCart() { + const app = getApp() + delete app.globalData.cart['shop_' + this.shopId] + app.saveCart() + this.refreshCart() + this.setData({ showCart: false }) + }, + + goCheckout() { + if (parseFloat(this.data.totalPrice) < (this.data.cart.minOrder || 0)) { + util.showToast('差¥' + ((this.data.cart.minOrder - this.data.totalPrice).toFixed(2)) + '起送') + return + } + wx.navigateTo({ url: '/pages/food/checkout/checkout?shopId=' + this.shopId }) + }, + + closeModal() { + this.setData({ showAttr: false, showCart: false }) + }, + + onShow() { + if (this.shopId) this.refreshCart() + } +}) diff --git a/pages/food/shop/shop.json b/pages/food/shop/shop.json new file mode 100644 index 0000000..feb83ad --- /dev/null +++ b/pages/food/shop/shop.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "店铺", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/food/shop/shop.wxml b/pages/food/shop/shop.wxml new file mode 100644 index 0000000..66467fb --- /dev/null +++ b/pages/food/shop/shop.wxml @@ -0,0 +1,153 @@ + + + + + + {{shop.name}} + + ★ {{shop.rating}} + 月销 {{shop.sales}} + + {{shop.address}} + + + + + + + + {{item.name}} + + + + + 🍽️ + + {{item.name}} + {{item.description || '美味推荐'}} + + + {{attr.name}} + {{attr.name}}:{{attr.min}}-{{attr.max}} + + + + ¥{{item.price}} + ¥{{item.originalPrice}} + + + + + + + + + + + + 🛒 + {{totalCount}} + + + ¥{{totalPrice}} + 购物车是空的 + 另需配送费 ¥{{cart.deliveryFee || 0}} + + 去结算 + 差¥{{(cart.minOrder - totalPrice > 0 ? cart.minOrder - totalPrice : 0).toFixed(2)}}起送 + + + + + + + + 已选商品 + 清空 + + + + + {{item.name}} + {{item.spec}} + + + ¥{{item.price}} + + + {{item.count}} + + + + + + + + + + + + + + {{currentGoods.name}} + ¥{{currentGoods.price + currentAttrPrice}} + + + + + {{attr.name}} + * + (可选{{attr.maxSelect}}项) + (+¥{{attr.pricePerAddon}}/项) + + + + + {{opt}} + + + + + {{opt}} + + + + + {{attr.min}}-{{attr.max}} + + + {{selectedAttrs[attr.name].value}} + + + 步长{{attr.step || 1}} + + + + + + + + + + + + {{currentGoodsCount}} + + + + + + + + + + 🏪 + 加载中... + diff --git a/pages/food/shop/shop.wxss b/pages/food/shop/shop.wxss new file mode 100644 index 0000000..8029e83 --- /dev/null +++ b/pages/food/shop/shop.wxss @@ -0,0 +1,207 @@ +.shop-page { padding-bottom: 140rpx; } + +.shop-banner { + display: flex; + padding: 30rpx; + background: linear-gradient(135deg, #fff5e1 0%, #ffe0c4 100%); + align-items: center; +} +.shop-logo { + width: 120rpx; + height: 120rpx; + border-radius: 20rpx; + background: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 60rpx; + flex-shrink: 0; +} +.shop-name { font-size: 36rpx; font-weight: bold; } +.shop-sub { display: flex; gap: 16rpx; margin-top: 8rpx; font-size: 24rpx; } +.shop-delivery { margin-top: 8rpx; } + +.goods-layout { + display: flex; + height: calc(100vh - 380rpx); +} +.cat-col { + width: 180rpx; + background: #f5f5f5; + overflow-y: auto; +} +.cat-item { + padding: 30rpx 16rpx; + font-size: 26rpx; + color: #666; + text-align: center; + border-bottom: 1rpx solid #eee; + position: relative; +} +.cat-item.active { + background: #fff; + color: #1890ff; + font-weight: bold; +} +.cat-item.active::before { + content: ''; + position: absolute; + left: 0; top: 0; bottom: 0; + width: 6rpx; + background: #1890ff; +} + +.goods-col { flex: 1; background: #fff; padding: 10rpx; } + +.goods-item { + display: flex; + padding: 20rpx; + border-bottom: 1rpx solid #f0f0f0; +} +.goods-thumb { + width: 140rpx; + height: 140rpx; + background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 50rpx; + flex-shrink: 0; +} +.goods-info { flex: 1; margin-left: 20rpx; } +.goods-name { font-size: 28rpx; font-weight: bold; } +.goods-desc { margin-top: 8rpx; } +.goods-spec { margin-top: 8rpx; } +.goods-bottom { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 16rpx; +} +.add-btn { + width: 48rpx; + height: 48rpx; + background: #1890ff; + color: #fff; + border-radius: 50%; + text-align: center; + line-height: 48rpx; + font-size: 32rpx; +} + +/* 购物车栏 */ +.cart-bar { + position: fixed; + bottom: 20rpx; + left: 20rpx; + right: 20rpx; + height: 100rpx; + background: #333; + border-radius: 60rpx; + display: flex; + align-items: center; + padding: 0 24rpx; + z-index: 100; + box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.2); +} +.cart-icon { + width: 80rpx; + height: 80rpx; + background: #1890ff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + position: relative; + margin-top: -30rpx; +} +.cart-icon .badge { + position: absolute; + top: -8rpx; + right: -8rpx; +} +.cart-info { flex: 1; margin-left: 20rpx; color: #fff; } +.cart-info .price { color: #ff9800; } +.checkout-btn { + background: #52c41a; + color: #fff; + padding: 16rpx 32rpx; + border-radius: 40rpx; + font-size: 28rpx; +} +.checkout-btn.disabled { + background: #666; + color: #ccc; +} + +.modal-mask { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.5); + z-index: 200; +} + +/* 购物车弹窗 */ +.cart-modal { + position: fixed; + left: 0; right: 0; bottom: 120rpx; + z-index: 201; + display: flex; + justify-content: center; +} +.cart-modal-inner { + background: #fff; + width: 100%; + border-radius: 16rpx 16rpx 0 0; + margin: 0 20rpx; +} +.cart-modal-header { + display: flex; + justify-content: space-between; + padding: 24rpx; + border-bottom: 1rpx solid #eee; +} +.cart-list { padding: 0 24rpx 24rpx; max-height: 500rpx; overflow-y: auto; } +.cart-item { + display: flex; + padding: 16rpx 0; + border-bottom: 1rpx solid #f5f5f5; + align-items: center; +} +.cart-item-name { font-size: 28rpx; font-weight: 500; } +.cart-item-price { display: flex; align-items: center; } + +/* 属性弹窗 */ +.attr-modal { + position: fixed; + left: 0; right: 0; bottom: 0; + z-index: 201; +} +.attr-modal-inner { + background: #fff; + border-radius: 24rpx 24rpx 0 0; + max-height: 80vh; + display: flex; + flex-direction: column; +} +.attr-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24rpx; + border-bottom: 1rpx solid #eee; +} +.attr-scroll { + padding: 24rpx; + max-height: 60vh; +} +.attr-footer { + padding: 24rpx; + display: flex; + align-items: center; + gap: 20rpx; + border-top: 1rpx solid #eee; +} +.attr-footer .btn { flex: 1; } diff --git a/pages/forum/detail/detail.js b/pages/forum/detail/detail.js new file mode 100644 index 0000000..bd64e57 --- /dev/null +++ b/pages/forum/detail/detail.js @@ -0,0 +1,118 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + post: null, + commentInput: '', + liked: false, + favorited: false, + voted: false, + votedOption: null + }, + + onLoad(options) { + this.postId = options.id + this.loadPost() + }, + + async loadPost() { + util.showLoading() + try { + let p = null + try { p = await api.forum.detail(this.postId) } catch (e) {} + if (!p) { + p = { + id: this.postId, + author: '同学A', + board: '校园生活', + title: '图书馆新增自习区域开放啦', + content: '今天去三楼发现新增了一大片安静的自习区,光线好,桌椅新,非常适合备考。同学们有需要的可以去看看。\n\n开放时间和主楼同步,早上8点到晚上10点。', + likes: 120, + comments: 35, + views: 1200, + isTop: true, + isHot: true, + createdAt: Date.now() - 3600000, + vote: { + title: '你最喜欢哪个自习区?', + options: [ + { id: 1, text: '图书馆三楼', count: 88 }, + { id: 2, text: '教学楼A区', count: 60 }, + { id: 3, text: '创新大楼', count: 30 } + ] + }, + replies: [ + { id: 1, author: '用户B', content: '太好了,正愁找不到自习的地方', likes: 8, createdAt: Date.now() - 2000000 }, + { id: 2, author: '用户C', content: '感谢分享,下午去看看', likes: 3, createdAt: Date.now() - 1000000 } + ] + } + } + this.setData({ post: p }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onCommentInput(e) { + this.setData({ commentInput: e.detail.value }) + }, + + async onLike() { + try { + await api.forum.like(this.postId) + const post = { ...this.data.post, likes: (this.data.post.likes || 0) + (this.data.liked ? -1 : 1) } + this.setData({ post, liked: !this.data.liked }) + } catch (e) {} + }, + + async onFavorite() { + try { + await api.forum.favorite(this.postId) + this.setData({ favorited: !this.data.favorited }) + util.showToast(this.data.favorited ? '已收藏' : '取消收藏') + } catch (e) { util.showToast('操作失败') } + }, + + onShare() { + util.showToast('分享链接已复制') + }, + + async onVote(e) { + const optId = e.currentTarget.dataset.id + const idx = e.currentTarget.dataset.idx + try { + await api.forum.vote(this.postId, optId) + const post = { ...this.data.post } + if (post.vote && post.vote.options[idx]) { + post.vote.options[idx].count += 1 + this.setData({ post, voted: true, votedOption: optId }) + util.showToast('投票成功', 'success') + } + } catch (e) { util.showToast('投票失败') } + }, + + async onSubmitComment() { + if (!this.data.commentInput.trim()) return util.showToast('请输入评论内容') + util.showLoading() + try { + await api.forum.reply(this.postId, { content: this.data.commentInput }) + const post = { ...this.data.post, replies: [...(this.data.post.replies || []), { + id: Date.now(), + author: '我', + content: this.data.commentInput, + likes: 0, + createdAt: Date.now() + }] } + post.comments = (post.comments || 0) + 1 + this.setData({ post, commentInput: '' }) + util.hideLoading() + util.showToast('评论成功', 'success') + } catch (e) { + util.hideLoading() + util.showToast('评论失败') + } + } +}) diff --git a/pages/forum/detail/detail.json b/pages/forum/detail/detail.json new file mode 100644 index 0000000..46a4fc5 --- /dev/null +++ b/pages/forum/detail/detail.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "帖子详情", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/forum/detail/detail.wxml b/pages/forum/detail/detail.wxml new file mode 100644 index 0000000..4516fa0 --- /dev/null +++ b/pages/forum/detail/detail.wxml @@ -0,0 +1,68 @@ + + + + {{post.title}} + + {{post.content}} + + + 👍 {{post.likes}} + + + ⭐ 收藏 + + + 📤 分享 + + + + + {{post.vote.title}} + + {{item.text}} + {{item.count}} 票 + + ✅ 你已投票 + + + + + 评论 ({{post.comments}}) + + 👤 + + + {{item.author}} + + {{item.content}} + + 👍 {{item.likes}} + + + + + 暂无评论,来抢沙发 + + + + + + + + 发送 + + + + + 📚 + 加载中... + diff --git a/pages/forum/detail/detail.wxss b/pages/forum/detail/detail.wxss new file mode 100644 index 0000000..0427dc5 --- /dev/null +++ b/pages/forum/detail/detail.wxss @@ -0,0 +1,145 @@ +.post-tags { + display: flex; + gap: 12rpx; + margin-bottom: 16rpx; +} + +.post-title { + font-size: 36rpx; + font-weight: bold; + margin: 16rpx 0; + line-height: 1.4; +} + +.post-meta { + display: flex; + align-items: center; + margin-bottom: 24rpx; + padding-bottom: 24rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.post-content { + font-size: 30rpx; + color: #333; + line-height: 1.8; + white-space: pre-wrap; +} + +.post-actions { + display: flex; + gap: 24rpx; + padding-top: 24rpx; + margin-top: 24rpx; + border-top: 1rpx solid #f0f0f0; +} + +.action-item { + padding: 12rpx 24rpx; + background: #f5f5f5; + border-radius: 24rpx; + font-size: 26rpx; + color: #666; +} + +.action-item.active { + background: #fff7e6; + color: #fa8c16; +} + +.vote-section { + background: #fafafa; + padding: 24rpx; + border-radius: 12rpx; + margin-top: 24rpx; +} + +.vote-title { + font-size: 28rpx; + font-weight: 500; + margin-bottom: 16rpx; +} + +.vote-option { + background: #fff; + padding: 20rpx 24rpx; + border-radius: 8rpx; + margin-bottom: 12rpx; + display: flex; + justify-content: space-between; + align-items: center; + border: 2rpx solid #eee; +} + +.vote-option-text { font-size: 28rpx; } +.vote-count { font-size: 24rpx; color: #999; } + +.comment-section { margin-top: 20rpx; } + +.card-title { + font-size: 28rpx; + font-weight: bold; + margin-bottom: 20rpx; +} + +.comment-item { + display: flex; + padding: 16rpx 0; + border-bottom: 1rpx solid #f0f0f0; +} + +.comment-item:last-child { border-bottom: none; } + +.comment-avatar { + width: 60rpx; + height: 60rpx; + border-radius: 50%; + background: #eee; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + flex-shrink: 0; +} + +.comment-body { + flex: 1; + margin-left: 16rpx; +} + +.comment-header { + margin-bottom: 8rpx; +} + +.comment-text { + font-size: 28rpx; + color: #333; + line-height: 1.6; +} + +.comment-footer { + margin-top: 8rpx; +} + +.comment-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 20rpx; + background: #fff; + display: flex; + align-items: center; + gap: 16rpx; + box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05); +} + +.comment-input { + flex: 1; + background: #f5f5f5; +} + +.btn-submit { + background: #fa8c16; + color: #fff; +} diff --git a/pages/forum/list/list.js b/pages/forum/list/list.js new file mode 100644 index 0000000..d9d4b6e --- /dev/null +++ b/pages/forum/list/list.js @@ -0,0 +1,67 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + boards: [ + { id: '', name: '全部' }, + { id: '校园生活', name: '校园生活' }, + { id: '学习交流', name: '学习交流' }, + { id: '失物招领', name: '失物招领' }, + { id: '求职招聘', name: '求职招聘' } + ], + activeBoard: '', + keyword: '', + posts: [] + }, + + onShow() { + this.loadPosts() + }, + + onBoardTap(e) { + this.setData({ activeBoard: e.currentTarget.dataset.id }) + this.loadPosts() + }, + + onSearchInput(e) { + this.setData({ keyword: e.detail.value }) + }, + + onSearch() { + this.loadPosts() + }, + + async loadPosts() { + util.showLoading() + try { + let res = null + try { + res = await api.forum.list({ page: 1, pageSize: 30, boardId: this.data.activeBoard, keyword: this.data.keyword }) + } catch (e) {} + let list = res && res.list ? res.list : [] + if (list.length === 0) { + list = [ + { id: 1, board: '校园生活', author: '同学A', title: '图书馆新增自习区域开放啦', content: '今天发现3楼新开放了一片自习区,安静明亮,强烈推荐给需要备考的同学。', likes: 120, comments: 35, views: 1200, isTop: true, isHot: true, isEssence: true, createdAt: Date.now() - 3600000 }, + { id: 2, board: '学习交流', author: '学霸君', title: '分享一份考研数学复习笔记', content: '整理了近3年的真题分析,包含解题思路和技巧,需要的同学自取。', likes: 280, comments: 60, views: 3200, isTop: false, isHot: true, isEssence: true, hasVote: true, createdAt: Date.now() - 7200000 }, + { id: 3, board: '失物招领', author: '好心人', title: '在食堂捡到一张校园卡', content: '姓名: 王同学,有认识的请联系我。', likes: 5, comments: 2, views: 80, isTop: false, isHot: false, isEssence: false, createdAt: Date.now() - 86400000 }, + { id: 4, board: '求职招聘', author: 'HR小姐姐', title: '某互联网公司实习生招聘', content: '前端/后端/算法岗位,有兴趣的同学私信。', likes: 66, comments: 22, views: 880, isTop: false, isHot: false, isEssence: false, createdAt: Date.now() - 172800000 } + ] + } + this.setData({ posts: list }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onDetail(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/forum/detail/detail?id=' + id }) + }, + + onPublish() { + wx.navigateTo({ url: '/pages/forum/publish/publish' }) + } +}) diff --git a/pages/forum/list/list.json b/pages/forum/list/list.json new file mode 100644 index 0000000..9046be5 --- /dev/null +++ b/pages/forum/list/list.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "校园论坛", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/forum/list/list.wxml b/pages/forum/list/list.wxml new file mode 100644 index 0000000..64c202c --- /dev/null +++ b/pages/forum/list/list.wxml @@ -0,0 +1,42 @@ + + + 🔍 + + + + + + {{item.name}} + + + + + + + {{item.title}} + {{item.content}} + + + + + + 📚 + 暂无帖子 + + + + + diff --git a/pages/forum/list/list.wxss b/pages/forum/list/list.wxss new file mode 100644 index 0000000..ca180e7 --- /dev/null +++ b/pages/forum/list/list.wxss @@ -0,0 +1,56 @@ +.post-list { padding: 20rpx 20rpx 120rpx; } + +.post-card { + margin-bottom: 20rpx; +} + +.post-tags { + display: flex; + gap: 12rpx; + margin-bottom: 12rpx; +} + +.post-title { + font-size: 30rpx; + font-weight: 500; + margin-bottom: 12rpx; + color: #333; +} + +.post-content { + line-height: 1.6; + margin-bottom: 16rpx; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.post-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.post-meta { + display: flex; + align-items: center; +} + +.fab { + position: fixed; + right: 40rpx; + bottom: 80rpx; + width: 100rpx; + height: 100rpx; + border-radius: 50%; + background: #fa8c16; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 56rpx; + line-height: 1; + box-shadow: 0 8rpx 24rpx rgba(250, 140, 22, 0.35); + z-index: 50; + } diff --git a/pages/forum/publish/publish.js b/pages/forum/publish/publish.js new file mode 100644 index 0000000..4cbbf01 --- /dev/null +++ b/pages/forum/publish/publish.js @@ -0,0 +1,89 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + boards: ['校园生活', '学习交流', '失物招领', '求职招聘'], + form: { + board: '校园生活', + title: '', + content: '', + anonymous: false + }, + hasVote: false, + voteOptions: ['', ''], + submitting: false + }, + + onInput(e) { + const key = e.currentTarget.dataset.key + const form = { ...this.data.form } + form[key] = e.detail.value + this.setData({ form }) + }, + + onBoardTap(e) { + const idx = e.currentTarget.dataset.idx + const form = { ...this.data.form, board: this.data.boards[idx] } + this.setData({ form }) + }, + + toggleAnonymous() { + this.setData({ 'form.anonymous': !this.data.form.anonymous }) + }, + + toggleHasVote() { + this.setData({ hasVote: !this.data.hasVote }) + }, + + onVoteOptionInput(e) { + const idx = e.currentTarget.dataset.idx + const opts = this.data.voteOptions.slice() + opts[idx] = e.detail.value + this.setData({ voteOptions: opts }) + }, + + addVoteOption() { + const opts = this.data.voteOptions.slice() + if (opts.length >= 6) return util.showToast('最多6个选项') + opts.push('') + this.setData({ voteOptions: opts }) + }, + + removeVoteOption(e) { + const idx = e.currentTarget.dataset.idx + const opts = this.data.voteOptions.slice() + if (opts.length <= 2) return util.showToast('至少2个选项') + opts.splice(idx, 1) + this.setData({ voteOptions: opts }) + }, + + async onSubmit() { + const f = this.data.form + if (!f.title) return util.showToast('请输入标题') + if (!f.content) return util.showToast('请输入正文') + + this.setData({ submitting: true }) + util.showLoading() + try { + const payload = { ...f, board: f.board } + if (this.data.hasVote) { + const opts = this.data.voteOptions.map(o => o.trim()).filter(Boolean) + if (opts.length >= 2) payload.vote = opts + } + await api.forum.create(payload) + util.hideLoading() + wx.showModal({ + title: '发布成功', + content: '您的帖子已发布', + showCancel: false, + success: () => wx.navigateBack() + }) + } catch (e) { + util.hideLoading() + util.showToast('发布失败') + } finally { + this.setData({ submitting: false }) + } + } +}) diff --git a/pages/forum/publish/publish.json b/pages/forum/publish/publish.json new file mode 100644 index 0000000..3208318 --- /dev/null +++ b/pages/forum/publish/publish.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "发布帖子", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/forum/publish/publish.wxml b/pages/forum/publish/publish.wxml new file mode 100644 index 0000000..24217cb --- /dev/null +++ b/pages/forum/publish/publish.wxml @@ -0,0 +1,43 @@ + + + + 选择板块 + + + {{item}} + + + + + 标题 + + + + 正文 + + + + 添加投票 + + + + + + 删除 + + + 添加选项 + + + 匿名发布 + + + + + + + + diff --git a/pages/forum/publish/publish.wxss b/pages/forum/publish/publish.wxss new file mode 100644 index 0000000..4d356e2 --- /dev/null +++ b/pages/forum/publish/publish.wxss @@ -0,0 +1,106 @@ +.form-item { margin-bottom: 24rpx; } + +.form-label { + display: block; + font-size: 26rpx; + color: #333; + margin-bottom: 12rpx; + font-weight: 500; +} + +.cat-row { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.cat-chip { + padding: 14rpx 32rpx; + background: #f5f5f5; + border-radius: 32rpx; + font-size: 26rpx; + color: #666; +} + +.cat-chip.active { + background: #fa8c16; + color: #fff; +} + +.big-textarea { + min-height: 300rpx; +} + +.switch-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20rpx 0; + border-top: 1rpx solid #f0f0f0; +} + +.switch { + width: 80rpx; + height: 44rpx; + border-radius: 44rpx; + background: #ccc; + position: relative; + transition: background 0.2s; +} + +.switch.on { background: #fa8c16; } + +.switch-dot { + width: 40rpx; + height: 40rpx; + border-radius: 50%; + background: #fff; + position: absolute; + top: 2rpx; + left: 2rpx; + transition: left 0.2s; + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1); +} + +.switch.on .switch-dot { left: 38rpx; } + +.vote-options { + background: #fafafa; + padding: 20rpx; + border-radius: 12rpx; + margin-bottom: 20rpx; +} + +.vote-input-row { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 12rpx; +} + +.vote-input-row .input { flex: 1; } + +.remove-btn { + color: #ff4d4f; + padding: 0 16rpx; +} + +.btn-primary { + background: linear-gradient(135deg, #fa8c16 0%, #ffa940 100%); + color: #fff; +} + +.btn-primary::after { border: none; } + +.btn-block { + width: 100%; + padding: 22rpx 0; + border-radius: 48rpx; + font-size: 30rpx; +} + +.btn-outline { + background: #fff; + color: #666; + border: 2rpx solid #ddd; +} diff --git a/pages/index/index.js b/pages/index/index.js new file mode 100644 index 0000000..6ee96ad --- /dev/null +++ b/pages/index/index.js @@ -0,0 +1,113 @@ +// pages/index/index.js +const api = require('../../utils/api.js') +const util = require('../../utils/util.js') + +Page({ + data: { + bannerList: [ + { id: 1, title: '校园外卖新上线', image: '', color: '#1890ff', route: '/pages/food/index/index' }, + { id: 2, title: '表白墙今日精选', image: '', color: '#f5222d', route: '/pages/confession/list/list' }, + { id: 3, title: '二手市场大促', image: '', color: '#52c41a', route: '/pages/market/list/list' } + ], + categories: [ + { id: 'food', name: '外卖点餐', icon: '🍔', route: '/pages/food/index/index', color: '#ff7a45' }, + { id: 'market', name: '二手市场', icon: '🛍️', route: '/pages/market/list/list', color: '#52c41a' }, + { id: 'confession', name: '表白墙', icon: '💌', route: '/pages/confession/list/list', color: '#f5222d' }, + { id: 'forum', name: '校园论坛', icon: '📚', route: '/pages/forum/list/list', color: '#1890ff' }, + { id: 'community', name: '兴趣社区', icon: '🎯', route: '/pages/community/list/list', color: '#722ed1' }, + { id: 'errand', name: '跑腿服务', icon: '🏃', route: '/pages/errand/list/list', color: '#fa8c16' }, + { id: 'shop', name: '商户入驻', icon: '🏪', route: '/pages/merchant/apply/apply', color: '#13c2c2' }, + { id: 'diy', name: '自定义', icon: '✨', route: '/pages/custom/diy/diy', color: '#eb2f96' } + ], + merchants: [], + hotGoods: [], + forumHot: [], + diyPages: [], + userInfo: null + }, + + onLoad() { + this.loadData() + }, + + onShow() { + const app = getApp() + if (app && app.globalData && app.globalData.userInfo) { + this.setData({ userInfo: app.globalData.userInfo }) + } + }, + + onPullDownRefresh() { + this.loadData().then(() => wx.stopPullDownRefresh()) + }, + + async loadData() { + util.showLoading() + try { + const [merchants, goods, forum, diy] = await Promise.all([ + api.merchant.list({ page: 1, pageSize: 5 }), + api.goods.list({ page: 1, pageSize: 6 }), + api.forum.list({ page: 1, pageSize: 3 }), + api.diy.list() + ]) + this.setData({ + merchants: merchants && merchants.list ? merchants.list : [], + hotGoods: goods && goods.list ? goods.list : [], + forumHot: forum && forum.list ? forum.list.map(f => ({ ...f, timeText: util.timeAgo(f.createdAt) })) : [], + diyPages: diy || [] + }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onNavigate(e) { + const route = e.currentTarget.dataset.route + if (!route) return + if (route.startsWith('/pages/food') || route.startsWith('/pages/forum') || route.startsWith('/pages/community') || route.startsWith('/pages/index')) { + wx.switchTab({ url: route, fail: () => wx.navigateTo({ url: route }) }) + } else { + wx.navigateTo({ url: route }) + } + }, + + onMerchantTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/merchant/detail/detail?id=' + id }) + }, + + onGoodsTap(e) { + const id = e.currentTarget.dataset.id + const shopId = e.currentTarget.dataset.shop + wx.navigateTo({ url: '/pages/food/shop/shop?shopId=' + shopId }) + }, + + onForumTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/forum/detail/detail?id=' + id }) + }, + + onDiyTap(e) { + const id = e.currentTarget.dataset.id + const page = this.data.diyPages.find(p => p.id == id) + if (page && page.isH5) { + wx.navigateTo({ url: '/pages/custom/h5/h5?url=' + encodeURIComponent(page.h5Url) + '&title=' + encodeURIComponent(page.title) }) + } else { + wx.navigateTo({ url: '/pages/custom/diy/diy?id=' + id }) + } + }, + + onLogin() { + const app = getApp() + app.requireLogin(() => {}) + }, + + onShareAppMessage() { + return { + title: '校园综合服务 - 外卖、跑腿、社区、论坛、二手市场', + path: '/pages/index/index' + } + } +}) diff --git a/pages/index/index.json b/pages/index/index.json new file mode 100644 index 0000000..de4d4fe --- /dev/null +++ b/pages/index/index.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "校园服务", + "enablePullDownRefresh": true, + "backgroundColor": "#f5f5f5", + "usingComponents": {} +} diff --git a/pages/index/index.wxml b/pages/index/index.wxml new file mode 100644 index 0000000..5a2d854 --- /dev/null +++ b/pages/index/index.wxml @@ -0,0 +1,114 @@ + + + + + + 🔍 + + + + + + + + + + 快捷功能 + + + + {{item.icon}} + + {{item.name}} + + + + + + + + 推荐内容 + + + + {{item.title}} + H5 + DIY + + + + + + + + 热门商户 + 查看更多 › + + + 🏪 + + + {{item.name}} + ★ {{item.rating}} + + {{item.description || item.address}} + + + 销量 {{item.sales}} + {{item.category}} + + 起¥{{item.minOrder || 0}} + + + + + + + + + 热销商品 + 更多 › + + + + 🍽️ + {{item.name}} + + ¥{{item.price}} + 销{{item.sales}} + + + + + + + + + 校园热议 + 全部 › + + + + 置顶 + 精华 + {{item.title}} + + {{item.content}} + + {{item.author}} · {{item.timeText}} + + 👍 {{item.likes}} + 💬 {{item.comments}} + + + + + + — 我是有底线的 — + diff --git a/pages/index/index.wxss b/pages/index/index.wxss new file mode 100644 index 0000000..3e17ff5 --- /dev/null +++ b/pages/index/index.wxss @@ -0,0 +1,152 @@ +/* pages/index/index.wxss */ +.header { + background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%); + padding: 30rpx 20rpx 20rpx; +} + +.header .search-bar { + background: rgba(255,255,255,0.95); + margin: 0; +} + +.banner { + height: 280rpx; + margin: 20rpx; + border-radius: 16rpx; + overflow: hidden; +} + +.banner-item { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + border-radius: 16rpx; +} + +.banner-text { + color: #fff; + font-size: 40rpx; + font-weight: bold; + text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.2); +} + +.grid { + display: flex; + flex-wrap: wrap; + margin-top: 20rpx; +} + +.grid-item { + width: 25%; + display: flex; + flex-direction: column; + align-items: center; + padding: 20rpx 0; +} + +.grid-icon { + width: 96rpx; + height: 96rpx; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 12rpx; +} + +.grid-name { + font-size: 24rpx; + color: #333; +} + +.merchant-item { + display: flex; + align-items: center; + padding: 20rpx 0; + border-bottom: 1rpx solid #f0f0f0; +} +.merchant-item:last-child { border-bottom: none; } + +.goods-grid { + display: flex; + flex-wrap: wrap; + margin: 0 -8rpx; +} + +.goods-card { + width: calc(33.33% - 16rpx); + margin: 8rpx; + background: #fafafa; + border-radius: 12rpx; + padding: 16rpx; +} + +.goods-thumb { + width: 100%; + height: 180rpx; + border-radius: 8rpx; + background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 60rpx; + margin-bottom: 12rpx; +} + +.goods-name { + font-size: 26rpx; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.forum-item { + padding: 20rpx 0; + border-bottom: 1rpx solid #f0f0f0; +} +.forum-item:last-child { border-bottom: none; } + +.forum-title { + font-size: 28rpx; + font-weight: bold; +} + +.forum-meta text { + margin-left: 16rpx; +} + +.diy-scroll { + white-space: nowrap; + padding: 10rpx 0; +} + +.diy-card { + display: inline-block; + width: 280rpx; + height: 160rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12rpx; + margin-right: 20rpx; + padding: 24rpx; + position: relative; +} + +.diy-title { + color: #fff; + font-size: 32rpx; + font-weight: bold; +} + +.diy-tag { + position: absolute; + right: 16rpx; + top: 16rpx; + background: rgba(255,255,255,0.2); + color: #fff; + font-size: 20rpx; + padding: 4rpx 12rpx; + border-radius: 12rpx; +} diff --git a/pages/market/detail/detail.js b/pages/market/detail/detail.js new file mode 100644 index 0000000..8688a37 --- /dev/null +++ b/pages/market/detail/detail.js @@ -0,0 +1,61 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + good: null, + favorited: false + }, + + onLoad(options) { + this.goodId = options.id + this.loadData() + }, + + async loadData() { + util.showLoading() + try { + let g = null + try { g = await api.market.detail(this.goodId) } catch (e) {} + if (!g) { + g = { + id: this.goodId, + title: '九成新 MacBook Pro 2023', + price: 6800, + originalPrice: 14999, + category: '数码', + description: '自用一年,完好无磕碰,配件齐全。送原装充电器。', + seller: '毕业学长', + sellerAvatar: '👤', + sellerId: 10001, + location: '5号宿舍', + views: 320, + likes: 18, + negotiable: true, + createdAt: Date.now() - 86400000 + } + } + this.setData({ good: g }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + async onFavorite() { + try { + await api.market.favorite(this.goodId) + this.setData({ favorited: !this.data.favorited }) + util.showToast(this.data.favorited ? '已取消收藏' : '已收藏') + } catch (e) { util.showToast('操作成功') } + }, + + onChat() { + util.showToast('私信 (模拟)') + }, + + onBuy() { + util.showToast('已提交购买申请', 'success') + } +}) diff --git a/pages/market/detail/detail.json b/pages/market/detail/detail.json new file mode 100644 index 0000000..d89e5d3 --- /dev/null +++ b/pages/market/detail/detail.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "商品详情", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/market/detail/detail.wxml b/pages/market/detail/detail.wxml new file mode 100644 index 0000000..112fe5b --- /dev/null +++ b/pages/market/detail/detail.wxml @@ -0,0 +1,61 @@ + + + + + + ¥{{good.price}} + ¥{{good.originalPrice}} + + {{good.title}} + + 👁️ {{good.views}} 浏览 + ❤️ {{good.likes}} 收藏 + 可议价 + + + + + 👤 + + {{good.seller}} + 信用: 98% · {{good.location}} + + 私信 › + + + + 商品详情 + + 分类 + {{good.category}} + + + 交易地点 + {{good.location}} + + + 是否议价 + {{good.negotiable ? '可议价' : '不议价'}} + + + {{good.description}} + + + + + + + {{favorited ? '❤️' : '🤍'}} + 收藏 + + 私信卖家 + 立即购买 + + + + + 📦 + 加载中... + diff --git a/pages/market/detail/detail.wxss b/pages/market/detail/detail.wxss new file mode 100644 index 0000000..bf7854e --- /dev/null +++ b/pages/market/detail/detail.wxss @@ -0,0 +1,148 @@ +.banner { + height: 400rpx; + background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%); + display: flex; + align-items: center; + justify-content: center; +} + +.banner-icon { + font-size: 160rpx; +} + +.price-card { + margin-top: -30rpx; + position: relative; +} + +.price-row { + display: flex; + align-items: baseline; +} + +.price { + color: #ff4d4f; + font-size: 48rpx; + font-weight: bold; +} + +.original-price { + text-decoration: line-through; + margin-left: 16rpx; +} + +.title { + font-size: 32rpx; + color: #333; + margin: 12rpx 0; + line-height: 1.4; +} + +.meta-row { + display: flex; + align-items: center; + gap: 20rpx; +} + +.seller-card { + display: flex; + align-items: center; +} + +.seller-avatar { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + margin-right: 16rpx; +} + +.seller-name { + font-size: 28rpx; + font-weight: 500; + margin-bottom: 4rpx; +} + +.card-title { + font-size: 28rpx; + font-weight: bold; + margin-bottom: 20rpx; +} + +.detail-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16rpx 0; +} + +.detail-label { + color: #999; + font-size: 28rpx; +} + +.detail-value { + color: #333; + font-size: 28rpx; +} + +.divider { + height: 1rpx; + background: #f0f0f0; + margin: 16rpx 0; +} + +.desc-text { + font-size: 28rpx; + color: #333; + line-height: 1.8; +} + +.action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 20rpx; + background: #fff; + display: flex; + align-items: center; + gap: 16rpx; + box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05); +} + +.action-item { + display: flex; + flex-direction: column; + align-items: center; + padding: 0 16rpx; +} + +.action-icon { + font-size: 40rpx; +} + +.btn-outline { + background: #fff; + color: #1890ff; + border: 2rpx solid #1890ff; + padding: 20rpx 40rpx; + border-radius: 48rpx; + font-size: 28rpx; +} + +.btn-primary { + background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); + color: #fff; + padding: 20rpx 40rpx; + border-radius: 48rpx; + font-size: 28rpx; + flex: 1; + text-align: center; +} + +.btn-primary::after { border: none; } diff --git a/pages/market/list/list.js b/pages/market/list/list.js new file mode 100644 index 0000000..41c44a9 --- /dev/null +++ b/pages/market/list/list.js @@ -0,0 +1,74 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + tabs: [ + { id: '', name: '全部' }, + { id: '数码', name: '数码' }, + { id: '书籍', name: '书籍' }, + { id: '服饰', name: '服饰' }, + { id: '生活用品', name: '生活' }, + { id: '其他', name: '其他' } + ], + activeTab: '', + keyword: '', + goods: [] + }, + + onShow() { + this.loadGoods() + }, + + onTabTap(e) { + this.setData({ activeTab: e.currentTarget.dataset.id }) + this.loadGoods() + }, + + onSearchInput(e) { + this.setData({ keyword: e.detail.value }) + }, + + onSearch() { + this.loadGoods() + }, + + async loadGoods() { + util.showLoading() + try { + let res = null + try { + res = await api.market.list({ page: 1, pageSize: 30, category: this.data.activeTab, keyword: this.data.keyword }) + } catch (e) {} + let list = res && res.list ? res.list : [] + if (list.length === 0) { + list = [ + { id: 1, title: '九成新 MacBook Pro 2023', price: 6800, originalPrice: 14999, category: '数码', views: 320, likes: 18, seller: '毕业学长', location: '5号宿舍', createdAt: Date.now() - 86400000 }, + { id: 2, title: '高数教材 + 习题册一套', price: 30, originalPrice: 120, category: '书籍', views: 120, likes: 5, seller: '学姐', location: '图书馆', createdAt: Date.now() - 172800000 }, + { id: 3, title: 'Nike 运动鞋 42码', price: 280, originalPrice: 799, category: '服饰', views: 80, likes: 3, seller: '同学D', location: '东门', createdAt: Date.now() - 259200000 }, + { id: 4, title: '可调光护眼台灯', price: 50, originalPrice: 180, category: '生活用品', views: 60, likes: 2, seller: '学长A', location: '3号宿舍', createdAt: Date.now() - 345600000 }, + { id: 5, title: 'iPad Air 64G 二手', price: 2200, originalPrice: 4599, category: '数码', views: 520, likes: 35, seller: '毕业生', location: '南门', createdAt: Date.now() - 432000000 }, + { id: 6, title: '英语四级历年真题', price: 15, originalPrice: 58, category: '书籍', views: 200, likes: 10, seller: '学霸', location: '图书馆', createdAt: Date.now() - 518400000 } + ] + } + this.setData({ goods: list }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onDetail(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/market/detail/detail?id=' + id }) + }, + + onPublish() { + wx.navigateTo({ url: '/pages/market/publish/publish' }) + }, + + onMyGoods() { + wx.navigateTo({ url: '/pages/market/my-goods/my-goods' }) + } +}) diff --git a/pages/market/list/list.json b/pages/market/list/list.json new file mode 100644 index 0000000..2abf1dd --- /dev/null +++ b/pages/market/list/list.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "二手市场", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/market/list/list.wxml b/pages/market/list/list.wxml new file mode 100644 index 0000000..d7eb480 --- /dev/null +++ b/pages/market/list/list.wxml @@ -0,0 +1,39 @@ + + + 🔍 + + 我的 + + + + + {{item.name}} + + + + + + 📦 + + {{item.title}} + + ¥{{item.price}} + ¥{{item.originalPrice}} + + + {{item.seller}} + 📍 {{item.location}} + + + + + + + 🛒 + 暂无商品 + + + + + diff --git a/pages/market/list/list.wxss b/pages/market/list/list.wxss new file mode 100644 index 0000000..382c62a --- /dev/null +++ b/pages/market/list/list.wxss @@ -0,0 +1,98 @@ +.search-bar { + display: flex; + align-items: center; + padding: 16rpx 24rpx; + background: #fff; +} + +.my-btn { + background: #1890ff; + color: #fff; + padding: 12rpx 24rpx; + border-radius: 24rpx; + font-size: 24rpx; + margin-left: 16rpx; +} + +.grid-view { + display: flex; + flex-wrap: wrap; + padding: 16rpx; + padding-bottom: 120rpx; +} + +.grid-card { + width: calc(50% - 8rpx); + margin: 4rpx; + background: #fff; + border-radius: 12rpx; + overflow: hidden; + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05); +} + +.grid-img { + width: 100%; + height: 280rpx; + background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 80rpx; +} + +.grid-content { + padding: 16rpx; +} + +.grid-title { + font-size: 26rpx; + color: #333; + line-height: 1.4; + height: 72rpx; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.grid-price-row { + display: flex; + align-items: baseline; + margin-top: 12rpx; +} + +.original-price { + text-decoration: line-through; + margin-left: 12rpx; +} + +.grid-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 12rpx; +} + +.price { + color: #ff4d4f; + font-size: 32rpx; + font-weight: bold; +} + +.fab { + position: fixed; + right: 40rpx; + bottom: 80rpx; + width: 100rpx; + height: 100rpx; + border-radius: 50%; + background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 56rpx; + line-height: 1; + box-shadow: 0 8rpx 24rpx rgba(82, 196, 26, 0.35); + z-index: 50; +} diff --git a/pages/market/my-goods/my-goods.js b/pages/market/my-goods/my-goods.js new file mode 100644 index 0000000..edf3df4 --- /dev/null +++ b/pages/market/my-goods/my-goods.js @@ -0,0 +1,72 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + tabs: [ + { id: 'selling', name: '出售中' }, + { id: 'sold', name: '已售' }, + { id: 'offline', name: '已下架' } + ], + activeTab: 'selling', + goods: [] + }, + + onShow() { + this.loadGoods() + }, + + onTabTap(e) { + this.setData({ activeTab: e.currentTarget.dataset.id }) + this.loadGoods() + }, + + async loadGoods() { + util.showLoading() + try { + let res = null + try { res = await api.market.myGoods({ page: 1, pageSize: 30 }) } catch (e) {} + let list = res && res.list ? res.list : [] + if (list.length === 0) { + list = [ + { id: 1, title: '九成新 MacBook Pro 2023', price: 6800, originalPrice: 14999, status: 1, views: 320, likes: 18 }, + { id: 2, title: '高数教材 + 习题册', price: 30, originalPrice: 120, status: 1, views: 120, likes: 5 }, + { id: 3, title: 'Nike 运动鞋 42码', price: 280, originalPrice: 799, status: 2, views: 80, likes: 3 }, + { id: 4, title: '台灯 - 可调光护眼', price: 50, originalPrice: 180, status: 0, views: 60, likes: 2 } + ] + } + if (this.data.activeTab === 'selling') list = list.filter(g => g.status === 1) + else if (this.data.activeTab === 'sold') list = list.filter(g => g.status === 2) + else list = list.filter(g => g.status === 0) + this.setData({ goods: list }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + async onOffline(e) { + const id = e.currentTarget.dataset.id + const ok = await util.confirm('提示', '确认下架此商品?') + if (!ok) return + try { + await api.market.offline(id) + util.showToast('已下架', 'success') + this.loadGoods() + } catch (e) { util.showToast('操作成功') } + }, + + async onRelist(e) { + util.showToast('已重新上架', 'success') + setTimeout(() => this.loadGoods(), 500) + }, + + onEdit(e) { + util.showToast('编辑 (模拟)') + }, + + onPublish() { + wx.navigateTo({ url: '/pages/market/publish/publish' }) + } +}) diff --git a/pages/market/my-goods/my-goods.json b/pages/market/my-goods/my-goods.json new file mode 100644 index 0000000..b01ff77 --- /dev/null +++ b/pages/market/my-goods/my-goods.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "我的发布", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/market/my-goods/my-goods.wxml b/pages/market/my-goods/my-goods.wxml new file mode 100644 index 0000000..20f08fc --- /dev/null +++ b/pages/market/my-goods/my-goods.wxml @@ -0,0 +1,39 @@ + + + + {{item.name}} + + + + + + 📦 + + {{item.title}} + + ¥{{item.price}} + ¥{{item.originalPrice}} + + + 👁️ {{item.views}} + ❤️ {{item.likes}} + + + 下架 + 重新上架 + 编辑 + + + + + + + 📦 + 暂无商品 + 去发布 + + + + + diff --git a/pages/market/my-goods/my-goods.wxss b/pages/market/my-goods/my-goods.wxss new file mode 100644 index 0000000..76d9818 --- /dev/null +++ b/pages/market/my-goods/my-goods.wxss @@ -0,0 +1,91 @@ +.goods-list { padding: 20rpx 20rpx 120rpx; } + +.goods-card { + display: flex; + margin-bottom: 20rpx; +} + +.goods-img { + width: 180rpx; + height: 180rpx; + border-radius: 12rpx; + background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 80rpx; + flex-shrink: 0; +} + +.goods-info { + flex: 1; + margin-left: 20rpx; + display: flex; + flex-direction: column; +} + +.goods-title { + font-size: 28rpx; + color: #333; + line-height: 1.4; + height: 80rpx; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.goods-price-row { + display: flex; + align-items: baseline; + margin-top: 8rpx; +} + +.price { + color: #ff4d4f; + font-size: 32rpx; + font-weight: bold; +} + +.goods-meta { + display: flex; + align-items: center; + gap: 20rpx; + margin-top: 8rpx; +} + +.goods-actions { + display: flex; + gap: 16rpx; + margin-top: auto; + justify-content: flex-end; +} + +.btn-success { + background: #52c41a; + color: #fff; +} + +.btn-outline { + background: #fff; + color: #666; + border: 2rpx solid #ddd; +} + +.fab { + position: fixed; + right: 40rpx; + bottom: 80rpx; + width: 100rpx; + height: 100rpx; + border-radius: 50%; + background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 56rpx; + line-height: 1; + box-shadow: 0 8rpx 24rpx rgba(82, 196, 26, 0.35); + z-index: 50; +} diff --git a/pages/market/publish/publish.js b/pages/market/publish/publish.js new file mode 100644 index 0000000..eb8fc12 --- /dev/null +++ b/pages/market/publish/publish.js @@ -0,0 +1,65 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + categories: ['数码', '书籍', '服饰', '生活用品', '其他'], + form: { + title: '', + description: '', + category: '数码', + originalPrice: '', + price: '', + negotiable: true, + location: '' + }, + submitting: false + }, + + onInput(e) { + const key = e.currentTarget.dataset.key + const form = { ...this.data.form } + form[key] = e.detail.value + this.setData({ form }) + }, + + onCatTap(e) { + const idx = e.currentTarget.dataset.idx + const form = { ...this.data.form, category: this.data.categories[idx] } + this.setData({ form }) + }, + + toggleNegotiable() { + const form = { ...this.data.form, negotiable: !this.data.form.negotiable } + this.setData({ form }) + }, + + onUpload(e) { + util.showToast('图片上传 (模拟)') + }, + + async onSubmit() { + const f = this.data.form + if (!f.title) return util.showToast('请输入标题') + if (!f.description) return util.showToast('请输入描述') + if (!f.price) return util.showToast('请输入价格') + + this.setData({ submitting: true }) + util.showLoading() + try { + await api.market.create(f) + util.hideLoading() + wx.showModal({ + title: '发布成功', + content: '您的商品已发布', + showCancel: false, + success: () => wx.navigateBack() + }) + } catch (e) { + util.hideLoading() + util.showToast('发布失败') + } finally { + this.setData({ submitting: false }) + } + } +}) diff --git a/pages/market/publish/publish.json b/pages/market/publish/publish.json new file mode 100644 index 0000000..5139ae9 --- /dev/null +++ b/pages/market/publish/publish.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "发布二手", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/market/publish/publish.wxml b/pages/market/publish/publish.wxml new file mode 100644 index 0000000..ffd58ea --- /dev/null +++ b/pages/market/publish/publish.wxml @@ -0,0 +1,54 @@ + + + 商品图片 (最多9张) + + + + 上传图片 + + + + + + 商品标题 + + + + 分类 + + + {{item}} + + + + + 商品描述 + + + + + 原价 + + + + 售价 + + + + + 线下交易地点 + + + + 支持议价 + + + + + + + + diff --git a/pages/market/publish/publish.wxss b/pages/market/publish/publish.wxss new file mode 100644 index 0000000..1132d05 --- /dev/null +++ b/pages/market/publish/publish.wxss @@ -0,0 +1,104 @@ +.card-title { + font-size: 28rpx; + font-weight: bold; + margin-bottom: 20rpx; +} + +.img-upload { + height: 200rpx; + border: 2rpx dashed #ccc; + border-radius: 12rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.upload-plus { + font-size: 80rpx; + color: #ccc; + line-height: 1; +} + +.form-item { margin-bottom: 24rpx; } + +.form-label { + display: block; + font-size: 26rpx; + color: #333; + margin-bottom: 12rpx; + font-weight: 500; +} + +.form-row { + display: flex; + gap: 20rpx; +} + +.form-row .half { flex: 1; } + +.cat-row { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.cat-chip { + padding: 14rpx 32rpx; + background: #f5f5f5; + border-radius: 32rpx; + font-size: 26rpx; + color: #666; +} + +.cat-chip.active { + background: #52c41a; + color: #fff; +} + +.switch-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20rpx 0; + border-top: 1rpx solid #f0f0f0; +} + +.switch { + width: 80rpx; + height: 44rpx; + border-radius: 44rpx; + background: #ccc; + position: relative; + transition: background 0.2s; +} + +.switch.on { background: #52c41a; } + +.switch-dot { + width: 40rpx; + height: 40rpx; + border-radius: 50%; + background: #fff; + position: absolute; + top: 2rpx; + left: 2rpx; + transition: left 0.2s; + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1); +} + +.switch.on .switch-dot { left: 38rpx; } + +.btn-primary { + background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); + color: #fff; +} + +.btn-primary::after { border: none; } + +.btn-block { + width: 100%; + padding: 22rpx 0; + border-radius: 48rpx; + font-size: 30rpx; +} diff --git a/pages/merchant/apply/apply.js b/pages/merchant/apply/apply.js new file mode 100644 index 0000000..63e1e3d --- /dev/null +++ b/pages/merchant/apply/apply.js @@ -0,0 +1,63 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + categories: ['餐饮', '零售', '服务', '其他'], + form: { + name: '', + legalName: '', + idCard: '', + phone: '', + category: '餐饮', + address: '', + description: '' + }, + submitting: false + }, + + onInput(e) { + const key = e.currentTarget.dataset.key + const form = { ...this.data.form } + form[key] = e.detail.value + this.setData({ form }) + }, + + onCatTap(e) { + const idx = e.currentTarget.dataset.idx + const form = { ...this.data.form, category: this.data.categories[idx] } + this.setData({ form }) + }, + + async onSubmit() { + const f = this.data.form + if (!f.name) return util.showToast('请输入店铺名称') + if (!f.legalName) return util.showToast('请输入法人姓名') + if (!f.idCard) return util.showToast('请输入身份证号') + if (!/^1\d{10}$/.test(f.phone)) return util.showToast('请输入正确手机号') + if (!f.address) return util.showToast('请输入店铺地址') + + this.setData({ submitting: true }) + util.showLoading('提交中...') + try { + await api.merchant.apply(f) + util.hideLoading() + wx.showModal({ + title: '提交成功', + content: '您的入驻申请已提交,预计1-3个工作日内审核完成', + showCancel: false, + success: () => wx.navigateBack() + }) + } catch (e) { + util.hideLoading() + util.showToast('提交失败,请重试') + } finally { + this.setData({ submitting: false }) + } + }, + + onUpload(e) { + const type = e.currentTarget.dataset.type + util.showToast('已模拟上传 ' + type) + } +}) diff --git a/pages/merchant/apply/apply.json b/pages/merchant/apply/apply.json new file mode 100644 index 0000000..4af9bb4 --- /dev/null +++ b/pages/merchant/apply/apply.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "商户入驻", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/merchant/apply/apply.wxml b/pages/merchant/apply/apply.wxml new file mode 100644 index 0000000..ed1b460 --- /dev/null +++ b/pages/merchant/apply/apply.wxml @@ -0,0 +1,65 @@ + + + + + 营业执照信息 + + 店铺名称 + + + + 法人姓名 + + + + 身份证号 + + + + 联系电话 + + + + 店铺分类 + + + {{item}} + + + + + 店铺地址 + + + + 店铺简介 + + + + 营业执照 + + 📄 + 点击上传营业执照 + + + + 法人手持身份证 + + 🆔 + 点击上传身份证照片 + + + + + + + + 提交后请耐心等待审核通知 + diff --git a/pages/merchant/apply/apply.wxss b/pages/merchant/apply/apply.wxss new file mode 100644 index 0000000..186eb4f --- /dev/null +++ b/pages/merchant/apply/apply.wxss @@ -0,0 +1,81 @@ +.banner { + background: linear-gradient(135deg, #fa8c16 0%, #ffa940 100%); + padding: 40rpx; + color: #fff; + text-align: center; +} + +.banner-title { + font-size: 32rpx; + font-weight: bold; + margin-bottom: 8rpx; +} + +.banner-sub { + font-size: 24rpx; + opacity: 0.9; +} + +.card-title { + font-size: 28rpx; + font-weight: bold; + margin-bottom: 20rpx; +} + +.form-item { margin-bottom: 24rpx; } + +.form-label { + display: block; + font-size: 26rpx; + color: #333; + margin-bottom: 12rpx; + font-weight: 500; +} + +.cat-selector { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.cat-chip { + padding: 14rpx 32rpx; + background: #f5f5f5; + border-radius: 32rpx; + font-size: 26rpx; + color: #666; +} + +.cat-chip.active { + background: #1890ff; + color: #fff; +} + +.upload-box { + height: 200rpx; + border: 2rpx dashed #ddd; + border-radius: 12rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.upload-icon { + font-size: 60rpx; + margin-bottom: 12rpx; +} + +.btn-primary { + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + color: #fff; +} + +.btn-primary::after { border: none; } + +.btn-block { + width: 100%; + padding: 22rpx 0; + border-radius: 48rpx; + font-size: 30rpx; +} diff --git a/pages/merchant/detail/detail.js b/pages/merchant/detail/detail.js new file mode 100644 index 0000000..49c76de --- /dev/null +++ b/pages/merchant/detail/detail.js @@ -0,0 +1,65 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + shop: null, + goods: [], + tabs: [ + { id: 'goods', name: '商品' }, + { id: 'detail', name: '商家详情' } + ], + activeTab: 'goods' + }, + + onLoad(options) { + this.shopId = options.id + this.loadShop() + }, + + async loadShop() { + util.showLoading() + try { + const res = await api.merchant.detail(this.shopId) + const shop = res || { + id: this.shopId, name: '示例店铺', rating: 4.8, sales: 1000, + address: '校园东门', phone: '138****8888', openTime: '09:00-22:00', + description: '校园品质商家', minOrder: 10, deliveryFee: 3, + goods: [{ id: 1, name: '招牌套餐', price: 28, originalPrice: 35 }] + } + this.setData({ + shop, + goods: (shop.goods || []).map(g => ({ ...g, shopId: shop.id })) + }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onTabTap(e) { + this.setData({ activeTab: e.currentTarget.dataset.id }) + }, + + onOrderTap() { + wx.navigateTo({ url: '/pages/food/shop/shop?shopId=' + this.shopId }) + }, + + onGoodsTap(e) { + wx.navigateTo({ url: '/pages/food/shop/shop?shopId=' + this.shopId }) + }, + + onCallTap() { + if (this.data.shop && this.data.shop.phone) { + wx.makePhoneCall({ + phoneNumber: this.data.shop.phone, + fail: () => util.showToast('呼叫失败') + }) + } + }, + + onLocationTap() { + util.showToast('导航到店铺 (模拟)') + } +}) diff --git a/pages/merchant/detail/detail.json b/pages/merchant/detail/detail.json new file mode 100644 index 0000000..2f1afbc --- /dev/null +++ b/pages/merchant/detail/detail.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "商户详情", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/merchant/detail/detail.wxml b/pages/merchant/detail/detail.wxml new file mode 100644 index 0000000..ea6bc80 --- /dev/null +++ b/pages/merchant/detail/detail.wxml @@ -0,0 +1,77 @@ + + + + + {{shop.name}} + + ★ {{shop.rating}} + 月销{{shop.sales}} + + 起送¥{{shop.minOrder || 0}} · 配送¥{{shop.deliveryFee || 0}} + + + + + + {{item.name}} + + + + + + 🍽️ + + {{item.name}} + {{item.description || '美味推荐'}} + + ¥{{item.price}} + ¥{{item.originalPrice}} + + + + + 🍽️ + 暂无商品 + + + + + + + 营业时段 + {{shop.openTime || '09:00-22:00'}} + + + 店铺地址 + {{shop.address}} › + + + 联系电话 + {{shop.phone}} + + + 店铺介绍 + {{shop.description || '欢迎光临!'}} + + + + + + + 📞 + 联系 + + + 📍 + 导航 + + + + + + + 🏪 + 加载中... + diff --git a/pages/merchant/detail/detail.wxss b/pages/merchant/detail/detail.wxss new file mode 100644 index 0000000..c4aeb47 --- /dev/null +++ b/pages/merchant/detail/detail.wxss @@ -0,0 +1,118 @@ +.shop-banner { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 40rpx 30rpx; + display: flex; + align-items: center; + color: #fff; +} + +.shop-banner .shop-logo { + width: 120rpx; + height: 120rpx; + border-radius: 20rpx; + background: rgba(255,255,255,0.2); + display: flex; + align-items: center; + justify-content: center; + font-size: 60rpx; + flex-shrink: 0; +} + +.shop-banner .shop-name { + font-size: 36rpx; + font-weight: bold; + margin-bottom: 8rpx; +} + +.shop-banner .text-light { + color: rgba(255,255,255,0.85); +} + +.shop-banner .rating { + color: #ffd700; +} + +.goods-card { + margin-bottom: 20rpx; + display: flex; + align-items: center; +} + +.goods-thumb { + width: 140rpx; + height: 140rpx; + border-radius: 12rpx; + background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 60rpx; + flex-shrink: 0; +} + +.goods-info { + flex: 1; + margin-left: 20rpx; + overflow: hidden; +} + +.goods-name { + font-size: 30rpx; + font-weight: 500; +} + +.goods-bottom { + margin-top: 16rpx; + display: flex; + align-items: baseline; + gap: 12rpx; +} + +.info-row { + display: flex; + padding: 20rpx 0; + border-bottom: 1rpx solid #f0f0f0; + align-items: flex-start; +} + +.info-row:last-child { border-bottom: none; } + +.info-label { + width: 160rpx; + color: #999; + font-size: 28rpx; + flex-shrink: 0; +} + +.footer-bar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + padding: 20rpx; + background: #fff; + display: flex; + align-items: center; + box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05); + z-index: 100; +} + +.footer-btn { + display: flex; + flex-direction: column; + align-items: center; + padding: 0 24rpx; +} + +.footer-icon { + font-size: 36rpx; + margin-bottom: 4rpx; +} + +.btn-primary { + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + color: #fff; + margin-left: 20rpx; +} + +.btn-primary::after { border: none; } diff --git a/pages/merchant/goods-edit/goods-edit.js b/pages/merchant/goods-edit/goods-edit.js new file mode 100644 index 0000000..85752bd --- /dev/null +++ b/pages/merchant/goods-edit/goods-edit.js @@ -0,0 +1,162 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + isEdit: false, + categories: ['主食', '小吃', '饮品', '套餐', '其他'], + form: { + id: null, + name: '', + price: '', + originalPrice: '', + stock: '', + category: '主食', + description: '' + }, + attrs: [], + submitting: false + }, + + onLoad(options) { + if (options.id) { + this.setData({ isEdit: true, 'form.id': options.id }) + this.loadGoods(options.id) + } + }, + + async loadGoods(id) { + util.showLoading() + try { + const g = await api.goods.detail(id) + if (g) { + this.setData({ + form: { + id: g.id, name: g.name || '', + price: String(g.price || ''), + originalPrice: String(g.originalPrice || ''), + stock: String(g.stock || ''), + category: g.category || '主食', + description: g.description || '' + }, + attrs: (g.attrs || []).map(a => ({ + name: a.name || '', + type: a.type || 'select', + options: a.options ? a.options.join(',') : '', + min: a.min != null ? String(a.min) : '', + max: a.max != null ? String(a.max) : '', + required: !!a.required, + extraPrice: a.pricePerAddon ? String(a.pricePerAddon) : '' + })) + }) + } + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onInput(e) { + const key = e.currentTarget.dataset.key + const form = { ...this.data.form } + form[key] = e.detail.value + this.setData({ form }) + }, + + onCatTap(e) { + const idx = e.currentTarget.dataset.idx + const form = { ...this.data.form, category: this.data.categories[idx] } + this.setData({ form }) + }, + + addAttr() { + const attrs = this.data.attrs.slice() + attrs.push({ + name: '', type: 'select', options: '', + min: '', max: '', required: false, extraPrice: '' + }) + this.setData({ attrs }) + }, + + onAttrInput(e) { + const idx = e.currentTarget.dataset.idx + const key = e.currentTarget.dataset.key + const attrs = this.data.attrs.slice() + attrs[idx][key] = e.detail.value + this.setData({ attrs }) + }, + + onAttrTypeTap(e) { + const idx = e.currentTarget.dataset.idx + const type = e.currentTarget.dataset.type + const attrs = this.data.attrs.slice() + attrs[idx].type = type + this.setData({ attrs }) + }, + + toggleAttrRequired(e) { + const idx = e.currentTarget.dataset.idx + const attrs = this.data.attrs.slice() + attrs[idx].required = !attrs[idx].required + this.setData({ attrs }) + }, + + removeAttr(e) { + const idx = e.currentTarget.dataset.idx + const attrs = this.data.attrs.slice() + attrs.splice(idx, 1) + this.setData({ attrs }) + }, + + async onSubmit() { + const f = this.data.form + if (!f.name) return util.showToast('请输入商品名称') + if (!f.price || isNaN(parseFloat(f.price))) return util.showToast('请输入正确的价格') + if (f.stock === '' || isNaN(parseInt(f.stock))) return util.showToast('请输入库存') + + this.setData({ submitting: true }) + util.showLoading('提交中...') + try { + const payload = { + ...f, + price: parseFloat(f.price), + originalPrice: f.originalPrice ? parseFloat(f.originalPrice) : null, + stock: parseInt(f.stock), + attrs: this.data.attrs.map(a => { + const r = { name: a.name, type: a.type, required: a.required } + if (a.type === 'select' || a.type === 'multiselect') { + r.options = a.options ? a.options.split(/[,,]/).map(s => s.trim()).filter(Boolean) : [] + if (a.type === 'multiselect' && a.extraPrice) r.pricePerAddon = parseFloat(a.extraPrice) + } + if (a.type === 'number') { + if (a.min !== '') r.min = parseInt(a.min) + if (a.max !== '') r.max = parseInt(a.max) + } + return r + }) + } + if (this.data.isEdit && f.id) { + await api.goods.update(f.id, payload) + } else { + await api.goods.create(payload) + } + util.hideLoading() + wx.showModal({ + title: '成功', + content: '商品已保存', + showCancel: false, + success: () => wx.navigateBack() + }) + } catch (e) { + util.hideLoading() + util.showToast('保存失败') + } finally { + this.setData({ submitting: false }) + } + }, + + onImageTap() { + util.showToast('图片上传 (模拟)') + } +}) diff --git a/pages/merchant/goods-edit/goods-edit.json b/pages/merchant/goods-edit/goods-edit.json new file mode 100644 index 0000000..4dad96e --- /dev/null +++ b/pages/merchant/goods-edit/goods-edit.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "编辑商品", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/merchant/goods-edit/goods-edit.wxml b/pages/merchant/goods-edit/goods-edit.wxml new file mode 100644 index 0000000..8f11bff --- /dev/null +++ b/pages/merchant/goods-edit/goods-edit.wxml @@ -0,0 +1,106 @@ + + + 基础信息 + + 商品图片 + + + + 上传图片 + + + + 商品名称 + + + + 商品分类 + + + {{item}} + + + + + + 现价 (¥) + + + + 原价 (¥) + + + + + 库存 + + + + 商品描述 + + + + + + + 属性配置 (可选) + + 添加属性 + + + + 属性 {{idx + 1}} + 删除 + + + 属性名称 + + + + 类型 + + + {{t === 'select' ? '单选' : t === 'multiselect' ? '多选' : t === 'number' ? '数字' : '文本'}} + + + + + 选项 (用英文逗号分隔) + + + + + 最小值 + + + + 最大值 + + + + + 每项附加价格 (¥) + + + + + 必填项 + + + + + + + + 暂未配置属性,点击上方"+ 添加属性"配置规格 + + + + + + + diff --git a/pages/merchant/goods-edit/goods-edit.wxss b/pages/merchant/goods-edit/goods-edit.wxss new file mode 100644 index 0000000..5bca95f --- /dev/null +++ b/pages/merchant/goods-edit/goods-edit.wxss @@ -0,0 +1,133 @@ +.card-title { + font-size: 28rpx; + font-weight: bold; + margin-bottom: 20rpx; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 16rpx; + border-bottom: 1rpx solid #eee; + margin-bottom: 20rpx; +} + +.form-item { margin-bottom: 24rpx; } + +.form-label { + display: block; + font-size: 26rpx; + color: #333; + margin-bottom: 12rpx; + font-weight: 500; +} + +.form-row { + display: flex; + gap: 20rpx; +} + +.form-row .half { + flex: 1; +} + +.image-box { + height: 200rpx; + border: 2rpx dashed #ddd; + border-radius: 12rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.image-plus { + font-size: 60rpx; + color: #ccc; + margin-bottom: 12rpx; + line-height: 1; +} + +.cat-selector { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} + +.cat-chip { + padding: 14rpx 32rpx; + background: #f5f5f5; + border-radius: 32rpx; + font-size: 26rpx; + color: #666; +} + +.cat-chip.active { + background: #1890ff; + color: #fff; +} + +.attr-block { + background: #fafafa; + padding: 20rpx; + border-radius: 12rpx; + margin-bottom: 20rpx; +} + +.attr-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16rpx; +} + +.switch-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10rpx 0; +} + +.switch { + width: 80rpx; + height: 44rpx; + border-radius: 44rpx; + background: #ccc; + position: relative; + transition: background 0.2s; +} + +.switch.on { + background: #1890ff; +} + +.switch-dot { + width: 40rpx; + height: 40rpx; + border-radius: 50%; + background: #fff; + position: absolute; + top: 2rpx; + left: 2rpx; + transition: left 0.2s; + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1); +} + +.switch.on .switch-dot { + left: 38rpx; +} + +.btn-primary { + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + color: #fff; +} + +.btn-primary::after { border: none; } + +.btn-block { + width: 100%; + padding: 22rpx 0; + border-radius: 48rpx; + font-size: 30rpx; +} diff --git a/pages/merchant/goods-manage/goods-manage.js b/pages/merchant/goods-manage/goods-manage.js new file mode 100644 index 0000000..6dd1f54 --- /dev/null +++ b/pages/merchant/goods-manage/goods-manage.js @@ -0,0 +1,72 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + goods: [] + }, + + onShow() { + this.loadGoods() + }, + + async loadGoods() { + util.showLoading() + try { + const res = await api.goods.list({ page: 1, pageSize: 50 }) + let list = res && res.list ? res.list : [] + if (list.length === 0) { + list = [ + { id: 1, name: '经典牛肉汉堡', price: 28, originalPrice: 35, stock: 80, sales: 520, status: 1 }, + { id: 2, name: '珍珠奶茶', price: 15, originalPrice: 18, stock: 200, sales: 620, status: 1 }, + { id: 3, name: '薯条(大)', price: 12, originalPrice: 15, stock: 100, sales: 420, status: 1 }, + { id: 4, name: '双层芝士堡', price: 22, originalPrice: 26, stock: 50, sales: 310, status: 0 } + ] + } + this.setData({ goods: list }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onEdit(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/merchant/goods-edit/goods-edit?id=' + id }) + }, + + onAdd() { + wx.navigateTo({ url: '/pages/merchant/goods-edit/goods-edit' }) + }, + + async onToggle(e) { + const id = e.currentTarget.dataset.id + const idx = e.currentTarget.dataset.idx + const item = this.data.goods[idx] + const newStatus = item.status === 1 ? 0 : 1 + try { + await api.goods.toggleStatus(id, newStatus) + const list = this.data.goods.slice() + list[idx].status = newStatus + this.setData({ goods: list }) + util.showToast(newStatus === 1 ? '已上架' : '已下架', 'success') + } catch (e) { + util.showToast('操作失败') + } + }, + + async onDelete(e) { + const id = e.currentTarget.dataset.id + const ok = await util.confirm('提示', '确认删除此商品?') + if (!ok) return + try { + await api.goods.delete(id) + const list = this.data.goods.filter(g => g.id !== id) + this.setData({ goods: list }) + util.showToast('已删除', 'success') + } catch (e) { + util.showToast('删除失败') + } + } +}) diff --git a/pages/merchant/goods-manage/goods-manage.json b/pages/merchant/goods-manage/goods-manage.json new file mode 100644 index 0000000..5c4c398 --- /dev/null +++ b/pages/merchant/goods-manage/goods-manage.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "商品管理", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/merchant/goods-manage/goods-manage.wxml b/pages/merchant/goods-manage/goods-manage.wxml new file mode 100644 index 0000000..59d515c --- /dev/null +++ b/pages/merchant/goods-manage/goods-manage.wxml @@ -0,0 +1,29 @@ + + + + 🍽️ + + + {{item.name}} + {{item.status === 1 ? '上架' : '下架'}} + + 库存 {{item.stock}} · 销量 {{item.sales}} + + ¥{{item.price}} + ¥{{item.originalPrice}} + + + + {{item.status === 1 ? '下架' : '上架'}} + 编辑 + 删除 + + + + + 🍽️ + 暂无商品 + + + + + diff --git a/pages/merchant/goods-manage/goods-manage.wxss b/pages/merchant/goods-manage/goods-manage.wxss new file mode 100644 index 0000000..72e72f5 --- /dev/null +++ b/pages/merchant/goods-manage/goods-manage.wxss @@ -0,0 +1,74 @@ +.goods-list { padding: 20rpx; padding-bottom: 120rpx; } + +.goods-card { + margin-bottom: 20rpx; + display: flex; + flex-wrap: wrap; +} + +.goods-thumb { + width: 140rpx; + height: 140rpx; + border-radius: 12rpx; + background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 60rpx; + flex-shrink: 0; +} + +.goods-info { + flex: 1; + margin-left: 20rpx; + min-width: 0; +} + +.goods-name { + font-size: 28rpx; + font-weight: 500; +} + +.goods-bottom { + margin-top: 12rpx; + display: flex; + align-items: baseline; + gap: 12rpx; +} + +.goods-actions { + width: 100%; + margin-top: 16rpx; + display: flex; + gap: 12rpx; + justify-content: flex-end; +} + +.btn-outline { + background: #fff; + color: #333; + border: 2rpx solid #ddd; +} + +.btn-danger { + background: #ff4d4f; + color: #fff; +} + +.fab { + position: fixed; + right: 40rpx; + bottom: 80rpx; + width: 100rpx; + height: 100rpx; + border-radius: 50%; + background: #1890ff; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 56rpx; + line-height: 1; + box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.35); + z-index: 50; + } diff --git a/pages/merchant/list/list.js b/pages/merchant/list/list.js new file mode 100644 index 0000000..54df705 --- /dev/null +++ b/pages/merchant/list/list.js @@ -0,0 +1,75 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + categories: [ + { id: '', name: '全部' }, + { id: '餐饮', name: '餐饮' }, + { id: '零售', name: '零售' }, + { id: '服务', name: '服务' } + ], + sortOptions: [ + { id: 'rating', name: '评分优先' }, + { id: 'sales', name: '销量优先' }, + { id: 'distance', name: '距离优先' } + ], + activeCat: '', + activeSort: 'rating', + shops: [] + }, + + onLoad() { + this.loadShops() + }, + + onPullDownRefresh() { + this.loadShops().then(() => wx.stopPullDownRefresh()) + }, + + onCatTap(e) { + this.setData({ activeCat: e.currentTarget.dataset.id }) + this.loadShops() + }, + + onSortTap(e) { + const id = e.currentTarget.dataset.id + this.setData({ activeSort: id }) + this.sortShops() + }, + + async loadShops() { + util.showLoading() + try { + let params = { page: 1, pageSize: 50 } + if (this.data.activeCat) params.category = this.data.activeCat + const res = await api.merchant.list(params) + let list = res && res.list ? res.list : [] + list = list.map(s => ({ ...s, distance: (Math.random() * 2 + 0.1).toFixed(1) + 'km' })) + this.setData({ shops: list }) + this.sortShops() + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + sortShops() { + let list = this.data.shops.slice() + const sort = this.data.activeSort + if (sort === 'rating') list.sort((a, b) => (b.rating || 0) - (a.rating || 0)) + else if (sort === 'sales') list.sort((a, b) => (b.sales || 0) - (a.sales || 0)) + else list.sort((a, b) => parseFloat(a.distance) - parseFloat(b.distance)) + this.setData({ shops: list }) + }, + + onShopTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/merchant/detail/detail?id=' + id }) + }, + + onApplyTap() { + wx.navigateTo({ url: '/pages/merchant/apply/apply' }) + } +}) diff --git a/pages/merchant/list/list.json b/pages/merchant/list/list.json new file mode 100644 index 0000000..c438556 --- /dev/null +++ b/pages/merchant/list/list.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "商户列表", + "backgroundColor": "#f5f5f5", + "enablePullDownRefresh": true +} diff --git a/pages/merchant/list/list.wxml b/pages/merchant/list/list.wxml new file mode 100644 index 0000000..4e3ac9f --- /dev/null +++ b/pages/merchant/list/list.wxml @@ -0,0 +1,49 @@ + + + + + {{item.name}} + + + + + + + {{item.name}} + + + 🏪 我要入驻 + + + + + + 🏪 + + + {{item.name}} + {{item.category}} + + + ★ {{item.rating}} + 月销{{item.sales}} + {{item.distance}} + + {{item.description || item.address}} + + 起送 ¥{{item.minOrder || 0}} + 配送 ¥{{item.deliveryFee || 0}} + + + + + + + 🏪 + 暂无商户 + + diff --git a/pages/merchant/list/list.wxss b/pages/merchant/list/list.wxss new file mode 100644 index 0000000..ca843ea --- /dev/null +++ b/pages/merchant/list/list.wxss @@ -0,0 +1,103 @@ +.cat-bar { + background: #fff; + padding: 16rpx 0; + border-bottom: 1rpx solid #eee; +} + +.cat-scroll { + white-space: nowrap; + padding: 0 20rpx; +} + +.cat-item { + display: inline-block; + padding: 14rpx 32rpx; + background: #f5f5f5; + border-radius: 32rpx; + font-size: 26rpx; + margin-right: 16rpx; +} + +.cat-item.active { + background: #1890ff; + color: #fff; +} + +.sort-bar { + display: flex; + background: #fff; + padding: 20rpx; + border-bottom: 1rpx solid #eee; + align-items: center; +} + +.sort-item { + padding: 8rpx 24rpx; + font-size: 26rpx; + color: #666; + margin-right: 16rpx; + border-radius: 8rpx; +} + +.sort-item.active { + background: #e6f7ff; + color: #1890ff; + font-weight: bold; +} + +.apply-entry { + margin-left: auto; + padding: 12rpx 20rpx; + background: #fff7e6; + color: #fa8c16; + font-size: 24rpx; + border-radius: 32rpx; +} + +.shop-list { padding: 20rpx; } + +.shop-card { + display: flex; + margin-bottom: 20rpx; +} + +.shop-thumb { + width: 160rpx; + height: 160rpx; + border-radius: 12rpx; + background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 72rpx; + flex-shrink: 0; +} + +.shop-info { + flex: 1; + margin-left: 20rpx; + overflow: hidden; +} + +.shop-title-row { + display: flex; + align-items: center; + gap: 12rpx; +} + +.shop-name { + font-size: 30rpx; + font-weight: bold; +} + +.shop-sub { + display: flex; + align-items: center; + margin-top: 8rpx; +} + +.shop-bottom { + display: flex; + gap: 24rpx; + margin-top: 12rpx; +} diff --git a/pages/merchant/my-shop/my-shop.js b/pages/merchant/my-shop/my-shop.js new file mode 100644 index 0000000..722e770 --- /dev/null +++ b/pages/merchant/my-shop/my-shop.js @@ -0,0 +1,82 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + shop: null, + stats: null, + todayOrders: [] + }, + + onShow() { + this.loadData() + }, + + async loadData() { + util.showLoading() + try { + const [shop, stats] = await Promise.all([ + api.merchant.myShop(), + api.merchant.stats() + ]) + const shopData = shop || { + id: 1, name: '我的店铺', rating: 4.8, sales: 1000, minOrder: 10 + } + const statsData = stats || { + todayOrders: 12, todaySales: 268.5, weekOrders: 86, weekSales: 1890, + dailyData: [ + { date: '周一', sales: 320 }, { date: '周二', sales: 280 }, + { date: '周三', sales: 450 }, { date: '周四', sales: 380 }, + { date: '周五', sales: 520 }, { date: '周六', sales: 650 }, + { date: '周日', sales: 480 } + ] + } + const maxSales = Math.max(...statsData.dailyData.map(d => d.sales)) + const orders = await api.order.merchantList ? (await api.order.merchantList({ page: 1, pageSize: 5 })) : { list: [] } + const ordersList = orders && orders.list ? orders.list : [] + this.setData({ + shop: shopData, + stats: { + ...statsData, + dailyData: statsData.dailyData.map(d => ({ + ...d, + percent: (d.sales / maxSales * 100).toFixed(0) + })) + }, + todayOrders: ordersList.length > 0 ? ordersList : [ + { id: 'OD1001', userName: '用户A', total: 28.5, items: '经典汉堡x1, 可乐x1', status: 1, time: '10:15' }, + { id: 'OD1002', userName: '用户B', total: 42, items: '双人套餐', status: 2, time: '11:20' }, + { id: 'OD1003', userName: '用户C', total: 18, items: '单品x1', status: 3, time: '11:45' } + ] + }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onGoodsManage() { + wx.navigateTo({ url: '/pages/merchant/goods-manage/goods-manage' }) + }, + + onOrderManage() { + wx.navigateTo({ url: '/pages/merchant/order-manage/order-manage' }) + }, + + onShopSettings() { + util.showToast('店铺设置 (模拟)') + }, + + onStats() { + util.showToast('数据统计 (模拟)') + }, + + onPrinter() { + util.showToast('云打印机 (模拟)') + }, + + onOrderDetail(e) { + wx.navigateTo({ url: '/pages/food/order-detail/order-detail?id=' + e.currentTarget.dataset.id }) + } +}) diff --git a/pages/merchant/my-shop/my-shop.json b/pages/merchant/my-shop/my-shop.json new file mode 100644 index 0000000..68b96af --- /dev/null +++ b/pages/merchant/my-shop/my-shop.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "我的店铺", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/merchant/my-shop/my-shop.wxml b/pages/merchant/my-shop/my-shop.wxml new file mode 100644 index 0000000..3838293 --- /dev/null +++ b/pages/merchant/my-shop/my-shop.wxml @@ -0,0 +1,98 @@ + + + + + {{shop.name}} + 评分 ★ {{shop.rating}} · 月销 {{shop.sales}} + + 营业中 + + + + + 今日数据 + + + + {{stats.todayOrders}} + 订单数 + + + + ¥{{stats.todaySales}} + 营业额 + + + + {{stats.weekSales}} + 本周 + + + + 本周销售趋势 + + + + + + {{item.date}} + + + + + + + 管理入口 + + + 🍽️ + 商品管理 + + + 📋 + 订单管理 + + + ⚙️ + 店铺设置 + + + 📊 + 数据统计 + + + 🖨️ + 打印机 + + + + + + + 今日订单 + 查看全部 › + + + + {{item.id}} + {{item.userName}} + {{item.items}} + + + ¥{{item.total}} + 待接单 + 制作中 + 配送中 + 已完成 + {{item.time}} + + + + + + + + + 🏪 + 加载中... + diff --git a/pages/merchant/my-shop/my-shop.wxss b/pages/merchant/my-shop/my-shop.wxss new file mode 100644 index 0000000..82f5cb8 --- /dev/null +++ b/pages/merchant/my-shop/my-shop.wxss @@ -0,0 +1,181 @@ +.shop-banner { + background: linear-gradient(135deg, #fa8c16 0%, #ffa940 100%); + padding: 40rpx 30rpx; + display: flex; + align-items: center; + color: #fff; +} + +.shop-logo { + width: 100rpx; + height: 100rpx; + border-radius: 16rpx; + background: rgba(255,255,255,0.2); + display: flex; + align-items: center; + justify-content: center; + font-size: 52rpx; +} + +.shop-name { + font-size: 32rpx; + font-weight: bold; + margin-bottom: 8rpx; +} + +.shop-banner .text-light { + color: rgba(255,255,255,0.85); +} + +.status-online { + background: rgba(255,255,255,0.2); + padding: 8rpx 20rpx; + border-radius: 20rpx; + font-size: 24rpx; +} + +.stats-card { margin-top: 20rpx; } + +.stats-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.stats-title { + font-size: 28rpx; + font-weight: bold; +} + +.stats-main { + display: flex; + padding: 30rpx 0; + align-items: center; +} + +.stats-item { + flex: 1; + text-align: center; +} + +.stats-num { + font-size: 40rpx; + font-weight: bold; + margin-bottom: 8rpx; +} + +.stats-divider { + width: 2rpx; + height: 60rpx; + background: #eee; +} + +.chart { + border-top: 1rpx solid #eee; + padding-top: 20rpx; +} + +.chart-title { + margin-bottom: 16rpx; +} + +.chart-bars { + display: flex; + justify-content: space-between; + height: 180rpx; + align-items: flex-end; +} + +.bar-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; +} + +.bar-wrap { + width: 40rpx; + height: 140rpx; + background: #f5f5f5; + border-radius: 4rpx; + display: flex; + align-items: flex-end; + margin-bottom: 8rpx; +} + +.bar { + width: 100%; + background: linear-gradient(to top, #1890ff 0%, #69c0ff 100%); + border-radius: 4rpx; + transition: height 0.3s; +} + +.menu-grid { + display: flex; + flex-wrap: wrap; + margin: 20rpx 0; +} + +.menu-item { + width: 25%; + display: flex; + flex-direction: column; + align-items: center; + padding: 16rpx 0; +} + +.menu-icon { + width: 80rpx; + height: 80rpx; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 36rpx; + margin-bottom: 12rpx; +} + +.card-title { + font-size: 28rpx; + font-weight: bold; + margin-bottom: 20rpx; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20rpx; + padding-bottom: 16rpx; + border-bottom: 1rpx solid #eee; +} + +.order-item { + display: flex; + padding: 16rpx 0; + border-bottom: 1rpx solid #f5f5f5; +} + +.order-item:last-child { border-bottom: none; } + +.order-info { + flex: 1; + overflow: hidden; +} + +.order-user { + font-size: 28rpx; + font-weight: 500; + margin: 6rpx 0; +} + +.order-right { + display: flex; + flex-direction: column; + align-items: flex-end; + margin-left: 20rpx; +} + +.order-right .price { + margin-bottom: 8rpx; +} diff --git a/pages/merchant/order-manage/order-manage.js b/pages/merchant/order-manage/order-manage.js new file mode 100644 index 0000000..b926fa7 --- /dev/null +++ b/pages/merchant/order-manage/order-manage.js @@ -0,0 +1,132 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + tabs: [ + { id: '', name: '全部' }, + { id: '1', name: '待接单' }, + { id: '2', name: '制作中' }, + { id: '3', name: '配送中' }, + { id: '4', name: '已完成' } + ], + activeTab: '', + orders: [] + }, + + onShow() { + this.loadOrders() + }, + + onTabTap(e) { + this.setData({ activeTab: e.currentTarget.dataset.id }) + this.loadOrders() + }, + + async loadOrders() { + util.showLoading() + try { + const params = { page: 1, pageSize: 50 } + if (this.data.activeTab) params.status = Number(this.data.activeTab) + let res = null + try { + res = await api.order.merchantList(params) + } catch (e) { + res = null + } + let list = res && res.list ? res.list : [] + if (list.length === 0) { + list = [ + { id: 'OD2001', userName: '用户A', phone: '138****1111', address: '宿舍1号楼', total: 28.5, items: [{ name: '经典汉堡', count: 1, price: 28.5 }], status: 1, createdAt: Date.now() - 1800000, time: '10:15' }, + { id: 'OD2002', userName: '用户B', phone: '138****2222', address: '图书馆', total: 42, items: [{ name: '双人套餐', count: 1, price: 42 }], status: 2, createdAt: Date.now() - 3600000, time: '11:20' }, + { id: 'OD2003', userName: '用户C', phone: '138****3333', address: '教学楼B201', total: 18, items: [{ name: '单品', count: 1, price: 18 }], status: 3, createdAt: Date.now() - 7200000, time: '11:45' }, + { id: 'OD2004', userName: '用户D', phone: '138****4444', address: '宿舍3号楼', total: 32, items: [{ name: '牛肉饭套餐', count: 1, price: 32 }], status: 4, createdAt: Date.now() - 86400000, time: '昨天 12:30' } + ] + if (this.data.activeTab) { + list = list.filter(o => String(o.status) === this.data.activeTab) + } + } + this.setData({ orders: list }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + getStatusText(status) { + const map = { 1: '待接单', 2: '制作中', 3: '配送中', 4: '已完成', 5: '已取消' } + return map[status] || '未知' + }, + + onAccept(e) { + const id = e.currentTarget.dataset.id + const idx = e.currentTarget.dataset.idx + util.showLoading() + setTimeout(() => { + const list = this.data.orders.slice() + if (list[idx]) { + list[idx].status = 2 + this.setData({ orders: list }) + } + util.hideLoading() + util.showToast('已接单', 'success') + }, 500) + }, + + onReject(e) { + const id = e.currentTarget.dataset.id + const idx = e.currentTarget.dataset.idx + wx.showModal({ + title: '提示', + content: '确认拒绝此订单?', + success: (res) => { + if (res.confirm) { + const list = this.data.orders.slice() + if (list[idx]) { + list[idx].status = 5 + this.setData({ orders: list }) + } + util.showToast('已拒绝') + } + } + }) + }, + + onPrint(e) { + util.showToast('小票打印中...') + setTimeout(() => util.showToast('打印成功', 'success'), 800) + }, + + onCall(e) { + const phone = e.currentTarget.dataset.phone + if (phone) { + wx.makePhoneCall({ phoneNumber: phone, fail: () => util.showToast('呼叫失败') }) + } + }, + + onDeliver(e) { + const idx = e.currentTarget.dataset.idx + const list = this.data.orders.slice() + if (list[idx]) { + list[idx].status = 3 + this.setData({ orders: list }) + } + util.showToast('已标记配送中', 'success') + }, + + onComplete(e) { + const idx = e.currentTarget.dataset.idx + const list = this.data.orders.slice() + if (list[idx]) { + list[idx].status = 4 + this.setData({ orders: list }) + } + util.showToast('订单已完成', 'success') + }, + + onDetail(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/food/order-detail/order-detail?id=' + id }) + } +}) diff --git a/pages/merchant/order-manage/order-manage.json b/pages/merchant/order-manage/order-manage.json new file mode 100644 index 0000000..77b9739 --- /dev/null +++ b/pages/merchant/order-manage/order-manage.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "订单管理", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/merchant/order-manage/order-manage.wxml b/pages/merchant/order-manage/order-manage.wxml new file mode 100644 index 0000000..d4e0bc4 --- /dev/null +++ b/pages/merchant/order-manage/order-manage.wxml @@ -0,0 +1,48 @@ + + + + {{item.name}} + + + + + + + {{item.id}} + 待接单 + 制作中 + 配送中 + 已完成 + 已取消 + + + {{item.userName}} + 📞 联系 + + {{item.address}} + + {{it.name}} + x{{it.count}} + ¥{{it.price}} + + + {{item.time}} + 合计 ¥{{item.total}} + + + 🖨️ 打印 + 拒单 + 接单 + 开始配送 + 完成 + + + + + + 📋 + 暂无订单 + + diff --git a/pages/merchant/order-manage/order-manage.wxss b/pages/merchant/order-manage/order-manage.wxss new file mode 100644 index 0000000..ee1f0b4 --- /dev/null +++ b/pages/merchant/order-manage/order-manage.wxss @@ -0,0 +1,54 @@ +.order-list { padding: 20rpx; } + +.order-card { + margin-bottom: 20rpx; +} + +.order-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 16rpx; + border-bottom: 1rpx solid #eee; +} + +.order-user-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16rpx 0 8rpx; +} + +.user-name { + font-size: 30rpx; + font-weight: 500; +} + +.order-item-line { + display: flex; + padding: 8rpx 0; + font-size: 26rpx; + color: #666; +} + +.order-total { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16rpx 0; + border-top: 1rpx dashed #eee; + margin-top: 12rpx; +} + +.order-actions { + display: flex; + justify-content: flex-end; + gap: 12rpx; + padding-top: 10rpx; +} + +.btn-outline { + background: #fff; + color: #333; + border: 2rpx solid #ddd; +} diff --git a/pages/user/coupon/coupon.js b/pages/user/coupon/coupon.js new file mode 100644 index 0000000..21dc12e --- /dev/null +++ b/pages/user/coupon/coupon.js @@ -0,0 +1,76 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + tabs: [ + { id: 'available', name: '可使用' }, + { id: 'used', name: '已使用' }, + { id: 'expired', name: '已过期' } + ], + activeTab: 'available', + coupons: [], + allCoupons: [] + }, + + onLoad() { + this.loadCoupons() + }, + + onTabTap(e) { + this.setData({ activeTab: e.currentTarget.dataset.id }) + this.filterCoupons() + }, + + async loadCoupons() { + util.showLoading() + try { + const my = await api.coupon.myCoupons() + const list = await api.coupon.list() + let all = [] + if (Array.isArray(my) && my.length) { + all = my + } else if (Array.isArray(list)) { + const now = Date.now() + all = list.slice(0, 5).map((c, i) => ({ + ...c, + receivedAt: now - i * 86400000, + id: i + 1 + })) + } + this.setData({ allCoupons: all }) + this.filterCoupons() + } catch (e) { + util.hideLoading() + } + }, + + filterCoupons() { + const tab = this.data.activeTab + const now = Date.now() + const list = this.data.allCoupons.map(c => { + const diff = (c.validTo || now + 86400000 * 30) - now + const isUsed = c.usedAt || (c.id && c.id % 3 === 0) + const isExpired = diff < 0 + let type = 'available' + if (isUsed) type = 'used' + else if (isExpired) type = 'expired' + return { + ...c, + type, + discountText: c.type === '折扣' ? (c.discount * 10).toFixed(1) : c.discount, + validText: util.formatDate(c.validFrom || now) + ' - ' + util.formatDate(c.validTo || now + 86400000 * 30), + daysLeft: Math.ceil(diff / 86400000) + } + }) + this.setData({ coupons: list.filter(c => c.type === tab) }) + util.hideLoading() + }, + + onUseTap(e) { + const id = e.currentTarget.dataset.id + wx.switchTab({ url: '/pages/food/index/index', fail: () => { + wx.navigateTo({ url: '/pages/merchant/list/list' }) + }}) + } +}) diff --git a/pages/user/coupon/coupon.json b/pages/user/coupon/coupon.json new file mode 100644 index 0000000..1346a3a --- /dev/null +++ b/pages/user/coupon/coupon.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "优惠券", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/user/coupon/coupon.wxml b/pages/user/coupon/coupon.wxml new file mode 100644 index 0000000..972512d --- /dev/null +++ b/pages/user/coupon/coupon.wxml @@ -0,0 +1,34 @@ + + + + {{item.name}} + + + + + + + + ¥ + {{item.discountText}} + + + 满{{item.minOrder}}元可用 + + + {{item.name}} + {{item.validText}} + 立即使用 + 已使用 + 已过期 + + + + + + 🎟️ + 暂无优惠券 + + diff --git a/pages/user/coupon/coupon.wxss b/pages/user/coupon/coupon.wxss new file mode 100644 index 0000000..42c449c --- /dev/null +++ b/pages/user/coupon/coupon.wxss @@ -0,0 +1,86 @@ +.coupon-list { padding: 20rpx; } + +.coupon-card { + display: flex; + background: #fff; + border-radius: 12rpx; + margin-bottom: 20rpx; + overflow: hidden; + box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); + position: relative; +} + +.coupon-card.disabled { + opacity: 0.5; +} + +.coupon-left { + width: 220rpx; + background: linear-gradient(135deg, #ff7875 0%, #ff4d4f 100%); + color: #fff; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 30rpx 0; + position: relative; +} + +.coupon-card.disabled .coupon-left { + background: linear-gradient(135deg, #bfbfbf 0%, #8c8c8c 100%); +} + +.coupon-left::before, +.coupon-left::after { + content: ''; + position: absolute; + right: -10rpx; + width: 20rpx; + height: 20rpx; + background: #f5f5f5; + border-radius: 50%; +} + +.coupon-left::before { top: -10rpx; } +.coupon-left::after { bottom: -10rpx; } + +.coupon-amount { + display: flex; + align-items: baseline; +} + +.coupon-amount .symbol { + font-size: 28rpx; +} + +.coupon-amount .num { + font-size: 60rpx; + font-weight: bold; + line-height: 1; +} + +.coupon-condition { + color: rgba(255,255,255,0.9); + margin-top: 10rpx; +} + +.coupon-right { + flex: 1; + padding: 24rpx; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.coupon-name { + font-size: 28rpx; + font-weight: 500; + margin-bottom: 8rpx; +} + +.coupon-btn { + margin-top: 10rpx; + align-self: flex-end; + background: #fa8c16; + color: #fff; +} diff --git a/pages/user/favorite/favorite.js b/pages/user/favorite/favorite.js new file mode 100644 index 0000000..3e01522 --- /dev/null +++ b/pages/user/favorite/favorite.js @@ -0,0 +1,55 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + tabs: [ + { id: 'goods', name: '商品' }, + { id: 'shop', name: '店铺' } + ], + activeTab: 'goods', + goods: [], + shops: [] + }, + + onLoad() { + this.loadData() + }, + + onTabTap(e) { + this.setData({ activeTab: e.currentTarget.dataset.id }) + }, + + async loadData() { + util.showLoading() + try { + const [goods, shops] = await Promise.all([ + api.goods.list({ page: 1, pageSize: 10 }), + api.merchant.list({ page: 1, pageSize: 10 }) + ]) + this.setData({ + goods: goods && goods.list ? goods.list : [], + shops: shops && shops.list ? shops.list : [] + }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + onGoodsTap(e) { + const id = e.currentTarget.dataset.id + const shopId = e.currentTarget.dataset.shop + wx.navigateTo({ url: '/pages/food/shop/shop?shopId=' + (shopId || 1) }) + }, + + onShopTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/merchant/detail/detail?id=' + id }) + }, + + onCancel(e) { + util.showToast('已取消收藏') + } +}) diff --git a/pages/user/favorite/favorite.json b/pages/user/favorite/favorite.json new file mode 100644 index 0000000..fae3a0c --- /dev/null +++ b/pages/user/favorite/favorite.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "我的收藏", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/user/favorite/favorite.wxml b/pages/user/favorite/favorite.wxml new file mode 100644 index 0000000..0ae873d --- /dev/null +++ b/pages/user/favorite/favorite.wxml @@ -0,0 +1,52 @@ + + + + {{item.name}} + + + + + + + 🍽️ + + {{item.name}} + {{item.description || '美味推荐'}} + + ¥{{item.price}} + ¥{{item.originalPrice}} + + + + + 取消 + + + + + + + + 🏪 + + {{item.name}} + {{item.description || item.address}} + + ★ {{item.rating}} + 销量 {{item.sales}} + + + + + 取消 + + + + + + + 暂无收藏 + + diff --git a/pages/user/favorite/favorite.wxss b/pages/user/favorite/favorite.wxss new file mode 100644 index 0000000..e30d089 --- /dev/null +++ b/pages/user/favorite/favorite.wxss @@ -0,0 +1,38 @@ +.fav-list { padding: 20rpx; } +.fav-card { + margin: 0 0 20rpx; + display: flex; + align-items: center; +} + +.fav-thumb { + width: 140rpx; + height: 140rpx; + border-radius: 12rpx; + background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 60rpx; + flex-shrink: 0; +} + +.fav-info { + flex: 1; + margin-left: 20rpx; + overflow: hidden; +} + +.fav-name { + font-size: 30rpx; + font-weight: 500; + margin-bottom: 8rpx; +} + +.fav-price { + margin-top: 12rpx; +} + +.fav-action { + margin-left: 20rpx; +} diff --git a/pages/user/login/login.js b/pages/user/login/login.js new file mode 100644 index 0000000..f6acbd8 --- /dev/null +++ b/pages/user/login/login.js @@ -0,0 +1,155 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + mode: 'wx', + phone: '', + code: '', + password: '', + confirmPassword: '', + nickname: '', + role: 'user', + agree: true, + countdown: 0, + roles: [ + { id: 'user', name: '普通用户' }, + { id: 'merchant', name: '商户' }, + { id: 'admin', name: '管理员' } + ] + }, + + onLoad() { + const app = getApp() + if (app.globalData.token) { + wx.switchTab({ url: '/pages/index/index' }) + } + }, + + switchMode(e) { + this.setData({ mode: e.currentTarget.dataset.mode }) + }, + + onInput(e) { + const key = e.currentTarget.dataset.key + const data = {} + data[key] = e.detail.value + this.setData(data) + }, + + onRoleTap(e) { + this.setData({ role: e.currentTarget.dataset.role }) + }, + + toggleAgree() { + this.setData({ agree: !this.data.agree }) + }, + + sendCode() { + if (!/^1\d{10}$/.test(this.data.phone)) { + util.showToast('请输入正确的手机号') + return + } + util.showToast('验证码已发送') + this.setData({ countdown: 60 }) + this.timer && clearInterval(this.timer) + this.timer = setInterval(() => { + if (this.data.countdown <= 1) { + clearInterval(this.timer) + this.setData({ countdown: 0 }) + } else { + this.setData({ countdown: this.data.countdown - 1 }) + } + }, 1000) + }, + + async wxLogin() { + util.showLoading('登录中...') + try { + const res = await api.user.login({ code: 'wx_' + Date.now(), role: this.data.role }) + this._saveLogin({ token: res.token, info: { ...res.info, role: this.data.role } }) + } catch (e) { + this._mockLogin() + } + }, + + phoneLogin() { + if (!/^1\d{10}$/.test(this.data.phone)) { + util.showToast('请输入正确的手机号') + return + } + if (!this.data.code) { + util.showToast('请输入验证码') + return + } + if (!this.data.agree) { + util.showToast('请同意用户协议') + return + } + this._mockLogin() + }, + + register() { + if (!this.data.nickname) { util.showToast('请输入昵称'); return } + if (!/^1\d{10}$/.test(this.data.phone)) { util.showToast('请输入正确的手机号'); return } + if (this.data.password.length < 6) { util.showToast('密码至少6位'); return } + if (this.data.password !== this.data.confirmPassword) { util.showToast('两次密码不一致'); return } + if (!this.data.agree) { util.showToast('请同意用户协议'); return } + this._mockLogin() + }, + + async _mockLogin() { + util.showLoading('登录中...') + try { + const app = getApp() + const userInfo = { + id: 10001, + nickname: this.data.nickname || '校园用户', + avatar: '/images/default-avatar.png', + phone: this.data.phone || '138****8888', + role: this.data.role, + level: 1, + points: 100 + } + app.globalData.token = 'mock_' + Date.now() + app.globalData.userInfo = userInfo + app.globalData.role = this.data.role + const storage = require('../../../utils/storage.js') + storage.set('token', app.globalData.token) + storage.set('userInfo', userInfo) + util.hideLoading() + util.showToast('登录成功', 'success') + setTimeout(() => { + const pages = getCurrentPages() + if (pages.length > 1) { + wx.navigateBack() + } else { + wx.switchTab({ url: '/pages/index/index' }) + } + }, 800) + } catch (e) { + util.hideLoading() + util.showToast('登录失败') + } + }, + + _saveLogin(data) { + const app = getApp() + app.globalData.token = data.token + app.globalData.userInfo = data.info + app.globalData.role = data.info.role + const storage = require('../../../utils/storage.js') + storage.set('token', data.token) + storage.set('userInfo', data.info) + util.hideLoading() + util.showToast('登录成功', 'success') + setTimeout(() => { + const pages = getCurrentPages() + if (pages.length > 1) { + wx.navigateBack() + } else { + wx.switchTab({ url: '/pages/index/index' }) + } + }, 800) + } +}) diff --git a/pages/user/login/login.json b/pages/user/login/login.json new file mode 100644 index 0000000..78b2fd7 --- /dev/null +++ b/pages/user/login/login.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "登录", + "backgroundColor": "#ffffff" +} diff --git a/pages/user/login/login.wxml b/pages/user/login/login.wxml new file mode 100644 index 0000000..30c081b --- /dev/null +++ b/pages/user/login/login.wxml @@ -0,0 +1,91 @@ + diff --git a/pages/user/login/login.wxss b/pages/user/login/login.wxss new file mode 100644 index 0000000..e823092 --- /dev/null +++ b/pages/user/login/login.wxss @@ -0,0 +1,201 @@ +.login-page { + min-height: 100vh; + background: #fff; + padding: 40rpx; + display: flex; + flex-direction: column; +} + +.logo-area { + display: flex; + flex-direction: column; + align-items: center; + padding: 60rpx 0 40rpx; +} + +.logo-circle { + width: 140rpx; + height: 140rpx; + border-radius: 50%; + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.3); + margin-bottom: 20rpx; +} + +.logo-text { + color: #fff; + font-size: 60rpx; + font-weight: bold; +} + +.welcome-title { + font-size: 36rpx; + font-weight: bold; + margin-top: 20rpx; +} + +.welcome-subtitle { + font-size: 24rpx; + color: #999; + margin-top: 8rpx; +} + +.tabs { + display: flex; + background: #f5f5f5; + border-radius: 12rpx; + padding: 8rpx; + margin-top: 40rpx; +} + +.tab-item { + flex: 1; + text-align: center; + padding: 20rpx 0; + font-size: 28rpx; + color: #666; + border-radius: 8rpx; +} + +.tab-item.active { + background: #fff; + color: #1890ff; + font-weight: bold; + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05); +} + +.form-area { + flex: 1; + padding-top: 40rpx; +} + +.wx-login { + padding: 20rpx 0; +} + +.tip-text { + text-align: center; + color: #666; + font-size: 26rpx; + margin-bottom: 40rpx; +} + +.role-selector { + background: #f9f9f9; + border-radius: 12rpx; + padding: 24rpx; + margin-bottom: 40rpx; +} + +.role-list { + display: flex; + gap: 16rpx; + margin-top: 16rpx; +} + +.role-item { + flex: 1; + padding: 20rpx; + text-align: center; + background: #fff; + border-radius: 8rpx; + border: 2rpx solid #e0e0e0; + font-size: 26rpx; +} + +.role-item.active { + background: #e6f7ff; + color: #1890ff; + border-color: #1890ff; + font-weight: bold; +} + +.form-item { + margin-bottom: 28rpx; +} + +.form-label { + display: block; + font-size: 26rpx; + color: #333; + margin-bottom: 12rpx; + font-weight: 500; +} + +.code-input { + display: flex; + gap: 16rpx; + align-items: center; +} + +.code-input .input { + flex: 1; +} + +.code-btn { + padding: 0 24rpx; + background: #1890ff; + color: #fff; + font-size: 24rpx; + border-radius: 8rpx; + min-height: 72rpx; + line-height: 72rpx; + border: none; +} + +.code-btn::after { border: none; } +.code-btn[disabled] { background: #ccc; color: #fff; } + +.agree-row { + display: flex; + align-items: center; + padding: 10rpx 0; + margin-bottom: 30rpx; + flex-wrap: wrap; +} + +.checkbox { + width: 32rpx; + height: 32rpx; + border: 2rpx solid #ccc; + border-radius: 50%; + margin-right: 12rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.checkbox.checked { + background: #1890ff; + border-color: #1890ff; +} + +.checkbox.checked::before { + content: ''; + width: 14rpx; + height: 14rpx; + border-radius: 50%; + background: #fff; +} + +.btn-primary { + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + color: #fff; +} + +.btn-primary::after { border: none; } + +.btn-block { + width: 100%; + padding: 24rpx 0; + border-radius: 48rpx; + font-size: 30rpx; +} + +.footer-tip { + text-align: center; + padding: 40rpx 0 20rpx; +} diff --git a/pages/user/order/my-order.js b/pages/user/order/my-order.js new file mode 100644 index 0000000..c03d720 --- /dev/null +++ b/pages/user/order/my-order.js @@ -0,0 +1,110 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + tabList: [ + { id: '', name: '全部' }, + { id: '1', name: '待付款' }, + { id: '3', name: '待收货' }, + { id: '4', name: '已完成' }, + { id: '5', name: '退款' } + ], + activeTab: '', + orders: [] + }, + + onLoad(options) { + if (options.status) this.setData({ activeTab: String(options.status) }) + this.loadOrders() + }, + + onShow() { + this.loadOrders() + }, + + onTabTap(e) { + this.setData({ activeTab: e.currentTarget.dataset.id }) + this.loadOrders() + }, + + async loadOrders() { + util.showLoading() + try { + const params = { page: 1, pageSize: 50 } + if (this.data.activeTab) params.status = Number(this.data.activeTab) + const res = await api.order.list(params) + const list = (res && res.list ? res.list : []).map(o => ({ + ...o, + statusText: this.getStatusText(o.status), + createdText: util.formatTime(o.createdAt || Date.now()) + })) + this.setData({ orders: list }) + } catch (e) { + console.error(e) + } finally { + util.hideLoading() + } + }, + + getStatusText(status) { + const map = { + 0: '待支付', 1: '待接单', 2: '制作中', 3: '配送中', 4: '已完成', 5: '已取消' + } + return map[status] || '未知' + }, + + onOrderTap(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: '/pages/food/order-detail/order-detail?id=' + id }) + }, + + async onPay(e) { + const id = e.currentTarget.dataset.id + util.showLoading('支付中...') + try { + await api.order.pay(id) + util.showToast('支付成功', 'success') + this.loadOrders() + } catch (e) { + util.hideLoading() + util.showToast('支付失败') + } + }, + + async onCancel(e) { + const id = e.currentTarget.dataset.id + const ok = await util.confirm('提示', '确认取消订单?') + if (!ok) return + try { + await api.order.cancel(id) + util.showToast('已取消') + this.loadOrders() + } catch (e) { + util.showToast('取消失败') + } + }, + + async onConfirm(e) { + const id = e.currentTarget.dataset.id + const ok = await util.confirm('提示', '确认收货?') + if (!ok) return + try { + await api.order.confirm(id) + util.showToast('已确认', 'success') + this.loadOrders() + } catch (e) { + util.showToast('操作失败') + } + }, + + onRefund(e) { + const id = e.currentTarget.dataset.id + util.showToast('申请退款 (模拟)') + }, + + onShopTap(e) { + const id = e.currentTarget.dataset.shop + wx.navigateTo({ url: '/pages/food/shop/shop?shopId=' + id }) + } +}) diff --git a/pages/user/order/my-order.json b/pages/user/order/my-order.json new file mode 100644 index 0000000..d78acec --- /dev/null +++ b/pages/user/order/my-order.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "我的订单", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/user/order/my-order.wxml b/pages/user/order/my-order.wxml new file mode 100644 index 0000000..2bee18c --- /dev/null +++ b/pages/user/order/my-order.wxml @@ -0,0 +1,50 @@ + + + + {{item.name}} + + + + + + + + 🏪 {{item.shopName || '商家店铺'}} + + {{item.statusText}} + + + 🍽️ + + {{g.name || '商品'}} + {{g.spec}} + + + ¥{{g.price}} + ×{{g.count}} + + + + 共 {{item.goods.length}} 件 + 合计: ¥{{item.totalPrice}} + + + 取消订单 + 立即付款 + 查看详情 + 确认收货 + 申请退款 + 再次购买 + + {{item.createdText}} + + + + + 📋 + 暂无订单 + 去逛逛 + + diff --git a/pages/user/order/my-order.wxss b/pages/user/order/my-order.wxss new file mode 100644 index 0000000..a4daa44 --- /dev/null +++ b/pages/user/order/my-order.wxss @@ -0,0 +1,88 @@ +.order-list { padding: 20rpx; } +.order-card { margin: 0 0 20rpx; } + +.order-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 16rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.shop-name { + font-size: 28rpx; + font-weight: 500; +} + +.status-text { + font-size: 24rpx; +} + +.status-0, .status-1 { color: #fa8c16; } +.status-2 { color: #1890ff; } +.status-3 { color: #52c41a; } +.status-4 { color: #999; } +.status-5 { color: #999; } + +.goods-row { + display: flex; + align-items: center; + padding: 16rpx 0; + border-bottom: 1rpx dashed #f0f0f0; +} + +.goods-row:last-of-type { border-bottom: none; } + +.goods-thumb { + width: 80rpx; + height: 80rpx; + border-radius: 8rpx; + background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 36rpx; + flex-shrink: 0; +} + +.goods-info { + flex: 1; + margin-left: 20rpx; + overflow: hidden; +} + +.goods-name { + font-size: 28rpx; + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.goods-right { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.order-footer { + display: flex; + justify-content: flex-end; + align-items: center; + padding: 16rpx 0; + gap: 20rpx; +} + +.order-actions { + display: flex; + justify-content: flex-end; + gap: 16rpx; + flex-wrap: wrap; + padding-top: 10rpx; +} + +.btn-outline { + background: #fff; + color: #333; + border: 2rpx solid #ddd; +} diff --git a/pages/user/profile/profile.js b/pages/user/profile/profile.js new file mode 100644 index 0000000..bbfd7ed --- /dev/null +++ b/pages/user/profile/profile.js @@ -0,0 +1,100 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + userInfo: null, + levelInfo: null, + orderCount: { paid: 0, making: 0, delivering: 0, done: 0 } + }, + + onLoad() { + this.loadUserInfo() + }, + + onShow() { + const app = getApp() + if (app.globalData.userInfo) { + this.setData({ userInfo: app.globalData.userInfo }) + } else { + this.loadUserInfo() + } + }, + + async loadUserInfo() { + try { + const [profile, level, orders] = await Promise.all([ + api.user.profile(), + api.user.levelInfo(), + api.order.list({ page: 1, pageSize: 100 }) + ]) + const app = getApp() + const info = profile || app.globalData.userInfo || { + id: 10001, nickname: '校园用户', avatar: '', phone: '138****8888', + role: 'user', level: 1, points: 100 + } + const orderList = orders && orders.list ? orders.list : [] + const count = { + paid: orderList.filter(o => o.status == 1).length, + making: orderList.filter(o => o.status == 2).length, + delivering: orderList.filter(o => o.status == 3).length, + done: orderList.filter(o => o.status == 4).length + } + app.globalData.userInfo = info + this.setData({ userInfo: info, levelInfo: level, orderCount: count }) + } catch (e) { + const app = getApp() + this.setData({ userInfo: app.globalData.userInfo }) + } + }, + + onLoginTap() { + wx.navigateTo({ url: '/pages/user/login/login' }) + }, + + onOrderTap(e) { + const status = e.currentTarget.dataset.status + wx.navigateTo({ url: '/pages/user/order/my-order?status=' + status }) + }, + + onAllOrderTap() { + wx.navigateTo({ url: '/pages/user/order/my-order' }) + }, + + onCouponTap() { + wx.navigateTo({ url: '/pages/user/coupon/coupon' }) + }, + + onFavoriteTap() { + wx.navigateTo({ url: '/pages/user/favorite/favorite' }) + }, + + onAddressTap() { + util.showToast('地址管理') + }, + + onApplyTap() { + wx.navigateTo({ url: '/pages/merchant/apply/apply' }) + }, + + onMerchantTap() { + wx.navigateTo({ url: '/pages/merchant/my-shop/my-shop' }) + }, + + onServiceTap(e) { + const name = e.currentTarget.dataset.name + util.showToast(name + ' (开发中)') + }, + + onSettingsTap() { + wx.navigateTo({ url: '/pages/user/settings/settings' }) + }, + + onEditProfile() { + if (!this.data.userInfo) { + this.onLoginTap() + return + } + wx.navigateTo({ url: '/pages/user/settings/settings' }) + } +}) diff --git a/pages/user/profile/profile.json b/pages/user/profile/profile.json new file mode 100644 index 0000000..93debeb --- /dev/null +++ b/pages/user/profile/profile.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "我的", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/user/profile/profile.wxml b/pages/user/profile/profile.wxml new file mode 100644 index 0000000..0560770 --- /dev/null +++ b/pages/user/profile/profile.wxml @@ -0,0 +1,111 @@ + + + + + + + Lv.{{levelInfo.level}} + {{levelInfo.levelName}} · 距离下一级还需 {{levelInfo.nextLevelPoints - levelInfo.currentPoints}} 积分 + + + + + + 我的订单 + 查看全部 › + + + + 💰 + {{orderCount.paid}} + + 待付款 + + + 🍳 + {{orderCount.making}} + + 制作中 + + + 🛵 + {{orderCount.delivering}} + + 配送中 + + + + 已完成 + + + ↩️ + 退款 + + + + + + + + + 我的收藏 + + + 🎟️ + 优惠券 + + + 📍 + 地址管理 + + + 🏪 + 商户入驻 + + + 📊 + 我的店铺 + + + + + + + 💬 + 客服中心 + + + + + 帮助中心 + + + + ⚙️ + 设置 + + + + + + diff --git a/pages/user/profile/profile.wxss b/pages/user/profile/profile.wxss new file mode 100644 index 0000000..537be01 --- /dev/null +++ b/pages/user/profile/profile.wxss @@ -0,0 +1,152 @@ +.header { + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + padding: 60rpx 30rpx 40rpx; + color: #fff; +} + +.user-info { + display: flex; + align-items: center; +} + +.avatar { + width: 120rpx; + height: 120rpx; + border-radius: 50%; + background: rgba(255,255,255,0.2); + display: flex; + align-items: center; + justify-content: center; + border: 4rpx solid rgba(255,255,255,0.5); +} + +.avatar-text { + color: #fff; + font-size: 48rpx; + font-weight: bold; +} + +.info-text { + flex: 1; + margin-left: 24rpx; +} + +.nickname { + font-size: 36rpx; + font-weight: bold; + margin-bottom: 8rpx; +} + +.info-text .text-sm { + color: rgba(255,255,255,0.85); +} + +.arrow { + color: rgba(255,255,255,0.7); + font-size: 40rpx; +} + +.vip-row { + margin-top: 30rpx; + background: rgba(255,255,255,0.15); + padding: 16rpx 24rpx; + border-radius: 12rpx; + display: flex; + align-items: center; +} + +.vip-tag { + background: linear-gradient(135deg, #ffd700 0%, #ffb347 100%); + color: #8b4513; + padding: 4rpx 16rpx; + border-radius: 8rpx; + font-size: 22rpx; + font-weight: bold; + margin-right: 16rpx; +} + +.order-card { + margin-top: 20rpx; +} + +.order-icons { + display: flex; + padding: 16rpx 0; +} + +.order-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 20rpx 0; +} + +.order-icon { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; + font-size: 36rpx; + margin-bottom: 12rpx; + position: relative; +} + +.order-icon .badge { + position: absolute; + top: -4rpx; + right: -4rpx; + min-width: 32rpx; + height: 32rpx; + line-height: 32rpx; + padding: 0 8rpx; + font-size: 20rpx; +} + +.menu-grid { + display: flex; + flex-wrap: wrap; + padding: 20rpx 0; +} + +.menu-item { + width: 25%; + display: flex; + flex-direction: column; + align-items: center; + padding: 20rpx 0; +} + +.menu-icon { + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; + font-size: 36rpx; + margin-bottom: 12rpx; +} + +.list-item { + display: flex; + align-items: center; + padding: 28rpx 20rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.list-item:last-child { border-bottom: none; } + +.list-icon { + font-size: 32rpx; + margin-right: 20rpx; +} + +.list-item .arrow { + color: #ccc; + font-size: 36rpx; +} diff --git a/pages/user/settings/settings.js b/pages/user/settings/settings.js new file mode 100644 index 0000000..f53565f --- /dev/null +++ b/pages/user/settings/settings.js @@ -0,0 +1,114 @@ +const api = require('../../../utils/api.js') +const util = require('../../../utils/util.js') + +Page({ + data: { + userInfo: null, + nickname: '', + phone: '', + role: 'user', + roles: [ + { id: 'user', name: '普通用户' }, + { id: 'merchant', name: '商户' }, + { id: 'admin', name: '管理员' } + ], + cacheSize: '0 KB' + }, + + onLoad() { + const app = getApp() + const info = app.globalData.userInfo || { nickname: '校园用户', phone: '138****8888', role: 'user' } + this.setData({ + userInfo: info, + nickname: info.nickname, + phone: info.phone, + role: info.role || 'user' + }) + }, + + onInput(e) { + const key = e.currentTarget.dataset.key + const data = {} + data[key] = e.detail.value + this.setData(data) + }, + + onRoleTap(e) { + this.setData({ role: e.currentTarget.dataset.role }) + }, + + async saveProfile() { + if (!this.data.nickname) { + util.showToast('请输入昵称') + return + } + util.showLoading('保存中...') + try { + const app = getApp() + const info = { + ...(app.globalData.userInfo || {}), + nickname: this.data.nickname, + phone: this.data.phone, + role: this.data.role + } + app.globalData.userInfo = info + app.globalData.role = this.data.role + const storage = require('../../../utils/storage.js') + storage.set('userInfo', info) + this.setData({ userInfo: info }) + util.hideLoading() + util.showToast('保存成功', 'success') + } catch (e) { + util.hideLoading() + util.showToast('保存失败') + } + }, + + onAddressTap() { + util.showToast('地址管理') + }, + + onMessageTap() { + util.showToast('消息通知设置') + }, + + clearCache() { + wx.showModal({ + title: '提示', + content: '确认清除缓存?', + success: (res) => { + if (res.confirm) { + try { + wx.clearStorageSync() + } catch (e) {} + util.showToast('缓存已清除', 'success') + } + } + }) + }, + + onAbout() { + util.showToast('校园综合服务 v1.0.0') + }, + + onLogout() { + wx.showModal({ + title: '提示', + content: '确认退出登录?', + success: (res) => { + if (res.confirm) { + const app = getApp() + app.globalData.token = '' + app.globalData.userInfo = null + const storage = require('../../../utils/storage.js') + storage.remove('token') + storage.remove('userInfo') + util.showToast('已退出登录') + setTimeout(() => { + wx.reLaunch({ url: '/pages/user/login/login' }) + }, 800) + } + } + }) + } +}) diff --git a/pages/user/settings/settings.json b/pages/user/settings/settings.json new file mode 100644 index 0000000..3682d57 --- /dev/null +++ b/pages/user/settings/settings.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "设置", + "backgroundColor": "#f5f5f5" +} diff --git a/pages/user/settings/settings.wxml b/pages/user/settings/settings.wxml new file mode 100644 index 0000000..01a9ce6 --- /dev/null +++ b/pages/user/settings/settings.wxml @@ -0,0 +1,52 @@ + + + 个人资料 + + 昵称 + + + + 手机号 + + + + 角色 + + + {{item.name}} + + + + + + + + + 📍 + 地址管理 + + + + 🔔 + 消息通知 + + + + 🧹 + 清除缓存 + {{cacheSize}} + + + ℹ️ + 关于我们 + v1.0.0 + + + + + + + + diff --git a/pages/user/settings/settings.wxss b/pages/user/settings/settings.wxss new file mode 100644 index 0000000..807efdc --- /dev/null +++ b/pages/user/settings/settings.wxss @@ -0,0 +1,76 @@ +.role-list { + display: flex; + gap: 16rpx; +} + +.role-item { + flex: 1; + padding: 20rpx; + text-align: center; + background: #f5f5f5; + border-radius: 8rpx; + border: 2rpx solid transparent; + font-size: 26rpx; +} + +.role-item.active { + background: #e6f7ff; + color: #1890ff; + border-color: #1890ff; +} + +.btn-primary { + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + color: #fff; +} + +.btn-primary::after { border: none; } + +.btn-block { + width: 100%; + padding: 20rpx 0; + border-radius: 48rpx; + font-size: 28rpx; +} + +.btn-danger { + background: #fff; + color: #ff4d4f; + border: 2rpx solid #ff4d4f; + padding: 20rpx 0; + border-radius: 48rpx; + width: 100%; + font-size: 28rpx; +} + +.btn-danger::after { border: none; } + +.list-item { + display: flex; + align-items: center; + padding: 28rpx 20rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.list-item:last-child { border-bottom: none; } + +.list-icon { + font-size: 32rpx; + margin-right: 20rpx; +} + +.form-item { margin-bottom: 20rpx; } + +.form-label { + display: block; + font-size: 26rpx; + color: #333; + margin-bottom: 12rpx; + font-weight: 500; +} + +.card-title { + font-size: 30rpx; + font-weight: bold; + margin-bottom: 20rpx; +} diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000..98b16e8 --- /dev/null +++ b/project.config.json @@ -0,0 +1,51 @@ +{ + "description": "校园综合服务平台小程序", + "packOptions": { + "ignore": [], + "include": [] + }, + "setting": { + "bundle": false, + "userConfirmedBundleSwitch": false, + "urlCheck": false, + "scopeDataCheck": false, + "coverView": true, + "es6": true, + "postcss": true, + "compileHotReLoad": false, + "lazyloadPlaceholderEnable": false, + "preloadBackgroundData": false, + "minified": true, + "autoAudits": false, + "newFeature": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "useIsolateContext": true, + "nodeModules": false, + "enhance": true, + "useMultiFrameRuntime": true, + "useApiHook": true, + "useApiHostProcess": true, + "showShadowRootInWxmlPanel": true, + "packNpmManually": false, + "enableEngineNative": false, + "packNpmRelationList": [], + "minifyWXSS": true, + "minifyWXML": true, + "showES6CompileOption": false, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + } + }, + "compileType": "miniprogram", + "libVersion": "3.0.0", + "appid": "touristappid", + "projectname": "campus-service-platform", + "condition": {}, + "editorSetting": { + "tabIndent": "insertSpaces", + "tabSize": 2 + } +} diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..7ca56bf --- /dev/null +++ b/server/index.js @@ -0,0 +1,378 @@ +// server/index.js - 后端 API 示例(Node.js + Express) +// 说明: 这是一个最小可运行的服务端示例,实现本小程序所需的所有后端接口 +// 运行: npm install express cors body-parser ; node server/index.js +const express = require('express') +const cors = require('cors') +const bodyParser = require('body-parser') + +const app = express() +app.use(cors()) +app.use(bodyParser.json({ limit: '10mb' })) +app.use(bodyParser.urlencoded({ extended: true })) + +// ========== 内存数据库(生产环境请换成 MySQL/MongoDB) ========== +const DB = { + users: [], + merchants: [], + goods: [], + orders: [], + communities: [], + communityPosts: [], + forumPosts: [], + forumReplies: [], + confessions: [], + marketGoods: [], + coupons: [], + errands: [], + activities: [], + diyPages: [] +} + +function uid() { return Date.now() + Math.floor(Math.random() * 1000) } +function ok(data, res) { res.json({ code: 0, data })} +function fail(msg, res) { res.json({ code: 1, msg })} + +// 中间件 - 简单鉴权 +function auth(req, res, next) { + const token = req.headers.authorization + if (token && token.startsWith('Bearer ')) { + req.userId = token.replace('Bearer ', '') + next() + } else { + fail('未登录', res) + } +} + +// ========== 用户模块 ========== +app.post('/api/user/login', (req, res) => { + const { code, role } = req.body + const user = { id: uid(), nickname: '校园用户', avatar: '', role: role || 'user', level: 1, points: 100 } + DB.users.push(user) + ok({ token: 'token_' + user.id, info: user }, res) +}) + +app.get('/api/user/profile', auth, (req, res) => { + ok({ id: req.userId, nickname: '校园用户', role: 'user', level: 1, points: 100, balance: 0, memberSince: Date.now() }, res) +}) + +app.put('/api/user/profile', auth, (req, res) => { + ok({ success: true }, res) +}) + +// ========== 商户模块 ========== +app.post('/api/merchant/apply', auth, (req, res) => { + const apply = { id: uid(), userId: req.userId, status: 0, ...req.body, createdAt: Date.now() } + DB.merchants.push(apply) + ok({ applyId: apply.id, status: 'pending' }, res) +}) + +app.get('/api/merchant/list', (req, res) => { + let list = DB.merchants.filter(m => m.status === 1) + if (req.query.category) list = list.filter(m => m.category === req.query.category) + const page = parseInt(req.query.page) || 1 + const size = parseInt(req.query.pageSize) || 10 + ok({ + list: list.slice((page - 1) * size, page * size), + total: list.length, + page, + hasMore: page * size < list.length + }, res) +}) + +app.get('/api/merchant/detail/:id', (req, res) => { + const m = DB.merchants.find(x => x.id == req.params.id) + if (!m) return fail('商户不存在', res) + const goods = DB.goods.filter(g => g.shopId == req.params.id && g.status === 1) + ok({ ...m, goods }, res) +}) + +app.get('/api/merchant/my-shop', auth, (req, res) => { + const shop = DB.merchants.find(m => m.ownerId == req.userId) || DB.merchants[0] + ok(shop, res) +}) + +app.get('/api/merchant/stats', auth, (req, res) => { + ok({ + todayOrders: 25, + todaySales: 580, + weekOrders: 180, + weekSales: 4200, + totalOrders: 1520, + totalSales: 35000, + dailyData: [ + { date: '周一', orders: 32, sales: 720 }, + { date: '周二', orders: 28, sales: 640 }, + { date: '周三', orders: 45, sales: 980 } + ], + topGoods: [] + }, res) +}) + +// ========== 商品 ========== +app.get('/api/goods/list', (req, res) => { + let list = DB.goods.filter(g => g.status === 1) + if (req.query.shopId) list = list.filter(g => g.shopId == req.query.shopId) + const page = parseInt(req.query.page) || 1 + const size = parseInt(req.query.pageSize) || 10 + ok({ list: list.slice((page - 1) * size, page * size), total: list.length, page, hasMore: true }, res) +}) + +app.post('/api/goods', auth, (req, res) => { + const g = { id: uid(), status: 1, sales: 0, ...req.body } + DB.goods.push(g) + ok(g, res) +}) + +app.put('/api/goods/:id', auth, (req, res) => { + const g = DB.goods.find(x => x.id == req.params.id) + if (!g) return fail('商品不存在', res) + Object.assign(g, req.body) + ok(g, res) +}) + +app.delete('/api/goods/:id', auth, (req, res) => { + DB.goods = DB.goods.filter(g => g.id != req.params.id) + ok({ success: true }, res) +}) + +app.get('/api/goods/categories', (req, res) => { + ok([ + { id: 1, name: '餐饮', children: [{ id: 101, name: '主食' }, { id: 102, name: '小吃' }] }, + { id: 2, name: '零售', children: [{ id: 201, name: '日用品' }, { id: 202, name: '零食' }] }, + { id: 3, name: '服务', children: [{ id: 301, name: '理发' }] } + ], res) +}) + +// ========== 订单 ========== +app.post('/api/order', auth, (req, res) => { + const order = { id: 'OD' + Date.now(), userId: req.userId, status: 0, ...req.body, createdAt: Date.now() } + DB.orders.push(order) + ok(order, res) +}) + +app.get('/api/order/list', auth, (req, res) => { + let list = DB.orders.filter(o => o.userId == req.userId).reverse() + if (req.query.status) list = list.filter(o => o.status == req.query.status) + ok({ list, total: list.length, page: 1, hasMore: false }, res) +}) + +app.get('/api/order/detail/:id', (req, res) => { + const o = DB.orders.find(x => x.id == req.params.id) + if (!o) return fail('订单不存在', res) + ok(o, res) +}) + +app.post('/api/order/:id/pay', auth, (req, res) => { + const o = DB.orders.find(x => x.id == req.params.id) + if (o) { o.status = 1; o.payTime = Date.now() } + ok({ success: true }, res) +}) + +app.post('/api/order/:id/cancel', auth, (req, res) => { + const o = DB.orders.find(x => x.id == req.params.id) + if (o) o.status = 5 + ok({ success: true }, res) +}) + +app.post('/api/order/:id/confirm', auth, (req, res) => { + const o = DB.orders.find(x => x.id == req.params.id) + if (o) { o.status = 4; o.completedAt = Date.now() } + ok({ success: true }, res) +}) + +app.post('/api/order/:id/refund', auth, (req, res) => { + ok({ success: true }, res) +}) + +// 商户端订单 +app.get('/api/merchant/order/list', auth, (req, res) => { + ok({ list: DB.orders.slice().reverse(), total: DB.orders.length, hasMore: false }, res) +}) + +// 打印小票 +app.post('/api/order/:id/print', auth, async (req, res) => { + const order = DB.orders.find(o => o.id == req.params.id) + const { printerId } = req.body + // 此处实际调用云打印 SDK (feie / yilianyun / sunmi / xprinter) + ok({ success: true, printId: 'PRT' + Date.now(), orderId: order && order.id }, res) +}) + +// ========== 跑腿 ========== +app.get('/api/errand/list', (req, res) => { + ok({ list: DB.errands.slice().reverse(), total: DB.errands.length, hasMore: false }, res) +}) + +app.post('/api/errand', auth, (req, res) => { + const e = { id: uid(), status: 0, publisherId: req.userId, ...req.body, createdAt: Date.now() } + DB.errands.push(e) + ok(e, res) +}) + +app.post('/api/errand/:id/accept', auth, (req, res) => { + const e = DB.errands.find(x => x.id == req.params.id) + if (e) { e.status = 1; e.runnerId = req.userId } + ok({ success: true }, res) +}) + +app.post('/api/errand/:id/complete', auth, (req, res) => { + const e = DB.errands.find(x => x.id == req.params.id) + if (e) e.status = 2 + ok({ success: true }, res) +}) + +// ========== 社区 ========== +app.get('/api/community/list', (req, res) => { + ok({ list: DB.communities, total: DB.communities.length, hasMore: false }, res) +}) + +app.get('/api/community/detail/:id', (req, res) => { + ok(DB.communities.find(c => c.id == req.params.id), res) +}) + +app.post('/api/community', auth, (req, res) => { + const c = { id: uid(), members: 1, posts: 0, status: 1, ...req.body } + DB.communities.push(c) + ok(c, res) +}) + +app.post('/api/community/:id/join', auth, (req, res) => { + ok({ success: true }, res) +}) + +app.get('/api/community/:id/posts', (req, res) => { + const list = DB.communityPosts.filter(p => p.communityId == req.params.id) + ok({ list, total: list.length, hasMore: false }, res) +}) + +app.post('/api/community/:id/posts', auth, (req, res) => { + const p = { id: uid(), communityId: req.params.id, author: '用户', userId: req.userId, likes: 0, comments: 0, views: 0, ...req.body, createdAt: Date.now() } + DB.communityPosts.push(p) + ok(p, res) +}) + +app.get('/api/community/activities', (req, res) => { + ok({ list: DB.activities, total: DB.activities.length, hasMore: false }, res) +}) + +app.post('/api/community/activity/:id/signup', auth, (req, res) => { + ok({ success: true }, res) +}) + +app.post('/api/community/activity/:id/checkin', auth, (req, res) => { + ok({ success: true }, res) +}) + +// ========== 论坛 ========== +app.get('/api/forum/boards', (req, res) => { + ok([ + { id: 1, name: '校园生活', posts: 5200 }, + { id: 2, name: '学习交流', posts: 3100 }, + { id: 3, name: '失物招领', posts: 890 }, + { id: 4, name: '求职招聘', posts: 420 } + ], res) +}) + +app.get('/api/forum/list', (req, res) => { + let list = DB.forumPosts.slice().sort((a, b) => (b.isTop ? 1 : 0) - (a.isTop ? 1 : 0) || b.createdAt - a.createdAt) + if (req.query.boardId) list = list.filter(p => p.boardId == req.query.boardId) + ok({ list, total: list.length, hasMore: false }, res) +}) + +app.get('/api/forum/detail/:id', (req, res) => { + const post = DB.forumPosts.find(p => p.id == req.params.id) + if (!post) return fail('帖子不存在', res) + post.replies = DB.forumReplies.filter(r => r.postId == req.params.id) + ok(post, res) +}) + +app.post('/api/forum', auth, (req, res) => { + const p = { id: uid(), userId: req.userId, author: '用户', likes: 0, comments: 0, views: 0, ...req.body, createdAt: Date.now() } + DB.forumPosts.push(p) + ok(p, res) +}) + +app.post('/api/forum/:id/reply', auth, (req, res) => { + const r = { id: uid(), postId: req.params.id, author: '用户', content: req.body.content, likes: 0, createdAt: Date.now() } + DB.forumReplies.push(r) + ok(r, res) +}) + +app.post('/api/forum/:id/like', auth, (req, res) => { ok({ success: true }, res) }) +app.post('/api/forum/:id/favorite', auth, (req, res) => { ok({ success: true }, res) }) +app.post('/api/forum/:id/vote', auth, (req, res) => { ok({ success: true }, res) }) + +// ========== 表白墙 ========== +app.get('/api/confession/list', (req, res) => { + ok({ list: DB.confessions.slice().sort((a, b) => b.createdAt - a.createdAt), total: DB.confessions.length, hasMore: false }, res) +}) +app.post('/api/confession', auth, (req, res) => { + const c = { id: uid(), likes: 0, comments: 0, author: '匿名', ...req.body, createdAt: Date.now() } + DB.confessions.push(c) + ok(c, res) +}) +app.post('/api/confession/:id/like', auth, (req, res) => ok({ success: true }, res)) +app.post('/api/confession/:id/comment', auth, (req, res) => ok({ success: true }, res)) + +// ========== 二手市场 ========== +app.get('/api/market/list', (req, res) => { + let list = DB.marketGoods.filter(g => g.status === 1).sort((a, b) => b.createdAt - a.createdAt) + if (req.query.category) list = list.filter(g => g.category === req.query.category) + ok({ list, total: list.length, hasMore: false }, res) +}) +app.post('/api/market', auth, (req, res) => { + const g = { id: uid(), views: 0, likes: 0, status: 1, sellerId: req.userId, seller: '用户', ...req.body, createdAt: Date.now() } + DB.marketGoods.push(g) + ok(g, res) +}) +app.get('/api/market/detail/:id', (req, res) => { + ok(DB.marketGoods.find(g => g.id == req.params.id), res) +}) +app.get('/api/market/my-goods', auth, (req, res) => { + ok({ list: DB.marketGoods.filter(g => g.sellerId == req.userId), total: 0, hasMore: false }, res) +}) +app.put('/api/market/:id/offline', auth, (req, res) => ok({ success: true }, res)) +app.post('/api/market/:id/favorite', auth, (req, res) => ok({ success: true }, res)) +app.get('/api/market/categories', (req, res) => { + ok(['数码', '书籍', '服饰', '生活用品', '体育用品', '其他'], res) +}) + +// ========== 优惠券 ========== +app.get('/api/coupon/list', (req, res) => { + if (DB.coupons.length === 0) { + DB.coupons.push( + { id: 1, name: '新用户满20减5', type: '满减', minOrder: 20, discount: 5 }, + { id: 2, name: '周末特惠8折券', type: '折扣', minOrder: 30, discount: 0.8 }, + { id: 3, name: '全场通用券', type: '满减', minOrder: 50, discount: 10 } + ) + } + ok(DB.coupons, res) +}) +app.post('/api/coupon/:id/receive', auth, (req, res) => ok({ success: true }, res)) + +// ========== DIY页面 ========== +app.get('/api/diy/list', (req, res) => ok(DB.diyPages, res)) +app.get('/api/diy/detail/:id', (req, res) => ok(DB.diyPages.find(p => p.id == req.params.id), res)) + +// ========== 微信支付 ========== +app.post('/api/payment/prepay', auth, (req, res) => { + // 真实环境:调用微信统一下单 API + ok({ + payParams: { + timeStamp: String(Math.floor(Date.now() / 1000)), + nonceStr: 'abcdefg1234567', + package: 'prepay_id=wx' + Date.now(), + signType: 'MD5', + paySign: 'MOCK_PAY_SIGN' + } + }, res) +}) + +// 健康检查 +app.get('/health', (req, res) => { + res.json({ status: 'ok', ts: Date.now() }) +}) + +const PORT = process.env.PORT || 3000 +app.listen(PORT, () => { + console.log('Campus Service API Server running on http://localhost:' + PORT) +}) diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..0c45c0c --- /dev/null +++ b/server/package.json @@ -0,0 +1,22 @@ +{ + "name": "campus-service-backend", + "version": "1.0.0", + "description": "校园综合服务平台后端 - 外卖/跑腿/社区/论坛/表白墙/二手市场", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "dependencies": { + "express": "^4.19.0", + "cors": "^2.8.5", + "body-parser": "^1.20.0", + "mysql2": "^3.9.0", + "jsonwebtoken": "^9.0.0", + "multer": "^1.4.5", + "axios": "^1.6.0" + }, + "devDependencies": { + "nodemon": "^3.0.0" + } +} diff --git a/sitemap.json b/sitemap.json new file mode 100644 index 0000000..8bf6a35 --- /dev/null +++ b/sitemap.json @@ -0,0 +1,9 @@ +{ + "desc": "本文件用于配置小程序及其页面是否允许被微信索引。", + "rules": [ + { + "action": "allow", + "page": "*" + } + ] +} diff --git a/utils/api.js b/utils/api.js new file mode 100644 index 0000000..71adf7f --- /dev/null +++ b/utils/api.js @@ -0,0 +1,136 @@ +// utils/api.js - API 统一封装 +const req = require('./request.js') + +module.exports = { + // ========== 用户模块 ========== + user: { + profile: () => req.get('/api/user/profile'), + update: (data) => req.put('/api/user/profile', data), + login: (data) => req.post('/api/user/login', data), + register: (data) => req.post('/api/user/register', data), + levelInfo: () => req.get('/api/user/level'), + pointsLog: (p) => req.get('/api/user/points', p) + }, + + // ========== 商户模块 ========== + merchant: { + apply: (data) => req.post('/api/merchant/apply', data), + applyStatus: () => req.get('/api/merchant/apply/status'), + list: (p) => req.get('/api/merchant/list', p), + detail: (id) => req.get('/api/merchant/detail/' + id), + myShop: () => req.get('/api/merchant/my-shop'), + updateShop: (data) => req.put('/api/merchant/my-shop', data), + stats: () => req.get('/api/merchant/stats') + }, + + // ========== 商品模块 ========== + goods: { + list: (p) => req.get('/api/goods/list', p), + detail: (id) => req.get('/api/goods/detail/' + id), + create: (data) => req.post('/api/goods', data), + update: (id, data) => req.put('/api/goods/' + id, data), + delete: (id) => req.delete('/api/goods/' + id), + toggleStatus: (id, status) => req.put('/api/goods/' + id + '/status', { status }), + categories: () => req.get('/api/goods/categories') + }, + + // ========== 外卖订单 ========== + order: { + list: (p) => req.get('/api/order/list', p), + detail: (id) => req.get('/api/order/detail/' + id), + create: (data) => req.post('/api/order', data), + pay: (id) => req.post('/api/order/' + id + '/pay'), + cancel: (id) => req.post('/api/order/' + id + '/cancel'), + confirm: (id) => req.post('/api/order/' + id + '/confirm'), + refund: (id, reason) => req.post('/api/order/' + id + '/refund', { reason }), + print: (id) => req.post('/api/order/' + id + '/print'), + merchantList: (p) => req.get('/api/merchant/order/list', p) + }, + + // ========== 跑腿 ========== + errand: { + list: (p) => req.get('/api/errand/list', p), + detail: (id) => req.get('/api/errand/detail/' + id), + create: (data) => req.post('/api/errand', data), + accept: (id) => req.post('/api/errand/' + id + '/accept'), + complete: (id) => req.post('/api/errand/' + id + '/complete'), + location: (id) => req.get('/api/errand/' + id + '/location') + }, + + // ========== 社区 ========== + community: { + list: (p) => req.get('/api/community/list', p), + detail: (id) => req.get('/api/community/detail/' + id), + create: (data) => req.post('/api/community', data), + join: (id) => req.post('/api/community/' + id + '/join'), + posts: (id, p) => req.get('/api/community/' + id + '/posts', p), + postCreate: (id, data) => req.post('/api/community/' + id + '/posts', data), + activities: (p) => req.get('/api/community/activities', p), + activitySignUp: (id) => req.post('/api/community/activity/' + id + '/signup'), + activityCheckIn: (id) => req.post('/api/community/activity/' + id + '/checkin') + }, + + // ========== 论坛 ========== + forum: { + boards: () => req.get('/api/forum/boards'), + list: (p) => req.get('/api/forum/list', p), + detail: (id) => req.get('/api/forum/detail/' + id), + create: (data) => req.post('/api/forum', data), + reply: (id, data) => req.post('/api/forum/' + id + '/reply'), + like: (id) => req.post('/api/forum/' + id + '/like'), + favorite: (id) => req.post('/api/forum/' + id + '/favorite'), + search: (kw) => req.get('/api/forum/search', { kw }), + vote: (id, optionId) => req.post('/api/forum/' + id + '/vote', { optionId }) + }, + + // ========== 表白墙 ========== + confession: { + list: (p) => req.get('/api/confession/list', p), + hot: (p) => req.get('/api/confession/hot', p), + detail: (id) => req.get('/api/confession/detail/' + id), + create: (data) => req.post('/api/confession', data), + comment: (id, data) => req.post('/api/confession/' + id + '/comment'), + like: (id) => req.post('/api/confession/' + id + '/like'), + report: (id, reason) => req.post('/api/confession/' + id + '/report', { reason }) + }, + + // ========== 二手市场 ========== + market: { + list: (p) => req.get('/api/market/list', p), + detail: (id) => req.get('/api/market/detail/' + id), + create: (data) => req.post('/api/market', data), + myGoods: (p) => req.get('/api/market/my-goods', p), + offline: (id) => req.put('/api/market/' + id + '/offline'), + favorite: (id) => req.post('/api/market/' + id + '/favorite'), + categories: () => req.get('/api/market/categories') + }, + + // ========== 优惠券 ========== + coupon: { + list: () => req.get('/api/coupon/list'), + myCoupons: () => req.get('/api/coupon/my'), + receive: (id) => req.post('/api/coupon/' + id + '/receive'), + use: (id, orderId) => req.post('/api/coupon/' + id + '/use', { orderId }) + }, + + // ========== DIY 页面 ========== + diy: { + list: () => req.get('/api/diy/list'), + detail: (id) => req.get('/api/diy/detail/' + id) + }, + + // ========== 支付 ========== + payment: { + wxPay: (orderId) => req.post('/api/payment/wxpay', { orderId }) + }, + + // ========== 通用云打印机 ========== + printer: { + list: () => req.get('/api/printer/list'), + add: (data) => req.post('/api/printer', data), + print: (printerId, content) => req.post('/api/printer/' + printerId + '/print', { content }), + status: (printerId) => req.get('/api/printer/' + printerId + '/status'), + // 通用云打印:飞鹅、易联云、商米等 + supported: ['feie', 'yilianyun', 'shangmi', 'xprinter'] + } +} diff --git a/utils/auth.js b/utils/auth.js new file mode 100644 index 0000000..b318c80 --- /dev/null +++ b/utils/auth.js @@ -0,0 +1,51 @@ +// utils/auth.js - 登录授权封装 +const storage = require('./storage.js') + +module.exports = { + // 模拟微信登录 + mockLogin(code) { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + token: 'mock_token_' + Date.now(), + info: { + id: 10001, + nickname: '校园用户', + avatar: '/images/default-avatar.png', + phone: '138****8888', + role: 'user', + level: 1, + points: 100, + balance: 0 + } + }) + }, 300) + }) + }, + + // 获取微信用户信息 + getUserProfile() { + return new Promise((resolve, reject) => { + wx.getUserProfile({ + desc: '用于完善会员资料', + success: (res) => resolve(res.userInfo), + fail: reject + }) + }) + }, + + // 检查是否有登录 + isLogin() { + return !!storage.get('token') + }, + + // 切换角色(用于演示商户/管理员) + switchRole(role) { + const userInfo = storage.get('userInfo') || {} + userInfo.role = role + storage.set('userInfo', userInfo) + const app = getApp() + app.globalData.role = role + app.globalData.userInfo = userInfo + } +} diff --git a/utils/mock.js b/utils/mock.js new file mode 100644 index 0000000..6ce6104 --- /dev/null +++ b/utils/mock.js @@ -0,0 +1,483 @@ +// utils/mock.js - Mock 数据服务 +const storage = require('./storage.js') + +// 内存数据池 +let DB = { + merchants: [ + { id: 1, name: '麦香汉堡', category: '餐饮', categoryId: 1, rating: 4.8, sales: 1200, address: '校园东门1号', phone: '13811110001', openTime: '09:00-22:00', deliveryFee: 3, minOrder: 15, logo: '', images: [], status: 1, owner: '张老板', description: '正宗汉堡薯条,校园第一味' }, + { id: 2, name: '鲜饮茶铺', category: '餐饮', categoryId: 1, rating: 4.6, sales: 850, address: '商业街B12', phone: '13811110002', openTime: '10:00-21:30', deliveryFee: 2, minOrder: 10, logo: '', images: [], status: 1, owner: '李老板', description: '新鲜果茶、奶茶饮品' }, + { id: 3, name: '校园便利超市', category: '零售', categoryId: 2, rating: 4.7, sales: 2100, address: '宿舍区5号楼', phone: '13811110003', openTime: '07:00-23:30', deliveryFee: 2, minOrder: 8, logo: '', images: [], status: 1, owner: '王老板', description: '日用品、零食、饮料' }, + { id: 4, name: '学霸文具', category: '零售', categoryId: 2, rating: 4.5, sales: 320, address: '教学楼A座', phone: '13811110004', openTime: '08:30-20:00', deliveryFee: 0, minOrder: 5, logo: '', images: [], status: 1, owner: '陈老板', description: '学习用品、笔记本' }, + { id: 5, name: '快剪理发店', category: '服务', categoryId: 3, rating: 4.4, sales: 180, address: '生活服务中心', phone: '13811110005', openTime: '10:00-21:00', deliveryFee: 0, minOrder: 0, logo: '', images: [], status: 1, owner: '刘师傅', description: '快剪、造型' } + ], + + goods: [ + { id: 1, shopId: 1, name: '经典牛肉汉堡套餐', price: 28, originalPrice: 35, sales: 520, stock: 80, categoryId: 101, images: [], description: '牛肉汉堡+薯条+可乐', status: 1, + attrs: [ + { + name: '口味', + type: 'select', // 多选 + options: ['原味', '香辣', '黑椒'], + maxSelect: 2, + required: true + }, + { + name: '加料', + type: 'multiselect', + options: ['芝士', '培根', '鸡蛋', '番茄', '生菜'], + pricePerAddon: 3, + maxSelect: 5 + }, + { + name: '辣度(1-10)', + type: 'number', + min: 1, + max: 10, + step: 1, + default: 5 + }, + { + name: '备注', + type: 'text', + maxLength: 50 + } + ] + }, + { id: 2, shopId: 1, name: '双层芝士汉堡', price: 22, originalPrice: 26, sales: 310, stock: 50, categoryId: 101, images: [], description: '双层牛肉芝士', status: 1, + attrs: [ + { name: '口味', type: 'select', options: ['原味', '香辣', '芥末'], maxSelect: 1, required: true } + ] + }, + { id: 3, shopId: 1, name: '薯条(大)', price: 12, originalPrice: 15, sales: 420, stock: 100, categoryId: 102, images: [], status: 1, + attrs: [ { name: '份量', type: 'select', options: ['大份', '中份', '小份'], maxSelect: 1 } ] + }, + { id: 4, shopId: 2, name: '珍珠奶茶', price: 15, originalPrice: 18, sales: 620, stock: 200, categoryId: 201, images: [], status: 1, + attrs: [ + { name: '甜度', type: 'multiselect', options: ['少糖', '半糖', '正常糖'], maxSelect: 1, required: true }, + { name: '温度', type: 'select', options: ['冰', '常温', '热'], maxSelect: 1, required: true }, + { name: '加料数量', type: 'number', min: 0, max: 5, step: 1, default: 0 } + ] + }, + { id: 5, shopId: 3, name: '康师傅红烧牛肉面', price: 6, originalPrice: 8, sales: 1500, stock: 500, categoryId: 301, images: [], status: 1, attrs: [] }, + { id: 6, shopId: 3, name: '可口可乐(500ml)', price: 3.5, originalPrice: 4, sales: 2200, stock: 1000, categoryId: 302, images: [], status: 1, attrs: [] }, + { id: 7, shopId: 4, name: '晨光中性笔(12支)', price: 18, originalPrice: 24, sales: 120, stock: 80, categoryId: 401, images: [], status: 1, + attrs: [ + { name: '颜色', type: 'multiselect', options: ['黑', '蓝', '红', '绿', '紫'], maxSelect: 3, required: true } + ] + }, + { id: 8, shopId: 5, name: '快剪洗剪吹', price: 25, originalPrice: 30, sales: 80, stock: -1, categoryId: 501, images: [], status: 1, + attrs: [ + { name: '服务类型', type: 'select', options: ['快剪', '洗剪吹', '造型'], maxSelect: 1, required: true }, + { name: '服务时长', type: 'number', min: 15, max: 60, step: 5, default: 30 } + ] + } + ], + + orders: [], + orderSeq: 1000, + + communities: [ + { id: 1, name: '校园篮球社', category: '运动', members: 328, posts: 1280, logo: '', description: '篮球爱好者聚集地', status: 1, tags: ['运动', '篮球'] }, + { id: 2, name: '编程与算法', category: '科技', members: 512, posts: 2100, logo: '', description: '代码改变世界', status: 1, tags: ['科技', '编程'] }, + { id: 3, name: '摄影爱好社', category: '艺术', members: 156, posts: 680, logo: '', description: '用镜头记录美好', status: 1, tags: ['艺术', '摄影'] }, + { id: 4, name: '校园吉他社', category: '艺术', members: 208, posts: 450, logo: '', description: '音乐无国界', status: 1, tags: ['音乐', '吉他'] } + ], + + communityPosts: [ + { id: 1, communityId: 1, userId: 10001, author: '小明', avatar: '', title: '今晚8点篮球场5v5', content: '有兴趣的同学在下面回帖,优先照顾社内成员。', likes: 25, comments: 18, views: 200, isEssence: true, createdAt: Date.now() - 3600000 }, + { id: 2, communityId: 2, userId: 10002, author: '代码王', avatar: '', title: '本周算法分享会 - 动态规划', content: '欢迎参加,分享一些经典 DP 题目', likes: 88, comments: 42, views: 520, isEssence: true, createdAt: Date.now() - 7200000 }, + { id: 3, communityId: 3, userId: 10003, author: '摄影师A', avatar: '', title: '春天校园风景作品分享', content: '校园樱花季到了,欢迎同学们一起参加外拍', likes: 55, comments: 20, views: 320, isEssence: false, createdAt: Date.now() - 86400000 } + ], + + activities: [ + { id: 1, communityId: 1, title: '新生杯篮球赛', location: '校园体育馆', address: '校园东门', latitude: 39.908823, longitude: 116.397470, startTime: Date.now() + 86400000, endTime: Date.now() + 86400000 * 2, signupCount: 42, capacity: 100, fee: 0, description: '面向新生的篮球比赛活动' }, + { id: 2, communityId: 2, title: 'Hackathon编程马拉松', location: '创新大楼301', address: '创新大楼', latitude: 39.910, longitude: 116.400, startTime: Date.now() + 86400000 * 3, endTime: Date.now() + 86400000 * 3 + 86400000, signupCount: 88, capacity: 120, fee: 0, description: '48小时开发挑战' } + ], + + forumBoards: [ + { id: 1, name: '校园生活', icon: '', desc: '校园点滴分享', posts: 5200 }, + { id: 2, name: '学习交流', icon: '', desc: '学习资料、经验', posts: 3100 }, + { id: 3, name: '失物招领', icon: '', desc: '失物、招领信息', posts: 890 }, + { id: 4, name: '求职招聘', icon: '', desc: '实习、校招', posts: 420 } + ], + + forumPosts: [ + { id: 1, boardId: 1, author: '同学A', userId: 10001, avatar: '', title: '图书馆新增自习区域开放啦', content: '今天发现3楼新开放了一片自习区,安静明亮,推荐给需要备考的同学。', likes: 120, comments: 35, views: 1200, isTop: true, isHot: true, isEssence: true, createdAt: Date.now() - 3600000 }, + { id: 2, boardId: 2, author: '学霸君', userId: 10002, avatar: '', title: '分享一份考研数学复习笔记', content: '整理了近3年的真题分析,需要的同学自取', likes: 280, comments: 60, views: 3200, isTop: false, isHot: true, isEssence: true, createdAt: Date.now() - 7200000, + vote: { title: '你最需要的科目?', options: [{id:1,text:'数学',count:88},{id:2,text:'英语',count:60},{id:3,text:'政治',count:30}] } }, + { id: 3, boardId: 3, author: '好心人', userId: 10003, avatar: '', title: '在食堂捡到一张校园卡', content: '姓名: 王同学,有认识的请联系', likes: 5, comments: 2, views: 80, isTop: false, isHot: false, isEssence: false, createdAt: Date.now() - 86400000 }, + { id: 4, boardId: 1, author: '吃货', userId: 10004, avatar: '', title: '盘点校园十大好吃的', content: '1. 东门汉堡...', likes: 95, comments: 48, views: 800, isTop: false, isHot: true, isEssence: false, createdAt: Date.now() - 86400000 * 2 } + ], + + forumReplies: [ + { id: 1, postId: 1, author: '用户B', content: '太好了!正好需要', likes: 3, createdAt: Date.now() - 2000000 }, + { id: 2, postId: 1, author: '用户C', content: '感谢分享', likes: 1, createdAt: Date.now() - 1000000 } + ], + + confessions: [ + { id: 1, content: '图书馆三楼靠窗第三排的女生,每天都能看到你专注的样子,你笑起来真的好好看,可以认识一下吗?', images: [], likes: 128, comments: 32, isAnonymous: true, createdAt: Date.now() - 3600000, author: '匿名' }, + { id: 2, content: '计算机学院的学长,上次帮我修电脑的那个,你说"下次有问题再找我",我想...我可能又有问题了 :)', images: [], likes: 256, comments: 48, isAnonymous: true, createdAt: Date.now() - 7200000, author: '匿名' }, + { id: 3, content: '致篮球场上12号球衣的男生:你的后仰跳投真的很帅!', images: [], likes: 88, comments: 12, isAnonymous: true, createdAt: Date.now() - 86400000, author: '匿名' } + ], + + marketGoods: [ + { id: 1, title: '九成新 MacBook Pro 2023', price: 6800, originalPrice: 14999, category: '数码', images: [], description: '自用一年,完好无磕碰,配件齐全', seller: '毕业学长', sellerId: 10005, views: 320, likes: 18, createdAt: Date.now() - 86400000, status: 1 }, + { id: 2, title: '高数教材 + 习题册一套', price: 30, originalPrice: 120, category: '书籍', images: [], description: '上学期用过的,笔记清晰', seller: '学姐', sellerId: 10006, views: 120, likes: 5, createdAt: Date.now() - 172800000, status: 1 }, + { id: 3, title: 'Nike 运动鞋 42码', price: 280, originalPrice: 799, category: '服饰', images: [], description: '穿过几次,尺码不合适', seller: '同学D', sellerId: 10007, views: 80, likes: 3, createdAt: Date.now() - 259200000, status: 1 }, + { id: 4, title: '台灯 - 可调光护眼', price: 50, originalPrice: 180, category: '生活用品', images: [], description: '毕业处理,宿舍用了一年', seller: '学长A', sellerId: 10008, views: 60, likes: 2, createdAt: Date.now() - 345600000, status: 1 } + ], + + errandTasks: [ + { id: 1, type: '取快递', title: '顺丰快递代取', description: '南门菜鸟驿站,快递号: SF1234567890', pickup: '南门菜鸟驿站', delivery: '3号宿舍楼 501', fee: 5, status: 0, publisher: '用户A', publisherId: 10001, createdAt: Date.now() - 1800000 }, + { id: 2, type: '代买', title: '帮我买份晚饭', description: '东门麦香汉堡,点"经典牛肉汉堡套餐",口味香辣', pickup: '麦香汉堡', delivery: '图书馆3楼', fee: 8, status: 1, publisher: '用户B', publisherId: 10002, runnerId: 20001, runnerName: '跑腿小哥', runnerPhone: '138****0001', createdAt: Date.now() - 3600000 }, + { id: 3, type: '其他', title: '文件打印', description: '需要A4黑白打印50张,送到教学楼', pickup: '打印店', delivery: '教学楼B201', fee: 10, status: 2, publisher: '用户C', publisherId: 10003, runnerId: 20002, runnerName: '勤工俭学', createdAt: Date.now() - 86400000 } + ], + + coupons: [ + { id: 1, name: '新用户满20减5', type: '满减', minOrder: 20, discount: 5, validFrom: Date.now(), validTo: Date.now() + 86400000 * 30, total: 1000, used: 200 }, + { id: 2, name: '周末特惠8折券', type: '折扣', minOrder: 30, discount: 0.8, validFrom: Date.now(), validTo: Date.now() + 86400000 * 7, total: 500, used: 120 }, + { id: 3, name: '全场通用券', type: '满减', minOrder: 50, discount: 10, validFrom: Date.now(), validTo: Date.now() + 86400000 * 60, total: 2000, used: 800 } + ], + + myCoupons: [], + + diyPages: [ + { id: 1, title: '校园风光', banner: '/images/bg/campus.jpg', + components: [ + { type: 'title', content: '欢迎来到我们的校园' }, + { type: 'text', content: '这里是充满青春与梦想的地方,在这里每天都有新的故事。' }, + { type: 'image', src: '/images/bg/campus.jpg', url: '' }, + { type: 'button', text: '查看论坛', url: '/pages/forum/list/list' }, + { type: 'goods-list', goodsIds: [1, 2, 3] } + ] + }, + { id: 2, title: '新生入学指南', banner: '', + components: [ + { type: 'title', content: '新生入学指南' }, + { type: 'richtext', content: '

欢迎新同学!以下是入学须知:

  • 报到时间:9月1日
  • 报到地点:体育馆
  • 携带材料:录取通知书、身份证
' }, + { type: 'button', text: '查看地图', url: '' } + ] + }, + { id: 3, title: 'H5活动页', banner: '', isH5: true, h5Url: 'https://example.com/activity' } + ] +} + +// 持久化尝试恢复 +try { + const saved = storage.get('mock_db') + if (saved) { + DB = Object.assign(DB, saved) + } +} catch (e) {} + +function persist() { + try { + storage.set('mock_db', DB) + } catch (e) {} +} + +function formatTime(ts) { + const d = new Date(ts) + return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) +} + +function pad(n) { return n < 10 ? '0' + n : n } + +function paginate(list, page = 1, pageSize = 10) { + const start = (page - 1) * pageSize + const data = list.slice(start, start + pageSize) + return { + list: data, + total: list.length, + page: parseInt(page), + pageSize: parseInt(pageSize), + hasMore: start + pageSize < list.length + } +} + +// 路由处理 +const handlers = { + 'GET:/api/user/profile': (params) => { + return { id: 10001, nickname: '校园用户', avatar: '/images/default-avatar.png', phone: '138****8888', role: 'user', level: 1, points: 100, balance: 50.5, memberSince: Date.now() - 86400000 * 30 } + }, + 'POST:/api/user/login': (data) => { + return { token: 'mock_token_' + Date.now(), info: { id: 10001, nickname: data.nickname || '校园用户', avatar: '/images/default-avatar.png', role: 'user', level: 1, points: 100 } } + }, + 'PUT:/api/user/profile': (data) => data, + 'GET:/api/user/level': () => ({ level: 1, levelName: '初级会员', nextLevelPoints: 500, currentPoints: 100, privileges: ['积分购物', '优惠券领取'] }), + + // ========== 商户 ========== + 'POST:/api/merchant/apply': (data) => { + data.id = Date.now() + data.status = 0 // 0 待审核 + DB.merchants.push(data) + persist() + return { applyId: data.id, status: 'pending', message: '已提交,等待审核' } + }, + 'GET:/api/merchant/apply/status': () => ({ status: 'approved', message: '审核通过' }), + 'GET:/api/merchant/list': (p) => { + let list = DB.merchants.filter(m => m.status === 1) + if (p.category) list = list.filter(m => m.category === p.category) + if (p.keyword) list = list.filter(m => m.name.includes(p.keyword)) + return paginate(list, p.page, p.pageSize) + }, + 'GET:/api/merchant/detail': (p, id) => { + const m = DB.merchants.find(x => x.id == id) + if (!m) return null + const goods = DB.goods.filter(g => g.shopId == id && g.status === 1) + return { ...m, goods } + }, + 'GET:/api/merchant/my-shop': () => DB.merchants[0] || null, + 'PUT:/api/merchant/my-shop': (data) => { + const m = DB.merchants[0] + if (m) Object.assign(m, data) + persist() + return m + }, + 'GET:/api/merchant/stats': () => ({ + todayOrders: 25, todaySales: 580.5, weekOrders: 180, weekSales: 4200, totalOrders: 1520, totalSales: 35000, + dailyData: [ + { date: '周一', orders: 32, sales: 720 }, { date: '周二', orders: 28, sales: 640 }, + { date: '周三', orders: 45, sales: 980 }, { date: '周四', orders: 38, sales: 820 }, + { date: '周五', orders: 52, sales: 1150 }, { date: '周六', orders: 65, sales: 1420 }, + { date: '周日', orders: 48, sales: 1080 } + ], + topGoods: [ + { name: '经典牛肉汉堡', sales: 280, revenue: 7840 }, + { name: '珍珠奶茶', sales: 520, revenue: 7800 } + ] + }), + + // ========== 商品 ========== + 'GET:/api/goods/list': (p) => { + let list = DB.goods.filter(g => g.status === 1) + if (p.shopId) list = list.filter(g => g.shopId == p.shopId) + if (p.categoryId) list = list.filter(g => g.categoryId == p.categoryId) + if (p.keyword) list = list.filter(g => g.name.includes(p.keyword)) + return paginate(list, p.page, p.pageSize) + }, + 'GET:/api/goods/detail': (p, id) => DB.goods.find(g => g.id == id), + 'POST:/api/goods': (data) => { + data.id = Date.now() + data.sales = 0 + data.status = 1 + if (!data.attrs) data.attrs = [] + DB.goods.push(data) + persist() + return data + }, + 'PUT:/api/goods': (data) => { + const g = DB.goods.find(x => x.id == data.id) + if (g) Object.assign(g, data) + persist() + return g + }, + 'DELETE:/api/goods': (data, id) => { + DB.goods = DB.goods.filter(g => g.id != id) + persist() + return { ok: true } + }, + 'GET:/api/goods/categories': () => [ + { id: 1, name: '餐饮', children: [{ id: 101, name: '主食' }, { id: 102, name: '小吃' }, { id: 103, name: '饮品' }] }, + { id: 2, name: '零售', children: [{ id: 201, name: '日用品' }, { id: 202, name: '零食' }] }, + { id: 3, name: '服务', children: [{ id: 301, name: '理发' }, { id: 302, name: '打印' }] } + ], + + // ========== 订单 ========== + 'GET:/api/order/list': (p) => { + let list = DB.orders.slice().reverse() + if (p.status) list = list.filter(o => o.status == p.status) + return paginate(list, p.page, p.pageSize) + }, + 'GET:/api/order/detail': (p, id) => DB.orders.find(o => o.id == id), + 'POST:/api/order': (data) => { + const order = { + id: 'OD' + (++DB.orderSeq), + userId: 10001, + shopId: data.shopId, + shopName: data.shopName, + goods: data.goods, + totalPrice: data.totalPrice, + deliveryFee: data.deliveryFee || 0, + couponDiscount: data.couponDiscount || 0, + payPrice: data.payPrice, + address: data.address, + remark: data.remark, + status: 0, // 0 待支付 1 已支付/待接单 2 制作中 3 配送中 4 已完成 5 已取消 + createdAt: Date.now(), + payTime: null, + completedAt: null, + logs: [{ time: Date.now(), status: '订单创建', text: '订单已创建,请尽快支付' }] + } + DB.orders.push(order) + persist() + return order + }, + 'POST:/api/order/pay': () => { + const order = DB.orders[DB.orders.length - 1] + if (order) { + order.status = 1 + order.payTime = Date.now() + order.logs.push({ time: Date.now(), status: '已支付', text: '支付成功,等待商家接单' }) + persist() + } + return { ok: true, payResult: 'success' } + }, + 'POST:/api/order/cancel': () => { + const order = DB.orders[DB.orders.length - 1] + if (order) { order.status = 5; order.logs.push({ time: Date.now(), status: '已取消', text: '订单已取消' }); persist() } + return { ok: true } + }, + 'POST:/api/order/confirm': () => { + const order = DB.orders[DB.orders.length - 1] + if (order) { + order.status = 4 + order.completedAt = Date.now() + order.logs.push({ time: Date.now(), status: '已完成', text: '订单已完成' }) + persist() + } + return { ok: true } + }, + 'POST:/api/order/print': () => { + // 通用云打印机 - 模拟 + return { ok: true, printers: DB.printers || [], printId: 'PR' + Date.now() } + }, + 'GET:/api/merchant/order/list': (p) => { + return paginate(DB.orders.slice().reverse(), p.page, p.pageSize) + }, + + // ========== 跑腿 ========== + 'GET:/api/errand/list': (p) => paginate(DB.errandTasks.slice().reverse(), p.page, p.pageSize), + 'GET:/api/errand/detail': (p, id) => DB.errandTasks.find(t => t.id == id), + 'POST:/api/errand': (data) => { + data.id = Date.now() + data.status = 0 + data.publisherId = 10001 + data.publisher = '用户A' + data.createdAt = Date.now() + DB.errandTasks.push(data) + persist() + return data + }, + 'POST:/api/errand/accept': () => ({ ok: true }), + 'POST:/api/errand/complete': () => ({ ok: true }), + 'GET:/api/errand/location': () => ({ latitude: 39.908823, longitude: 116.397470, timestamp: Date.now() }), + + // ========== 社区 ========== + 'GET:/api/community/list': (p) => paginate(DB.communities, p.page, p.pageSize), + 'GET:/api/community/detail': (p, id) => DB.communities.find(c => c.id == id), + 'POST:/api/community': (data) => { data.id = Date.now(); data.members = 1; data.posts = 0; data.status = 1; DB.communities.push(data); persist(); return data }, + 'POST:/api/community/join': () => ({ ok: true }), + 'GET:/api/community/posts': (p, id) => paginate(DB.communityPosts.filter(cp => cp.communityId == id), p.page, p.pageSize), + 'POST:/api/community/post': (data) => { data.id = Date.now(); data.createdAt = Date.now(); data.likes = 0; data.comments = 0; data.views = 0; DB.communityPosts.push(data); persist(); return data }, + 'GET:/api/community/activities': (p) => paginate(DB.activities, p.page, p.pageSize), + 'POST:/api/community/activity/signup': () => ({ ok: true }), + 'POST:/api/community/activity/checkin': () => ({ ok: true }), + + // ========== 论坛 ========== + 'GET:/api/forum/boards': () => DB.forumBoards, + 'GET:/api/forum/list': (p) => { + let list = DB.forumPosts.slice().sort((a, b) => (b.isTop ? 1 : 0) - (a.isTop ? 1 : 0) || b.createdAt - a.createdAt) + if (p.boardId) list = list.filter(f => f.boardId == p.boardId) + if (p.keyword) list = list.filter(f => f.title.includes(p.keyword) || f.content.includes(p.keyword)) + return paginate(list, p.page, p.pageSize) + }, + 'GET:/api/forum/detail': (p, id) => { + const post = DB.forumPosts.find(f => f.id == id) + if (post) { + post.replies = DB.forumReplies.filter(r => r.postId == id) + return post + } + return null + }, + 'POST:/api/forum': (data) => { data.id = Date.now(); data.createdAt = Date.now(); data.likes = 0; data.comments = 0; data.views = 0; DB.forumPosts.push(data); persist(); return data }, + 'POST:/api/forum/reply': (data, id) => { const r = { id: Date.now(), postId: id, author: '当前用户', content: data.content, likes: 0, createdAt: Date.now() }; DB.forumReplies.push(r); persist(); return r }, + 'POST:/api/forum/like': () => ({ ok: true }), + 'POST:/api/forum/favorite': () => ({ ok: true }), + 'POST:/api/forum/vote': (data, id) => { const post = DB.forumPosts.find(f => f.id == id); if (post && post.vote) { const opt = post.vote.options.find(o => o.id == data.optionId); if (opt) opt.count++; } persist(); return { ok: true } }, + + // ========== 表白墙 ========== + 'GET:/api/confession/list': (p) => paginate(DB.confessions.slice().sort((a, b) => b.createdAt - a.createdAt), p.page, p.pageSize), + 'GET:/api/confession/hot': (p) => paginate(DB.confessions.slice().sort((a, b) => b.likes - a.likes), p.page, p.pageSize), + 'POST:/api/confession': (data) => { data.id = Date.now(); data.likes = 0; data.comments = 0; data.createdAt = Date.now(); data.isAnonymous = true; data.author = '匿名'; DB.confessions.push(data); persist(); return data }, + 'POST:/api/confession/like': () => ({ ok: true }), + 'POST:/api/confession/comment': () => ({ ok: true }), + + // ========== 二手市场 ========== + 'GET:/api/market/list': (p) => { + let list = DB.marketGoods.filter(g => g.status === 1).sort((a, b) => b.createdAt - a.createdAt) + if (p.category) list = list.filter(g => g.category === p.category) + if (p.keyword) list = list.filter(g => g.title.includes(p.keyword)) + return paginate(list, p.page, p.pageSize) + }, + 'GET:/api/market/detail': (p, id) => DB.marketGoods.find(g => g.id == id), + 'POST:/api/market': (data) => { data.id = Date.now(); data.createdAt = Date.now(); data.views = 0; data.likes = 0; data.status = 1; DB.marketGoods.push(data); persist(); return data }, + 'GET:/api/market/my-goods': (p) => paginate(DB.marketGoods, p.page, p.pageSize), + 'PUT:/api/market/offline': () => ({ ok: true }), + 'POST:/api/market/favorite': () => ({ ok: true }), + 'GET:/api/market/categories': () => ['数码', '书籍', '服饰', '生活用品', '体育用品', '其他'], + + // ========== 优惠券 ========== + 'GET:/api/coupon/list': () => DB.coupons, + 'GET:/api/coupon/my': () => DB.myCoupons, + 'POST:/api/coupon/receive': (data, id) => { + const c = DB.coupons.find(x => x.id == id) + if (c) { + DB.myCoupons.push({ ...c, receivedAt: Date.now(), id: Date.now() }) + persist() + } + return { ok: true } + }, + + // ========== DIY ========== + 'GET:/api/diy/list': () => DB.diyPages, + 'GET:/api/diy/detail': (p, id) => DB.diyPages.find(d => d.id == id), + + // ========== 云打印机 ========== + 'GET:/api/printer/list': () => DB.printers || [], + 'POST:/api/printer': (data) => { + if (!DB.printers) DB.printers = [] + data.id = Date.now() + data.status = 'online' + DB.printers.push(data) + persist() + return data + }, + 'POST:/api/printer/print': (data) => { + // 模拟通用云打印接口调用 + return { ok: true, printId: 'PRT' + Date.now(), timestamp: Date.now() } + } +} + +module.exports = { + handle(url, method, data) { + // 提取id模式: /api/xxx/{id}/yyy + let key = (method || 'GET').toUpperCase() + ':' + url + // 尝试直接匹配 + if (handlers[key]) return handlers[key](data) + + // 尝试按 id 模式匹配 + const parts = url.split('/').filter(Boolean) + for (let i = 0; i < parts.length; i++) { + // 尝试把最后一个部分当 ID + const testParts = parts.slice(0, parts.length - i).join('/') + const id = parts[parts.length - i - 1] + if (i >= 1) { + const tryKey = (method || 'GET').toUpperCase() + ':/' + testParts + if (handlers[tryKey]) return handlers[tryKey](data, id) + } + } + + // 尝试更宽松匹配 - 去掉最后一节 + for (let i = 1; i < parts.length; i++) { + const tryUrl = '/' + parts.slice(0, parts.length - i).join('/') + const tryKey = (method || 'GET').toUpperCase() + ':' + tryUrl + if (handlers[tryKey]) { + return handlers[tryKey](data, parts.slice(parts.length - i).join('/')) + } + } + + console.warn('No mock handler for:', key) + return null + } +} diff --git a/utils/payment.js b/utils/payment.js new file mode 100644 index 0000000..03f88fe --- /dev/null +++ b/utils/payment.js @@ -0,0 +1,80 @@ +// utils/payment.js - 微信支付封装 +const util = require('./util.js') + +/** + * 发起微信支付 + * @param {Object} params { orderId, amount, title } + */ +function pay(params) { + return new Promise(async (resolve, reject) => { + try { + // 1. 先调用后端下单接口获取 prepay_id + const orderRes = await _request('/api/payment/prepay', { + orderId: params.orderId, + amount: params.amount, + title: params.title || '订单支付' + }) + + // 2. mock fallback: 直接模拟成功 + const payParams = orderRes && orderRes.payParams ? orderRes.payParams : _mockPayParams() + + // 3. 调起微信支付 + wx.requestPayment({ + timeStamp: payParams.timeStamp, + nonceStr: payParams.nonceStr, + package: payParams.package, + signType: payParams.signType || 'MD5', + paySign: payParams.paySign, + success: () => resolve({ success: true, orderId: params.orderId }), + fail: (err) => { + if (err.errMsg === 'requestPayment:fail cancel') { + reject(new Error('用户取消')) + } else { + // 模拟支付 + wx.showModal({ + title: '支付', + content: '模拟环境,是否继续支付 ¥' + (params.amount / 100).toFixed(2) + '?', + success: (res) => { + if (res.confirm) resolve({ success: true, orderId: params.orderId }) + else reject(new Error('用户取消')) + } + }) + } + } + }) + } catch (e) { + reject(e) + } + }) +} + +function _mockPayParams() { + return { + timeStamp: String(Math.floor(Date.now() / 1000)), + nonceStr: _random(16), + package: 'prepay_id=wx' + Date.now(), + signType: 'MD5', + paySign: 'MOCKPAYSIGN' + _random(10) + } +} + +function _random(n) { + let s = '' + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + for (let i = 0; i < n; i++) s += chars.charAt(Math.floor(Math.random() * chars.length)) + return s +} + +function _request(url, data) { + return new Promise((resolve) => { + wx.request({ + url: (getApp() && getApp().globalData && getApp().globalData.apiBase || '') + url, + method: 'POST', + data: data || {}, + success: (res) => resolve(res.data && res.data.data ? res.data.data : res.data), + fail: () => resolve(null) + }) + }) +} + +module.exports = { pay } diff --git a/utils/printer.js b/utils/printer.js new file mode 100644 index 0000000..e021d37 --- /dev/null +++ b/utils/printer.js @@ -0,0 +1,163 @@ +// utils/printer.js - 通用云打印机集成 +// 支持: 飞鹅(Feie) / 易联云(Yilianyun) / 商米(Sunmi) / Xprinter / 通用 HTTP 云打印 +const STORAGE_KEY = 'printers_config' + +/** + * 构建打印小票文本 + * @param {Object} order 订单 + * @returns {string} 小票 + */ +function buildReceipt(order) { + const lines = [] + lines.push('' + (order.shopName || '店铺') + ' 打印小票') + lines.push('订单号: ' + (order.id || '#' + Date.now())) + lines.push('时间: ' + new Date().toLocaleString()) + lines.push('----------------------------') + lines.push('收货信息:') + if (order.address) { + lines.push((order.address.name || '') + ' ' + (order.address.phone || '')) + lines.push(order.address.address || '') + } + lines.push('----------------------------') + lines.push('商品列表:') + let total = 0 + ;(order.goods || []).forEach(g => { + const price = Number(g.price || 0) + const count = Number(g.count || 1) + total += price * count + lines.push(g.name + ' x' + count + ' ¥' + price.toFixed(2)) + if (g.spec) lines.push(' (' + g.spec + ')') + }) + lines.push('----------------------------') + lines.push('配送费: ¥' + (order.deliveryFee || 0).toFixed(2)) + lines.push('合计: ¥' + (Number(order.payPrice || total + (order.deliveryFee || 0))).toFixed(2) + '') + if (order.remark) lines.push('备注: ' + order.remark) + lines.push('https://example.com/order/' + (order.id || '') + '') + lines.push('') + return lines.join('\n') +} + +function addPrinter(printer) { + const list = getPrinters() + printer.id = Date.now() + printer.status = 'online' + list.push(printer) + try { wx.setStorageSync(STORAGE_KEY, list) } catch (e) {} + return printer +} + +function getPrinters() { + try { return wx.getStorageSync(STORAGE_KEY) || [] } catch (e) { return [] } +} + +function removePrinter(id) { + const list = getPrinters().filter(p => p.id != id) + try { wx.setStorageSync(STORAGE_KEY, list) } catch (e) {} +} + +function print(printerId, content) { + const list = getPrinters() + const p = list.find(x => x.id == printerId) + if (!p) return Promise.reject(new Error('打印机不存在')) + switch (p.vendor) { + case 'feie': return _callFeie(p, content) + case 'yilianyun': return _callYilianyun(p, content) + case 'sunmi': return _callSunmi(p, content) + case 'xprinter': return _callXprinter(p, content) + default: return _callGeneric(p, content) + } +} + +function _callFeie(printer, content) { + return new Promise((resolve, reject) => { + wx.request({ + url: printer.apiUrl || 'https://api.feieyun.cn/Api/Open/', + method: 'POST', + data: { + user: printer.user, + stime: Math.floor(Date.now() / 1000), + sig: printer.sig, + apiname: 'Open_printMsg', + sn: printer.sn, + content: content, + times: 1 + }, + success: (res) => resolve(res.data), + fail: reject + }) + }) +} + +function _callYilianyun(printer, content) { + return new Promise((resolve, reject) => { + wx.request({ + url: printer.apiUrl || 'https://open-api.10ss.net/v2/print/index', + method: 'POST', + data: { machine_code: printer.sn, content: content, msign: printer.msign || '', time: Math.floor(Date.now() / 1000) }, + success: (res) => resolve(res.data), + fail: reject + }) + }) +} + +function _callSunmi(printer, content) { + return new Promise((resolve, reject) => { + wx.request({ + url: printer.apiUrl || 'https://api.sunmi.com/v1/printer/print', + method: 'POST', + header: { 'Authorization': 'Bearer ' + (printer.token || '') }, + data: { sn: printer.sn, content: content }, + success: (res) => resolve(res.data), + fail: reject + }) + }) +} + +function _callXprinter(printer, content) { + return new Promise((resolve, reject) => { + wx.request({ + url: printer.apiUrl || 'https://api.xprinter.cn/print', + method: 'POST', + data: { sn: printer.sn, content: content, key: printer.key }, + success: (res) => resolve(res.data), + fail: reject + }) + }) +} + +function _callGeneric(printer, content) { + return new Promise((resolve) => { + if (!printer.apiUrl) { + console.log('[模拟打印]', content) + resolve({ ok: true, msg: '本地模拟打印成功,请配置 API URL 后正式使用' }) + return + } + wx.request({ + url: printer.apiUrl, + method: 'POST', + data: { sn: printer.sn, content: content, token: printer.token }, + success: (res) => resolve(res.data), + fail: (err) => { + console.log('[打印失败-模拟]', content) + resolve({ ok: true, msg: 'API不可达,已转为本地模拟' }) + } + }) + }) +} + +const VENDORS = [ + { id: 'feie', name: '飞鹅云', fields: ['user', 'ukey', 'sn', 'sig'], url: 'http://api.feieyun.cn/' }, + { id: 'yilianyun', name: '易联云', fields: ['client_id', 'client_secret', 'sn', 'msign'], url: 'https://dev.yilianyun.cn/' }, + { id: 'sunmi', name: '商米云', fields: ['token', 'sn'], url: 'https://developer.sunmi.com/' }, + { id: 'xprinter', name: '芯烨Xprinter', fields: ['key', 'sn'], url: 'https://api.xprinter.cn/' }, + { id: 'generic', name: '自定义HTTP云打印', fields: ['apiUrl', 'token', 'sn'], url: '' } +] + +module.exports = { + buildReceipt, + addPrinter, + getPrinters, + removePrinter, + print, + VENDORS +} diff --git a/utils/request.js b/utils/request.js new file mode 100644 index 0000000..94afbc6 --- /dev/null +++ b/utils/request.js @@ -0,0 +1,90 @@ +// utils/request.js - 网络请求封装 +const app = () => getApp() + +class Request { + constructor() { + this.baseURL = '' + this.timeout = 15000 + } + + request(options) { + const appInstance = app() + return new Promise((resolve, reject) => { + // 模拟模式:直接使用mock数据 + if (!options.url || options.mock !== false) { + // 当未配置真实后端时使用 mock + this._mockHandle(options).then(resolve).catch(reject) + return + } + + wx.request({ + url: (options.baseURL || this.baseURL) + options.url, + method: options.method || 'GET', + data: options.data || {}, + header: Object.assign( + { + 'Content-Type': 'application/json', + Authorization: appInstance && appInstance.globalData.token ? + 'Bearer ' + appInstance.globalData.token : '' + }, + options.header || {} + ), + timeout: this.timeout, + success: (res) => { + if (res.statusCode === 200) { + if (res.data.code === 0 || res.data.code === 200) { + resolve(res.data.data || res.data) + } else { + wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) + reject(res.data) + } + } else if (res.statusCode === 401) { + appInstance && appInstance.logout() + wx.navigateTo({ url: '/pages/user/login/login' }) + reject(res) + } else { + reject(res) + } + }, + fail: reject + }) + }) + } + + // Mock 处理器 + _mockHandle(options) { + return new Promise((resolve) => { + setTimeout(() => { + const mock = require('./mock.js') + const result = mock.handle(options.url, options.method, options.data) + resolve(result) + }, 300) + } + + get(url, data, options = {}) { + return this.request({ url, method: 'GET', data, ...options }) + } + + post(url, data, options = {}) { + return this.request({ url, method: 'POST', data, ...options }) + } + + put(url, data, options = {}) { + return this.request({ url, method: 'PUT', data, ...options }) + } + + delete(url, data, options = {}) { + return this.request({ url, method: 'DELETE', data, ...options }) + } + + upload(filePath, name = 'file') { + return new Promise((resolve, reject) => { + // 模拟上传 + setTimeout(() => { + resolve({ url: 'https://img.cdn.example.com/upload/' + Date.now() + '.jpg' }) + }, 500) + }) + } +} + +module.exports = new Request() diff --git a/utils/storage.js b/utils/storage.js new file mode 100644 index 0000000..70a5d9c --- /dev/null +++ b/utils/storage.js @@ -0,0 +1,31 @@ +// utils/storage.js - 本地存储封装 +module.exports = { + set(key, value) { + try { + wx.setStorageSync(key, value) + } catch (e) { + console.error('storage.set error:', e) + } + }, + + get(key, defaultValue = null) { + try { + const v = wx.getStorageSync(key) + return v !== '' ? v : defaultValue + } catch (e) { + return defaultValue + } + }, + + remove(key) { + try { + wx.removeStorageSync(key) + } catch (e) {} + }, + + clear() { + try { + wx.clearStorageSync() + } catch (e) {} + } +} diff --git a/utils/util.js b/utils/util.js new file mode 100644 index 0000000..57f7993 --- /dev/null +++ b/utils/util.js @@ -0,0 +1,139 @@ +// utils/util.js - 通用工具 +function formatTime(date) { + if (typeof date === 'number') date = new Date(date) + const year = date.getFullYear() + const month = date.getMonth() + 1 + const day = date.getDate() + const hour = date.getHours() + const minute = date.getMinutes() + return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}` +} + +function formatDate(date) { + if (typeof date === 'number') date = new Date(date) + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +} + +function pad(n) { return n < 10 ? '0' + n : n } + +function formatPrice(price) { + return '¥' + parseFloat(price).toFixed(2) +} + +function formatCount(num) { + if (num >= 10000) return (num / 10000).toFixed(1) + 'w' + if (num >= 1000) return (num / 1000).toFixed(1) + 'k' + return num +} + +function timeAgo(ts) { + const diff = Date.now() - ts + if (diff < 60 * 1000) return '刚刚' + if (diff < 3600 * 1000) return Math.floor(diff / 60000) + '分钟前' + if (diff < 86400 * 1000) return Math.floor(diff / 3600000) + '小时前' + if (diff < 86400 * 1000 * 7) return Math.floor(diff / 86400000) + '天前' + return formatDate(ts) +} + +function showToast(title, icon = 'none') { + wx.showToast({ title, icon, duration: 2000 }) +} + +function showLoading(title = '加载中...') { + wx.showLoading({ title, mask: true }) +} + +function hideLoading() { + wx.hideLoading() +} + +function confirm(title, content) { + return new Promise((resolve) => { + wx.showModal({ + title, content, + confirmText: '确定', + cancelText: '取消', + success: (res) => resolve(res.confirm) + }) + }) +} + +function navigateTo(url) { + wx.navigateTo({ url }) +} + +function switchTab(url) { + wx.switchTab({ url }) +} + +function setTabBarBadge(index, text) { + if (text) { + wx.setTabBarBadge({ index, text: String(text) }).catch(() => {}) + } else { + wx.removeTabBarBadge({ index }).catch(() => {}) + } +} + +// 计算购物车总价 +function calcCartTotal(cart) { + let total = 0 + Object.values(cart).forEach(shop => { + shop.goods.forEach(g => { total += g.price * g.count }) + }) + return total +} + +// 计算购物车数量 +function calcCartCount(cart) { + let count = 0 + Object.values(cart).forEach(shop => { + shop.goods.forEach(g => { count += g.count }) + }) + return count +} + +// 安全解析 JSON +function safeParse(str, defaultValue) { + try { return JSON.parse(str) } catch(e) { return defaultValue } +} + +// 生成随机字符串 +function randomStr(len = 16) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + let s = '' + for (let i = 0; i < len; i++) s += chars.charAt(Math.floor(Math.random() * chars.length)) + return s +} + +// 防抖 +function debounce(fn, wait = 300) { + let timer + return function(...args) { + if (timer) clearTimeout(timer) + timer = setTimeout(() => fn.apply(this, args), wait) + } +} + +// 节流 +function throttle(fn, wait = 300) { + let last = 0 + return function(...args) { + const now = Date.now() + if (now - last >= wait) { + last = now + fn.apply(this, args) + } + } +} + +// 深拷贝 +function deepClone(obj) { + return JSON.parse(JSON.stringify(obj)) +} + +module.exports = { + formatTime, formatDate, formatPrice, formatCount, timeAgo, + showToast, showLoading, hideLoading, confirm, navigateTo, switchTab, + calcCartTotal, calcCartCount, safeParse, randomStr, + debounce, throttle, deepClone +} From 44188eba57522167cd5a05df697e9b95d0b100fd Mon Sep 17 00:00:00 2001 From: k123456214 Date: Sat, 20 Jun 2026 12:53:02 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E5=A4=9A=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=BC=80=E5=8F=91=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- preview.html | 671 +++++++++++++++ server/package-lock.json | 1718 ++++++++++++++++++++++++++++++++++++++ server/package.json | 5 +- 3 files changed, 2391 insertions(+), 3 deletions(-) create mode 100644 preview.html create mode 100644 server/package-lock.json diff --git a/preview.html b/preview.html new file mode 100644 index 0000000..2ba8f05 --- /dev/null +++ b/preview.html @@ -0,0 +1,671 @@ + + + + + +校园综合服务平台 - 功能预览 + + + + + +
+

🏫 校园综合服务平台

+

多商户 · 外卖点餐 · 跑腿服务 · 社区论坛 · 表白墙 · 二手市场

+
+ + + + +
+
+
+
+
+
+ + + +
🎉 校园外卖新上线,下单立减5元
+ +
+
+
🍔
+ 外卖点餐 +
+
+
🛍️
+ 二手市场 +
+
+
💌
+ 表白墙 +
+
+
📚
+ 校园论坛 +
+
+
🎯
+ 兴趣社区 +
+
+
🏃
+ 跑腿服务 +
+
+
🏪
+ 商户入驻 +
+
+
+ 自定义页 +
+
+ +
+
+ 🔥 热门商户 + 查看更多 › +
+
+ +
+
麦香汉堡★ 4.8
+
月销1200 · 起送¥15 · 配送¥3
+
+ 餐饮 + 支持外卖 +
+
+
+
+ +
+
鲜饮茶铺★ 4.6
+
月销850 · 起送¥10 · 配送¥2
+
+ 餐饮 + 新品 +
+
+
+
+ +
+
+ 💬 校园热议 + 全部 › +
+
+
+
+ 精华 + 图书馆新增自习区域开放啦 +
+
同学A · 1小时前
+
+ 👍 120 + 💬 35 +
+
+
+
+
+ +
+
🏠首页
+
🍔外卖
+
🎯社区
+
📚论坛
+
👤我的
+
+
+
+
+ + +
+
+ 🍔 +
+

外卖点餐

+

支持多规格商品属性(单选/多选/数字输入),实时价格计算,购物车管理

+
+
+
+ 🏪 +
+

多商户入驻

+

商户提交资质→平台审核→入驻成功→商品管理→订单处理

+
+
+
+ 🏃 +
+

校园跑腿

+

发布任务→骑手接单→实时位置追踪→完成确认

+
+
+
+ 🎯 +
+

兴趣社区

+

创建/加入社区,发帖互动,活动发布与报名,签到统计

+
+
+
+ 💌 +
+

表白墙

+

匿名投稿→审核机制→时间线展示→互动评论

+
+
+
+ 🛍️ +
+

二手市场

+

商品发布→分类筛选→议价功能→交易担保

+
+
+
+ 🖨️ +
+

云打印机

+

支持飞鹅/易联云/商米/芯烨/通用HTTP云打印,自动生成小票

+
+
+
+ +
+

DIY自定义

+

运营后台拖拽配置页面组件,支持嵌入H5,脱离代码发版

+
+
+
+
+ + + + + +
+
+ + 👋 管理员 | 退出 +
+
+
+
📊 数据概览
+
🏪 商户管理
+
🍔 商品管理
+
📦 订单管理
+
🎟️ 优惠券
+
🎯 社区管理
+
📚 论坛管理
+
💌 表白墙
+
🛍️ 二手市场
+
🏃 跑腿管理
+
🖨️ 打印机
+
✨ DIY页面
+
👥 用户管理
+
⚙️ 系统设置
+
+
+ +
+
+
1,280
+
今日订单
+
↑ 12.5%
+
+
+
¥35,800
+
今日销售额
+
↑ 8.3%
+
+
+
428
+
活跃用户
+
↑ 15.2%
+
+
+
86
+
入驻商户
+
↑ 3
+
+
+ + +
+
📋 待审核商户入驻申请
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
店铺名称申请人联系方式申请时间状态操作
张姐麻辣烫张丽138****12342026-06-20待审核 + + +
学霸打印店李明139****56782026-06-19已通过
校园水果店王强137****90122026-06-18已拒绝
+
+ + +
+
📦 最新订单
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
订单号店铺金额状态时间操作
OD10005麦香汉堡¥36.00配送中06-20 14:32
OD10004鲜饮茶铺¥22.00已完成06-20 12:18
OD10003校园便利¥15.50已完成06-20 11:05
+
+
+
+
+ + + + + + + + diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..ee29d48 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,1718 @@ +{ + "name": "campus-service-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "campus-service-backend", + "version": "1.0.0", + "dependencies": { + "axios": "^1.6.0", + "body-parser": "^1.20.0", + "cors": "^2.8.5", + "express": "^4.19.0", + "jsonwebtoken": "^9.0.0", + "multer": "^1.4.4-lts.1" + }, + "devDependencies": { + "nodemon": "^3.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", + "dependencies": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==", + "dependencies": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", + "integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", + "deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/server/package.json b/server/package.json index 0c45c0c..f6cbaaf 100644 --- a/server/package.json +++ b/server/package.json @@ -1,7 +1,7 @@ { "name": "campus-service-backend", "version": "1.0.0", - "description": "校园综合服务平台后端 - 外卖/跑腿/社区/论坛/表白墙/二手市场", + "description": "校园综合服务平台后端", "main": "index.js", "scripts": { "start": "node index.js", @@ -11,9 +11,8 @@ "express": "^4.19.0", "cors": "^2.8.5", "body-parser": "^1.20.0", - "mysql2": "^3.9.0", "jsonwebtoken": "^9.0.0", - "multer": "^1.4.5", + "multer": "^1.4.4-lts.1", "axios": "^1.6.0" }, "devDependencies": { From 8170073b5a159f07efdaa3e6980a3537ce4cb050 Mon Sep 17 00:00:00 2001 From: k123456214 Date: Sat, 20 Jun 2026 13:41:13 +0000 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E5=A4=9A=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=BC=80=E5=8F=91=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- taro-app/README.md | 241 ++++++++++++++ taro-app/babel.config.js | 13 + taro-app/config/index.ts | 77 +++++ taro-app/package.json | 62 ++++ taro-app/project.config.weapp.json | 32 ++ taro-app/src/app.config.ts | 61 ++++ taro-app/src/app.scss | 314 ++++++++++++++++++ taro-app/src/app.tsx | 16 + taro-app/src/global.d.ts | 35 ++ .../community/activity/activity.config.ts | 4 + .../pages/community/activity/activity.scss | 46 +++ .../src/pages/community/activity/activity.tsx | 50 +++ .../pages/community/create/create.config.ts | 4 + .../src/pages/community/create/create.scss | 80 +++++ .../src/pages/community/create/create.tsx | 50 +++ .../pages/community/detail/detail.config.ts | 4 + .../src/pages/community/detail/detail.scss | 64 ++++ .../src/pages/community/detail/detail.tsx | 71 ++++ .../src/pages/community/list/list.config.ts | 4 + taro-app/src/pages/community/list/list.scss | 68 ++++ taro-app/src/pages/community/list/list.tsx | 70 ++++ .../pages/confession/detail/detail.config.ts | 4 + .../src/pages/confession/detail/detail.scss | 81 +++++ .../src/pages/confession/detail/detail.tsx | 83 +++++ .../src/pages/confession/list/list.config.ts | 4 + taro-app/src/pages/confession/list/list.scss | 88 +++++ taro-app/src/pages/confession/list/list.tsx | 66 ++++ .../confession/publish/publish.config.ts | 4 + .../src/pages/confession/publish/publish.scss | 85 +++++ .../src/pages/confession/publish/publish.tsx | 54 +++ taro-app/src/pages/custom/diy/diy.config.ts | 4 + taro-app/src/pages/custom/diy/diy.scss | 101 ++++++ taro-app/src/pages/custom/diy/diy.tsx | 131 ++++++++ taro-app/src/pages/custom/h5/h5.config.ts | 4 + taro-app/src/pages/custom/h5/h5.scss | 56 ++++ taro-app/src/pages/custom/h5/h5.tsx | 58 ++++ .../src/pages/errand/detail/detail.config.ts | 4 + taro-app/src/pages/errand/detail/detail.scss | 88 +++++ taro-app/src/pages/errand/detail/detail.tsx | 95 ++++++ taro-app/src/pages/errand/list/list.config.ts | 4 + taro-app/src/pages/errand/list/list.scss | 92 +++++ taro-app/src/pages/errand/list/list.tsx | 68 ++++ .../pages/errand/publish/publish.config.ts | 4 + .../src/pages/errand/publish/publish.scss | 79 +++++ taro-app/src/pages/errand/publish/publish.tsx | 67 ++++ .../pages/food/checkout/checkout.config.ts | 4 + .../src/pages/food/checkout/checkout.scss | 105 ++++++ taro-app/src/pages/food/checkout/checkout.tsx | 144 ++++++++ taro-app/src/pages/food/index/index.config.ts | 5 + taro-app/src/pages/food/index/index.scss | 87 +++++ taro-app/src/pages/food/index/index.tsx | 88 +++++ .../food/order-detail/order-detail.config.ts | 4 + .../pages/food/order-detail/order-detail.scss | 90 +++++ .../pages/food/order-detail/order-detail.tsx | 126 +++++++ taro-app/src/pages/food/shop/shop.config.ts | 4 + taro-app/src/pages/food/shop/shop.scss | 184 ++++++++++ taro-app/src/pages/food/shop/shop.tsx | 293 ++++++++++++++++ .../src/pages/forum/detail/detail.config.ts | 4 + taro-app/src/pages/forum/detail/detail.scss | 92 +++++ taro-app/src/pages/forum/detail/detail.tsx | 103 ++++++ taro-app/src/pages/forum/list/list.config.ts | 4 + taro-app/src/pages/forum/list/list.scss | 86 +++++ taro-app/src/pages/forum/list/list.tsx | 70 ++++ .../src/pages/forum/publish/publish.config.ts | 4 + taro-app/src/pages/forum/publish/publish.scss | 79 +++++ taro-app/src/pages/forum/publish/publish.tsx | 49 +++ taro-app/src/pages/index/index.config.ts | 5 + taro-app/src/pages/index/index.scss | 204 ++++++++++++ taro-app/src/pages/index/index.tsx | 164 +++++++++ .../src/pages/market/detail/detail.config.ts | 4 + taro-app/src/pages/market/detail/detail.scss | 114 +++++++ taro-app/src/pages/market/detail/detail.tsx | 74 +++++ taro-app/src/pages/market/list/list.config.ts | 4 + taro-app/src/pages/market/list/list.scss | 132 ++++++++ taro-app/src/pages/market/list/list.tsx | 93 ++++++ .../pages/market/my-goods/my-goods.config.ts | 4 + .../src/pages/market/my-goods/my-goods.scss | 106 ++++++ .../src/pages/market/my-goods/my-goods.tsx | 69 ++++ .../pages/market/publish/publish.config.ts | 4 + .../src/pages/market/publish/publish.scss | 93 ++++++ taro-app/src/pages/market/publish/publish.tsx | 76 +++++ .../src/pages/merchant/apply/apply.config.ts | 4 + taro-app/src/pages/merchant/apply/apply.scss | 92 +++++ taro-app/src/pages/merchant/apply/apply.tsx | 97 ++++++ .../pages/merchant/detail/detail.config.ts | 4 + .../src/pages/merchant/detail/detail.scss | 89 +++++ taro-app/src/pages/merchant/detail/detail.tsx | 71 ++++ .../merchant/goods-edit/goods-edit.config.ts | 4 + .../pages/merchant/goods-edit/goods-edit.scss | 164 +++++++++ .../pages/merchant/goods-edit/goods-edit.tsx | 205 ++++++++++++ .../goods-manage/goods-manage.config.ts | 4 + .../merchant/goods-manage/goods-manage.scss | 81 +++++ .../merchant/goods-manage/goods-manage.tsx | 76 +++++ .../src/pages/merchant/list/list.config.ts | 5 + taro-app/src/pages/merchant/list/list.scss | 69 ++++ taro-app/src/pages/merchant/list/list.tsx | 81 +++++ .../pages/merchant/my-shop/my-shop.config.ts | 4 + .../src/pages/merchant/my-shop/my-shop.scss | 61 ++++ .../src/pages/merchant/my-shop/my-shop.tsx | 64 ++++ .../order-manage/order-manage.config.ts | 4 + .../merchant/order-manage/order-manage.scss | 102 ++++++ .../merchant/order-manage/order-manage.tsx | 83 +++++ .../src/pages/user/coupon/coupon.config.ts | 4 + taro-app/src/pages/user/coupon/coupon.scss | 102 ++++++ taro-app/src/pages/user/coupon/coupon.tsx | 75 +++++ .../pages/user/favorite/favorite.config.ts | 4 + .../src/pages/user/favorite/favorite.scss | 69 ++++ taro-app/src/pages/user/favorite/favorite.tsx | 82 +++++ taro-app/src/pages/user/login/login.config.ts | 4 + taro-app/src/pages/user/login/login.scss | 115 +++++++ taro-app/src/pages/user/login/login.tsx | 122 +++++++ .../src/pages/user/order/my-order.config.ts | 4 + taro-app/src/pages/user/order/my-order.scss | 107 ++++++ taro-app/src/pages/user/order/my-order.tsx | 94 ++++++ .../src/pages/user/profile/profile.config.ts | 4 + taro-app/src/pages/user/profile/profile.scss | 110 ++++++ taro-app/src/pages/user/profile/profile.tsx | 113 +++++++ .../pages/user/settings/settings.config.ts | 4 + .../src/pages/user/settings/settings.scss | 64 ++++ taro-app/src/pages/user/settings/settings.tsx | 104 ++++++ taro-app/src/services/api.ts | 83 +++++ taro-app/src/utils/index.ts | 112 +++++++ taro-app/src/utils/printer.ts | 106 ++++++ taro-app/src/utils/request.ts | 178 ++++++++++ taro-app/tsconfig.json | 31 ++ 125 files changed, 8516 insertions(+) create mode 100644 taro-app/README.md create mode 100644 taro-app/babel.config.js create mode 100644 taro-app/config/index.ts create mode 100644 taro-app/package.json create mode 100644 taro-app/project.config.weapp.json create mode 100644 taro-app/src/app.config.ts create mode 100644 taro-app/src/app.scss create mode 100644 taro-app/src/app.tsx create mode 100644 taro-app/src/global.d.ts create mode 100644 taro-app/src/pages/community/activity/activity.config.ts create mode 100644 taro-app/src/pages/community/activity/activity.scss create mode 100644 taro-app/src/pages/community/activity/activity.tsx create mode 100644 taro-app/src/pages/community/create/create.config.ts create mode 100644 taro-app/src/pages/community/create/create.scss create mode 100644 taro-app/src/pages/community/create/create.tsx create mode 100644 taro-app/src/pages/community/detail/detail.config.ts create mode 100644 taro-app/src/pages/community/detail/detail.scss create mode 100644 taro-app/src/pages/community/detail/detail.tsx create mode 100644 taro-app/src/pages/community/list/list.config.ts create mode 100644 taro-app/src/pages/community/list/list.scss create mode 100644 taro-app/src/pages/community/list/list.tsx create mode 100644 taro-app/src/pages/confession/detail/detail.config.ts create mode 100644 taro-app/src/pages/confession/detail/detail.scss create mode 100644 taro-app/src/pages/confession/detail/detail.tsx create mode 100644 taro-app/src/pages/confession/list/list.config.ts create mode 100644 taro-app/src/pages/confession/list/list.scss create mode 100644 taro-app/src/pages/confession/list/list.tsx create mode 100644 taro-app/src/pages/confession/publish/publish.config.ts create mode 100644 taro-app/src/pages/confession/publish/publish.scss create mode 100644 taro-app/src/pages/confession/publish/publish.tsx create mode 100644 taro-app/src/pages/custom/diy/diy.config.ts create mode 100644 taro-app/src/pages/custom/diy/diy.scss create mode 100644 taro-app/src/pages/custom/diy/diy.tsx create mode 100644 taro-app/src/pages/custom/h5/h5.config.ts create mode 100644 taro-app/src/pages/custom/h5/h5.scss create mode 100644 taro-app/src/pages/custom/h5/h5.tsx create mode 100644 taro-app/src/pages/errand/detail/detail.config.ts create mode 100644 taro-app/src/pages/errand/detail/detail.scss create mode 100644 taro-app/src/pages/errand/detail/detail.tsx create mode 100644 taro-app/src/pages/errand/list/list.config.ts create mode 100644 taro-app/src/pages/errand/list/list.scss create mode 100644 taro-app/src/pages/errand/list/list.tsx create mode 100644 taro-app/src/pages/errand/publish/publish.config.ts create mode 100644 taro-app/src/pages/errand/publish/publish.scss create mode 100644 taro-app/src/pages/errand/publish/publish.tsx create mode 100644 taro-app/src/pages/food/checkout/checkout.config.ts create mode 100644 taro-app/src/pages/food/checkout/checkout.scss create mode 100644 taro-app/src/pages/food/checkout/checkout.tsx create mode 100644 taro-app/src/pages/food/index/index.config.ts create mode 100644 taro-app/src/pages/food/index/index.scss create mode 100644 taro-app/src/pages/food/index/index.tsx create mode 100644 taro-app/src/pages/food/order-detail/order-detail.config.ts create mode 100644 taro-app/src/pages/food/order-detail/order-detail.scss create mode 100644 taro-app/src/pages/food/order-detail/order-detail.tsx create mode 100644 taro-app/src/pages/food/shop/shop.config.ts create mode 100644 taro-app/src/pages/food/shop/shop.scss create mode 100644 taro-app/src/pages/food/shop/shop.tsx create mode 100644 taro-app/src/pages/forum/detail/detail.config.ts create mode 100644 taro-app/src/pages/forum/detail/detail.scss create mode 100644 taro-app/src/pages/forum/detail/detail.tsx create mode 100644 taro-app/src/pages/forum/list/list.config.ts create mode 100644 taro-app/src/pages/forum/list/list.scss create mode 100644 taro-app/src/pages/forum/list/list.tsx create mode 100644 taro-app/src/pages/forum/publish/publish.config.ts create mode 100644 taro-app/src/pages/forum/publish/publish.scss create mode 100644 taro-app/src/pages/forum/publish/publish.tsx create mode 100644 taro-app/src/pages/index/index.config.ts create mode 100644 taro-app/src/pages/index/index.scss create mode 100644 taro-app/src/pages/index/index.tsx create mode 100644 taro-app/src/pages/market/detail/detail.config.ts create mode 100644 taro-app/src/pages/market/detail/detail.scss create mode 100644 taro-app/src/pages/market/detail/detail.tsx create mode 100644 taro-app/src/pages/market/list/list.config.ts create mode 100644 taro-app/src/pages/market/list/list.scss create mode 100644 taro-app/src/pages/market/list/list.tsx create mode 100644 taro-app/src/pages/market/my-goods/my-goods.config.ts create mode 100644 taro-app/src/pages/market/my-goods/my-goods.scss create mode 100644 taro-app/src/pages/market/my-goods/my-goods.tsx create mode 100644 taro-app/src/pages/market/publish/publish.config.ts create mode 100644 taro-app/src/pages/market/publish/publish.scss create mode 100644 taro-app/src/pages/market/publish/publish.tsx create mode 100644 taro-app/src/pages/merchant/apply/apply.config.ts create mode 100644 taro-app/src/pages/merchant/apply/apply.scss create mode 100644 taro-app/src/pages/merchant/apply/apply.tsx create mode 100644 taro-app/src/pages/merchant/detail/detail.config.ts create mode 100644 taro-app/src/pages/merchant/detail/detail.scss create mode 100644 taro-app/src/pages/merchant/detail/detail.tsx create mode 100644 taro-app/src/pages/merchant/goods-edit/goods-edit.config.ts create mode 100644 taro-app/src/pages/merchant/goods-edit/goods-edit.scss create mode 100644 taro-app/src/pages/merchant/goods-edit/goods-edit.tsx create mode 100644 taro-app/src/pages/merchant/goods-manage/goods-manage.config.ts create mode 100644 taro-app/src/pages/merchant/goods-manage/goods-manage.scss create mode 100644 taro-app/src/pages/merchant/goods-manage/goods-manage.tsx create mode 100644 taro-app/src/pages/merchant/list/list.config.ts create mode 100644 taro-app/src/pages/merchant/list/list.scss create mode 100644 taro-app/src/pages/merchant/list/list.tsx create mode 100644 taro-app/src/pages/merchant/my-shop/my-shop.config.ts create mode 100644 taro-app/src/pages/merchant/my-shop/my-shop.scss create mode 100644 taro-app/src/pages/merchant/my-shop/my-shop.tsx create mode 100644 taro-app/src/pages/merchant/order-manage/order-manage.config.ts create mode 100644 taro-app/src/pages/merchant/order-manage/order-manage.scss create mode 100644 taro-app/src/pages/merchant/order-manage/order-manage.tsx create mode 100644 taro-app/src/pages/user/coupon/coupon.config.ts create mode 100644 taro-app/src/pages/user/coupon/coupon.scss create mode 100644 taro-app/src/pages/user/coupon/coupon.tsx create mode 100644 taro-app/src/pages/user/favorite/favorite.config.ts create mode 100644 taro-app/src/pages/user/favorite/favorite.scss create mode 100644 taro-app/src/pages/user/favorite/favorite.tsx create mode 100644 taro-app/src/pages/user/login/login.config.ts create mode 100644 taro-app/src/pages/user/login/login.scss create mode 100644 taro-app/src/pages/user/login/login.tsx create mode 100644 taro-app/src/pages/user/order/my-order.config.ts create mode 100644 taro-app/src/pages/user/order/my-order.scss create mode 100644 taro-app/src/pages/user/order/my-order.tsx create mode 100644 taro-app/src/pages/user/profile/profile.config.ts create mode 100644 taro-app/src/pages/user/profile/profile.scss create mode 100644 taro-app/src/pages/user/profile/profile.tsx create mode 100644 taro-app/src/pages/user/settings/settings.config.ts create mode 100644 taro-app/src/pages/user/settings/settings.scss create mode 100644 taro-app/src/pages/user/settings/settings.tsx create mode 100644 taro-app/src/services/api.ts create mode 100644 taro-app/src/utils/index.ts create mode 100644 taro-app/src/utils/printer.ts create mode 100644 taro-app/src/utils/request.ts create mode 100644 taro-app/tsconfig.json diff --git a/taro-app/README.md b/taro-app/README.md new file mode 100644 index 0000000..e270710 --- /dev/null +++ b/taro-app/README.md @@ -0,0 +1,241 @@ +# 🎓 校园综合服务平台 · 多端小程序(Taro 4.x) + +一套代码,多端发布。基于 **Taro 4.x + React + TypeScript** 构建,覆盖微信/支付宝/抖音/QQ/京东小程序、H5、React Native 等主流平台。 + +## ✨ 功能清单 + +### 🍔 外卖点餐系统 +- 商户列表(分类筛选、评分、月销、起送价、配送费) +- 店铺页:左侧分类、右侧商品、底部浮动购物车 +- **商品多规格属性**:支持 `select`(单选)/`multiselect`(多选,可带附加价)/`number`(数字步进)/`text`(备注) +- 购物车动态计算:基础价 + 附加价 × 数量 +- 结算页:地址选择、优惠券、支付方式(微信/余额/券) +- 订单详情:制作中/配送中/已送达状态流、打印小票 + +### 🏪 多商户服务 +- **商户入驻申请表单**:营业执照、法人信息、联系方式、店铺分类/地址 +- 管理员审核流程(模拟状态通知) +- 商户列表/详情页、分类筛选、排序 +- **商户后台首页**:今日订单/销售额、本周销售数据、最新订单 +- **商品管理**:新增/编辑/下架、动态属性配置 +- **商品编辑**:动态添加/删除属性、设置属性类型与选项 +- **订单管理**:接单/拒单/打印小票/联系用户 + +### 🎯 兴趣社区 +- 社区列表(卡片式,分类标签:运动/科技/艺术/音乐/其他) +- 社区详情:动态 / 活动 / 成员 Tab +- 创建社区:名称、分类、简介、图标、公告 +- 活动页:活动卡片、报名按钮、已报名人数 + +### 📚 校园论坛 +- **板块**:校园生活 / 学习交流 / 失物招领 / 求职招聘 +- **帖子列表**:置顶帖、精华帖、热帖排序、点赞/评论数 +- **帖子详情**:完整正文、点赞、收藏、投票、评论列表 +- **发帖**:选择板块、标题、正文、添加选项投票、匿名开关 + +### 💌 表白墙 +- **时间线**展示(最新/热门 Tab) +- **匿名投稿**:文字 + 图片上传、审核机制 +- 详情页:点赞、举报、匿名评论 + +### 🛍️ 二手市场 +- **瀑布流商品卡**(数码/书籍/服饰/生活用品/其他) +- 发布页:图片、标题、描述、分类、原价/售价、议价开关、线下交易地 +- 商品详情:大图、卖家信息、信用分、私信/购买 +- 我的商品:出售中 / 已下架 / 已售出 Tab + +### 🏃 跑腿服务 +- **任务列表** Tab:我发布的 / 我接的 / 附近任务 +- 发布任务:类型、地址、描述、费用 +- 任务详情:发单人、接单人、状态流、接单按钮 + +### 👤 用户中心 +- 个人资料(头像、昵称、积分、等级) +- 登录页(手机号 + 验证码、微信授权) +- 我的订单(全部/待付款/待收货/已完成/退款) +- 优惠券中心(我的券) +- 我的收藏(商品/店铺 Tab) +- 设置(资料、地址、消息通知、角色切换、清缓存) + +### ✨ DIY 自定义页面 + H5 +- `diy`:读取服务端页面组件配置(title/richtext/image/button/goods-list/text/divider/empty),动态渲染,脱离代码发版 +- `h5`:`web-view` 嵌入外部 H5,支持复制链接、分享 + +## 🧩 技术架构 + +``` +taro-app/ +├─ src/ +│ ├─ app.tsx # 应用入口 +│ ├─ app.config.ts # 全局路由 + Tab Bar +│ ├─ app.scss # 全局样式 / 设计令牌 +│ ├─ services/api.ts # 业务 API 聚合层 +│ ├─ utils/ +│ │ ├─ index.ts # 通用工具(toast/format/navigate/storage...) +│ │ ├─ request.ts # 统一请求(含内置 mock 数据池,零后端可演示) +│ │ └─ printer.ts # ★ 通用云打印机(飞鹅/易联云/商米/芯烨/自定义) +│ ├─ pages/ +│ │ ├─ index/index.tsx # 首页(快捷入口 + 商户 + 论坛) +│ │ ├─ food/ # 外卖点餐 +│ │ ├─ merchant/ # 商户管理 +│ │ ├─ community/ # 兴趣社区 +│ │ ├─ forum/ # 校园论坛 +│ │ ├─ confession/ # 表白墙 +│ │ ├─ market/ # 二手市场 +│ │ ├─ errand/ # 跑腿服务 +│ │ ├─ custom/ # DIY 自定义页面 + H5 +│ │ └─ user/ # 用户中心 +│ └─ global.d.ts +├─ config/index.ts # Taro 打包配置 +├─ babel.config.js +├─ tsconfig.json +└─ package.json +``` + +### 设计令牌(Design Tokens) +| Token | 值 | 用途 | +|---|---|---| +| primary | `#1890ff` | 品牌主色 | +| success | `#52c41a` | 成功/绿色操作 | +| warning | `#faad14` | 警告/评分 | +| danger | `#f5222d` | 价格/删除/举报 | +| bg-color | `#f5f5f5` | 背景 | +| 圆角 | `16rpx` | 卡片、按钮 | +| 阴影 | `0 2rpx 12rpx rgba(0,0,0,.04)` | 卡片阴影 | + +### 🖨️ 通用云打印机(utils/printer.ts) +- **飞鹅云 Feie**:`POST https://api.feieyun.cn/Api/Open/` +- **易联云 Yilianyun**:`POST https://open-api.10ss.net/print/index` +- **商米 Sunmi**:`POST https://api.sunmi.com/printer/print`(Bearer Token) +- **芯烨 Xprinter**:`POST https://api.xprinter.cn/print` +- **自定义 HTTP**:任意 REST 接口 +- 小票模板:`店铺名称 / order_xxx / 自动切纸` +- 未配置 API 时,自动降级为本地模拟打印 + 控制台输出 + +## 🚀 快速开始 + +### 1. 安装依赖 +```bash +cd taro-app +npm install +``` + +### 2. 启动开发(选一或多选) +```bash +# 微信小程序 +npm run dev:weapp # 然后用 微信开发者工具 打开 dist/weapp 目录 + +# 支付宝小程序 +npm run dev:alipay + +# 抖音/头条小程序 +npm run dev:tt + +# QQ 小程序 +npm run dev:qq + +# 京东小程序 +npm run dev:jd + +# H5 网页(可直接浏览器访问) +npm run dev:h5 + +# React Native +npm run dev:rn +``` + +### 3. 生产构建 +```bash +npm run build:weapp # 微信小程序产物:dist/weapp +npm run build:h5 # H5 产物:dist/h5 (可部署至 Nginx/Vercel) +``` + +### 4. 接入后端(可选) +修改 `src/utils/request.ts` 中的 `BASE_URL` 指向你的服务端即可。当前默认包含 **完整 mock 数据池**,不启动后端也能演示全部页面。 + +配套参考服务端见 `../server/index.js`(Node.js + Express,60+ 条 REST API)。 + +### 5. 微信开发者工具导入 +1. 打开「微信开发者工具」 +2. 选择「导入项目」 +3. 项目目录:`taro-app/dist/weapp` +4. AppID:填你自己的(或"测试号") +5. 开始预览/真机调试 + +同样的方式,`dist/alipay` 导入到支付宝开发者工具,`dist/tt` 导入到抖音开发者工具,以此类推。 + +## 📦 打包与发布 + +| 平台 | 产物目录 | 发布方式 | +|---|---|---| +| 微信小程序 | `dist/weapp` | 微信开发者工具 → 上传 | +| 支付宝小程序 | `dist/alipay` | 支付宝开放平台 | +| 抖音/头条小程序 | `dist/tt` | 抖音开放平台 | +| QQ 小程序 | `dist/qq` | QQ 开放平台 | +| 京东小程序 | `dist/jd` | 京东小程序 | +| H5 网页 | `dist/h5` | 部署至 CDN / Nginx / Vercel | +| React Native | `dist/rn` | Xcode / Android Studio | + +## 📁 页面地图(全部 37 个页面) + +``` +pages/ +├─ index/index # 首页(快捷入口/商户/论坛) +├─ food/ +│ ├─ index/index # 外卖商户列表 +│ ├─ shop/shop # 店铺页 + 购物车 + 多规格属性选择 +│ ├─ checkout/checkout # 结算(地址、优惠券、支付方式) +│ └─ order-detail/order-detail # 订单详情 / 状态流 / 云打印 +├─ merchant/ +│ ├─ list/list # 商户列表 +│ ├─ detail/detail # 商户详情 +│ ├─ apply/apply # 商户入驻申请 +│ ├─ my-shop/my-shop # 商户后台首页(数据看板) +│ ├─ goods-manage/goods-manage # 商品管理 +│ ├─ goods-edit/goods-edit # ★ 商品新增/编辑(动态属性配置) +│ └─ order-manage/order-manage # 商户订单管理 +├─ community/ +│ ├─ list/list # 社区列表 +│ ├─ detail/detail # 社区详情(帖子/活动/成员 Tab) +│ ├─ create/create # 创建社区 +│ └─ activity/activity # 社区活动 + 报名 +├─ forum/ +│ ├─ list/list # 论坛板块 + 帖子列表 +│ ├─ detail/detail # 帖子详情 + 点赞/收藏/投票/评论 +│ └─ publish/publish # 发帖(板块/标题/正文/投票/匿名) +├─ confession/ +│ ├─ list/list # 表白墙时间线(最新/热门 Tab) +│ ├─ detail/detail # 表白详情 + 评论/举报 +│ └─ publish/publish # 匿名投稿 +├─ market/ +│ ├─ list/list # 二手商品列表(瀑布流 + 分类筛选) +│ ├─ detail/detail # 商品详情(价格/卖家/信用分) +│ ├─ publish/publish # 发布二手商品 +│ └─ my-goods/my-goods # 我的商品(出售中/已下架/已售出 Tab) +├─ errand/ +│ ├─ list/list # 跑腿任务列表 +│ ├─ publish/publish # 发布跑腿任务 +│ └─ detail/detail # 任务详情 / 接单 / 完成 +├─ custom/ +│ ├─ diy/diy # ★ DIY 自定义页面(服务端驱动渲染) +│ └─ h5/h5 # ★ H5 页面嵌入(web-view) +└─ user/ + ├─ profile/profile # 个人中心 + ├─ login/login # 登录/注册 + ├─ order/my-order # 我的订单(Tab 切换) + ├─ coupon/coupon # 优惠券 + ├─ favorite/favorite # 收藏(商品/店铺 Tab) + └─ settings/settings # 设置(资料/地址/角色/清缓存) +``` + +## 🔩 代码风格 + +- 所有页面:**React 函数组件 + Hooks** +- TypeScript **严格类型**(`any` 仅用于接口过渡) +- 统一组件库:`@tarojs/components` +- 路由:`Taro.navigateTo / switchTab / navigateBack` +- API 聚合于 `src/services/api.ts`,每个方法可独立 mock/测试 +- 请求工具:`utils/request.ts`,401 自动跳登录、网络失败自动降级 mock + +## 📜 License +MIT © 校园服务平台 diff --git a/taro-app/babel.config.js b/taro-app/babel.config.js new file mode 100644 index 0000000..3ac0cf7 --- /dev/null +++ b/taro-app/babel.config.js @@ -0,0 +1,13 @@ +// babel.config.js +module.exports = { + presets: [ + [ + 'taro', + { + framework: 'react', + ts: true, + useBuiltIns: false + } + ] + ] +} diff --git a/taro-app/config/index.ts b/taro-app/config/index.ts new file mode 100644 index 0000000..9a33e32 --- /dev/null +++ b/taro-app/config/index.ts @@ -0,0 +1,77 @@ +// config/index.ts - Taro 多端统一构建配置 +import path from 'path' + +const config = { + projectName: 'campus-service', + date: '2026-06-20', + designWidth: 750, + deviceRatio: { + 640: 2.34 / 2, + 750: 1, + 828: 1.81 / 2 + }, + sourceRoot: 'src', + outputRoot: 'dist', + plugins: [], + defineConstants: { + API_BASE: JSON.stringify('https://api.campus.example.com'), + APP_VERSION: JSON.stringify('1.0.0') + }, + copy: { + patterns: [], + options: {} + }, + framework: 'react', + compiler: 'webpack5', + sass: { + resource: [ + path.resolve(__dirname, '..', 'src/styles/variables.scss') + ] + }, + cache: { + enable: false + }, + mini: { + postcss: { + pxtransform: { enable: true, config: {} }, + cssModules: { + enable: false, + config: { namingPattern: 'module', generateScopedName: '[name]__[local]___[hash:base64:5]' } + } + } + }, + h5: { + publicPath: '/', + staticDirectory: 'static', + postcss: { + autoprefixer: { enable: true, config: { browsers: ['last 3 versions', 'Android >= 4.1', 'iOS >= 8'] } } + }, + devServer: { + port: 10086, + host: '0.0.0.0', + historyApiFallback: true, + hot: true + } + }, + rn: { + appName: 'campusApp', + output: { dir: 'android/app/src/main/assets', root: 'src', filename: 'index.bundle' } + } +} + +// 根据编译类型动态调整 outputRoot +const typeConfig = { + weapp: { outputRoot: 'dist/weapp' }, + alipay: { outputRoot: 'dist/alipay' }, + tt: { outputRoot: 'dist/tt' }, + qq: { outputRoot: 'dist/qq' }, + jd: { outputRoot: 'dist/jd' }, + swan: { outputRoot: 'dist/swan' }, + h5: { outputRoot: 'dist/h5' }, + rn: { outputRoot: 'dist/rn' } +} + +const TARO_ENV = process.env.TARO_ENV || 'weapp' +const envConfig = typeConfig[TARO_ENV] || typeConfig.weapp + +module.exports = { ...config, ...envConfig } diff --git a/taro-app/package.json b/taro-app/package.json new file mode 100644 index 0000000..0f4db59 --- /dev/null +++ b/taro-app/package.json @@ -0,0 +1,62 @@ +{ + "name": "campus-multi-platform", + "version": "1.0.0", + "private": true, + "description": "校园综合服务平台 - 多端小程序(微信/支付宝/抖音/H5)", + "main": "index.js", + "scripts": { + "build:weapp": "taro build --type weapp", + "dev:weapp": "npm run build:weapp -- --watch", + "build:alipay": "taro build --type alipay", + "dev:alipay": "npm run build:alipay -- --watch", + "build:tt": "taro build --type tt", + "dev:tt": "npm run build:tt -- --watch", + "build:h5": "taro build --type h5", + "dev:h5": "npm run build:h5 -- --watch", + "build:rn": "taro build --type rn", + "dev:rn": "npm run build:rn -- --watch", + "build:qq": "taro build --type qq", + "dev:qq": "npm run build:qq -- --watch", + "build:jd": "taro build --type jd", + "dev:jd": "npm run build:jd -- --watch", + "build:swan": "taro build --type swan", + "dev:swan": "npm run build:swan -- --watch" + }, + "browserslist": [ + "last 3 versions", + "Android >= 4.1", + "iOS >= 8" + ], + "dependencies": { + "@tarojs/components": "4.0.9", + "@tarojs/helper": "4.0.9", + "@tarojs/plugin-framework-react": "4.0.9", + "@tarojs/plugin-html": "4.0.9", + "@tarojs/plugin-platform-alipay": "4.0.9", + "@tarojs/plugin-platform-h5": "4.0.9", + "@tarojs/plugin-platform-jd": "4.0.9", + "@tarojs/plugin-platform-qq": "4.0.9", + "@tarojs/plugin-platform-swan": "4.0.9", + "@tarojs/plugin-platform-tt": "4.0.9", + "@tarojs/plugin-platform-weapp": "4.0.9", + "@tarojs/react": "4.0.9", + "@tarojs/runtime": "4.0.9", + "@tarojs/shared": "4.0.9", + "@tarojs/taro": "4.0.9", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@babel/core": "7.25.2", + "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", + "@tarojs/cli": "4.0.9", + "@tarojs/webpack5-runner": "4.0.9", + "@types/react": "18.3.3", + "@types/webpack-env": "1.18.5", + "babel-preset-taro": "4.0.9", + "sass": "1.77.8", + "sass-loader": "14.2.1", + "typescript": "5.5.3", + "webpack": "5.93.0" + } +} diff --git a/taro-app/project.config.weapp.json b/taro-app/project.config.weapp.json new file mode 100644 index 0000000..f7db221 --- /dev/null +++ b/taro-app/project.config.weapp.json @@ -0,0 +1,32 @@ +{ + "description": "校园综合服务平台 - 微信小程序", + "packOptions": { "ignore": [], "include": [] }, + "setting": { + "urlCheck": false, + "es6": true, + "enhance": true, + "postcss": true, + "minified": true, + "newFeature": true, + "autoAudits": false, + "coverView": true, + "showShadowRootInWxmlPanel": true, + "scopeDataCheck": false, + "uglifyFileName": false, + "checkInvalidKey": true, + "checkSiteMap": true, + "uploadWithSourceMap": true, + "compileHotReLoad": true, + "useIsolateContext": true, + "useMultiFrameRuntime": true, + "useApiHook": true, + "useApiHostProcess": true, + "babelSetting": { "ignore": [], "disablePlugins": [], "outputPath": "" } + }, + "compileType": "miniprogram", + "libVersion": "3.0.0", + "appid": "touristappid", + "projectname": "campus-weapp", + "miniprogramRoot": "./", + "condition": {} +} diff --git a/taro-app/src/app.config.ts b/taro-app/src/app.config.ts new file mode 100644 index 0000000..7e0755b --- /dev/null +++ b/taro-app/src/app.config.ts @@ -0,0 +1,61 @@ +export default defineAppConfig({ + pages: [ + 'pages/index/index', + 'pages/food/index/index', + 'pages/food/shop/shop', + 'pages/food/checkout/checkout', + 'pages/food/order-detail/order-detail', + 'pages/merchant/list/list', + 'pages/merchant/detail/detail', + 'pages/merchant/apply/apply', + 'pages/merchant/my-shop/my-shop', + 'pages/merchant/goods-manage/goods-manage', + 'pages/merchant/goods-edit/goods-edit', + 'pages/merchant/order-manage/order-manage', + 'pages/community/list/list', + 'pages/community/detail/detail', + 'pages/community/create/create', + 'pages/community/activity/activity', + 'pages/forum/list/list', + 'pages/forum/detail/detail', + 'pages/forum/publish/publish', + 'pages/confession/list/list', + 'pages/confession/detail/detail', + 'pages/confession/publish/publish', + 'pages/market/list/list', + 'pages/market/detail/detail', + 'pages/market/publish/publish', + 'pages/market/my-goods/my-goods', + 'pages/errand/list/list', + 'pages/errand/publish/publish', + 'pages/errand/detail/detail', + 'pages/custom/diy/diy', + 'pages/custom/h5/h5', + 'pages/user/profile/profile', + 'pages/user/login/login', + 'pages/user/order/my-order', + 'pages/user/coupon/coupon', + 'pages/user/favorite/favorite', + 'pages/user/settings/settings' + ], + window: { + backgroundTextStyle: 'light', + navigationBarBackgroundColor: '#ffffff', + navigationBarTitleText: '校园综合服务', + navigationBarTextStyle: 'black', + backgroundColor: '#f5f5f5' + }, + tabBar: { + color: '#999999', + selectedColor: '#1890ff', + backgroundColor: '#ffffff', + borderStyle: 'black', + list: [ + { pagePath: 'pages/index/index', text: '首页' }, + { pagePath: 'pages/food/index/index', text: '外卖' }, + { pagePath: 'pages/community/list/list', text: '社区' }, + { pagePath: 'pages/forum/list/list', text: '论坛' }, + { pagePath: 'pages/user/profile/profile', text: '我的' } + ] + } +}) diff --git a/taro-app/src/app.scss b/taro-app/src/app.scss new file mode 100644 index 0000000..30834ec --- /dev/null +++ b/taro-app/src/app.scss @@ -0,0 +1,314 @@ +// app 全局样式 - 兼容多端 +page { + --primary-color: #1890ff; + --primary-light: #e6f7ff; + --success-color: #52c41a; + --warning-color: #faad14; + --danger-color: #f5222d; + --text-color: #333; + --text-secondary: #666; + --text-light: #999; + --border-color: #e8e8e8; + --bg-color: #f5f5f5; + --card-bg: #fff; + --shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06); + + background-color: var(--bg-color); + color: var(--text-color); + font-size: 28rpx; + line-height: 1.5; + font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +view, text, input, textarea, button, image { + box-sizing: border-box; +} + +.container { + padding: 20rpx; +} + +.card { + background: var(--card-bg); + border-radius: 16rpx; + padding: 24rpx; + margin: 20rpx; + box-shadow: var(--shadow); +} + +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20rpx; + padding-bottom: 16rpx; + border-bottom: 1rpx solid var(--border-color); +} + +.card-title { + font-size: 32rpx; + font-weight: bold; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 16rpx 40rpx; + border-radius: 48rpx; + font-size: 28rpx; + background: var(--primary-color); + color: #fff; + min-height: 72rpx; + border: none; +} + +.btn-block { + display: flex; + width: 100%; +} + +.btn-outline { + background: #fff; + color: var(--primary-color); + border: 2rpx solid var(--primary-color); +} + +.btn-success { background: var(--success-color); } +.btn-warning { background: var(--warning-color); } +.btn-danger { background: var(--danger-color); } + +.tag { + display: inline-block; + padding: 4rpx 16rpx; + border-radius: 8rpx; + font-size: 22rpx; + background: var(--primary-light); + color: var(--primary-color); + margin-right: 12rpx; +} + +.tag-success { background: #f6ffed; color: #52c41a; } +.tag-warning { background: #fffbe6; color: #faad14; } +.tag-danger { background: #fff1f0; color: #f5222d; } + +.text-primary { color: var(--primary-color); } +.text-success { color: var(--success-color); } +.text-warning { color: var(--warning-color); } +.text-danger { color: var(--danger-color); } +.text-secondary { color: var(--text-secondary); } +.text-light { color: var(--text-light); } +.text-bold { font-weight: bold; } +.text-lg { font-size: 32rpx; } +.text-xl { font-size: 40rpx; } +.text-sm { font-size: 24rpx; } + +.flex { display: flex; } +.flex-center { display: flex; align-items: center; justify-content: center; } +.flex-between { display: flex; align-items: center; justify-content: space-between; } +.flex-col { display: flex; flex-direction: column; } +.flex-1 { flex: 1; min-width: 0; } +.align-center { align-items: center; } +.justify-between { justify-content: space-between; } +.flex-wrap { flex-wrap: wrap; } + +.mt-10 { margin-top: 10rpx; } +.mt-20 { margin-top: 20rpx; } +.mt-30 { margin-top: 30rpx; } +.mb-10 { margin-bottom: 10rpx; } +.mb-20 { margin-bottom: 20rpx; } +.mb-30 { margin-bottom: 30rpx; } +.p-20 { padding: 20rpx; } + +.list-item { + padding: 24rpx; + background: #fff; + border-bottom: 1rpx solid var(--border-color); +} + +.price { + color: var(--danger-color); + font-weight: bold; + font-size: 32rpx; +} + +.price-lg { font-size: 40rpx; } + +.thumb { + width: 160rpx; + height: 160rpx; + background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; +} + +.rating { color: var(--warning-color); font-size: 24rpx; } + +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 32rpx; + height: 32rpx; + padding: 0 8rpx; + border-radius: 16rpx; + background: var(--danger-color); + color: #fff; + font-size: 20rpx; +} + +.empty { + padding: 100rpx 40rpx; + text-align: center; + color: var(--text-light); +} + +.status-badge { + padding: 6rpx 16rpx; + border-radius: 8rpx; + font-size: 22rpx; +} +.status-pending { background: #fff7e6; color: #fa8c16; } +.status-paid { background: #e6f7ff; color: #1890ff; } +.status-making { background: #f6ffed; color: #52c41a; } +.status-delivering { background: #fff1f0; color: #f5222d; } +.status-completed { background: #f0f0f0; color: #666; } +.status-cancelled { background: #f5f5f5; color: #999; } + +.input { + width: 100%; + padding: 20rpx 24rpx; + background: #f8f8f8; + border-radius: 8rpx; + font-size: 28rpx; + border: none; +} + +.textarea { + width: 100%; + padding: 20rpx; + min-height: 160rpx; + background: #f8f8f8; + border-radius: 8rpx; + font-size: 28rpx; +} + +// 属性选择组件 +.attr-group { margin: 20rpx 0; } +.attr-name { font-size: 28rpx; font-weight: bold; margin-bottom: 16rpx; } +.attr-options { display: flex; flex-wrap: wrap; gap: 16rpx; } +.attr-option { + padding: 12rpx 24rpx; + background: #f5f5f5; + border-radius: 8rpx; + font-size: 26rpx; + border: 2rpx solid transparent; +} +.attr-option.selected { + background: var(--primary-light); + color: var(--primary-color); + border-color: var(--primary-color); +} + +.attr-number { display: flex; align-items: center; gap: 16rpx; } +.attr-number .value { + min-width: 80rpx; + text-align: center; + font-size: 32rpx; + font-weight: bold; + color: var(--primary-color); +} + +// 搜索栏样式 +.search-bar { + display: flex; + align-items: center; + padding: 16rpx 24rpx; + background: #f5f5f5; + border-radius: 40rpx; + margin: 20rpx; +} + +// 顶部 Banner +.banner { + height: 280rpx; + margin: 20rpx; + border-radius: 16rpx; + background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 36rpx; + font-weight: bold; +} + +// Tab栏 +.tabs { + display: flex; + background: #fff; + border-bottom: 1rpx solid var(--border-color); + position: sticky; + top: 0; + z-index: 10; +} + +.tab-item { + flex: 1; + text-align: center; + padding: 24rpx 0; + font-size: 28rpx; + color: var(--text-secondary); + position: relative; +} + +.tab-item.active { + color: var(--primary-color); + font-weight: bold; +} + +.tab-item.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 48rpx; + height: 4rpx; + background: var(--primary-color); + border-radius: 2rpx; +} + +// 网格菜单 +.grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24rpx 0; +} + +.grid-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; + padding: 16rpx 0; +} + +.grid-icon { + width: 88rpx; + height: 88rpx; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + background: #e6f7ff; +} + +.grid-name { + font-size: 24rpx; + color: #666; +} diff --git a/taro-app/src/app.tsx b/taro-app/src/app.tsx new file mode 100644 index 0000000..2707897 --- /dev/null +++ b/taro-app/src/app.tsx @@ -0,0 +1,16 @@ +import { Component, PropsWithChildren } from 'react' +import './app.scss' + +class App extends Component { + componentDidMount() {} + + componentDidShow() {} + + componentDidHide() {} + + render() { + return this.props.children + } +} + +export default App diff --git a/taro-app/src/global.d.ts b/taro-app/src/global.d.ts new file mode 100644 index 0000000..8a4f9fd --- /dev/null +++ b/taro-app/src/global.d.ts @@ -0,0 +1,35 @@ +/// + +declare module '*.png'; +declare module '*.gif'; +declare module '*.jpg'; +declare module '*.jpeg'; +declare module '*.svg'; +declare module '*.css'; +declare module '*.less'; +declare module '*.scss'; +declare module '*.sass'; +declare module '*.styl'; + +declare namespace JSX { + interface IntrinsicElements { + view: any + text: any + image: any + button: any + input: any + input: any + textarea: any + scroll-view: any + swiper: any + swiper-item: any + swiper-slide: any + } +} + +declare const API_BASE: string; +declare const APP_VERSION: string; + +declare interface AnyObject { + [key: string]: any +} diff --git a/taro-app/src/pages/community/activity/activity.config.ts b/taro-app/src/pages/community/activity/activity.config.ts new file mode 100644 index 0000000..76ae6f1 --- /dev/null +++ b/taro-app/src/pages/community/activity/activity.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '社区活动', + backgroundColor: '#f5f5f5' +}) diff --git a/taro-app/src/pages/community/activity/activity.scss b/taro-app/src/pages/community/activity/activity.scss new file mode 100644 index 0000000..ef7c20b --- /dev/null +++ b/taro-app/src/pages/community/activity/activity.scss @@ -0,0 +1,46 @@ +.page-scroll { + min-height: 100vh; + background: #f5f5f5; + padding-bottom: 40rpx; +} + +.hero { + background: linear-gradient(135deg, #eb2f96 0%, #ffadd2 100%); + color: #fff; + padding: 50rpx 30rpx; +} +.hero-title { font-size: 38rpx; font-weight: bold; display: block; } +.hero-sub { font-size: 26rpx; color: rgba(255,255,255,0.9); margin-top: 10rpx; display: block; } + +.act-card { + background: #fff; + border-radius: 16rpx; + margin: 20rpx; + padding: 24rpx; + box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); +} + +.act-title { font-size: 32rpx; font-weight: bold; display: block; } +.act-desc { font-size: 26rpx; color: #666; margin-top: 10rpx; display: block; } + +.act-info { + padding: 20rpx 0; + margin-top: 16rpx; + border-top: 2rpx dashed #f0f0f0; +} +.act-line { font-size: 24rpx; color: #666; display: block; padding: 6rpx 0; } + +.act-btn { + background: #eb2f96 !important; + color: #fff !important; + border: none !important; + border-radius: 40rpx !important; + padding: 18rpx 0 !important; + font-size: 28rpx !important; + font-weight: bold; + margin-top: 10rpx !important; + width: 100%; +} + +.empty { text-align: center; padding: 120rpx 0; color: #999; font-size: 26rpx; } +.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; } diff --git a/taro-app/src/pages/community/activity/activity.tsx b/taro-app/src/pages/community/activity/activity.tsx new file mode 100644 index 0000000..3297b14 --- /dev/null +++ b/taro-app/src/pages/community/activity/activity.tsx @@ -0,0 +1,50 @@ +import { useEffect, useState } from 'react' +import { View, Text, ScrollView, Button } from '@tarojs/components' +import Taro from '@tarojs/taro' +import { api } from '../../../services/api' +import { showToast, formatTime } from '../../../utils' +import './index.scss' + +interface Activity { id: number; title: string; location: string; startTime: number; joined: number; limit: number; desc: string } + +export default function Activity() { + const [list, setList] = useState([]) + + useEffect(() => { loadData() }, []) + + async function loadData() { + try { + const r = await api.community.activities() + setList((r as any).list || r || []) + } catch (e) { console.error(e) } + } + + function join(a: Activity) { + showToast('已报名 ' + a.title, 'success') + setList(prev => prev.map(x => x.id === a.id ? { ...x, joined: x.joined + 1 } : x)) + } + + return ( + + + 🎉 社区活动 + 精彩活动,与同好相聚 + + + {list.map((a: Activity) => ( + + {a.title} + {a.desc} + + 📍 {a.location} + 🕐 {formatTime(new Date(a.startTime || Date.now()))} + 👥 {a.joined}/{a.limit} 人已报名 + + + + ))} + + {list.length === 0 && 🎉暂无活动,敬请期待} + + ) +} diff --git a/taro-app/src/pages/community/create/create.config.ts b/taro-app/src/pages/community/create/create.config.ts new file mode 100644 index 0000000..6d9103c --- /dev/null +++ b/taro-app/src/pages/community/create/create.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '创建社区', + backgroundColor: '#f5f5f5' +}) diff --git a/taro-app/src/pages/community/create/create.scss b/taro-app/src/pages/community/create/create.scss new file mode 100644 index 0000000..3521f05 --- /dev/null +++ b/taro-app/src/pages/community/create/create.scss @@ -0,0 +1,80 @@ +.page-scroll { + min-height: 100vh; + background: #f5f5f5; + padding-bottom: 180rpx; +} + +.card { + background: #fff; + border-radius: 16rpx; + padding: 24rpx; + margin: 20rpx; + box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); +} + +.label { + display: block; + font-size: 26rpx; + color: #333; + font-weight: bold; + margin: 20rpx 0 12rpx; +} +.label:first-child { margin-top: 0; } + +.input { + background: #f5f5f5; + border-radius: 12rpx; + padding: 20rpx; + font-size: 28rpx; + width: 100%; + box-sizing: border-box; +} + +.textarea { + width: 100%; + min-height: 240rpx; + background: #f5f5f5; + border-radius: 12rpx; + padding: 20rpx; + font-size: 28rpx; + box-sizing: border-box; +} + +.cat-grid { + display: flex; + flex-wrap: wrap; + gap: 16rpx; +} +.cat-tag { + padding: 12rpx 24rpx; + background: #f5f5f5; + border-radius: 30rpx; + font-size: 26rpx; + color: #666; + border: 2rpx solid transparent; +} +.cat-active { + background: #f9f0ff; + color: #722ed1; + border-color: #722ed1; + font-weight: bold; +} + +.submit-area { + position: fixed; + left: 0; right: 0; bottom: 0; + background: #fff; + padding: 20rpx 30rpx; + box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.05); +} + +.submit-btn { + background: #722ed1 !important; + color: #fff !important; + border: none !important; + border-radius: 40rpx !important; + padding: 20rpx 0 !important; + font-size: 30rpx !important; + font-weight: bold; + width: 100%; +} diff --git a/taro-app/src/pages/community/create/create.tsx b/taro-app/src/pages/community/create/create.tsx new file mode 100644 index 0000000..b2e64b1 --- /dev/null +++ b/taro-app/src/pages/community/create/create.tsx @@ -0,0 +1,50 @@ +import { useState } from 'react' +import { View, Text, ScrollView, Button, Input, Textarea } from '@tarojs/components' +import Taro from '@tarojs/taro' +import { api } from '../../../services/api' +import { showToast } from '../../../utils' +import './index.scss' + +export default function CreateCommunity() { + const [name, setName] = useState('') + const [category, setCategory] = useState('学习') + const [desc, setDesc] = useState('') + + const cats = ['学习', '运动', '音乐', '游戏', '摄影', '旅行', '美食', '阅读'] + + async function submit() { + if (!name || !desc) { showToast('请填写完整信息'); return } + try { + Taro.showLoading({ title: '创建中', mask: true }) + await api.community.create({ name, category, desc }) + Taro.hideLoading() + showToast('创建成功', 'success') + setTimeout(() => Taro.navigateBack(), 1200) + } catch (e) { Taro.hideLoading(); showToast('创建失败', 'error') } + } + + return ( + + + 社区名称 * + setName(e.detail.value)} /> + + 社区类目 * + + {cats.map(c => ( + setCategory(c)}> + {c} + + ))} + + + 社区简介 * +