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 @@ + + + + + + 校园综合服务 + 外卖 · 跑腿 · 社区 · 论坛 + + + + 微信登录 + 手机号 + 注册 + + + + + + + + + + + + © 2025 校园综合服务 · 让校园更美好 + + 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/preview.html b/preview.html new file mode 100644 index 0000000..2ba8f05 --- /dev/null +++ b/preview.html @@ -0,0 +1,671 @@ + + + + + +校园综合服务平台 - 功能预览 + + + + + +
+

🏫 校园综合服务平台

+

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

+
+ +
+ 📱 功能预览 + ✨ 功能模块 + ⚙️ 管理后台 + 🔌 API +
+ + +
+
+
+
+
+
+ + + +
🎉 校园外卖新上线,下单立减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/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-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 new file mode 100644 index 0000000..f6cbaaf --- /dev/null +++ b/server/package.json @@ -0,0 +1,21 @@ +{ + "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", + "jsonwebtoken": "^9.0.0", + "multer": "^1.4.4-lts.1", + "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/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} + + ))} + + + 社区简介 * +