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.location}}
+ 📅 即将开始
+
+ {{item.description}}
+
+
+
+
+
+ 🎯
+ 暂无活动
+
+
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.location}}
+ 📅 即将开始
+
+
+
+ 🎯
+ 暂无活动
+
+
+
+
+
+
+ 👤
+ 成员 {{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}}
+
+
+
+
+
+
+
+ 🎯
+ 暂无社区
+
+
+ +
+
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.content}}
+
+
+ ❤️ {{confession.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.content}}
+
+
+
+
+
+ 💌
+ 暂无表白
+
+
+ +
+
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.pickup}}
+ →
+ {{item.delivery}}
+
+
+
+
+
+
+ 🏃
+ 暂无任务
+ 发布新任务
+
+
+ +
+
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}}
+
+
+
+
+
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}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{attr.name}}
+ *
+ (可选{{attr.maxSelect}}项)
+ (+¥{{attr.pricePerAddon}}/项)
+
+
+
+
+ {{opt}}
+
+
+
+
+ {{opt}}
+
+
+
+
+ {{attr.min}}-{{attr.max}}
+
+
+ {{selectedAttrs[attr.name].value}}
+
+
+ 步长{{attr.step || 1}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🏪
+ 加载中...
+
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.board}}
+
+ {{post.title}}
+
+ 作者: {{post.author}}
+ 👁️ {{post.views}}
+
+ {{post.content}}
+
+
+ 👍 {{post.likes}}
+
+
+ ⭐ 收藏
+
+
+ 📤 分享
+
+
+
+
+ {{post.vote.title}}
+
+ {{item.text}}
+ {{item.count}} 票
+
+ ✅ 你已投票
+
+
+
+
+
+
+
+
+
+
+
+ 📚
+ 加载中...
+
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.board}}
+
+ {{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.title}}
+
+
+
+
+
+
+ 快捷功能
+
+
+
+ {{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}}
+
+
+
+
+
+
+
+ 🛒
+ 暂无商品
+
+
+ +
+
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}}
+
+
+
+
+
+ 现价 (¥)
+
+
+
+ 原价 (¥)
+
+
+
+
+ 库存
+
+
+
+ 商品描述
+
+
+
+
+
+
+
+
+
+ 属性名称
+
+
+
+ 类型
+
+
+ {{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.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 @@
+
+
+
+ 校
+
+ 校园综合服务
+ 外卖 · 跑腿 · 社区 · 论坛
+
+
+
+ 微信登录
+ 手机号
+ 注册
+
+
+
+
+ 一键登录,尽享校园生活
+
+ 选择角色 (演示)
+
+
+ {{item.name}}
+
+
+
+
+
+
+
+
+ 手机号
+
+
+
+ 验证码
+
+
+
+
+
+
+
+ 已阅读并同意
+ 《用户协议》
+ 和
+ 《隐私政策》
+
+
+
+
+
+
+ 昵称
+
+
+
+ 手机号
+
+
+
+ 密码
+
+
+
+ 确认密码
+
+
+
+
+ 已阅读并同意
+ 《用户协议》
+
+
+
+
+
+
+
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}}
+
+
+
+
+
+
+
+ 🍽️
+
+ {{g.name || '商品'}}
+ {{g.spec}}
+
+
+ ¥{{g.price}}
+ ×{{g.count}}
+
+
+
+
+ 取消订单
+ 立即付款
+ 查看详情
+ 确认收货
+ 申请退款
+ 再次购买
+
+ {{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 @@
+
+
+
+
+
+
+
+ 💰
+ {{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 @@
+
+
+
+
+
+校园综合服务平台 - 功能预览
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🎉 校园外卖新上线,下单立减5元
+
+
+
+
+
+ 🔥 热门商户
+ 查看更多 ›
+
+
+
🍔
+
+
麦香汉堡★ 4.8
+
月销1200 · 起送¥15 · 配送¥3
+
+ 餐饮
+ 支持外卖
+
+
+
+
+
🧋
+
+
鲜饮茶铺★ 4.6
+
月销850 · 起送¥10 · 配送¥2
+
+ 餐饮
+ 新品
+
+
+
+
+
+
+
+ 💬 校园热议
+ 全部 ›
+
+
+
+
+ 精华
+ 图书馆新增自习区域开放啦
+
+
同学A · 1小时前
+
+ 👍 120
+ 💬 35
+
+
+
+
+
+
+
+
🏠首页
+
🍔外卖
+
🎯社区
+
📚论坛
+
👤我的
+
+
+
+
+
+
+
+
+
🍔
+
+
外卖点餐
+
支持多规格商品属性(单选/多选/数字输入),实时价格计算,购物车管理
+
+
+
+
🏪
+
+
多商户入驻
+
商户提交资质→平台审核→入驻成功→商品管理→订单处理
+
+
+
+
🏃
+
+
校园跑腿
+
发布任务→骑手接单→实时位置追踪→完成确认
+
+
+
+
🎯
+
+
兴趣社区
+
创建/加入社区,发帖互动,活动发布与报名,签到统计
+
+
+
+
💌
+
+
表白墙
+
匿名投稿→审核机制→时间线展示→互动评论
+
+
+
+
🛍️
+
+
二手市场
+
商品发布→分类筛选→议价功能→交易担保
+
+
+
+
🖨️
+
+
云打印机
+
支持飞鹅/易联云/商米/芯烨/通用HTTP云打印,自动生成小票
+
+
+
+
✨
+
+
DIY自定义
+
运营后台拖拽配置页面组件,支持嵌入H5,脱离代码发版
+
+
+
+
+
+
+
+
+
🍔 外卖点餐(支持商品多规格属性)
+
+ 商品属性系统支持4种类型:
+ ① 单选(select) - 如口味:原味/香辣/黑椒
+ ② 多选(multiselect) - 如加料:芝士+3元、培根+5元(可配置最多选几项)
+ ③ 数字(number) - 如辣度1-10(可配置范围和步长)
+ ④ 文本(text) - 备注信息(可配置最大长度)
+ 每种属性可设为必填,价格实时计算(基础价 + 附加价×数量)
+
+
+ 示例:经典牛肉汉堡 ¥28
+ ├─ 口味(必选): 原味 | 香辣 ← 点击选择
+ ├─ 加料: 芝士(+3) | 培根(+5) | 鸡蛋(+3) ← 可多选
+ ├─ 辣度: [-] 5 [+] ← 数字步进器
+ └─ 备注: [输入框] ← 文本输入
+ 最终价格: ¥28 + ¥3(芝士) + ¥5(培根) = ¥36
+
+
+
+
+
🖨️ 通用云打印机集成
+
+ 支持5大品牌云打印机对接:
+ • 飞鹅云 - 国内最主流,API地址: api.feieyun.cn
+ • 易联云 - 高性价比,API地址: open-api.10ss.net
+ • 商米云 - 硬件大厂,API地址: api.sunmi.com
+ • 芯烨Xprinter - 经典品牌,API地址: api.xprinter.cn
+ • 通用HTTP - 任意RESTful API对接
+ 订单创建后自动生成小票内容(含商品列表、收货信息、二维码),调用云端打印。
+
+
+
+
+
✨ DIY自定义页面系统
+
+ 后端配置页面组件数组,前端动态渲染,无需发版:
+ 支持组件:title / richtext / text / image / button / goods-list / divider / empty
+ 按钮支持跳转小程序页面或外部H5链接
+ 可在运营后台拖拽配置,自定义首页、活动页等
+
+
+
+
+
📋 六大业务模块总览
+
+ ① 多商户服务 - 商户入驻/审核/商品管理/订单处理/数据统计
+ ② 校园外卖 - 购物车/多规格属性/结算/微信支付/订单跟踪
+ ③ 跑腿服务 - 任务发布/骑手抢单/实时位置/完成确认
+ ④ 兴趣社区 - 创建社区/发帖互动/活动报名/签到统计
+ ⑤ 校园论坛 - 板块分类/发帖回复/点赞收藏/投票/精华置顶
+ ⑥ 表白墙 - 匿名投稿/审核机制/时间线/评论互动
+ ⑦ 二手市场 - 商品发布/分类议价/收藏关注/信用评分
+ ⑧ H5嵌入 - web-view组件承载外部页面,支持分享
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1,280
+
今日订单
+
↑ 12.5%
+
+
+
¥35,800
+
今日销售额
+
↑ 8.3%
+
+
+
+
+
+
+
+
📋 待审核商户入驻申请
+
+
+
+ | 店铺名称 |
+ 申请人 |
+ 联系方式 |
+ 申请时间 |
+ 状态 |
+ 操作 |
+
+
+
+
+ | 张姐麻辣烫 |
+ 张丽 |
+ 138****1234 |
+ 2026-06-20 |
+ 待审核 |
+
+
+
+ |
+
+
+ | 学霸打印店 |
+ 李明 |
+ 139****5678 |
+ 2026-06-19 |
+ 已通过 |
+ — |
+
+
+ | 校园水果店 |
+ 王强 |
+ 137****9012 |
+ 2026-06-18 |
+ 已拒绝 |
+ — |
+
+
+
+
+
+
+
+
📦 最新订单
+
+
+
+ | 订单号 |
+ 店铺 |
+ 金额 |
+ 状态 |
+ 时间 |
+ 操作 |
+
+
+
+
+ | OD10005 |
+ 麦香汉堡 |
+ ¥36.00 |
+ 配送中 |
+ 06-20 14:32 |
+ |
+
+
+ | OD10004 |
+ 鲜饮茶铺 |
+ ¥22.00 |
+ 已完成 |
+ 06-20 12:18 |
+ |
+
+
+ | OD10003 |
+ 校园便利 |
+ ¥15.50 |
+ 已完成 |
+ 06-20 11:05 |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
🔌 后端 API 服务
+
+ 后端服务已启动: http://localhost:3000
+ 接口列表 (Node.js + Express + 内存数据库):
+ GET /health - 服务健康检查
+ GET /api/user/profile - 获取用户信息
+ POST /api/user/login - 用户登录
+ GET /api/merchant/list - 商户列表
+ GET /api/merchant/detail/:id - 商户详情
+ POST /api/merchant/apply - 商户入驻申请
+ GET /api/goods/list - 商品列表
+ POST /api/goods - 创建商品
+ PUT /api/goods/:id - 更新商品
+ GET /api/order/list - 订单列表
+ POST /api/order - 创建订单
+ POST /api/order/:id/pay - 订单支付
+ POST /api/order/:id/print - 云打印小票
+ GET /api/community/list - 社区列表
+ GET /api/forum/boards - 论坛板块
+ GET /api/forum/list - 帖子列表
+ GET /api/confession/list - 表白墙列表
+ GET /api/market/list - 二手市场列表
+ GET /api/diy/list - DIY页面列表
+ GET /api/coupon/list - 优惠券列表
+ POST /api/payment/prepay - 微信支付预下单
+ GET /api/errand/list - 跑腿任务列表
+ POST /api/errand - 发布跑腿任务
+ POST /api/errand/:id/accept - 骑手接单
+ POST /api/errand/:id/complete - 任务完成
+
+
+
+
+
🗄️ 数据库设计
+
+ 核心数据表:
+ users - id, nickname, phone, role, level, points, balance
+ merchants - id, name, category, ownerId, status, rating, sales, address, phone
+ goods - id, shopId, name, price, attrs(JSON多规格), stock, status
+ orders - id, userId, shopId, goods(JSON), totalPrice, payPrice, status, address, logs
+ communities - id, name, category, ownerId, members, posts, tags
+ forum_posts - id, boardId, userId, title, content, likes, isTop, isEssence, vote
+ confessions - id, content, images, likes, comments, isAnonymous, status
+ market_goods - id, title, price, category, sellerId, views, likes, status
+ errands - id, type, title, pickup, delivery, fee, status, publisherId, runnerId
+ coupons - id, name, type, minOrder, discount, validFrom, validTo
+ diy_pages - id, title, components(JSON), isH5, h5Url
+ printers - id, vendor, sn, name, status
+
+
+
+
+
+
+
+
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}
+
+ ))}
+
+
+ 社区简介 *
+
+
+
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/community/detail/detail.config.ts b/taro-app/src/pages/community/detail/detail.config.ts
new file mode 100644
index 0000000..1244fde
--- /dev/null
+++ b/taro-app/src/pages/community/detail/detail.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '社区详情',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/community/detail/detail.scss b/taro-app/src/pages/community/detail/detail.scss
new file mode 100644
index 0000000..49dc5c5
--- /dev/null
+++ b/taro-app/src/pages/community/detail/detail.scss
@@ -0,0 +1,64 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 40rpx;
+}
+
+.hero {
+ background: linear-gradient(135deg, #722ed1 0%, #b37feb 100%);
+ color: #fff;
+ padding: 50rpx 30rpx;
+ text-align: center;
+}
+.hero-icon { font-size: 80rpx; display: block; }
+.hero-title { font-size: 38rpx; font-weight: bold; margin-top: 16rpx; display: block; }
+.hero-sub { font-size: 26rpx; color: rgba(255,255,255,0.9); margin-top: 10rpx; display: block; }
+
+.hero-stats {
+ display: flex;
+ margin-top: 30rpx;
+ background: rgba(255,255,255,0.15);
+ border-radius: 16rpx;
+ padding: 20rpx 0;
+}
+.stat { flex: 1; }
+.stat-num { font-size: 32rpx; font-weight: bold; display: block; }
+.stat-label { font-size: 22rpx; color: rgba(255,255,255,0.85); }
+
+.actions {
+ background: #fff;
+ margin: 20rpx;
+ padding: 20rpx;
+ border-radius: 16rpx;
+}
+
+.join-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%;
+}
+.join-btn.joined { background: #52c41a !important; }
+
+.card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin: 0 20rpx 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.card-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
+
+.post-item {
+ padding: 20rpx 0;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.post-item:last-child { border-bottom: none; }
+.post-title { font-size: 28rpx; font-weight: bold; display: block; }
+.post-meta { display: flex; gap: 20rpx; margin-top: 12rpx; font-size: 22rpx; color: #999; }
+
+.empty { text-align: center; padding: 60rpx 0; color: #999; font-size: 26rpx; }
diff --git a/taro-app/src/pages/community/detail/detail.tsx b/taro-app/src/pages/community/detail/detail.tsx
new file mode 100644
index 0000000..4e8c1ac
--- /dev/null
+++ b/taro-app/src/pages/community/detail/detail.tsx
@@ -0,0 +1,71 @@
+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, timeAgo } from '../../../utils'
+import './index.scss'
+
+interface Post { id: number; title: string; author: string; likes: number; comments: number; createdAt: number }
+
+export default function CommunityDetail() {
+ const router = Taro.useRouter()
+ const id = Number(router.params?.id || 1)
+ const [detail, setDetail] = useState(null)
+ const [posts, setPosts] = useState([])
+ const [joined, setJoined] = useState(false)
+
+ useEffect(() => { loadData() }, [id])
+
+ async function loadData() {
+ try {
+ const [d, p] = await Promise.all([api.community.detail(id), api.community.posts(id)])
+ setDetail(d)
+ setPosts((p as any).list || p || [])
+ } catch (e) { console.error(e) }
+ }
+
+ async function join() {
+ try {
+ await api.community.join(id)
+ setJoined(true)
+ showToast('已加入', 'success')
+ } catch (e) { showToast('操作失败', 'error') }
+ }
+
+ return (
+
+
+ 🎨
+ {detail?.name || '兴趣社区'}
+ {detail?.desc || '一群志趣相投的伙伴'}
+
+ {detail?.members || 100}成员
+ {posts.length}帖子
+ ⭐精选
+
+
+
+
+
+
+
+
+ 🔥 社区动态
+ {posts.map((p: Post) => (
+ Taro.navigateTo({ url: '/pages/forum/detail/detail?id=' + p.id })}>
+ {p.title}
+
+ {p.author}
+ {timeAgo(p.createdAt || Date.now())}
+ 👍 {p.likes}
+ 💬 {p.comments}
+
+
+ ))}
+ {posts.length === 0 && 暂无动态,快来发布第一篇}
+
+
+ )
+}
diff --git a/taro-app/src/pages/community/list/list.config.ts b/taro-app/src/pages/community/list/list.config.ts
new file mode 100644
index 0000000..5229c10
--- /dev/null
+++ b/taro-app/src/pages/community/list/list.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '兴趣社区',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/community/list/list.scss b/taro-app/src/pages/community/list/list.scss
new file mode 100644
index 0000000..441c9e6
--- /dev/null
+++ b/taro-app/src/pages/community/list/list.scss
@@ -0,0 +1,68 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 40rpx;
+}
+
+.hero {
+ background: linear-gradient(135deg, #722ed1 0%, #b37feb 100%);
+ color: #fff;
+ padding: 40rpx 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; }
+
+.quick-row {
+ display: flex;
+ background: #fff;
+ margin: 20rpx;
+ border-radius: 16rpx;
+ padding: 24rpx 0;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.quick-item {
+ flex: 1;
+ display: flex; flex-direction: column; align-items: center;
+ gap: 10rpx;
+}
+.quick-icon { font-size: 44rpx; }
+.quick-text { font-size: 24rpx; color: #333; }
+
+.comm-card {
+ display: flex;
+ align-items: center;
+ background: #fff;
+ margin: 0 20rpx 20rpx;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.comm-logo {
+ width: 110rpx; height: 110rpx;
+ background: linear-gradient(135deg, #d3adf7 0%, #b37feb 100%);
+ border-radius: 20rpx;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 44rpx;
+ color: #fff;
+ flex-shrink: 0;
+}
+
+.comm-body { flex: 1; margin-left: 20rpx; min-width: 0; }
+.comm-name { font-size: 30rpx; font-weight: bold; display: block; }
+.comm-desc { font-size: 24rpx; color: #666; margin-top: 8rpx; display: block; }
+.comm-meta { display: flex; gap: 16rpx; margin-top: 16rpx; }
+.comm-cat { background: #f9f0ff; color: #722ed1; padding: 4rpx 14rpx; border-radius: 20rpx; font-size: 22rpx; }
+.comm-members { font-size: 22rpx; color: #999; }
+
+.join-btn {
+ background: #722ed1 !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 30rpx !important;
+ padding: 10rpx 24rpx !important;
+ font-size: 24rpx !important;
+ flex-shrink: 0;
+}
+
+.empty { text-align: center; padding: 100rpx 0; color: #999; font-size: 26rpx; }
diff --git a/taro-app/src/pages/community/list/list.tsx b/taro-app/src/pages/community/list/list.tsx
new file mode 100644
index 0000000..5d3ef7a
--- /dev/null
+++ b/taro-app/src/pages/community/list/list.tsx
@@ -0,0 +1,70 @@
+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 } from '../../../utils'
+import './index.scss'
+
+interface Community { id: number; name: string; desc: string; members: number; category: string }
+
+export default function CommunityList() {
+ const [list, setList] = useState([])
+
+ useEffect(() => { loadData() }, [])
+
+ async function loadData() {
+ try {
+ const r = await api.community.list()
+ setList((r as any).list || r || [])
+ } catch (e) { console.error(e) }
+ }
+
+ function onDetail(id: number) { Taro.navigateTo({ url: '/pages/community/detail/detail?id=' + id }) }
+ function onActivity() { Taro.navigateTo({ url: '/pages/community/activity/activity' }) }
+ function onCreate() { Taro.navigateTo({ url: '/pages/community/create/create' }) }
+
+ return (
+
+
+ 🎯 兴趣社区
+ 找到你的同好,一起交流成长
+
+
+
+
+ ✍️
+ 创建社区
+
+
+ 🎉
+ 社区活动
+
+ Taro.navigateTo({ url: '/pages/forum/list/list' })}>
+ 💬
+ 校园论坛
+
+ Taro.navigateTo({ url: '/pages/confession/list/list' })}>
+ 💌
+ 表白墙
+
+
+
+ {list.map((c: Community) => (
+ onDetail(c.id)}>
+ 🎨
+
+ {c.name}
+ {c.desc}
+
+ {c.category}
+ {c.members || 0} 位成员
+
+
+
+
+ ))}
+
+ {list.length === 0 && 暂无社区,快来创建第一个}
+
+ )
+}
diff --git a/taro-app/src/pages/confession/detail/detail.config.ts b/taro-app/src/pages/confession/detail/detail.config.ts
new file mode 100644
index 0000000..c9358c8
--- /dev/null
+++ b/taro-app/src/pages/confession/detail/detail.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '表白详情',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/confession/detail/detail.scss b/taro-app/src/pages/confession/detail/detail.scss
new file mode 100644
index 0000000..0ce7d55
--- /dev/null
+++ b/taro-app/src/pages/confession/detail/detail.scss
@@ -0,0 +1,81 @@
+.page-scroll {
+ min-height: 100vh;
+ background: linear-gradient(180deg, #fff0f6 0%, #f5f5f5 200rpx);
+ padding-bottom: 180rpx;
+}
+
+.card {
+ background: #fff;
+ margin: 20rpx;
+ padding: 30rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.head { display: flex; align-items: center; }
+.avatar {
+ width: 72rpx; height: 72rpx;
+ background: linear-gradient(135deg, #f5222d 0%, #ffadd2 100%);
+ border-radius: 50%;
+ color: #fff;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 28rpx; font-weight: bold;
+ flex-shrink: 0;
+}
+.head-body { margin-left: 16rpx; }
+.author { font-size: 28rpx; font-weight: bold; display: block; }
+.time { font-size: 22rpx; color: #999; margin-top: 4rpx; display: block; }
+
+.title { font-size: 36rpx; font-weight: bold; color: #f5222d; margin-top: 24rpx; display: block; }
+.content { font-size: 28rpx; color: #333; line-height: 1.8; margin-top: 20rpx; display: block; }
+
+.like-row {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-top: 30rpx;
+ gap: 12rpx;
+ padding: 20rpx 0;
+ border-top: 2rpx dashed #f0f0f0;
+}
+.like-icon { font-size: 44rpx; }
+.like-num { font-size: 26rpx; color: #f5222d; font-weight: bold; }
+
+.comments {
+ background: #fff;
+ margin: 0 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+}
+.comment-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
+.comment { padding: 16rpx 0; border-bottom: 2rpx solid #f5f5f5; }
+.comment:last-child { border-bottom: none; }
+.comment-author { font-size: 26rpx; font-weight: bold; display: block; }
+.comment-content { font-size: 26rpx; color: #333; margin-top: 6rpx; display: block; }
+.comment-time { font-size: 22rpx; color: #999; margin-top: 6rpx; display: block; }
+.empty-mini { text-align: center; padding: 40rpx 0; color: #999; font-size: 26rpx; }
+
+.reply-bar {
+ position: fixed;
+ left: 0; right: 0; bottom: 0;
+ background: #fff;
+ padding: 20rpx;
+ display: flex;
+ gap: 16rpx;
+ align-items: center;
+ box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.05);
+}
+.reply-input {
+ flex: 1;
+ background: #f5f5f5;
+ border-radius: 40rpx;
+ padding: 18rpx 28rpx;
+ font-size: 28rpx;
+}
+.reply-btn {
+ background: #f5222d !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 40rpx !important;
+ padding: 16rpx 30rpx !important;
+ font-size: 28rpx !important;
+}
diff --git a/taro-app/src/pages/confession/detail/detail.tsx b/taro-app/src/pages/confession/detail/detail.tsx
new file mode 100644
index 0000000..b34cc78
--- /dev/null
+++ b/taro-app/src/pages/confession/detail/detail.tsx
@@ -0,0 +1,83 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Button, Input } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { showToast, formatTime } from '../../../utils'
+import './index.scss'
+
+interface Comment { id: number; author: string; content: string; createdAt: number }
+
+export default function ConfessionDetail() {
+ const router = Taro.useRouter()
+ const id = Number(router.params?.id || 1)
+ const [detail, setDetail] = useState(null)
+ const [comments, setComments] = useState([])
+ const [reply, setReply] = useState('')
+ const [liked, setLiked] = useState(false)
+
+ useEffect(() => { loadData() }, [id])
+
+ async function loadData() {
+ try {
+ const d = await api.confession.detail(id)
+ setDetail(d)
+ setComments((d as any).comments || [])
+ } catch (e) { console.error(e) }
+ }
+
+ async function like() {
+ try {
+ await api.confession.like(id)
+ setLiked(true)
+ setDetail({ ...detail, likes: (detail?.likes || 0) + 1 })
+ showToast('已点赞', 'success')
+ } catch (e) { showToast('操作失败', 'error') }
+ }
+
+ async function send() {
+ if (!reply.trim()) return
+ try {
+ await api.confession.comment(id, { content: reply })
+ setComments([{ id: Date.now(), author: '我', content: reply, createdAt: Date.now() }, ...comments])
+ setReply('')
+ showToast('评论成功', 'success')
+ } catch (e) { showToast('操作失败', 'error') }
+ }
+
+ return (
+
+
+
+ {detail?.anonymous ? '?' : (detail?.author || 'A')[0]}
+
+ {detail?.anonymous ? '匿名用户' : detail?.author}
+ {formatTime(new Date(detail?.createdAt || Date.now()))}
+
+
+ {detail?.title || '致那个ta'}
+ {detail?.content || '内容...'}
+
+ {liked ? '❤️' : '🤍'}
+ {(detail?.likes || 0) + (liked ? 1 : 0)}
+
+
+
+
+ 评论 {comments.length}
+ {comments.map(c => (
+
+ {c.author}
+ {c.content}
+ {formatTime(new Date(c.createdAt))}
+
+ ))}
+ {comments.length === 0 && 还没有评论,抢沙发}
+
+
+
+ setReply(e.detail.value)} />
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/confession/list/list.config.ts b/taro-app/src/pages/confession/list/list.config.ts
new file mode 100644
index 0000000..10e4a99
--- /dev/null
+++ b/taro-app/src/pages/confession/list/list.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '表白墙',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/confession/list/list.scss b/taro-app/src/pages/confession/list/list.scss
new file mode 100644
index 0000000..f6beb1b
--- /dev/null
+++ b/taro-app/src/pages/confession/list/list.scss
@@ -0,0 +1,88 @@
+.page-scroll {
+ min-height: 100vh;
+ background: linear-gradient(180deg, #fff0f6 0%, #f5f5f5 200rpx);
+ padding-bottom: 140rpx;
+}
+
+.hero {
+ background: linear-gradient(135deg, #f5222d 0%, #ffadd2 100%);
+ color: #fff;
+ padding: 40rpx 30rpx;
+}
+.hero-title { font-size: 38rpx; font-weight: bold; display: block; }
+.hero-sub { font-size: 24rpx; color: rgba(255,255,255,0.9); margin-top: 8rpx; display: block; }
+
+.tab-row {
+ display: flex;
+ background: #fff;
+ margin: 20rpx;
+ padding: 12rpx;
+ border-radius: 40rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.tab {
+ flex: 1;
+ text-align: center;
+ padding: 16rpx 0;
+ font-size: 26rpx;
+ color: #666;
+ border-radius: 30rpx;
+}
+.tab.active {
+ background: #f5222d;
+ color: #fff;
+ font-weight: bold;
+}
+
+.card {
+ background: #fff;
+ margin: 0 20rpx 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.head { display: flex; align-items: center; }
+.avatar {
+ width: 64rpx; height: 64rpx;
+ background: linear-gradient(135deg, #f5222d 0%, #ffadd2 100%);
+ border-radius: 50%;
+ color: #fff;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 24rpx; font-weight: bold;
+ flex-shrink: 0;
+}
+.head-body { flex: 1; margin-left: 16rpx; }
+.author { font-size: 28rpx; font-weight: bold; display: block; }
+.time { font-size: 22rpx; color: #999; margin-top: 4rpx; display: block; }
+
+.title { font-size: 30rpx; font-weight: bold; margin-top: 20rpx; display: block; color: #333; }
+.content { font-size: 28rpx; color: #666; margin-top: 12rpx; display: block; line-height: 1.7; }
+
+.foot {
+ display: flex;
+ gap: 30rpx;
+ margin-top: 20rpx;
+ padding-top: 16rpx;
+ border-top: 2rpx solid #f5f5f5;
+ font-size: 24rpx;
+ color: #999;
+}
+
+.empty { text-align: center; padding: 120rpx 0; color: #999; font-size: 26rpx; }
+.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
+
+.fab {
+ position: fixed;
+ right: 40rpx;
+ bottom: 60rpx;
+ width: 100rpx;
+ height: 100rpx;
+ background: #f5222d;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 24rpx rgba(245, 34, 45, 0.4);
+ z-index: 100;
+}
diff --git a/taro-app/src/pages/confession/list/list.tsx b/taro-app/src/pages/confession/list/list.tsx
new file mode 100644
index 0000000..03b315e
--- /dev/null
+++ b/taro-app/src/pages/confession/list/list.tsx
@@ -0,0 +1,66 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Button } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { timeAgo } from '../../../utils'
+import './index.scss'
+
+interface Item { id: number; title: string; content: string; anonymous: boolean; author: string; likes: number; comments: number; createdAt: number; gender: string }
+
+export default function ConfessionList() {
+ const [list, setList] = useState- ([])
+ const [tab, setTab] = useState('all')
+
+ useEffect(() => { loadData() }, [tab])
+
+ async function loadData() {
+ try {
+ const r = await api.confession.list({ gender: tab === 'all' ? undefined : tab })
+ setList((r as any).list || r || [])
+ } catch (e) { console.error(e) }
+ }
+
+ function onDetail(id: number) { Taro.navigateTo({ url: '/pages/confession/detail/detail?id=' + id }) }
+ function onPublish() { Taro.navigateTo({ url: '/pages/confession/publish/publish' }) }
+
+ return (
+
+
+ 💌 表白墙
+ 说出心里话,邂逅心动瞬间
+
+
+
+ {[{ k: 'all', t: '全部' }, { k: '男', t: '男生' }, { k: '女', t: '女生' }].map(x => (
+ setTab(x.k)}>
+ {x.t}
+
+ ))}
+
+
+ {list.map((item: Item) => (
+ onDetail(item.id)}>
+
+ {item.anonymous ? '?' : (item.author || 'A')[0]}
+
+ {item.anonymous ? '匿名用户' : item.author}
+ {timeAgo(item.createdAt)}
+
+
+ {item.title}
+ {item.content}
+
+ 👍 {item.likes}
+ 💬 {item.comments}
+
+
+ ))}
+
+ {list.length === 0 && 💌还没有表白,快来第一条}
+
+
+ +
+
+
+ )
+}
diff --git a/taro-app/src/pages/confession/publish/publish.config.ts b/taro-app/src/pages/confession/publish/publish.config.ts
new file mode 100644
index 0000000..bf1391d
--- /dev/null
+++ b/taro-app/src/pages/confession/publish/publish.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '发布表白',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/confession/publish/publish.scss b/taro-app/src/pages/confession/publish/publish.scss
new file mode 100644
index 0000000..9508b93
--- /dev/null
+++ b/taro-app/src/pages/confession/publish/publish.scss
@@ -0,0 +1,85 @@
+.page-scroll {
+ min-height: 100vh;
+ background: linear-gradient(180deg, #fff0f6 0%, #f5f5f5 200rpx);
+ 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: 400rpx;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ padding: 20rpx;
+ font-size: 28rpx;
+ box-sizing: border-box;
+}
+
+.row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 20rpx;
+}
+.label-row { font-size: 26rpx; color: #333; font-weight: bold; }
+
+.gender-row { display: flex; gap: 20rpx; }
+.gender {
+ flex: 1;
+ padding: 20rpx 0;
+ text-align: center;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ font-size: 28rpx;
+ color: #666;
+ border: 2rpx solid transparent;
+}
+.gender.active {
+ background: #fff0f6;
+ color: #f5222d;
+ border-color: #f5222d;
+ 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: #f5222d !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/confession/publish/publish.tsx b/taro-app/src/pages/confession/publish/publish.tsx
new file mode 100644
index 0000000..563f78d
--- /dev/null
+++ b/taro-app/src/pages/confession/publish/publish.tsx
@@ -0,0 +1,54 @@
+import { useState } from 'react'
+import { View, Text, ScrollView, Button, Input, Textarea, Switch } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { showToast } from '../../../utils'
+import './index.scss'
+
+export default function ConfessionPublish() {
+ const [title, setTitle] = useState('')
+ const [content, setContent] = useState('')
+ const [anonymous, setAnonymous] = useState(true)
+ const [gender, setGender] = useState('女')
+
+ async function submit() {
+ if (!title || !content) { showToast('请填写完整'); return }
+ try {
+ Taro.showLoading({ title: '发布中', mask: true })
+ await api.confession.create({ title, content, anonymous, gender })
+ Taro.hideLoading()
+ showToast('发布成功', 'success')
+ setTimeout(() => Taro.navigateBack(), 1200)
+ } catch (e) { Taro.hideLoading(); showToast('发布失败', 'error') }
+ }
+
+ return (
+
+
+ 标题 *
+ setTitle(e.detail.value)} />
+
+ 内容 *
+
+
+
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/custom/diy/diy.config.ts b/taro-app/src/pages/custom/diy/diy.config.ts
new file mode 100644
index 0000000..d257443
--- /dev/null
+++ b/taro-app/src/pages/custom/diy/diy.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '自定义页面',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/custom/diy/diy.scss b/taro-app/src/pages/custom/diy/diy.scss
new file mode 100644
index 0000000..105b6f5
--- /dev/null
+++ b/taro-app/src/pages/custom/diy/diy.scss
@@ -0,0 +1,101 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 60rpx;
+}
+
+.hero {
+ background: linear-gradient(135deg, #eb2f96 0%, #ffadd2 100%);
+ color: #fff;
+ padding: 50rpx 30rpx;
+ text-align: center;
+}
+.hero-title { font-size: 40rpx; font-weight: bold; display: block; }
+
+.content { padding: 20rpx; }
+
+.block {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.block-title { text-align: center; font-size: 36rpx; font-weight: bold; color: #333; padding: 30rpx 20rpx; }
+
+.block-text { font-size: 28rpx; color: #666; line-height: 1.8; }
+
+.block-richtext {
+ background: #fffbe6;
+ border-left: 8rpx solid #faad14;
+ font-size: 28rpx;
+ color: #595959;
+ line-height: 1.8;
+ padding: 24rpx;
+}
+
+.block-image {
+ background: linear-gradient(135deg, #f9f0ff 0%, #d3adf7 100%);
+ min-height: 300rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 40rpx;
+}
+.image { width: 100%; height: auto; }
+.image-emoji { font-size: 160rpx; }
+
+.block-button-wrap {
+ background: transparent;
+ box-shadow: none;
+ padding: 0;
+}
+.block-button {
+ background: linear-gradient(135deg, #eb2f96 0%, #ffadd2 100%) !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 40rpx !important;
+ padding: 24rpx 0 !important;
+ font-size: 30rpx !important;
+ font-weight: bold;
+ width: 100%;
+ box-shadow: 0 6rpx 20rpx rgba(235,47,150,0.3);
+}
+
+.block-divider {
+ background: transparent;
+ box-shadow: none;
+ padding: 20rpx 0;
+}
+.divider-line { height: 2rpx; background: #e8e8e8; }
+
+.block-goods {
+ padding: 20rpx;
+}
+.goods-item {
+ display: flex;
+ align-items: center;
+ padding: 16rpx 0;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.goods-item:last-child { border-bottom: none; }
+.goods-icon { font-size: 40rpx; margin-right: 20rpx; }
+.goods-name { flex: 1; font-size: 28rpx; color: #333; }
+.goods-price { color: #f5222d; font-size: 28rpx; font-weight: bold; }
+
+.block-empty {
+ text-align: center;
+ color: #bbb;
+ font-size: 24rpx;
+ background: transparent;
+ box-shadow: none;
+ padding: 30rpx 0;
+}
+
+.footer-note {
+ text-align: center;
+ color: #bbb;
+ font-size: 22rpx;
+ padding: 20rpx 0;
+}
diff --git a/taro-app/src/pages/custom/diy/diy.tsx b/taro-app/src/pages/custom/diy/diy.tsx
new file mode 100644
index 0000000..5d3030c
--- /dev/null
+++ b/taro-app/src/pages/custom/diy/diy.tsx
@@ -0,0 +1,131 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Button, Image, Navigator } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { showToast, formatPrice } from '../../../utils'
+import './index.scss'
+
+interface Block {
+ id: number
+ type: 'title' | 'richtext' | 'image' | 'button' | 'divider' | 'goods-list' | 'text' | 'empty'
+ content?: string
+ src?: string
+ text?: string
+ url?: string
+ style?: any
+ items?: any[]
+}
+
+export default function DiyPage() {
+ const router = Taro.useRouter()
+ const id = Number(router.params?.id || 1)
+ const [page, setPage] = useState<{ title: string; blocks: Block[] } | null>(null)
+
+ useEffect(() => { loadData() }, [id])
+
+ async function loadData() {
+ try {
+ const r: any = await api.diy.detail(id)
+ if (r && r.blocks) {
+ setPage({ title: r.title || '自定义页面', blocks: r.blocks })
+ } else {
+ // 默认 demo 数据
+ setPage({
+ title: r?.title || '校园专属活动',
+ blocks: [
+ { id: 1, type: 'image', src: '', content: '🎊' },
+ { id: 2, type: 'title', content: '欢迎来到校园综合服务' },
+ { id: 3, type: 'text', content: '一个专注校园生活的综合服务平台,提供外卖点餐、二手交易、跑腿代办、表白墙、兴趣社区等功能。' },
+ { id: 4, type: 'divider' },
+ { id: 5, type: 'title', content: '🔥 精选好物' },
+ {
+ id: 6,
+ type: 'goods-list',
+ items: [
+ { id: 1, name: '校园外卖优惠券', price: 10, icon: '🎫' },
+ { id: 2, name: '二手教材便宜卖', price: 25, icon: '📚' },
+ { id: 3, name: '代取快递服务', price: 5, icon: '📦' },
+ ]
+ },
+ { id: 7, type: 'divider' },
+ { id: 8, type: 'richtext', content: '点击下方按钮,立即体验校园服务,下载APP享更多优惠。活动期间,新用户立减10元。' },
+ { id: 9, type: 'button', text: '🎁 立即领取', url: '/pages/market/list/list' },
+ { id: 10, type: 'empty', content: '— END —' },
+ ]
+ })
+ }
+ } catch (e) {
+ console.error(e)
+ setPage({
+ title: '自定义页面',
+ blocks: [
+ { id: 1, type: 'title', content: '自定义动态页面' },
+ { id: 2, type: 'text', content: '此页面内容由 API 返回,可通过管理后台配置组件顺序、图片、按钮跳转等。' },
+ { id: 3, type: 'image', content: '✨' },
+ { id: 4, type: 'button', text: '返回首页', url: '/pages/index/index' },
+ ]
+ })
+ }
+ }
+
+ function onBlockClick(block: Block) {
+ if (block.type === 'button' && block.url) {
+ Taro.navigateTo({ url: block.url }).catch(() => showToast('跳转失败'))
+ }
+ }
+
+ function renderBlock(b: Block, index: number) {
+ switch (b.type) {
+ case 'title':
+ return {b.content || ''}
+ case 'text':
+ return {b.content || ''}
+ case 'richtext':
+ return {b.content || ''}
+ case 'image':
+ return (
+
+ {b.src ? : {b.content || '🖼'}}
+
+ )
+ case 'button':
+ return (
+
+
+
+ )
+ case 'divider':
+ return
+ case 'goods-list':
+ return (
+
+ {(b.items || []).map((it: any) => (
+ showToast('点击 ' + it.name)}>
+ {it.icon || '📦'}
+ {it.name}
+ {formatPrice(it.price)}
+
+ ))}
+
+ )
+ case 'empty':
+ return {b.content || ''}
+ default:
+ return null
+ }
+ }
+
+ return (
+
+
+ {page?.title || '自定义页面'}
+
+
+ {page?.blocks.map((b, i) => renderBlock(b, i))}
+
+
+ — 以上内容动态渲染 —
+
+
+ )
+}
diff --git a/taro-app/src/pages/custom/h5/h5.config.ts b/taro-app/src/pages/custom/h5/h5.config.ts
new file mode 100644
index 0000000..337b73e
--- /dev/null
+++ b/taro-app/src/pages/custom/h5/h5.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: 'H5 链接',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/custom/h5/h5.scss b/taro-app/src/pages/custom/h5/h5.scss
new file mode 100644
index 0000000..9b3206b
--- /dev/null
+++ b/taro-app/src/pages/custom/h5/h5.scss
@@ -0,0 +1,56 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 40rpx;
+}
+
+.hero {
+ background: linear-gradient(135deg, #13c2c2 0%, #5cdbd3 100%);
+ color: #fff;
+ padding: 50rpx 30rpx;
+}
+.hero-title { font-size: 38rpx; font-weight: bold; display: block; }
+.hero-sub { font-size: 24rpx; color: rgba(255,255,255,0.9); margin-top: 10rpx; display: block; }
+
+.card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.card-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
+
+.url-box {
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ padding: 20rpx;
+ margin-bottom: 20rpx;
+}
+.url-text { font-size: 26rpx; color: #666; word-break: break-all; }
+
+.open-btn {
+ background: #13c2c2 !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 40rpx !important;
+ padding: 20rpx 0 !important;
+ font-size: 28rpx !important;
+ font-weight: bold;
+ width: 100%;
+}
+
+.link-item {
+ display: flex;
+ align-items: center;
+ padding: 20rpx 0;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.link-item:last-child { border-bottom: none; }
+.link-icon { font-size: 36rpx; margin-right: 16rpx; }
+.link-name { flex: 1; font-size: 28rpx; color: #333; }
+.link-arrow { font-size: 32rpx; color: #ccc; }
+
+.tip { background: #fffbe6; border: 2rpx solid #ffe58f; }
+.tip-title { font-size: 28rpx; font-weight: bold; color: #ad6800; display: block; margin-bottom: 10rpx; }
+.tip-text { font-size: 26rpx; color: #8c6e00; line-height: 1.6; display: block; }
diff --git a/taro-app/src/pages/custom/h5/h5.tsx b/taro-app/src/pages/custom/h5/h5.tsx
new file mode 100644
index 0000000..17bea9a
--- /dev/null
+++ b/taro-app/src/pages/custom/h5/h5.tsx
@@ -0,0 +1,58 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Navigator, Button } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import './index.scss'
+
+export default function H5Page() {
+ const router = Taro.useRouter()
+ const url = router.params?.url || ''
+ const [loaded, setLoaded] = useState(false)
+
+ useEffect(() => {
+ setTimeout(() => setLoaded(true), 500)
+ }, [])
+
+ const menuItems = [
+ { icon: '🎓', name: '校园官网', url: 'https://example.edu.cn' },
+ { icon: '📖', name: '图书馆系统', url: 'https://example.edu.cn/lib' },
+ { icon: '📊', name: '成绩查询', url: 'https://example.edu.cn/score' },
+ { icon: '🏫', name: '教务处', url: 'https://example.edu.cn/jwc' },
+ { icon: '💼', name: '就业中心', url: 'https://example.edu.cn/job' },
+ { icon: '🏥', name: '校医院', url: 'https://example.edu.cn/hospital' },
+ ]
+
+ return (
+
+
+ 🌐 H5 链接
+ 打开外部网页与校内系统
+
+
+ {url && (
+
+ 当前链接
+ {url}
+
+
+ )}
+
+
+ 常用链接
+ {menuItems.map((item, idx) => (
+ Taro.showToast({ title: '即将打开 ' + item.name, icon: 'none' })}>
+ {item.icon}
+ {item.name}
+ ›
+
+ ))}
+
+
+
+ 💡 使用提示
+ H5 页面会在系统浏览器中打开以获得更好的兼容性体验。校园系统链接需要校园网环境访问。
+
+
+ )
+}
+
+function showToast(title: string) { Taro.showToast({ title, icon: 'none' }) }
diff --git a/taro-app/src/pages/errand/detail/detail.config.ts b/taro-app/src/pages/errand/detail/detail.config.ts
new file mode 100644
index 0000000..82a037a
--- /dev/null
+++ b/taro-app/src/pages/errand/detail/detail.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '任务详情',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/errand/detail/detail.scss b/taro-app/src/pages/errand/detail/detail.scss
new file mode 100644
index 0000000..13f40c7
--- /dev/null
+++ b/taro-app/src/pages/errand/detail/detail.scss
@@ -0,0 +1,88 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 180rpx;
+}
+
+.hero {
+ background: linear-gradient(135deg, #fa8c16 0%, #ffc069 100%);
+ color: #fff;
+ padding: 40rpx 30rpx;
+ text-align: center;
+}
+.reward { font-size: 24rpx; color: rgba(255,255,255,0.9); display: block; }
+.price { font-size: 80rpx; font-weight: bold; display: block; margin-top: 10rpx; }
+.status-row { margin-top: 20rpx; }
+.status {
+ display: inline-block;
+ padding: 8rpx 24rpx;
+ border-radius: 30rpx;
+ font-size: 24rpx;
+ font-weight: bold;
+ color: #fff;
+}
+
+.card {
+ background: #fff;
+ margin: 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.title { font-size: 34rpx; font-weight: bold; display: block; }
+.type { display: inline-block; background: #fff7e6; color: #fa8c16; padding: 4rpx 16rpx; border-radius: 8rpx; font-size: 22rpx; margin-top: 12rpx; }
+.desc { font-size: 28rpx; color: #666; margin-top: 16rpx; display: block; line-height: 1.6; }
+
+.card-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
+
+.info-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 14rpx 0;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.info-row:last-child { border-bottom: none; }
+.info-label { font-size: 26rpx; color: #999; }
+.info-value { font-size: 26rpx; color: #333; text-align: right; max-width: 60%; }
+
+.publisher { display: flex; align-items: center; }
+.avatar {
+ width: 80rpx; height: 80rpx;
+ background: linear-gradient(135deg, #fa8c16 0%, #ffc069 100%);
+ border-radius: 50%;
+ color: #fff;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 32rpx;
+ font-weight: bold;
+}
+.publisher-name { font-size: 28rpx; font-weight: bold; display: block; }
+.publisher-meta { font-size: 22rpx; color: #999; margin-top: 4rpx; display: block; }
+
+.action-bar {
+ position: fixed;
+ left: 0; right: 0; bottom: 0;
+ background: #fff;
+ padding: 20rpx 30rpx;
+ box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.05);
+}
+.accept-btn {
+ background: #fa8c16 !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 40rpx !important;
+ padding: 20rpx 0 !important;
+ font-size: 30rpx !important;
+ font-weight: bold;
+ width: 100%;
+}
+.done-btn {
+ background: #52c41a !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/errand/detail/detail.tsx b/taro-app/src/pages/errand/detail/detail.tsx
new file mode 100644
index 0000000..6c360d2
--- /dev/null
+++ b/taro-app/src/pages/errand/detail/detail.tsx
@@ -0,0 +1,95 @@
+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, formatPrice, formatTime } from '../../../utils'
+import './index.scss'
+
+export default function ErrandDetail() {
+ const router = Taro.useRouter()
+ const id = Number(router.params?.id || 1)
+ const [detail, setDetail] = useState(null)
+
+ useEffect(() => { loadData() }, [id])
+
+ async function loadData() {
+ try {
+ const d = await api.errand.detail(id)
+ setDetail(d)
+ } catch (e) { console.error(e) }
+ }
+
+ async function accept() {
+ try {
+ Taro.showLoading({ title: '接单中' })
+ await api.errand.accept(id)
+ Taro.hideLoading()
+ showToast('接单成功', 'success')
+ if (detail) setDetail({ ...detail, status: 'taken' })
+ } catch (e) { Taro.hideLoading(); showToast('操作失败', 'error') }
+ }
+
+ async function complete() {
+ try {
+ Taro.showLoading({ title: '处理中' })
+ await api.errand.complete(id)
+ Taro.hideLoading()
+ showToast('已完成', 'success')
+ if (detail) setDetail({ ...detail, status: 'done' })
+ } catch (e) { Taro.hideLoading(); showToast('操作失败', 'error') }
+ }
+
+ const statusColor: Record = { pending: '#fa8c16', taken: '#1890ff', done: '#52c41a', cancelled: '#999' }
+ const statusText: Record = { pending: '可接单', taken: '进行中', done: '已完成', cancelled: '已取消' }
+
+ return (
+
+
+ 报酬
+ {formatPrice(detail?.price || 0)}
+
+
+ {statusText[detail?.status] || '可接单'}
+
+
+
+
+
+ {detail?.title || '任务标题'}
+ {detail?.type || '其他'}
+ {detail?.desc || '任务描述'}
+
+
+
+ 📍 任务详情
+ 取件地址{detail?.pickup || '校内菜鸟驿站'}
+ 送达地址{detail?.delivery || '3号宿舍楼'}
+ 期望时间{detail?.expectedTime ? formatTime(new Date(detail.expectedTime)) : '今日 18:00 前'}
+ 联系电话{detail?.phone || '13800138000'}
+
+
+
+ 👤 发布人
+
+ {(detail?.publisher || 'P')[0]}
+
+ {detail?.publisher || '匿名用户'}
+ 发布于 {formatTime(new Date(detail?.createdAt || Date.now()))}
+
+
+
+
+
+ {detail?.status !== 'taken' && detail?.status !== 'done' && (
+
+ )}
+ {detail?.status === 'taken' && (
+
+ )}
+ {detail?.status === 'done' && (
+
+ )}
+
+
+ )
+}
diff --git a/taro-app/src/pages/errand/list/list.config.ts b/taro-app/src/pages/errand/list/list.config.ts
new file mode 100644
index 0000000..f09573c
--- /dev/null
+++ b/taro-app/src/pages/errand/list/list.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '校园跑腿',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/errand/list/list.scss b/taro-app/src/pages/errand/list/list.scss
new file mode 100644
index 0000000..975b969
--- /dev/null
+++ b/taro-app/src/pages/errand/list/list.scss
@@ -0,0 +1,92 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 140rpx;
+}
+
+.hero {
+ background: linear-gradient(135deg, #fa8c16 0%, #ffc069 100%);
+ color: #fff;
+ padding: 40rpx 30rpx;
+}
+.hero-title { font-size: 36rpx; font-weight: bold; display: block; }
+.hero-sub { font-size: 24rpx; color: rgba(255,255,255,0.9); margin-top: 6rpx; display: block; }
+
+.tab-bar {
+ display: flex;
+ background: #fff;
+ padding: 12rpx;
+ margin: 20rpx;
+ border-radius: 40rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.tab {
+ flex: 1;
+ padding: 16rpx 0;
+ text-align: center;
+ font-size: 26rpx;
+ color: #666;
+ border-radius: 30rpx;
+}
+.tab.active {
+ background: #fa8c16;
+ color: #fff;
+ font-weight: bold;
+}
+
+.task-card {
+ background: #fff;
+ margin: 0 20rpx 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.task-head {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+}
+.task-icon {
+ width: 80rpx; height: 80rpx;
+ background: linear-gradient(135deg, #fff7e6 0%, #ffd591 100%);
+ border-radius: 16rpx;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 40rpx;
+ flex-shrink: 0;
+}
+.task-head-body { flex: 1; min-width: 0; }
+.task-title { font-size: 30rpx; font-weight: bold; display: block; }
+.task-type { font-size: 22rpx; color: #fa8c16; background: #fff7e6; padding: 2rpx 14rpx; border-radius: 8rpx; display: inline-block; margin-top: 6rpx; }
+.task-price { color: #f5222d; font-size: 36rpx; font-weight: bold; }
+
+.task-desc { font-size: 26rpx; color: #666; margin-top: 16rpx; display: block; line-height: 1.5; }
+
+.task-foot {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 20rpx;
+ padding-top: 16rpx;
+ border-top: 2rpx solid #f5f5f5;
+ font-size: 22rpx;
+ color: #999;
+}
+
+.empty { text-align: center; padding: 120rpx 0; color: #999; font-size: 26rpx; }
+.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
+
+.fab {
+ position: fixed;
+ right: 40rpx;
+ bottom: 60rpx;
+ width: 100rpx;
+ height: 100rpx;
+ background: #fa8c16;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 24rpx rgba(250, 140, 22, 0.4);
+ z-index: 100;
+}
diff --git a/taro-app/src/pages/errand/list/list.tsx b/taro-app/src/pages/errand/list/list.tsx
new file mode 100644
index 0000000..7724f65
--- /dev/null
+++ b/taro-app/src/pages/errand/list/list.tsx
@@ -0,0 +1,68 @@
+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, formatPrice, timeAgo } from '../../../utils'
+import './index.scss'
+
+interface Task { id: number; title: string; type: string; desc: string; price: number; publisher: string; location: string; createdAt: number; status: string }
+
+export default function ErrandList() {
+ const [list, setList] = useState([])
+ const [tab, setTab] = useState('all')
+
+ useEffect(() => { loadData() }, [tab])
+
+ async function loadData() {
+ try {
+ const r = await api.errand.list({ status: tab === 'all' ? undefined : tab })
+ setList((r as any).list || r || [])
+ } catch (e) { console.error(e) }
+ }
+
+ function onDetail(id: number) { Taro.navigateTo({ url: '/pages/errand/detail/detail?id=' + id }) }
+ function onPublish() { Taro.navigateTo({ url: '/pages/errand/publish/publish' }) }
+
+ const typeIcon: Record = { 取快递: '📦', 代购: '🛒', 代打印: '🖨', 其他: '🏃' }
+
+ return (
+
+
+ 🏃 校园跑腿
+ 下单有人接,帮你省时间
+
+
+
+ {[{ k: 'all', t: '全部' }, { k: 'pending', t: '可接' }, { k: 'taken', t: '进行中' }, { k: 'done', t: '已完成' }].map(x => (
+ setTab(x.k)}>
+ {x.t}
+
+ ))}
+
+
+ {list.map((t: Task) => (
+ onDetail(t.id)}>
+
+ {typeIcon[t.type] || '🏃'}
+
+ {t.title}
+ {t.type}
+
+ {formatPrice(t.price)}
+
+ {t.desc}
+
+ 发布人: {t.publisher}
+ {timeAgo(t.createdAt)}
+
+
+ ))}
+
+ {list.length === 0 && 🏃暂无跑腿任务}
+
+
+ +
+
+
+ )
+}
diff --git a/taro-app/src/pages/errand/publish/publish.config.ts b/taro-app/src/pages/errand/publish/publish.config.ts
new file mode 100644
index 0000000..2ff5539
--- /dev/null
+++ b/taro-app/src/pages/errand/publish/publish.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '发布跑腿',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/errand/publish/publish.scss b/taro-app/src/pages/errand/publish/publish.scss
new file mode 100644
index 0000000..2ebe108
--- /dev/null
+++ b/taro-app/src/pages/errand/publish/publish.scss
@@ -0,0 +1,79 @@
+.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: 200rpx;
+ 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: #fff7e6;
+ color: #fa8c16;
+ border-color: #fa8c16;
+ 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: #fa8c16 !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/errand/publish/publish.tsx b/taro-app/src/pages/errand/publish/publish.tsx
new file mode 100644
index 0000000..cfae46e
--- /dev/null
+++ b/taro-app/src/pages/errand/publish/publish.tsx
@@ -0,0 +1,67 @@
+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 ErrandPublish() {
+ const [title, setTitle] = useState('')
+ const [type, setType] = useState('取快递')
+ const [desc, setDesc] = useState('')
+ const [pickup, setPickup] = useState('')
+ const [delivery, setDelivery] = useState('')
+ const [phone, setPhone] = useState('')
+ const [price, setPrice] = useState('')
+ const types = ['取快递', '代购', '代打印', '其他']
+
+ async function submit() {
+ if (!title || !price || !pickup || !delivery) { showToast('请完善信息'); return }
+ try {
+ Taro.showLoading({ title: '发布中', mask: true })
+ await api.errand.create({
+ title, type, desc, pickup, delivery, phone, price: Number(price)
+ })
+ Taro.hideLoading()
+ showToast('发布成功', 'success')
+ setTimeout(() => Taro.navigateBack(), 1200)
+ } catch (e) { Taro.hideLoading(); showToast('发布失败', 'error') }
+ }
+
+ return (
+
+
+ 任务标题 *
+ setTitle(e.detail.value)} />
+
+ 任务类型 *
+
+ {types.map(t => (
+ setType(t)}>
+ {t}
+
+ ))}
+
+
+ 取件地址 *
+ setPickup(e.detail.value)} />
+
+ 送达地址 *
+ setDelivery(e.detail.value)} />
+
+ 联系电话
+ setPhone(e.detail.value)} />
+
+ 报酬 (¥) *
+ setPrice(e.detail.value)} />
+
+ 备注
+
+
+
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/food/checkout/checkout.config.ts b/taro-app/src/pages/food/checkout/checkout.config.ts
new file mode 100644
index 0000000..3751543
--- /dev/null
+++ b/taro-app/src/pages/food/checkout/checkout.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '确认订单',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/food/checkout/checkout.scss b/taro-app/src/pages/food/checkout/checkout.scss
new file mode 100644
index 0000000..210f557
--- /dev/null
+++ b/taro-app/src/pages/food/checkout/checkout.scss
@@ -0,0 +1,105 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding: 20rpx;
+ padding-bottom: 180rpx;
+}
+
+.card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.address-card {
+ background: linear-gradient(135deg, #fff 0%, #f0f9ff 100%);
+ display: flex;
+ align-items: flex-start;
+}
+
+.addr-icon {
+ width: 72rpx; height: 72rpx;
+ background: #e6f7ff;
+ border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 32rpx;
+ flex-shrink: 0;
+}
+
+.addr-body { flex: 1; margin-left: 20rpx; }
+.addr-phone { font-size: 30rpx; font-weight: bold; display: block; }
+.addr-detail { font-size: 26rpx; color: #666; margin-top: 10rpx; display: block; }
+.addr-edit { margin-top: 16rpx; }
+.addr-edit text { font-size: 24rpx; color: #1890ff; }
+
+.card-title-row { margin-bottom: 20rpx; }
+.card-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
+
+.order-item {
+ display: flex; justify-content: space-between; align-items: center;
+ padding: 16rpx 0;
+}
+.order-item-name { font-size: 28rpx; color: #333; }
+.order-item-right { display: flex; align-items: center; gap: 20rpx; }
+.order-item-qty { color: #999; font-size: 26rpx; }
+.order-item-price { color: #333; font-size: 28rpx; font-weight: bold; }
+
+.divider { height: 2rpx; background: #f5f5f5; margin: 16rpx 0; }
+
+.row { display: flex; justify-content: space-between; align-items: center; padding: 12rpx 0; }
+.row-label { font-size: 26rpx; color: #666; }
+.row-label-flex { flex: 1; display: flex; justify-content: space-between; align-items: center; }
+.row-value { font-size: 26rpx; color: #333; }
+.row-value.discount { color: #f5222d; }
+
+.remark-input {
+ width: 100%;
+ min-height: 160rpx;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ padding: 20rpx;
+ font-size: 26rpx;
+ box-sizing: border-box;
+}
+
+.pay-item {
+ display: flex; align-items: center;
+ padding: 20rpx 0;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.pay-item:last-child { border-bottom: none; }
+.pay-icon { font-size: 36rpx; margin-right: 16rpx; }
+.pay-name { flex: 1; font-size: 28rpx; }
+.radio {
+ width: 40rpx; height: 40rpx;
+ border-radius: 50%;
+ border: 2rpx solid #ddd;
+ display: flex; align-items: center; justify-content: center;
+ color: #fff;
+ font-size: 24rpx;
+}
+.radio-active { background: #1890ff; border-color: #1890ff; }
+
+.total-bar {
+ position: fixed;
+ left: 0; right: 0; bottom: 0;
+ background: #fff;
+ padding: 20rpx 30rpx;
+ display: flex; align-items: center;
+ box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.05);
+}
+
+.total-left { flex: 1; display: flex; align-items: baseline; }
+.total-label { font-size: 26rpx; color: #666; }
+.total-value { color: #f5222d; font-size: 40rpx; font-weight: bold; }
+
+.pay-btn {
+ background: #1890ff !important;
+ color: #fff !important;
+ border-radius: 40rpx !important;
+ padding: 20rpx 50rpx !important;
+ font-size: 30rpx !important;
+ font-weight: bold;
+}
diff --git a/taro-app/src/pages/food/checkout/checkout.tsx b/taro-app/src/pages/food/checkout/checkout.tsx
new file mode 100644
index 0000000..81cae4f
--- /dev/null
+++ b/taro-app/src/pages/food/checkout/checkout.tsx
@@ -0,0 +1,144 @@
+import { useEffect, useState, useMemo } from 'react'
+import { View, Text, ScrollView, Button, Input, Textarea, Switch, Form } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { showToast, formatPrice } from '../../../utils'
+import './index.scss'
+
+interface CartItem { key: string; goodsId: number; name: string; qty: number; price: number }
+
+export default function Checkout() {
+ const [items, setItems] = useState([])
+ const [shopId, setShopId] = useState(1)
+ const [address, setAddress] = useState('学生宿舍 3号楼 202室')
+ const [phone, setPhone] = useState('13800138000')
+ const [remark, setRemark] = useState('')
+ const [useCoupon, setUseCoupon] = useState(false)
+ const [coupon, setCoupon] = useState(null)
+ const [payMethod, setPayMethod] = useState('wechat')
+
+ useEffect(() => {
+ try {
+ const its = Taro.getStorageSync('checkout_items') as CartItem[] || []
+ const sid = Taro.getStorageSync('checkout_shop') as number || 1
+ setItems(its)
+ setShopId(sid)
+ } catch (e) { console.error(e) }
+ loadCoupon()
+ }, [])
+
+ async function loadCoupon() {
+ try {
+ const r = await api.coupon.myCoupons()
+ const list = (r as any).list || r || []
+ if (list[0]) setCoupon(list[0])
+ } catch (e) { console.error(e) }
+ }
+
+ const subtotal = useMemo(() => items.reduce((s, i) => s + i.price * i.qty, 0), [items])
+ const deliveryFee = 3
+ const discount = useCoupon && coupon ? (coupon.discount || 5) : 0
+ const total = Math.max(0, subtotal + deliveryFee - discount)
+
+ async function submit() {
+ if (!address || !phone) { showToast('请完善收货信息'); return }
+ if (items.length === 0) { showToast('购物车为空'); return }
+ try {
+ Taro.showLoading({ title: '下单中...', mask: true })
+ const r = await api.order.create({
+ merchantId: shopId,
+ items,
+ address, phone, remark,
+ useCoupon: useCoupon ? coupon?.id : null,
+ payMethod,
+ total
+ })
+ Taro.hideLoading()
+ showToast('下单成功', 'success')
+ setTimeout(() => {
+ Taro.redirectTo({ url: '/pages/food/order-detail/order-detail?id=' + ((r as any).id || 1) })
+ }, 1200)
+ } catch (e) {
+ Taro.hideLoading()
+ showToast('下单失败', 'error')
+ }
+ }
+
+ return (
+
+
+ 📍
+
+ {phone}
+ {address}
+
+ ✏️ 点击修改地址
+
+
+
+
+
+
+ 🍜 商品清单
+
+ {items.map(i => (
+
+ {i.name}
+
+ x{i.qty}
+ {formatPrice(i.price * i.qty)}
+
+
+ ))}
+
+ 商品小计{formatPrice(subtotal)}
+ 配送费{formatPrice(deliveryFee)}
+
+
+ 优惠券
+ setUseCoupon(e.detail.value)} color="#1890ff" />
+
+ -{formatPrice(discount)}
+
+
+
+
+ 📝 订单备注
+
+
+
+ 💳 支付方式
+ {[
+ { id: 'wechat', name: '微信支付', icon: '💚' },
+ { id: 'alipay', name: '支付宝', icon: '💙' },
+ { id: 'balance', name: '余额支付', icon: '💰' },
+ ].map(p => (
+ setPayMethod(p.id)}>
+ {p.icon}
+ {p.name}
+
+ {payMethod === p.id && ✓}
+
+
+ ))}
+
+
+
+
+ 合计:
+ {formatPrice(total)}
+
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/food/index/index.config.ts b/taro-app/src/pages/food/index/index.config.ts
new file mode 100644
index 0000000..1b5ea41
--- /dev/null
+++ b/taro-app/src/pages/food/index/index.config.ts
@@ -0,0 +1,5 @@
+export default definePageConfig({
+ navigationBarTitleText: '外卖点餐',
+ enablePullDownRefresh: true,
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/food/index/index.scss b/taro-app/src/pages/food/index/index.scss
new file mode 100644
index 0000000..1751abe
--- /dev/null
+++ b/taro-app/src/pages/food/index/index.scss
@@ -0,0 +1,87 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 40rpx;
+}
+
+.food-header {
+ background: linear-gradient(135deg, #ff7a45 0%, #ffd591 100%);
+ padding: 30rpx 20rpx;
+}
+
+.food-title { padding: 10rpx 20rpx; }
+.food-title-text { color: #fff; font-size: 36rpx; font-weight: bold; }
+
+.search-bar {
+ display: flex; align-items: center; padding: 16rpx 24rpx;
+ background: rgba(255,255,255,0.95); border-radius: 40rpx;
+ margin: 10rpx 20rpx 0;
+}
+
+.search-icon { margin-right: 12rpx; color: #999; }
+.search-input { flex: 1; font-size: 26rpx; color: #333; }
+
+.banner { height: 240rpx; margin: 20rpx; }
+.banner-item {
+ width: 100%; height: 100%;
+ background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%);
+ border-radius: 16rpx;
+ display: flex; align-items: center; justify-content: center;
+ color: #fff; font-size: 30rpx; font-weight: bold;
+}
+.banner-orange { background: linear-gradient(135deg, #ff7a45 0%, #ffd591 100%); }
+.banner-green { background: linear-gradient(135deg, #52c41a 0%, #95de64 100%); }
+
+.cat-scroll {
+ white-space: nowrap;
+ padding: 20rpx 0;
+ background: #fff;
+ margin: 0 0 20rpx;
+}
+
+.cat-item {
+ display: inline-block;
+ padding: 16rpx 28rpx;
+ margin-left: 20rpx;
+ background: #f5f5f5;
+ border-radius: 32rpx;
+ font-size: 26rpx;
+ color: #666;
+}
+
+.cat-active {
+ background: #1890ff;
+ color: #fff;
+ font-weight: bold;
+}
+
+.merchant-list { padding: 0 20rpx; }
+
+.shop-card {
+ display: flex;
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.shop-thumb {
+ width: 140rpx; height: 140rpx;
+ background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
+ border-radius: 12rpx;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 48rpx; flex-shrink: 0;
+}
+
+.shop-info { flex: 1; margin-left: 20rpx; min-width: 0; }
+.shop-info-top { display: flex; justify-content: space-between; align-items: center; }
+.shop-name { font-size: 30rpx; font-weight: bold; }
+.shop-rating { color: #faad14; font-size: 24rpx; }
+.shop-desc { font-size: 24rpx; color: #999; margin-top: 8rpx; }
+.shop-meta { display: flex; flex-wrap: wrap; margin-top: 12rpx; gap: 12rpx; }
+.shop-meta-item { font-size: 22rpx; color: #666; background: #fafafa; padding: 4rpx 12rpx; border-radius: 8rpx; }
+.shop-address { background: #e6f7ff; color: #1890ff; }
+
+.empty { text-align: center; padding: 80rpx 0; color: #999; }
+.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
diff --git a/taro-app/src/pages/food/index/index.tsx b/taro-app/src/pages/food/index/index.tsx
new file mode 100644
index 0000000..fcca940
--- /dev/null
+++ b/taro-app/src/pages/food/index/index.tsx
@@ -0,0 +1,88 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Swiper, SwiperItem, Input, Image } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { showToast, formatPrice } from '../../../utils'
+import './index.scss'
+
+interface Merchant { id: number; name: string; category: string; rating: number; sales: number; address: string; minOrder: number; deliveryFee: number }
+
+export default function FoodIndex() {
+ const [merchants, setMerchants] = useState([])
+ const [keyword, setKeyword] = useState('')
+ const [activeCat, setActiveCat] = useState('全部')
+ const categories = ['全部', '快餐', '汉堡', '奶茶', '咖啡', '小吃', '甜品', '中式', '日式', '韩式']
+
+ useEffect(() => { loadData() }, [])
+
+ async function loadData() {
+ try {
+ const r = await api.merchant.list({ category: activeCat === '全部' ? undefined : activeCat })
+ setMerchants((r as any).list || r || [])
+ } catch (e) { console.error(e) }
+ }
+
+ function onCatTap(c: string) { setActiveCat(c); loadData() }
+ function onShopTap(id: number) { Taro.navigateTo({ url: '/pages/food/shop/shop?shopId=' + id }) }
+
+ return (
+
+
+
+ 🍔 外卖点餐
+
+
+ 🔍
+ setKeyword(e.detail.value)}
+ confirmType="search"
+ />
+
+
+
+
+ 🎉 新用户立减10元
+ 🚚 满30免配送费
+ ⏰ 限时秒杀活动
+
+
+
+ {categories.map(c => (
+ onCatTap(c)}
+ >
+ {c}
+
+ ))}
+
+
+
+ {merchants.map((m: Merchant) => (
+ onShopTap(m.id)}>
+ 🏪
+
+
+ {m.name}
+ ★ {Number(m.rating).toFixed(1)}
+
+ {m.category} · 月销{m.sales}单
+
+ 起送 {formatPrice(m.minOrder)}
+ 配送 {formatPrice(m.deliveryFee)}
+ {m.address}
+
+
+
+ ))}
+ {merchants.length === 0 && (
+ 🍽️暂无商户
+ )}
+
+
+ )
+}
diff --git a/taro-app/src/pages/food/order-detail/order-detail.config.ts b/taro-app/src/pages/food/order-detail/order-detail.config.ts
new file mode 100644
index 0000000..686933e
--- /dev/null
+++ b/taro-app/src/pages/food/order-detail/order-detail.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '订单详情',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/food/order-detail/order-detail.scss b/taro-app/src/pages/food/order-detail/order-detail.scss
new file mode 100644
index 0000000..7645d09
--- /dev/null
+++ b/taro-app/src/pages/food/order-detail/order-detail.scss
@@ -0,0 +1,90 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding: 20rpx;
+ padding-bottom: 180rpx;
+}
+
+.status-card {
+ background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%);
+ color: #fff;
+ border-radius: 16rpx;
+ padding: 40rpx 30rpx;
+ display: flex;
+ align-items: center;
+ margin-bottom: 20rpx;
+}
+.status-icon {
+ width: 80rpx; height: 80rpx;
+ border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 40rpx;
+ background: rgba(255,255,255,0.2);
+ color: #fff;
+}
+.status-info-body { margin-left: 20rpx; flex: 1; }
+.status-text { font-size: 34rpx; font-weight: bold; display: block; }
+.status-hint { font-size: 24rpx; color: rgba(255,255,255,0.85); margin-top: 8rpx; display: block; }
+
+.card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.addr-row { display: flex; align-items: flex-start; }
+.addr-icon { font-size: 30rpx; margin-right: 16rpx; margin-top: 4rpx; }
+.addr-body { flex: 1; }
+.addr-phone { font-size: 30rpx; font-weight: bold; display: block; }
+.addr-detail { font-size: 26rpx; color: #666; margin-top: 8rpx; display: block; }
+
+.card-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
+
+.item-row {
+ display: flex; align-items: center;
+ padding: 14rpx 0;
+}
+.item-name { flex: 1; font-size: 28rpx; color: #333; }
+.item-qty { color: #999; font-size: 26rpx; width: 100rpx; text-align: right; }
+.item-price { color: #333; font-size: 28rpx; width: 140rpx; text-align: right; font-weight: bold; }
+
+.divider { height: 2rpx; background: #f5f5f5; margin: 16rpx 0; }
+
+.sub-row { display: flex; justify-content: space-between; align-items: center; padding: 8rpx 0; font-size: 26rpx; color: #666; }
+.total-row { font-size: 28rpx; margin-top: 10rpx; padding-top: 16rpx; border-top: 2rpx dashed #eee; }
+
+.info-row { display: flex; justify-content: space-between; align-items: center; padding: 12rpx 0; }
+.info-label { font-size: 26rpx; color: #999; }
+.info-value { font-size: 26rpx; color: #333; }
+
+.action-bar {
+ position: fixed;
+ left: 0; right: 0; bottom: 0;
+ background: #fff;
+ padding: 20rpx 30rpx;
+ display: flex; gap: 20rpx;
+ box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.05);
+}
+
+.btn-outline {
+ flex: 1;
+ background: #fff !important;
+ color: #666 !important;
+ border: 2rpx solid #ddd !important;
+ border-radius: 40rpx !important;
+ padding: 20rpx 0 !important;
+ font-size: 28rpx !important;
+}
+
+.btn-primary {
+ flex: 1;
+ background: #1890ff !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 40rpx !important;
+ padding: 20rpx 0 !important;
+ font-size: 28rpx !important;
+ font-weight: bold;
+}
diff --git a/taro-app/src/pages/food/order-detail/order-detail.tsx b/taro-app/src/pages/food/order-detail/order-detail.tsx
new file mode 100644
index 0000000..3132721
--- /dev/null
+++ b/taro-app/src/pages/food/order-detail/order-detail.tsx
@@ -0,0 +1,126 @@
+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, formatPrice, formatTime } from '../../../utils'
+import './index.scss'
+
+interface OrderItem { name: string; qty: number; price: number }
+
+export default function OrderDetail() {
+ const router = Taro.useRouter()
+ const id = Number(router.params?.id || 1)
+ const [order, setOrder] = useState(null)
+
+ useEffect(() => { loadData() }, [id])
+
+ async function loadData() {
+ try {
+ const r = await api.order.detail(id)
+ setOrder(r)
+ } catch (e) { console.error(e) }
+ }
+
+ const statusMap: Record = {
+ pending: { text: '待付款', color: '#faad14' },
+ paid: { text: '待接单', color: '#1890ff' },
+ cooking: { text: '制作中', color: '#1890ff' },
+ delivering: { text: '配送中', color: '#722ed1' },
+ done: { text: '已完成', color: '#52c41a' },
+ cancelled: { text: '已取消', color: '#999' }
+ }
+ const status = order?.status || 'paid'
+ const statusInfo = statusMap[status] || statusMap.paid
+
+ async function cancel() {
+ try {
+ Taro.showLoading({ title: '处理中' })
+ await api.order.cancel(id)
+ Taro.hideLoading()
+ showToast('已取消', 'success')
+ loadData()
+ } catch (e) { Taro.hideLoading(); showToast('操作失败', 'error') }
+ }
+
+ async function confirm() {
+ try {
+ Taro.showLoading({ title: '处理中' })
+ await api.order.confirm(id)
+ Taro.hideLoading()
+ showToast('已确认', 'success')
+ loadData()
+ } catch (e) { Taro.hideLoading(); showToast('操作失败', 'error') }
+ }
+
+ async function pay() {
+ try {
+ Taro.showLoading({ title: '支付中' })
+ await api.order.pay(id)
+ Taro.hideLoading()
+ showToast('支付成功', 'success')
+ loadData()
+ } catch (e) { Taro.hideLoading(); showToast('支付失败', 'error') }
+ }
+
+ const items: OrderItem[] = order?.items || [
+ { name: '招牌牛肉面', qty: 1, price: 25 },
+ { name: '奶茶(大杯)', qty: 2, price: 15 },
+ ]
+ const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0)
+
+ return (
+
+
+
+ {status === 'delivering' ? '🛵' : status === 'done' ? '✓' : '📦'}
+
+
+ {statusInfo.text}
+ 预计 30 分钟送达
+
+
+
+
+
+ 📍
+
+ {order?.phone || '13800138000'}
+ {order?.address || '学生宿舍 3号楼 202室'}
+
+
+
+
+
+ 🍜 商品明细
+ {items.map((i, idx) => (
+
+ {i.name}
+ x{i.qty}
+ {formatPrice(i.price)}
+
+ ))}
+
+ 商品小计{formatPrice(subtotal)}
+ 配送费{formatPrice(order?.deliveryFee || 3)}
+ 优惠-{formatPrice(order?.discount || 0)}
+ 合计{formatPrice(order?.total || subtotal + 3)}
+
+
+
+ 📄 订单信息
+ 订单编号{order?.orderNo || 'OD' + Date.now()}
+ 下单时间{order?.createdAt ? formatTime(new Date(order.createdAt)) : formatTime(new Date())}
+ 支付方式{order?.payMethod === 'alipay' ? '支付宝' : '微信支付'}
+ {order?.remark && 备注{order.remark}}
+
+
+
+ {status === 'pending' && }
+ {status === 'pending' && }
+ {(status === 'paid' || status === 'cooking') && }
+ {status === 'delivering' && }
+ {status === 'done' && }
+
+
+ )
+}
diff --git a/taro-app/src/pages/food/shop/shop.config.ts b/taro-app/src/pages/food/shop/shop.config.ts
new file mode 100644
index 0000000..a7d84be
--- /dev/null
+++ b/taro-app/src/pages/food/shop/shop.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '商户详情',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/food/shop/shop.scss b/taro-app/src/pages/food/shop/shop.scss
new file mode 100644
index 0000000..09b5849
--- /dev/null
+++ b/taro-app/src/pages/food/shop/shop.scss
@@ -0,0 +1,184 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 140rpx;
+}
+
+.shop-header {
+ background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%);
+ padding: 40rpx 30rpx;
+ color: #fff;
+}
+.shop-title-bar { display: flex; align-items: baseline; gap: 20rpx; }
+.shop-title { font-size: 36rpx; font-weight: bold; }
+.shop-sub { font-size: 24rpx; color: rgba(255,255,255,0.9); }
+.shop-info-text { font-size: 26rpx; color: rgba(255,255,255,0.85); margin-top: 14rpx; }
+.shop-tags { display: flex; gap: 16rpx; margin-top: 20rpx; }
+.shop-tag { background: rgba(255,255,255,0.22); padding: 6rpx 16rpx; border-radius: 8rpx; font-size: 22rpx; }
+
+.shop-body {
+ display: flex;
+ background: #fff;
+ margin-top: 20rpx;
+ border-radius: 16rpx 16rpx 0 0;
+ min-height: 60vh;
+}
+
+.cat-col {
+ width: 180rpx;
+ background: #fafafa;
+ border-right: 2rpx solid #f0f0f0;
+ padding: 20rpx 0;
+}
+
+.cat-item {
+ padding: 24rpx 20rpx;
+ text-align: center;
+ font-size: 26rpx;
+ color: #666;
+ border-left: 6rpx solid transparent;
+}
+
+.cat-item.active {
+ background: #fff;
+ color: #1890ff;
+ font-weight: bold;
+ border-left: 6rpx solid #1890ff;
+}
+
+.goods-col { flex: 1; padding: 20rpx; }
+
+.goods-item {
+ display: flex;
+ padding: 24rpx 0;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.goods-item:last-child { border-bottom: none; }
+
+.goods-thumb {
+ width: 140rpx; height: 140rpx;
+ background: linear-gradient(135deg, #fff1e6 0%, #ffd6a5 100%);
+ border-radius: 12rpx;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 48rpx; flex-shrink: 0;
+}
+
+.goods-info { flex: 1; margin-left: 20rpx; min-width: 0; display: flex; flex-direction: column; justify-content: space-between; }
+.goods-name { font-size: 28rpx; font-weight: bold; }
+.goods-desc { font-size: 22rpx; color: #999; margin-top: 8rpx; }
+.goods-bottom { display: flex; justify-content: space-between; align-items: center; margin-top: 10rpx; }
+.goods-price { color: #f5222d; font-size: 32rpx; font-weight: bold; }
+.add-btn {
+ width: 48rpx; height: 48rpx;
+ background: #1890ff; color: #fff;
+ border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 36rpx; font-weight: bold;
+}
+
+.empty { text-align: center; padding: 80rpx 0; color: #999; }
+
+.cart-bar {
+ position: fixed;
+ left: 20rpx; right: 20rpx; bottom: 20rpx;
+ background: #fff;
+ border-radius: 60rpx;
+ padding: 14rpx 14rpx 14rpx 24rpx;
+ display: flex;
+ align-items: center;
+ box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.15);
+}
+
+.cart-icon {
+ width: 80rpx; height: 80rpx;
+ background: #1890ff;
+ border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 40rpx;
+ color: #fff;
+ position: relative;
+}
+.cart-badge {
+ position: absolute;
+ top: -6rpx; right: -6rpx;
+ min-width: 32rpx; height: 32rpx;
+ background: #f5222d; color: #fff;
+ border-radius: 16rpx;
+ font-size: 20rpx;
+ display: flex; align-items: center; justify-content: center;
+ padding: 0 8rpx;
+}
+
+.cart-info { flex: 1; margin-left: 20rpx; }
+.cart-total { color: #f5222d; font-size: 32rpx; font-weight: bold; display: block; }
+.cart-delivery { color: #999; font-size: 22rpx; }
+
+.checkout-btn {
+ background: #52c41a;
+ color: #fff;
+ padding: 20rpx 32rpx;
+ border-radius: 40rpx;
+ font-weight: bold;
+}
+.checkout-btn.disabled { background: #bbb; }
+
+.modal-mask {
+ position: fixed; inset: 0;
+ background: rgba(0,0,0,0.5);
+ display: flex; align-items: flex-end;
+ z-index: 999;
+}
+.modal {
+ background: #fff;
+ width: 100%;
+ border-radius: 24rpx 24rpx 0 0;
+ max-height: 85vh;
+ display: flex; flex-direction: column;
+}
+.modal-header { padding: 30rpx; border-bottom: 2rpx solid #f5f5f5; display: flex; justify-content: space-between; align-items: center; }
+.modal-title { font-size: 32rpx; font-weight: bold; }
+.modal-price { color: #f5222d; font-size: 30rpx; font-weight: bold; }
+.modal-body { padding: 20rpx 30rpx; flex: 1; overflow: hidden; }
+.modal-footer { padding: 20rpx 30rpx; border-top: 2rpx solid #f5f5f5; }
+
+.attr-block { padding: 20rpx 0; border-bottom: 2rpx solid #f5f5f5; }
+.attr-block:last-child { border-bottom: none; }
+.attr-name { font-size: 28rpx; font-weight: bold; margin-bottom: 20rpx; display: block; }
+.attr-tip { font-size: 22rpx; color: #999; font-weight: normal; }
+
+.attr-options { display: flex; flex-wrap: wrap; gap: 16rpx; }
+.opt {
+ padding: 14rpx 24rpx;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ font-size: 26rpx;
+ color: #666;
+ display: flex; align-items: center; gap: 8rpx;
+ border: 2rpx solid transparent;
+}
+.opt-active {
+ background: #e6f7ff;
+ color: #1890ff;
+ border-color: #1890ff;
+ font-weight: bold;
+}
+.opt-price { font-size: 22rpx; color: #f5222d; }
+
+.num-row { display: flex; align-items: center; gap: 24rpx; }
+.num-btn {
+ width: 56rpx; height: 56rpx;
+ border-radius: 50%;
+ background: #f5f5f5;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 32rpx;
+ color: #333;
+}
+.num-val { font-size: 30rpx; font-weight: bold; min-width: 60rpx; text-align: center; }
+
+.add-cart-btn {
+ background: #1890ff !important;
+ color: #fff !important;
+ border-radius: 40rpx !important;
+ font-size: 30rpx !important;
+ padding: 20rpx 0 !important;
+}
diff --git a/taro-app/src/pages/food/shop/shop.tsx b/taro-app/src/pages/food/shop/shop.tsx
new file mode 100644
index 0000000..26e6732
--- /dev/null
+++ b/taro-app/src/pages/food/shop/shop.tsx
@@ -0,0 +1,293 @@
+import { useEffect, useState, useMemo } from 'react'
+import { View, Text, ScrollView, Button, Image } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { showToast, formatPrice } from '../../../utils'
+import './index.scss'
+
+interface AttrOption { id: number; name: string; pricePerAddon?: number }
+interface Attribute {
+ id: number;
+ name: string;
+ type: 'select' | 'multiselect' | 'number';
+ options?: AttrOption[];
+ min?: number;
+ max?: number;
+ step?: number;
+}
+interface Goods { id: number; name: string; price: number; desc?: string; stock: number; category?: string; attributes?: Attribute[] }
+
+export default function Shop() {
+ const router = Taro.useRouter()
+ const shopId = Number(router.params?.shopId || 1)
+ const [shop, setShop] = useState(null)
+ const [goods, setGoods] = useState([])
+ const [categories, setCategories] = useState([])
+ const [activeCat, setActiveCat] = useState('all')
+ const [cart, setCart] = useState }>>({})
+ const [activeGoods, setActiveGoods] = useState(null)
+ const [tempAttrs, setTempAttrs] = useState>({})
+ const [tempQty, setTempQty] = useState(1)
+
+ useEffect(() => { loadShop() }, [shopId])
+
+ async function loadShop() {
+ try {
+ const [s, g] = await Promise.all([api.merchant.detail(shopId), api.goods.list({ merchantId: shopId })])
+ setShop(s)
+ const list = (g as any).list || g || []
+ setGoods(list)
+ const cats = Array.from(new Set(list.map((x: Goods) => x.category || '其他'))) as string[]
+ setCategories(cats.length ? cats : ['推荐'])
+ } catch (e) { console.error(e) }
+ }
+
+ const filtered = useMemo(() => {
+ if (activeCat === 'all') return goods
+ return goods.filter(g => (g.category || '其他') === activeCat)
+ }, [goods, activeCat])
+
+ const cartTotal = useMemo(() => {
+ let total = 0; let count = 0
+ Object.values(cart).forEach(item => {
+ let price = item.goods.price
+ item.goods.attributes?.forEach(attr => {
+ const sel = item.selectedAttrs[attr.id]
+ if (attr.type === 'select' && attr.options) {
+ const opt = attr.options.find(o => o.id === sel)
+ if (opt?.pricePerAddon) price += opt.pricePerAddon
+ } else if (attr.type === 'multiselect' && Array.isArray(sel)) {
+ sel.forEach((oid: number) => {
+ const opt = attr.options?.find(o => o.id === oid)
+ if (opt?.pricePerAddon) price += opt.pricePerAddon
+ })
+ } else if (attr.type === 'number' && typeof sel === 'number') {
+ price += (sel - (attr.min || 0)) * 2
+ }
+ })
+ total += price * item.qty
+ count += item.qty
+ })
+ return { total, count }
+ }, [cart])
+
+ function openGoods(g: Goods) {
+ const defaults: Record = {}
+ g.attributes?.forEach(attr => {
+ if (attr.type === 'select' && attr.options?.length) defaults[attr.id] = attr.options[0].id
+ else if (attr.type === 'multiselect') defaults[attr.id] = []
+ else if (attr.type === 'number') defaults[attr.id] = attr.min || 1
+ })
+ setTempAttrs(defaults)
+ setTempQty(1)
+ setActiveGoods(g)
+ }
+
+ function closeGoods() { setActiveGoods(null) }
+
+ function addToCart() {
+ if (!activeGoods) return
+ const key = buildCartKey(activeGoods, tempAttrs)
+ setCart(prev => {
+ const next = { ...prev }
+ if (next[key]) next[key] = { ...next[key], qty: next[key].qty + tempQty }
+ else next[key] = { goods: activeGoods, qty: tempQty, selectedAttrs: { ...tempAttrs } }
+ return next
+ })
+ setActiveGoods(null)
+ showToast('已加入购物车', 'success')
+ }
+
+ function buildCartKey(g: Goods, attrs: Record): number {
+ let s = g.id * 1000000
+ Object.keys(attrs).forEach(k => {
+ const v = attrs[Number(k)]
+ s += (typeof v === 'number' ? v : Array.isArray(v) ? v.reduce((a, b) => a + b, 0) : 0)
+ })
+ return s
+ }
+
+ function itemPrice(g: Goods, attrs: Record): number {
+ let price = g.price
+ g.attributes?.forEach(attr => {
+ const sel = attrs[attr.id]
+ if (attr.type === 'select' && attr.options) {
+ const opt = attr.options.find(o => o.id === sel)
+ if (opt?.pricePerAddon) price += opt.pricePerAddon
+ } else if (attr.type === 'multiselect' && Array.isArray(sel)) {
+ sel.forEach((oid: number) => {
+ const opt = attr.options?.find(o => o.id === oid)
+ if (opt?.pricePerAddon) price += opt.pricePerAddon
+ })
+ }
+ })
+ return price
+ }
+
+ function incCart(key: number) { setCart(p => ({ ...p, [key]: { ...p[key], qty: p[key].qty + 1 } })) }
+ function decCart(key: number) {
+ setCart(p => {
+ const next = { ...p }
+ if (next[key].qty > 1) next[key] = { ...next[key], qty: next[key].qty - 1 }
+ else delete next[key]
+ return next
+ })
+ }
+
+ function goCheckout() {
+ if (cartTotal.count === 0) { showToast('请先选择商品'); return }
+ const items = Object.entries(cart).map(([k, item]) => ({
+ key: k, goodsId: item.goods.id, name: item.goods.name, qty: item.qty,
+ price: itemPrice(item.goods, item.selectedAttrs),
+ attrs: item.selectedAttrs
+ }))
+ Taro.setStorageSync('checkout_items', items)
+ Taro.setStorageSync('checkout_shop', shopId)
+ Taro.navigateTo({ url: '/pages/food/checkout/checkout' })
+ }
+
+ return (
+
+
+
+ {shop?.name || '商户详情'}
+ ★ {Number(shop?.rating || 4.8).toFixed(1)} · 月销{shop?.sales || 100}
+
+ {shop?.address || '校园综合服务中心'}
+
+ 起送 {formatPrice(shop?.minOrder || 15)}
+ 配送 {formatPrice(shop?.deliveryFee || 3)}
+
+
+
+
+
+ setActiveCat('all')}
+ >全部
+ {categories.map(c => (
+ setActiveCat(c)}
+ >{c}
+ ))}
+
+
+ {filtered.map(g => (
+ openGoods(g)}>
+ 🍜
+
+ {g.name}
+ {g.desc || '美味可口'}
+
+ {formatPrice(g.price)}
+ { e.stopPropagation(); openGoods(g) }}>
+ +
+
+
+
+
+ ))}
+ {filtered.length === 0 && 该分类暂无商品}
+
+
+
+
+ cartTotal.count > 0 && setCart({})}>
+ 🛒
+ {cartTotal.count > 0 && {cartTotal.count}}
+
+
+ {formatPrice(cartTotal.total)}
+ 另需配送费 {formatPrice(shop?.deliveryFee || 3)}
+
+ 0 ? '' : ' disabled')} onClick={goCheckout}>
+ 去结算
+
+
+
+ {activeGoods && (
+
+ e.stopPropagation()}>
+
+ {activeGoods.name}
+ {formatPrice(activeGoods.price)} 起
+
+
+ {activeGoods.attributes?.map(attr => (
+
+ {attr.name}
+ {attr.type === 'multiselect' && (可多选)}
+ {attr.type === 'number' && (数量)}
+
+ {attr.type === 'select' && attr.options && (
+
+ {attr.options.map(opt => (
+ setTempAttrs({ ...tempAttrs, [attr.id]: opt.id })}
+ >
+ {opt.name}
+ {opt.pricePerAddon ? +{formatPrice(opt.pricePerAddon)} : null}
+
+ ))}
+
+ )}
+ {attr.type === 'multiselect' && attr.options && (
+
+ {attr.options.map(opt => {
+ const arr: number[] = Array.isArray(tempAttrs[attr.id]) ? tempAttrs[attr.id] : []
+ const checked = arr.includes(opt.id)
+ return (
+ {
+ const next = checked ? arr.filter(x => x !== opt.id) : [...arr, opt.id]
+ setTempAttrs({ ...tempAttrs, [attr.id]: next })
+ }}
+ >
+ {opt.name}
+ {opt.pricePerAddon ? +{formatPrice(opt.pricePerAddon)} : null}
+
+ )
+ })}
+
+ )}
+ {attr.type === 'number' && (
+
+ setTempAttrs({ ...tempAttrs, [attr.id]: Math.max(attr.min || 1, (tempAttrs[attr.id] || 1) - 1) })}>
+ -
+
+ {tempAttrs[attr.id] || attr.min || 1}
+ setTempAttrs({ ...tempAttrs, [attr.id]: Math.min(attr.max || 99, (tempAttrs[attr.id] || 1) + 1) })}>
+ +
+
+
+ )}
+
+ ))}
+
+ 购买数量
+
+ setTempQty(Math.max(1, tempQty - 1))}>-
+ {tempQty}
+ setTempQty(tempQty + 1)}>+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/taro-app/src/pages/forum/detail/detail.config.ts b/taro-app/src/pages/forum/detail/detail.config.ts
new file mode 100644
index 0000000..231c5f6
--- /dev/null
+++ b/taro-app/src/pages/forum/detail/detail.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '帖子详情',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/forum/detail/detail.scss b/taro-app/src/pages/forum/detail/detail.scss
new file mode 100644
index 0000000..97a61e3
--- /dev/null
+++ b/taro-app/src/pages/forum/detail/detail.scss
@@ -0,0 +1,92 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 180rpx;
+}
+
+.content-card {
+ background: #fff;
+ margin: 20rpx;
+ padding: 30rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.title { font-size: 38rpx; font-weight: bold; display: block; }
+.meta { display: flex; gap: 20rpx; margin-top: 16rpx; font-size: 24rpx; color: #999; }
+.author { color: #1890ff; }
+.time { color: #999; }
+.content { font-size: 30rpx; color: #333; line-height: 1.8; margin-top: 30rpx; display: block; }
+
+.action-card {
+ display: flex;
+ background: #fff;
+ margin: 0 20rpx 20rpx;
+ padding: 20rpx 0;
+ border-radius: 16rpx;
+}
+.act {
+ flex: 1;
+ display: flex; flex-direction: column; align-items: center;
+ gap: 8rpx;
+}
+.act-icon { font-size: 40rpx; }
+.act-num { font-size: 24rpx; color: #666; }
+
+.comment-card {
+ background: #fff;
+ margin: 0 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+}
+.comment-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
+
+.comment-item {
+ display: flex;
+ padding: 20rpx 0;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.comment-item:last-child { border-bottom: none; }
+
+.avatar {
+ width: 64rpx; height: 64rpx;
+ background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%);
+ border-radius: 50%;
+ color: #fff;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 24rpx;
+ font-weight: bold;
+ flex-shrink: 0;
+}
+
+.comment-body { flex: 1; margin-left: 16rpx; min-width: 0; }
+.comment-author { font-size: 26rpx; font-weight: bold; display: block; }
+.comment-content { font-size: 28rpx; color: #333; margin-top: 8rpx; display: block; line-height: 1.6; }
+.comment-time { font-size: 22rpx; color: #999; margin-top: 10rpx; display: block; }
+
+.empty-mini { text-align: center; padding: 40rpx 0; color: #999; font-size: 26rpx; }
+
+.reply-bar {
+ position: fixed;
+ left: 0; right: 0; bottom: 0;
+ background: #fff;
+ padding: 20rpx;
+ display: flex;
+ gap: 16rpx;
+ align-items: center;
+ box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.05);
+}
+.reply-input {
+ flex: 1;
+ background: #f5f5f5;
+ border-radius: 40rpx;
+ padding: 18rpx 28rpx;
+ font-size: 28rpx;
+}
+.reply-btn {
+ background: #1890ff !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 40rpx !important;
+ padding: 16rpx 30rpx !important;
+ font-size: 28rpx !important;
+}
diff --git a/taro-app/src/pages/forum/detail/detail.tsx b/taro-app/src/pages/forum/detail/detail.tsx
new file mode 100644
index 0000000..67d8a75
--- /dev/null
+++ b/taro-app/src/pages/forum/detail/detail.tsx
@@ -0,0 +1,103 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Button, Input } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { showToast, formatTime } from '../../../utils'
+import './index.scss'
+
+interface Comment { id: number; author: string; content: string; createdAt: number; likes: number }
+
+export default function ForumDetail() {
+ const router = Taro.useRouter()
+ const id = Number(router.params?.id || 1)
+ const [detail, setDetail] = useState(null)
+ const [comments, setComments] = useState([])
+ const [newComment, setNewComment] = useState('')
+ const [liked, setLiked] = useState(false)
+ const [fav, setFav] = useState(false)
+
+ useEffect(() => { loadData() }, [id])
+
+ async function loadData() {
+ try {
+ const d = await api.forum.detail(id)
+ setDetail(d)
+ setComments((d as any).comments || [])
+ } catch (e) { console.error(e) }
+ }
+
+ async function like() {
+ try {
+ await api.forum.like(id)
+ setLiked(true)
+ setDetail({ ...detail, likes: (detail?.likes || 0) + 1 })
+ showToast('已点赞', 'success')
+ } catch (e) { showToast('操作失败', 'error') }
+ }
+
+ async function favorite() {
+ try {
+ await api.forum.favorite(id)
+ setFav(true)
+ showToast('已收藏', 'success')
+ } catch (e) { showToast('操作失败', 'error') }
+ }
+
+ async function sendComment() {
+ if (!newComment.trim()) return
+ try {
+ await api.forum.reply(id, { content: newComment })
+ setComments([{ id: Date.now(), author: '我', content: newComment, createdAt: Date.now(), likes: 0 }, ...comments])
+ setNewComment('')
+ showToast('评论成功', 'success')
+ } catch (e) { showToast('操作失败', 'error') }
+ }
+
+ return (
+
+
+ {detail?.title || '帖子标题'}
+
+ {detail?.author || '匿名用户'}
+ {formatTime(new Date(detail?.createdAt || Date.now()))}
+
+ {detail?.content || '这是帖子内容。'}
+
+
+
+
+ {liked ? '👍' : '👍'}
+ {(detail?.likes || 0) + (liked ? 1 : 0)}
+
+
+ {fav ? '⭐' : '☆'}
+ 收藏
+
+
+ 💬
+ {comments.length}
+
+
+
+
+ 评论 {comments.length}
+ {comments.map((c: Comment) => (
+
+ {(c.author || 'A')[0]}
+
+ {c.author}
+ {c.content}
+ {formatTime(new Date(c.createdAt))}
+
+
+ ))}
+ {comments.length === 0 && 暂无评论,快来说两句}
+
+
+
+ setNewComment(e.detail.value)} />
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/forum/list/list.config.ts b/taro-app/src/pages/forum/list/list.config.ts
new file mode 100644
index 0000000..55a347d
--- /dev/null
+++ b/taro-app/src/pages/forum/list/list.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '校园论坛',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/forum/list/list.scss b/taro-app/src/pages/forum/list/list.scss
new file mode 100644
index 0000000..1deca77
--- /dev/null
+++ b/taro-app/src/pages/forum/list/list.scss
@@ -0,0 +1,86 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 140rpx;
+}
+
+.hero {
+ background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%);
+ color: #fff;
+ padding: 30rpx 30rpx;
+}
+.hero-title { font-size: 36rpx; font-weight: bold; display: block; }
+.hero-sub { font-size: 24rpx; color: rgba(255,255,255,0.9); margin-top: 6rpx; display: block; }
+
+.search-bar {
+ display: flex;
+ align-items: center;
+ background: rgba(255,255,255,0.95);
+ border-radius: 40rpx;
+ padding: 14rpx 24rpx;
+ margin-top: 20rpx;
+}
+.search-icon { margin-right: 12rpx; color: #999; font-size: 28rpx; }
+.search-input { flex: 1; font-size: 26rpx; color: #333; }
+
+.cat-bar {
+ background: #fff;
+ white-space: nowrap;
+ padding: 20rpx 0;
+}
+.cat-item {
+ display: inline-block;
+ padding: 12rpx 28rpx;
+ background: #f5f5f5;
+ border-radius: 30rpx;
+ font-size: 26rpx;
+ color: #666;
+ margin-left: 20rpx;
+}
+.cat-item.active {
+ background: #1890ff;
+ color: #fff;
+ font-weight: bold;
+}
+
+.post-card {
+ background: #fff;
+ border-radius: 16rpx;
+ margin: 20rpx;
+ padding: 24rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.post-title { font-size: 30rpx; font-weight: bold; display: block; }
+.post-content { font-size: 26rpx; color: #666; margin-top: 12rpx; display: block; line-height: 1.6; }
+
+.post-foot {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16rpx;
+ margin-top: 20rpx;
+ padding-top: 16rpx;
+ border-top: 2rpx solid #f5f5f5;
+ align-items: center;
+}
+.post-cat { background: #e6f7ff; color: #1890ff; padding: 4rpx 14rpx; border-radius: 20rpx; font-size: 22rpx; }
+.post-author { font-size: 22rpx; color: #666; }
+.post-time { font-size: 22rpx; color: #999; }
+.post-stat { font-size: 22rpx; color: #999; }
+
+.empty { text-align: center; padding: 120rpx 0; color: #999; font-size: 26rpx; }
+.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
+
+.fab {
+ position: fixed;
+ right: 40rpx;
+ bottom: 60rpx;
+ width: 100rpx;
+ height: 100rpx;
+ background: #1890ff;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.4);
+ z-index: 100;
+}
diff --git a/taro-app/src/pages/forum/list/list.tsx b/taro-app/src/pages/forum/list/list.tsx
new file mode 100644
index 0000000..c724b18
--- /dev/null
+++ b/taro-app/src/pages/forum/list/list.tsx
@@ -0,0 +1,70 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Button, Input } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { timeAgo } from '../../../utils'
+import './index.scss'
+
+interface Post { id: number; title: string; content: string; author: string; likes: number; comments: number; createdAt: number; category: string }
+
+export default function ForumList() {
+ const [list, setList] = useState([])
+ const [keyword, setKeyword] = useState('')
+ const [cat, setCat] = useState('全部')
+ const cats = ['全部', '校园', '学习', '生活', '吐槽', '求助', '分享']
+
+ useEffect(() => { loadData() }, [cat])
+
+ async function loadData() {
+ try {
+ const r = await api.forum.list({ category: cat === '全部' ? undefined : cat })
+ setList((r as any).list || r || [])
+ } catch (e) { console.error(e) }
+ }
+
+ function onDetail(id: number) { Taro.navigateTo({ url: '/pages/forum/detail/detail?id=' + id }) }
+ function onPublish() { Taro.navigateTo({ url: '/pages/forum/publish/publish' }) }
+
+ const filtered = list.filter(p => !keyword || p.title.includes(keyword) || p.content.includes(keyword))
+
+ return (
+
+
+ 📚 校园论坛
+ 畅所欲言,分享校园点滴
+
+ 🔍
+ setKeyword(e.detail.value)} confirmType="search" />
+
+
+
+
+ {cats.map(c => (
+ setCat(c)}>
+ {c}
+
+ ))}
+
+
+ {filtered.map((p: Post) => (
+ onDetail(p.id)}>
+ {p.title}
+ {p.content}
+
+ {p.category}
+ {p.author}
+ {timeAgo(p.createdAt || Date.now())}
+ 👍 {p.likes}
+ 💬 {p.comments}
+
+
+ ))}
+
+ {filtered.length === 0 && 📝暂无帖子}
+
+
+ +
+
+
+ )
+}
diff --git a/taro-app/src/pages/forum/publish/publish.config.ts b/taro-app/src/pages/forum/publish/publish.config.ts
new file mode 100644
index 0000000..6e4fa6e
--- /dev/null
+++ b/taro-app/src/pages/forum/publish/publish.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '发布帖子',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/forum/publish/publish.scss b/taro-app/src/pages/forum/publish/publish.scss
new file mode 100644
index 0000000..21c90cd
--- /dev/null
+++ b/taro-app/src/pages/forum/publish/publish.scss
@@ -0,0 +1,79 @@
+.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: 400rpx;
+ 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: #e6f7ff;
+ color: #1890ff;
+ border-color: #1890ff;
+ 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: #1890ff !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/forum/publish/publish.tsx b/taro-app/src/pages/forum/publish/publish.tsx
new file mode 100644
index 0000000..420aba0
--- /dev/null
+++ b/taro-app/src/pages/forum/publish/publish.tsx
@@ -0,0 +1,49 @@
+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 ForumPublish() {
+ const [title, setTitle] = useState('')
+ const [content, setContent] = useState('')
+ const [category, setCategory] = useState('校园')
+ const cats = ['校园', '学习', '生活', '吐槽', '求助', '分享']
+
+ async function submit() {
+ if (!title || !content) { showToast('请填写完整'); return }
+ try {
+ Taro.showLoading({ title: '发布中', mask: true })
+ await api.forum.create({ title, content, category })
+ Taro.hideLoading()
+ showToast('发布成功', 'success')
+ setTimeout(() => Taro.navigateBack(), 1200)
+ } catch (e) { Taro.hideLoading(); showToast('发布失败', 'error') }
+ }
+
+ return (
+
+
+ 标题 *
+ setTitle(e.detail.value)} />
+
+ 分类 *
+
+ {cats.map(c => (
+ setCategory(c)}>
+ {c}
+
+ ))}
+
+
+ 内容 *
+
+
+
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/index/index.config.ts b/taro-app/src/pages/index/index.config.ts
new file mode 100644
index 0000000..4fa866c
--- /dev/null
+++ b/taro-app/src/pages/index/index.config.ts
@@ -0,0 +1,5 @@
+export default definePageConfig({
+ navigationBarTitleText: '校园服务',
+ enablePullDownRefresh: true,
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/index/index.scss b/taro-app/src/pages/index/index.scss
new file mode 100644
index 0000000..3f69349
--- /dev/null
+++ b/taro-app/src/pages/index/index.scss
@@ -0,0 +1,204 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 40rpx;
+}
+
+.header {
+ background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%);
+ padding: 30rpx 20rpx;
+}
+
+.header-title {
+ color: #fff;
+ font-size: 36rpx;
+ font-weight: bold;
+ padding: 10rpx 20rpx;
+}
+
+.search-bar {
+ display: flex;
+ align-items: center;
+ padding: 16rpx 24rpx;
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 40rpx;
+ margin: 10rpx 20rpx 0;
+}
+
+.search-icon {
+ margin-right: 12rpx;
+ color: #999;
+}
+
+.search-text {
+ flex: 1;
+ color: #999;
+ font-size: 26rpx;
+}
+
+.banner {
+ height: 280rpx;
+ margin: 20rpx;
+}
+
+.banner-item {
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%);
+ border-radius: 16rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-size: 32rpx;
+ font-weight: bold;
+}
+
+.banner-orange {
+ background: linear-gradient(135deg, #f5222d 0%, #ff7875 100%);
+}
+
+.banner-green {
+ background: linear-gradient(135deg, #52c41a 0%, #95de64 100%);
+}
+
+.card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20rpx;
+ padding-bottom: 16rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+}
+
+.card-title {
+ font-size: 30rpx;
+ font-weight: bold;
+}
+
+.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;
+}
+
+.grid-name {
+ font-size: 24rpx;
+ color: #666;
+}
+
+.merchant-item {
+ display: flex;
+ align-items: center;
+ padding: 20rpx 0;
+ border-bottom: 1rpx solid #f0f0f0;
+}
+
+.merchant-item:last-child { border-bottom: none; }
+
+.thumb {
+ width: 140rpx;
+ height: 140rpx;
+ background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
+ border-radius: 12rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 48rpx;
+ flex-shrink: 0;
+}
+
+.rating { color: #faad14; font-size: 24rpx; }
+
+.tag {
+ display: inline-block;
+ padding: 4rpx 16rpx;
+ border-radius: 8rpx;
+ font-size: 22rpx;
+ background: #e6f7ff;
+ color: #1890ff;
+}
+
+.forum-item {
+ padding: 20rpx 0;
+ border-bottom: 1rpx solid #f0f0f0;
+}
+
+.forum-item:last-child { border-bottom: none; }
+
+.forum-meta text { margin-left: 16rpx; }
+
+.coupon-scroll {
+ white-space: nowrap;
+}
+
+.coupon-card {
+ display: inline-flex;
+ width: 320rpx;
+ margin-right: 20rpx;
+ background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
+ border-radius: 12rpx;
+ border: 2rpx dashed #ff9800;
+ padding: 16rpx;
+ align-items: center;
+}
+
+.coupon-price {
+ font-size: 36rpx;
+ padding-right: 16rpx;
+ border-right: 2rpx dashed #ff9800;
+}
+
+.coupon-info {
+ padding-left: 16rpx;
+ flex: 1;
+}
+
+.footer-note {
+ text-align: center;
+ color: #999;
+ font-size: 24rpx;
+ padding: 40rpx 0;
+}
+
+.text-primary { color: #1890ff; }
+.text-success { color: #52c41a; }
+.text-warning { color: #faad14; }
+.text-danger { color: #f5222d; }
+.text-secondary { color: #666; }
+.text-light { color: #999; }
+.text-bold { font-weight: bold; }
+.text-lg { font-size: 30rpx; }
+.text-sm { font-size: 24rpx; }
+
+.flex-between { display: flex; align-items: center; justify-content: space-between; }
+.mt-10 { margin-top: 10rpx; }
+.mt-20 { margin-top: 20rpx; }
+.ml-20 { margin-left: 20rpx; }
+.flex-1 { flex: 1; min-width: 0; }
diff --git a/taro-app/src/pages/index/index.tsx b/taro-app/src/pages/index/index.tsx
new file mode 100644
index 0000000..fd025dc
--- /dev/null
+++ b/taro-app/src/pages/index/index.tsx
@@ -0,0 +1,164 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Swiper, SwiperItem } from '@tarojs/components'
+import Taro, { useRouter } from '@tarojs/taro'
+import { api } from '../../services/api'
+import { showToast } from '../../utils'
+import './index.scss'
+
+interface Merchant { id: number; name: string; category: string; rating: number; sales: number; address: string }
+interface Goods { id: number; name: string; price: number }
+interface Post { id: number; title: string; author: string; likes: number; comments: number; createdAt: number }
+
+export default function Index() {
+ const [merchants, setMerchants] = useState([])
+ const [goods, setGoods] = useState([])
+ const [posts, setPosts] = useState([])
+ const [coupons, setCoupons] = useState([])
+
+ useEffect(() => {
+ initData()
+ }, [])
+
+ async function initData() {
+ try {
+ const [m, g, p, c] = await Promise.all([
+ api.merchant.list(),
+ api.goods.list(),
+ api.forum.list(),
+ api.coupon.list()
+ ])
+ setMerchants((m as any).list || m || [])
+ setGoods((g as any).list || g || [])
+ setPosts((p as any).list || p || [])
+ setCoupons(c || [])
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ function onMerchantTap(id: number) {
+ Taro.navigateTo({ url: '/pages/food/shop/shop?shopId=' + id })
+ }
+
+ function onForumTap(id: number) {
+ Taro.navigateTo({ url: '/pages/forum/detail/detail?id=' + id })
+ }
+
+ function onMarketTap() { Taro.switchTab({ url: '/pages/community/list/list' }).catch(() => Taro.navigateTo({ url: '/pages/market/list/list' })) }
+
+ const categories = [
+ { icon: '🍔', name: '外卖点餐', url: '/pages/food/index/index', color: '#ff7a45' },
+ { icon: '🛍️', name: '二手市场', url: '/pages/market/list/list', color: '#52c41a' },
+ { icon: '💌', name: '表白墙', url: '/pages/confession/list/list', color: '#f5222d' },
+ { icon: '📚', name: '校园论坛', url: '/pages/forum/list/list', color: '#1890ff' },
+ { icon: '🎯', name: '兴趣社区', url: '/pages/community/list/list', color: '#722ed1' },
+ { icon: '🏃', name: '跑腿服务', url: '/pages/errand/list/list', color: '#fa8c16' },
+ { icon: '🏪', name: '商户入驻', url: '/pages/merchant/apply/apply', color: '#13c2c2' },
+ { icon: '✨', name: '自定义', url: '/pages/custom/diy/diy?id=1', color: '#eb2f96' }
+ ]
+
+ return (
+
+
+
+ 🎓 校园综合服务
+
+
+ 🔍
+ 搜索商品、帖子、社区...
+
+
+
+
+
+ 🎉 外卖新上线,下单立减
+
+
+ 💌 表白墙,说出你的心声
+
+
+ 🛍️ 二手市场,发现好物
+
+
+
+
+
+ {categories.map(cat => (
+ Taro.navigateTo({ url: cat.url })}>
+
+ {cat.icon}
+
+ {cat.name}
+
+ ))}
+
+
+
+ {coupons.length > 0 && (
+
+
+ 🎟️ 优惠券
+ 全部 ›
+
+
+ {coupons.slice(0, 4).map(c => (
+
+
+ {c.type === '满减' ? '¥' + c.discount : (c.discount * 10).toFixed(1) + '折'}
+
+
+ {c.name}
+ 满¥{c.minOrder}可用
+
+
+ ))}
+
+
+ )}
+
+
+
+ 🔥 热门商户
+ Taro.navigateTo({ url: '/pages/merchant/list/list' })}>查看更多 ›
+
+ {merchants.map(m => (
+ onMerchantTap(m.id)}>
+ 🏪
+
+
+ {m.name}
+ ★ {Number(m.rating).toFixed(1)}
+
+ {m.address} · 月销{m.sales}
+
+ {m.category}
+
+
+
+ ))}
+
+
+
+
+ 💬 校园热议
+
+ {posts.map(p => (
+ onForumTap(p.id)}>
+ {p.title}
+
+ {p.author}
+
+ 👍 {p.likes}
+ 💬 {p.comments}
+
+
+
+ ))}
+
+
+
+ — 我是有底线的 —
+
+
+ )
+}
diff --git a/taro-app/src/pages/market/detail/detail.config.ts b/taro-app/src/pages/market/detail/detail.config.ts
new file mode 100644
index 0000000..7fde5cb
--- /dev/null
+++ b/taro-app/src/pages/market/detail/detail.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '商品详情',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/market/detail/detail.scss b/taro-app/src/pages/market/detail/detail.scss
new file mode 100644
index 0000000..6e27f7f
--- /dev/null
+++ b/taro-app/src/pages/market/detail/detail.scss
@@ -0,0 +1,114 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 180rpx;
+}
+
+.image-box {
+ background: linear-gradient(135deg, #f6ffed 0%, #b7eb8f 100%);
+ height: 500rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.image-emoji { font-size: 180rpx; }
+
+.info-card {
+ background: #fff;
+ margin: 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.price { color: #f5222d; font-size: 48rpx; font-weight: bold; display: block; }
+.original-price { color: #999; font-size: 26rpx; text-decoration: line-through; display: block; margin-top: 4rpx; }
+.title { font-size: 32rpx; font-weight: bold; margin-top: 16rpx; display: block; }
+.desc { font-size: 26rpx; color: #666; margin-top: 12rpx; display: block; line-height: 1.6; }
+
+.meta-row {
+ display: flex;
+ gap: 20rpx;
+ margin-top: 20rpx;
+ padding-top: 20rpx;
+ border-top: 2rpx solid #f5f5f5;
+}
+.meta-item { font-size: 24rpx; color: #999; }
+
+.seller-card {
+ background: #fff;
+ margin: 0 20rpx 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ display: flex;
+ align-items: center;
+}
+.seller-avatar {
+ width: 80rpx; height: 80rpx;
+ background: linear-gradient(135deg, #52c41a 0%, #95de64 100%);
+ border-radius: 50%;
+ color: #fff;
+ font-size: 32rpx;
+ font-weight: bold;
+ display: flex; align-items: center; justify-content: center;
+}
+.seller-body { flex: 1; margin-left: 16rpx; }
+.seller-name { font-size: 28rpx; font-weight: bold; display: block; }
+.seller-desc { font-size: 22rpx; color: #999; margin-top: 4rpx; display: block; }
+.seller-btn {
+ background: #e6f7ff !important;
+ color: #1890ff !important;
+ border: none !important;
+ border-radius: 30rpx !important;
+ padding: 12rpx 28rpx !important;
+ font-size: 26rpx !important;
+}
+
+.desc-card {
+ background: #fff;
+ margin: 0 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+}
+.desc-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 16rpx; }
+.desc-text { font-size: 28rpx; color: #333; line-height: 1.8; display: block; }
+
+.action-bar {
+ position: fixed;
+ left: 0; right: 0; bottom: 0;
+ background: #fff;
+ padding: 20rpx;
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+ box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.05);
+}
+.action-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 0 20rpx;
+ gap: 4rpx;
+}
+.action-icon { font-size: 36rpx; }
+.action-text { font-size: 22rpx; color: #666; }
+
+.chat-btn {
+ background: #fff !important;
+ color: #52c41a !important;
+ border: 2rpx solid #52c41a !important;
+ border-radius: 40rpx !important;
+ padding: 18rpx 30rpx !important;
+ font-size: 28rpx !important;
+ flex: 1;
+}
+
+.buy-btn {
+ background: #52c41a !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 40rpx !important;
+ padding: 18rpx 30rpx !important;
+ font-size: 28rpx !important;
+ font-weight: bold;
+ flex: 1;
+}
diff --git a/taro-app/src/pages/market/detail/detail.tsx b/taro-app/src/pages/market/detail/detail.tsx
new file mode 100644
index 0000000..e290f88
--- /dev/null
+++ b/taro-app/src/pages/market/detail/detail.tsx
@@ -0,0 +1,74 @@
+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, formatPrice, formatTime } from '../../../utils'
+import './index.scss'
+
+export default function MarketDetail() {
+ const router = Taro.useRouter()
+ const id = Number(router.params?.id || 1)
+ const [detail, setDetail] = useState(null)
+ const [fav, setFav] = useState(false)
+
+ useEffect(() => { loadData() }, [id])
+
+ async function loadData() {
+ try {
+ const d = await api.market.detail(id)
+ setDetail(d)
+ } catch (e) { console.error(e) }
+ }
+
+ async function favorite() {
+ try {
+ await api.market.favorite(id)
+ setFav(true)
+ showToast('已收藏', 'success')
+ } catch (e) { showToast('操作失败', 'error') }
+ }
+
+ function contact() { showToast('已发送消息', 'success') }
+
+ return (
+
+
+ 📦
+
+
+
+ {formatPrice(detail?.price || 0)}
+ {formatPrice(detail?.originalPrice || 0)}
+ {detail?.title || '商品标题'}
+ {detail?.desc || '商品描述'}
+
+ 📍 {detail?.location || '校内'}
+ 📅 {formatTime(new Date(detail?.createdAt || Date.now()))}
+
+
+
+
+ {(detail?.seller || 'S')[0]}
+
+ {detail?.seller || '卖家'}
+ 诚信卖家 · 评分 4.9
+
+
+
+
+
+ 📝 商品描述
+ {detail?.fullDesc || detail?.desc || '成色九成新,功能完好,低价转让,非诚勿扰。校内当面交易,支持验货。'}
+
+
+
+
+ {fav ? '❤️' : '🤍'}
+ {fav ? '已收藏' : '收藏'}
+
+
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/market/list/list.config.ts b/taro-app/src/pages/market/list/list.config.ts
new file mode 100644
index 0000000..2b44a4c
--- /dev/null
+++ b/taro-app/src/pages/market/list/list.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '二手市集',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/market/list/list.scss b/taro-app/src/pages/market/list/list.scss
new file mode 100644
index 0000000..ccc39c4
--- /dev/null
+++ b/taro-app/src/pages/market/list/list.scss
@@ -0,0 +1,132 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 140rpx;
+}
+
+.hero {
+ background: linear-gradient(135deg, #52c41a 0%, #95de64 100%);
+ color: #fff;
+ padding: 30rpx 30rpx;
+}
+.hero-title { font-size: 36rpx; font-weight: bold; display: block; }
+.hero-sub { font-size: 24rpx; color: rgba(255,255,255,0.9); margin-top: 6rpx; display: block; }
+
+.search-bar {
+ display: flex;
+ align-items: center;
+ background: rgba(255,255,255,0.95);
+ border-radius: 40rpx;
+ padding: 14rpx 24rpx;
+ margin-top: 20rpx;
+}
+.search-icon { margin-right: 12rpx; color: #999; font-size: 28rpx; }
+.search-input { flex: 1; font-size: 26rpx; color: #333; }
+
+.cat-bar {
+ background: #fff;
+ white-space: nowrap;
+ padding: 20rpx 0;
+}
+.cat-item {
+ display: inline-block;
+ padding: 12rpx 28rpx;
+ background: #f5f5f5;
+ border-radius: 30rpx;
+ font-size: 26rpx;
+ color: #666;
+ margin-left: 20rpx;
+}
+.cat-item.active {
+ background: #52c41a;
+ color: #fff;
+ font-weight: bold;
+}
+
+.quick-row {
+ display: flex;
+ background: #fff;
+ margin: 20rpx;
+ border-radius: 16rpx;
+ padding: 20rpx 0;
+}
+.quick-item {
+ flex: 1;
+ display: flex; flex-direction: column; align-items: center;
+ gap: 8rpx;
+}
+.quick-icon { font-size: 44rpx; }
+.quick-text { font-size: 22rpx; color: #333; }
+
+.grid {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 0 20rpx;
+ gap: 20rpx;
+}
+
+.goods-card {
+ width: calc(50% - 10rpx);
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+ box-sizing: border-box;
+}
+
+.goods-image {
+ width: 100%;
+ height: 260rpx;
+ background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
+ border-radius: 12rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 80rpx;
+}
+
+.goods-title {
+ font-size: 26rpx;
+ color: #333;
+ margin-top: 12rpx;
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.price-row {
+ display: flex;
+ align-items: baseline;
+ gap: 10rpx;
+ margin-top: 8rpx;
+}
+.price { color: #f5222d; font-size: 32rpx; font-weight: bold; }
+.original-price { color: #999; font-size: 22rpx; text-decoration: line-through; }
+
+.goods-meta {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 10rpx;
+ font-size: 22rpx;
+ color: #999;
+}
+
+.empty { text-align: center; padding: 120rpx 0; color: #999; font-size: 26rpx; }
+.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
+
+.fab {
+ position: fixed;
+ right: 40rpx;
+ bottom: 60rpx;
+ width: 100rpx;
+ height: 100rpx;
+ background: #52c41a;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 24rpx rgba(82, 196, 26, 0.4);
+ z-index: 100;
+}
diff --git a/taro-app/src/pages/market/list/list.tsx b/taro-app/src/pages/market/list/list.tsx
new file mode 100644
index 0000000..4bc24ed
--- /dev/null
+++ b/taro-app/src/pages/market/list/list.tsx
@@ -0,0 +1,93 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Input } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { formatPrice, timeAgo } from '../../../utils'
+import './index.scss'
+
+interface Goods { id: number; title: string; price: number; originalPrice: number; seller: string; location: string; createdAt: number; category: string }
+
+export default function MarketList() {
+ const [list, setList] = useState([])
+ const [keyword, setKeyword] = useState('')
+ const [cat, setCat] = useState('全部')
+ const cats = ['全部', '数码', '教材', '生活用品', '服饰', '运动', '美妆', '其他']
+
+ useEffect(() => { loadData() }, [cat])
+
+ async function loadData() {
+ try {
+ const r = await api.market.list({ category: cat === '全部' ? undefined : cat })
+ setList((r as any).list || r || [])
+ } catch (e) { console.error(e) }
+ }
+
+ function onDetail(id: number) { Taro.navigateTo({ url: '/pages/market/detail/detail?id=' + id }) }
+ function onPublish() { Taro.navigateTo({ url: '/pages/market/publish/publish' }) }
+ function onMyGoods() { Taro.navigateTo({ url: '/pages/market/my-goods/my-goods' }) }
+
+ const filtered = list.filter(g => !keyword || g.title.includes(keyword))
+
+ return (
+
+
+ 🛍️ 二手市集
+ 发现好物,闲置变宝
+
+ 🔍
+ setKeyword(e.detail.value)} confirmType="search" />
+
+
+
+
+ {cats.map(c => (
+ setCat(c)}>
+ {c}
+
+ ))}
+
+
+
+
+ 💰
+ 发布闲置
+
+
+ 📦
+ 我的发布
+
+ Taro.navigateTo({ url: '/pages/user/favorite/favorite' })}>
+ ❤️
+ 我的收藏
+
+ {}}>
+ ⭐
+ 超值推荐
+
+
+
+
+ {filtered.map((g: Goods) => (
+ onDetail(g.id)}>
+ 📦
+ {g.title}
+
+ {formatPrice(g.price)}
+ {formatPrice(g.originalPrice)}
+
+
+ 📍 {g.location}
+ {timeAgo(g.createdAt)}
+
+
+ ))}
+
+
+ {filtered.length === 0 && 🛍️暂无商品}
+
+
+ +
+
+
+ )
+}
diff --git a/taro-app/src/pages/market/my-goods/my-goods.config.ts b/taro-app/src/pages/market/my-goods/my-goods.config.ts
new file mode 100644
index 0000000..960ba5c
--- /dev/null
+++ b/taro-app/src/pages/market/my-goods/my-goods.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '我的发布',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/market/my-goods/my-goods.scss b/taro-app/src/pages/market/my-goods/my-goods.scss
new file mode 100644
index 0000000..0de6f8f
--- /dev/null
+++ b/taro-app/src/pages/market/my-goods/my-goods.scss
@@ -0,0 +1,106 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 140rpx;
+}
+
+.tab-bar {
+ display: flex;
+ background: #fff;
+ padding: 12rpx;
+ margin: 20rpx;
+ border-radius: 40rpx;
+}
+.tab {
+ flex: 1;
+ padding: 16rpx 0;
+ text-align: center;
+ font-size: 26rpx;
+ color: #666;
+ border-radius: 30rpx;
+}
+.tab.active {
+ background: #52c41a;
+ color: #fff;
+ font-weight: bold;
+}
+
+.goods-card {
+ display: flex;
+ background: #fff;
+ margin: 0 20rpx 20rpx;
+ padding: 20rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.goods-image {
+ width: 180rpx;
+ height: 180rpx;
+ background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
+ border-radius: 12rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 72rpx;
+ flex-shrink: 0;
+}
+
+.goods-body { flex: 1; margin-left: 20rpx; min-width: 0; display: flex; flex-direction: column; }
+.goods-title { font-size: 28rpx; font-weight: bold; display: block; }
+.price-row { display: flex; align-items: baseline; gap: 10rpx; margin-top: 8rpx; }
+.price { color: #f5222d; font-size: 32rpx; font-weight: bold; }
+.original-price { color: #999; font-size: 22rpx; text-decoration: line-through; }
+
+.goods-meta {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 10rpx;
+ font-size: 22rpx;
+}
+.views { color: #999; }
+.status { font-weight: bold; }
+
+.btn-row {
+ display: flex;
+ gap: 12rpx;
+ margin-top: 14rpx;
+ justify-content: flex-end;
+}
+.mini-btn {
+ background: #f5f5f5 !important;
+ color: #333 !important;
+ border: none !important;
+ border-radius: 20rpx !important;
+ padding: 10rpx 20rpx !important;
+ font-size: 22rpx !important;
+ margin-left: 0 !important;
+}
+.mini-btn-danger {
+ background: #fff1f0 !important;
+ color: #f5222d !important;
+ border: none !important;
+ border-radius: 20rpx !important;
+ padding: 10rpx 20rpx !important;
+ font-size: 22rpx !important;
+ margin-left: 0 !important;
+}
+
+.empty { text-align: center; padding: 120rpx 0; color: #999; font-size: 26rpx; }
+.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
+
+.fab {
+ position: fixed;
+ right: 40rpx;
+ bottom: 60rpx;
+ width: 100rpx;
+ height: 100rpx;
+ background: #52c41a;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 24rpx rgba(82, 196, 26, 0.4);
+ z-index: 100;
+}
diff --git a/taro-app/src/pages/market/my-goods/my-goods.tsx b/taro-app/src/pages/market/my-goods/my-goods.tsx
new file mode 100644
index 0000000..a83c70e
--- /dev/null
+++ b/taro-app/src/pages/market/my-goods/my-goods.tsx
@@ -0,0 +1,69 @@
+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, formatPrice } from '../../../utils'
+import './index.scss'
+
+interface Goods { id: number; title: string; price: number; originalPrice: number; status: string; views: number; createdAt: number }
+
+export default function MyGoods() {
+ const [list, setList] = useState([])
+ const [tab, setTab] = useState('all')
+
+ useEffect(() => { loadData() }, [tab])
+
+ async function loadData() {
+ try {
+ const r = await api.market.myGoods()
+ setList((r as any).list || r || [])
+ } catch (e) { console.error(e) }
+ }
+
+ function onEdit(id: number) { Taro.navigateTo({ url: '/pages/market/publish/publish?id=' + id }) }
+ function onRemove(id: number) {
+ Taro.showModal({ title: '提示', content: '确认删除?', success: (res) => { if (res.confirm) { setList(prev => prev.filter(x => x.id !== id)); showToast('已删除', 'success') } } })
+ }
+
+ const statusColor: Record = { on: '#52c41a', off: '#999', sold: '#f5222d' }
+ const statusText: Record = { on: '在售', off: '已下架', sold: '已售出' }
+
+ return (
+
+
+ {[{ k: 'all', t: '全部' }, { k: 'on', t: '在售' }, { k: 'off', t: '下架' }, { k: 'sold', t: '已售' }].map(x => (
+ setTab(x.k)}>
+ {x.t}
+
+ ))}
+
+
+ {list.map((g: Goods) => (
+
+ 📦
+
+ {g.title}
+
+ {formatPrice(g.price)}
+ {formatPrice(g.originalPrice)}
+
+
+ 👁 {g.views || 0} 浏览
+ {statusText[g.status] || '在售'}
+
+
+
+
+
+
+
+ ))}
+
+ {list.length === 0 && 📦还没有发布商品}
+
+ Taro.navigateTo({ url: '/pages/market/publish/publish' })}>
+ +
+
+
+ )
+}
diff --git a/taro-app/src/pages/market/publish/publish.config.ts b/taro-app/src/pages/market/publish/publish.config.ts
new file mode 100644
index 0000000..c63b3bf
--- /dev/null
+++ b/taro-app/src/pages/market/publish/publish.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '发布商品',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/market/publish/publish.scss b/taro-app/src/pages/market/publish/publish.scss
new file mode 100644
index 0000000..331706f
--- /dev/null
+++ b/taro-app/src/pages/market/publish/publish.scss
@@ -0,0 +1,93 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 180rpx;
+}
+
+.upload-box {
+ background: #fff;
+ margin: 20rpx;
+ padding: 60rpx 0;
+ border-radius: 16rpx;
+ text-align: center;
+ border: 2rpx dashed #d9d9d9;
+}
+.upload-icon { font-size: 80rpx; display: block; }
+.upload-text { font-size: 26rpx; color: #999; margin-top: 16rpx; display: block; }
+
+.card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin: 0 20rpx 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;
+}
+
+.row-2 { display: flex; gap: 20rpx; }
+.row-item { flex: 1; }
+
+.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: #f6ffed;
+ color: #52c41a;
+ border-color: #52c41a;
+ 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: #52c41a !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/market/publish/publish.tsx b/taro-app/src/pages/market/publish/publish.tsx
new file mode 100644
index 0000000..bc2d277
--- /dev/null
+++ b/taro-app/src/pages/market/publish/publish.tsx
@@ -0,0 +1,76 @@
+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 MarketPublish() {
+ const [title, setTitle] = useState('')
+ const [price, setPrice] = useState('')
+ const [originalPrice, setOriginalPrice] = useState('')
+ const [category, setCategory] = useState('数码')
+ const [location, setLocation] = useState('校内')
+ const [desc, setDesc] = useState('')
+ const cats = ['数码', '教材', '生活用品', '服饰', '运动', '美妆', '其他']
+
+ async function submit() {
+ if (!title || !price) { showToast('请完善商品信息'); return }
+ try {
+ Taro.showLoading({ title: '发布中', mask: true })
+ await api.market.create({
+ title,
+ price: Number(price),
+ originalPrice: Number(originalPrice || price),
+ category, location, desc
+ })
+ Taro.hideLoading()
+ showToast('发布成功', 'success')
+ setTimeout(() => Taro.navigateBack(), 1200)
+ } catch (e) { Taro.hideLoading(); showToast('发布失败', 'error') }
+ }
+
+ return (
+
+
+ 📷
+ 点击上传商品图片
+
+
+
+ 商品标题 *
+ setTitle(e.detail.value)} />
+
+
+
+ 售价 (¥)
+ setPrice(e.detail.value)} />
+
+
+ 原价 (¥)
+ setOriginalPrice(e.detail.value)} />
+
+
+
+ 分类
+
+ {cats.map(c => (
+ setCategory(c)}>
+ {c}
+
+ ))}
+
+
+ 交易地点
+ setLocation(e.detail.value)} />
+
+ 商品描述
+
+
+
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/merchant/apply/apply.config.ts b/taro-app/src/pages/merchant/apply/apply.config.ts
new file mode 100644
index 0000000..12bb421
--- /dev/null
+++ b/taro-app/src/pages/merchant/apply/apply.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '商户入驻',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/merchant/apply/apply.scss b/taro-app/src/pages/merchant/apply/apply.scss
new file mode 100644
index 0000000..5875064
--- /dev/null
+++ b/taro-app/src/pages/merchant/apply/apply.scss
@@ -0,0 +1,92 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 180rpx;
+}
+
+.tip {
+ background: linear-gradient(135deg, #fff7e6 0%, #ffe7ba 100%);
+ color: #ad6800;
+ padding: 20rpx 30rpx;
+ margin: 20rpx;
+ border-radius: 12rpx;
+ font-size: 26rpx;
+}
+
+.card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin: 0 20rpx 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;
+ box-sizing: border-box;
+ width: 100%;
+}
+
+.textarea {
+ width: 100%;
+ min-height: 180rpx;
+ 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: 14rpx 24rpx;
+ background: #f5f5f5;
+ border-radius: 30rpx;
+ font-size: 26rpx;
+ color: #666;
+ border: 2rpx solid transparent;
+}
+.cat-active {
+ background: #e6f7ff;
+ color: #1890ff;
+ border-color: #1890ff;
+ font-weight: bold;
+}
+
+.row-2 { display: flex; gap: 20rpx; }
+.row-item { flex: 1; }
+
+.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/merchant/apply/apply.tsx b/taro-app/src/pages/merchant/apply/apply.tsx
new file mode 100644
index 0000000..c96e1cf
--- /dev/null
+++ b/taro-app/src/pages/merchant/apply/apply.tsx
@@ -0,0 +1,97 @@
+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 Apply() {
+ const [form, setForm] = useState({
+ name: '',
+ category: '快餐',
+ phone: '',
+ address: '',
+ hours: '09:00 - 22:00',
+ desc: '',
+ license: '',
+ minOrder: 15,
+ deliveryFee: 3
+ })
+
+ function updateField(field: string, value: any) {
+ setForm(prev => ({ ...prev, [field]: value }))
+ }
+
+ async function submit() {
+ if (!form.name || !form.phone || !form.address) {
+ showToast('请填写完整信息')
+ return
+ }
+ try {
+ Taro.showLoading({ title: '提交中', mask: true })
+ await api.merchant.apply(form)
+ Taro.hideLoading()
+ showToast('已提交,等待审核', 'success')
+ setTimeout(() => Taro.navigateBack(), 1500)
+ } catch (e) {
+ Taro.hideLoading()
+ showToast('提交失败', 'error')
+ }
+ }
+
+ const cats = ['快餐', '汉堡', '奶茶', '咖啡', '小吃', '甜品', '中式', '日式', '韩式', '生活服务']
+
+ return (
+
+
+ ✏️ 请如实填写店铺信息,我们将在1-2个工作日内完成审核
+
+
+
+ 店铺名称 *
+ updateField('name', e.detail.value)} />
+
+ 经营类目 *
+
+ {cats.map(c => (
+ updateField('category', c)}>
+ {c}
+
+ ))}
+
+
+ 联系电话 *
+ updateField('phone', e.detail.value)} />
+
+ 店铺地址 *
+ updateField('address', e.detail.value)} />
+
+ 营业时间
+ updateField('hours', e.detail.value)} />
+
+ 店铺介绍
+
+
+
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/merchant/detail/detail.config.ts b/taro-app/src/pages/merchant/detail/detail.config.ts
new file mode 100644
index 0000000..a7d84be
--- /dev/null
+++ b/taro-app/src/pages/merchant/detail/detail.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '商户详情',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/merchant/detail/detail.scss b/taro-app/src/pages/merchant/detail/detail.scss
new file mode 100644
index 0000000..647d64f
--- /dev/null
+++ b/taro-app/src/pages/merchant/detail/detail.scss
@@ -0,0 +1,89 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 160rpx;
+}
+
+.hero {
+ background: linear-gradient(135deg, #722ed1 0%, #b37feb 100%);
+ padding: 60rpx 30rpx 40rpx;
+ color: #fff;
+ text-align: center;
+}
+.hero-logo {
+ width: 120rpx; height: 120rpx;
+ background: rgba(255,255,255,0.25);
+ border-radius: 50%;
+ display: inline-flex; align-items: center; justify-content: center;
+ font-size: 60rpx;
+}
+.hero-title { font-size: 38rpx; font-weight: bold; display: block; margin-top: 20rpx; }
+.hero-sub { font-size: 26rpx; color: rgba(255,255,255,0.9); margin-top: 10rpx; display: block; }
+
+.hero-stats { display: flex; margin-top: 40rpx; background: rgba(255,255,255,0.15); border-radius: 16rpx; padding: 20rpx 0; }
+.stat-item { flex: 1; }
+.stat-num { font-size: 34rpx; font-weight: bold; display: block; }
+.stat-label { font-size: 22rpx; color: rgba(255,255,255,0.85); }
+
+.card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.card-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
+
+.desc { font-size: 26rpx; color: #666; line-height: 1.8; display: block; }
+
+.info-row { display: flex; justify-content: space-between; padding: 14rpx 0; border-bottom: 2rpx solid #f5f5f5; }
+.info-row:last-child { border-bottom: none; }
+.info-label { font-size: 26rpx; color: #999; }
+.info-value { font-size: 26rpx; color: #333; }
+
+.goods-item {
+ display: flex;
+ padding: 16rpx 0;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.goods-item:last-child { border-bottom: none; }
+.goods-thumb {
+ width: 110rpx; height: 110rpx;
+ background: linear-gradient(135deg, #fff1e6 0%, #ffd6a5 100%);
+ border-radius: 12rpx;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 44rpx; flex-shrink: 0;
+}
+.goods-info { flex: 1; margin-left: 20rpx; min-width: 0; }
+.goods-name { font-size: 28rpx; font-weight: bold; display: block; }
+.goods-desc { font-size: 22rpx; color: #999; margin-top: 6rpx; display: block; }
+.goods-price { color: #f5222d; font-size: 28rpx; font-weight: bold; margin-top: 10rpx; display: block; }
+
+.action-row {
+ position: fixed;
+ left: 0; right: 0; bottom: 0;
+ background: #fff;
+ padding: 20rpx 30rpx;
+ display: flex; gap: 20rpx;
+ box-shadow: 0 -2rpx 12rpx rgba(0,0,0,0.05);
+}
+
+.btn-outline {
+ flex: 1;
+ background: #fff !important;
+ color: #666 !important;
+ border: 2rpx solid #ddd !important;
+ border-radius: 40rpx !important;
+ padding: 20rpx 0 !important;
+ font-size: 28rpx !important;
+}
+.btn-primary {
+ flex: 1;
+ background: #722ed1 !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 40rpx !important;
+ padding: 20rpx 0 !important;
+ font-size: 28rpx !important;
+ font-weight: bold;
+}
diff --git a/taro-app/src/pages/merchant/detail/detail.tsx b/taro-app/src/pages/merchant/detail/detail.tsx
new file mode 100644
index 0000000..f776a77
--- /dev/null
+++ b/taro-app/src/pages/merchant/detail/detail.tsx
@@ -0,0 +1,71 @@
+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 } from '../../../utils'
+import './index.scss'
+
+export default function MerchantDetail() {
+ const router = Taro.useRouter()
+ const id = Number(router.params?.id || 1)
+ const [merchant, setMerchant] = useState(null)
+ const [goods, setGoods] = useState([])
+
+ useEffect(() => { loadData() }, [id])
+
+ async function loadData() {
+ try {
+ const [m, g] = await Promise.all([api.merchant.detail(id), api.goods.list({ merchantId: id })])
+ setMerchant(m)
+ setGoods((g as any).list || g || [])
+ } catch (e) { console.error(e) }
+ }
+
+ return (
+
+
+ 🏪
+ {merchant?.name || '校园美食'}
+ {merchant?.category || '中餐'} · ★ {Number(merchant?.rating || 4.8).toFixed(1)}
+
+ {merchant?.sales || 100}月销
+ {goods.length}商品
+ {Number(merchant?.rating || 4.8).toFixed(1)}评分
+
+
+
+
+ 📝 商户介绍
+ {merchant?.desc || '用心做好每一份美食,校园配送快速准时,新鲜食材,干净卫生。'}
+
+
+
+ 📍 店铺信息
+ 地址{merchant?.address || '校园综合服务中心'}
+ 营业时间{merchant?.hours || '09:00 - 22:00'}
+ 联系电话{merchant?.phone || '13800138000'}
+ 起送价¥{merchant?.minOrder || 15}
+ 配送费¥{merchant?.deliveryFee || 3}
+
+
+
+ 🛍 热销商品
+ {goods.slice(0, 6).map((g: any) => (
+
+ 🍜
+
+ {g.name}
+ {g.desc || '美味可口'}
+ ¥{g.price}
+
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/merchant/goods-edit/goods-edit.config.ts b/taro-app/src/pages/merchant/goods-edit/goods-edit.config.ts
new file mode 100644
index 0000000..f3caab4
--- /dev/null
+++ b/taro-app/src/pages/merchant/goods-edit/goods-edit.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '编辑商品',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/merchant/goods-edit/goods-edit.scss b/taro-app/src/pages/merchant/goods-edit/goods-edit.scss
new file mode 100644
index 0000000..aef4450
--- /dev/null
+++ b/taro-app/src/pages/merchant/goods-edit/goods-edit.scss
@@ -0,0 +1,164 @@
+.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: 160rpx;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ padding: 20rpx;
+ font-size: 28rpx;
+ box-sizing: border-box;
+}
+
+.row-2 { display: flex; gap: 20rpx; }
+.row-item { flex: 1; }
+
+.cat-picker {
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ padding: 20rpx;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 28rpx;
+}
+
+.cat-grid {
+ display: flex; flex-wrap: wrap;
+ gap: 16rpx;
+ margin-top: 16rpx;
+}
+.cat-tag {
+ padding: 12rpx 22rpx;
+ background: #f5f5f5;
+ border-radius: 30rpx;
+ font-size: 26rpx;
+ color: #666;
+ border: 2rpx solid transparent;
+}
+.cat-active {
+ background: #e6f7ff;
+ color: #1890ff;
+ border-color: #1890ff;
+ font-weight: bold;
+}
+
+.section-head { margin-bottom: 20rpx; }
+.section-title { font-size: 30rpx; font-weight: bold; display: block; }
+.section-desc { font-size: 22rpx; color: #999; margin-top: 6rpx; display: block; }
+
+.type-row {
+ display: flex;
+ gap: 16rpx;
+ margin-bottom: 20rpx;
+ flex-wrap: wrap;
+}
+
+.type-btn {
+ background: #f0f9ff !important;
+ color: #1890ff !important;
+ border: 2rpx solid #bae7ff !important;
+ border-radius: 30rpx !important;
+ padding: 12rpx 20rpx !important;
+ font-size: 24rpx !important;
+ margin-left: 0 !important;
+}
+
+.attr-card {
+ background: #fafafa;
+ border: 2rpx solid #eee;
+ border-radius: 12rpx;
+ padding: 20rpx;
+ margin-top: 20rpx;
+}
+
+.attr-head {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ margin-bottom: 10rpx;
+}
+.attr-idx { font-size: 26rpx; font-weight: bold; color: #333; }
+.attr-type { background: #1890ff; color: #fff; padding: 4rpx 16rpx; border-radius: 20rpx; font-size: 22rpx; }
+.attr-del { margin-left: auto; color: #f5222d; font-size: 24rpx; }
+
+.opt-row {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ margin-bottom: 12rpx;
+}
+.opt-name { flex: 2; background: #fff; border-radius: 8rpx; padding: 16rpx; font-size: 26rpx; box-sizing: border-box; }
+.opt-price { flex: 1; background: #fff; border-radius: 8rpx; padding: 16rpx; font-size: 26rpx; box-sizing: border-box; }
+.opt-del {
+ width: 48rpx; height: 48rpx;
+ background: #fff1f0;
+ color: #f5222d;
+ border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 32rpx;
+}
+
+.add-opt-btn {
+ background: #fff !important;
+ color: #1890ff !important;
+ border: 2rpx dashed #91d5ff !important;
+ border-radius: 12rpx !important;
+ padding: 14rpx 0 !important;
+ font-size: 26rpx !important;
+ width: 100%;
+ margin-top: 10rpx !important;
+}
+
+.num-row-3 { display: flex; gap: 16rpx; }
+.num-item { flex: 1; }
+
+.empty-tip { text-align: center; padding: 60rpx 0; color: #999; font-size: 26rpx; }
+
+.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: #1890ff !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/merchant/goods-edit/goods-edit.tsx b/taro-app/src/pages/merchant/goods-edit/goods-edit.tsx
new file mode 100644
index 0000000..bdea054
--- /dev/null
+++ b/taro-app/src/pages/merchant/goods-edit/goods-edit.tsx
@@ -0,0 +1,205 @@
+import { useEffect, 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'
+
+interface AttrOption { id: number; name: string; pricePerAddon?: number }
+interface Attribute { id: number; name: string; type: 'select' | 'multiselect' | 'number'; options?: AttrOption[]; min?: number; max?: number; step?: number }
+
+export default function GoodsEdit() {
+ const router = Taro.useRouter()
+ const id = Number(router.params?.id || 0)
+ const isEdit = id > 0
+
+ const [name, setName] = useState('')
+ const [price, setPrice] = useState(0)
+ const [stock, setStock] = useState(100)
+ const [category, setCategory] = useState('快餐')
+ const [desc, setDesc] = useState('')
+ const [attrs, setAttrs] = useState([])
+
+ useEffect(() => { if (isEdit) loadData() }, [id])
+
+ async function loadData() {
+ try {
+ const g: any = await api.goods.detail(id)
+ if (g) {
+ setName(g.name || '')
+ setPrice(Number(g.price) || 0)
+ setStock(Number(g.stock) || 100)
+ setCategory(g.category || '快餐')
+ setDesc(g.desc || '')
+ setAttrs(g.attributes || [])
+ }
+ } catch (e) { console.error(e) }
+ }
+
+ function addAttr(type: 'select' | 'multiselect' | 'number') {
+ const newAttr: Attribute = {
+ id: Date.now(),
+ name: type === 'number' ? '数量' : (type === 'multiselect' ? '加料' : '规格'),
+ type,
+ options: type === 'number' ? undefined : [{ id: Date.now() + 1, name: '默认' }],
+ min: type === 'number' ? 1 : undefined,
+ max: type === 'number' ? 10 : undefined,
+ step: type === 'number' ? 1 : undefined
+ }
+ setAttrs([...attrs, newAttr])
+ }
+
+ function updateAttr(attrId: number, field: string, value: any) {
+ setAttrs(prev => prev.map(a => a.id === attrId ? { ...a, [field]: value } : a))
+ }
+
+ function removeAttr(attrId: number) {
+ setAttrs(prev => prev.filter(a => a.id !== attrId))
+ }
+
+ function addOption(attrId: number) {
+ setAttrs(prev => prev.map(a => a.id === attrId && a.options
+ ? { ...a, options: [...a.options, { id: Date.now(), name: '新选项' }] }
+ : a))
+ }
+
+ function updateOption(attrId: number, optId: number, field: string, value: any) {
+ setAttrs(prev => prev.map(a => a.id === attrId && a.options
+ ? { ...a, options: a.options.map(o => o.id === optId ? { ...o, [field]: value } : o) }
+ : a))
+ }
+
+ function removeOption(attrId: number, optId: number) {
+ setAttrs(prev => prev.map(a => a.id === attrId && a.options
+ ? { ...a, options: a.options.filter(o => o.id !== optId) }
+ : a))
+ }
+
+ async function submit() {
+ if (!name || price <= 0) { showToast('请完善商品信息'); return }
+ try {
+ Taro.showLoading({ title: '保存中', mask: true })
+ const payload = { name, price, stock, category, desc, attributes: attrs }
+ if (isEdit) await api.goods.update(id, payload)
+ else await api.goods.create(payload)
+ Taro.hideLoading()
+ showToast(isEdit ? '已更新' : '已创建', 'success')
+ setTimeout(() => Taro.navigateBack(), 1200)
+ } catch (e) {
+ Taro.hideLoading()
+ showToast('保存失败', 'error')
+ }
+ }
+
+ const cats = ['快餐', '汉堡', '奶茶', '咖啡', '小吃', '甜品', '中式', '日式', '韩式']
+
+ return (
+
+
+ 商品名称 *
+ setName(e.detail.value)} />
+
+ 销售价 (¥) *
+ setPrice(Number(e.detail.value) || 0)} />
+
+
+
+ 库存
+ setStock(Number(e.detail.value) || 0)} />
+
+
+ 分类
+
+ {category}
+ ▾
+
+
+
+
+ {cats.map(c => (
+ setCategory(c)}>
+ {c}
+
+ ))}
+
+
+ 商品描述
+
+
+
+
+ 商品规格 / 选项
+ 支持动态添加选择、多选、数量等属性
+
+
+
+
+
+
+
+
+ {attrs.map((attr, idx) => (
+
+
+ 属性 {idx + 1}
+ {attr.type === 'select' ? '单选' : attr.type === 'multiselect' ? '多选' : '数字'}
+ removeAttr(attr.id)}>删除
+
+
+ 属性名称
+ updateAttr(attr.id, 'name', e.detail.value)} />
+
+ {(attr.type === 'select' || attr.type === 'multiselect') && attr.options && (
+ <>
+ 选项列表
+ {attr.options.map((opt, oi) => (
+
+ updateOption(attr.id, opt.id, 'name', e.detail.value)}
+ />
+ updateOption(attr.id, opt.id, 'pricePerAddon', Number(e.detail.value) || 0)}
+ />
+ removeOption(attr.id, opt.id)}>×
+
+ ))}
+
+ >
+ )}
+
+ {attr.type === 'number' && (
+
+
+ 最小值
+ updateAttr(attr.id, 'min', Number(e.detail.value) || 0)} />
+
+
+ 最大值
+ updateAttr(attr.id, 'max', Number(e.detail.value) || 0)} />
+
+
+ 步长
+ updateAttr(attr.id, 'step', Number(e.detail.value) || 1)} />
+
+
+ )}
+
+ ))}
+
+ {attrs.length === 0 && 暂无属性,点击上方按钮添加}
+
+
+
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/merchant/goods-manage/goods-manage.config.ts b/taro-app/src/pages/merchant/goods-manage/goods-manage.config.ts
new file mode 100644
index 0000000..27cd126
--- /dev/null
+++ b/taro-app/src/pages/merchant/goods-manage/goods-manage.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '商品管理',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/merchant/goods-manage/goods-manage.scss b/taro-app/src/pages/merchant/goods-manage/goods-manage.scss
new file mode 100644
index 0000000..d19c269
--- /dev/null
+++ b/taro-app/src/pages/merchant/goods-manage/goods-manage.scss
@@ -0,0 +1,81 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding: 20rpx;
+}
+
+.head-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 20rpx 24rpx;
+ margin-bottom: 20rpx;
+}
+.head-title { font-size: 28rpx; color: #666; }
+
+.add-btn {
+ background: #1890ff !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 30rpx !important;
+ padding: 10rpx 24rpx !important;
+ font-size: 26rpx !important;
+}
+
+.goods-card {
+ display: flex;
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 20rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.goods-thumb {
+ width: 140rpx; height: 140rpx;
+ background: linear-gradient(135deg, #fff1e6 0%, #ffd6a5 100%);
+ border-radius: 12rpx;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 48rpx; flex-shrink: 0;
+}
+
+.goods-body { flex: 1; margin-left: 20rpx; min-width: 0; }
+.goods-name { font-size: 30rpx; font-weight: bold; display: block; }
+.goods-meta { font-size: 22rpx; color: #999; margin-top: 8rpx; display: block; }
+.goods-price { color: #f5222d; font-size: 32rpx; font-weight: bold; margin-top: 16rpx; display: block; }
+
+.goods-side { display: flex; flex-direction: column; justify-content: space-between; align-items: flex-end; }
+
+.status-row { display: flex; flex-direction: column; align-items: flex-end; gap: 8rpx; }
+.status-label { font-size: 22rpx; color: #666; }
+
+.btn-row { display: flex; gap: 12rpx; }
+
+.mini-btn {
+ background: #f5f5f5 !important;
+ color: #333 !important;
+ border: none !important;
+ border-radius: 20rpx !important;
+ padding: 10rpx 20rpx !important;
+ font-size: 22rpx !important;
+ margin-left: 0 !important;
+}
+
+.mini-btn-danger {
+ background: #fff1f0 !important;
+ color: #f5222d !important;
+ border: none !important;
+ border-radius: 20rpx !important;
+ padding: 10rpx 20rpx !important;
+ font-size: 22rpx !important;
+ margin-left: 0 !important;
+}
+
+.empty {
+ text-align: center;
+ padding: 100rpx 0;
+ color: #999;
+ font-size: 26rpx;
+}
diff --git a/taro-app/src/pages/merchant/goods-manage/goods-manage.tsx b/taro-app/src/pages/merchant/goods-manage/goods-manage.tsx
new file mode 100644
index 0000000..ea29954
--- /dev/null
+++ b/taro-app/src/pages/merchant/goods-manage/goods-manage.tsx
@@ -0,0 +1,76 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Button, Switch } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { showToast, formatPrice } from '../../../utils'
+import './index.scss'
+
+interface Goods { id: number; name: string; price: number; status: number; stock: number; sales: number }
+
+export default function GoodsManage() {
+ const [list, setList] = useState([])
+
+ useEffect(() => { loadData() }, [])
+
+ async function loadData() {
+ try {
+ const r = await api.goods.list()
+ setList((r as any).list || r || [])
+ } catch (e) { console.error(e) }
+ }
+
+ async function toggleStatus(g: Goods) {
+ try {
+ const next = g.status === 1 ? 0 : 1
+ await api.goods.toggleStatus(g.id, next)
+ setList(prev => prev.map(x => x.id === g.id ? { ...x, status: next } : x))
+ showToast('操作成功', 'success')
+ } catch (e) { showToast('操作失败', 'error') }
+ }
+
+ async function remove(id: number) {
+ try {
+ Taro.showLoading({ title: '删除中' })
+ await api.goods.remove(id)
+ Taro.hideLoading()
+ setList(prev => prev.filter(x => x.id !== id))
+ showToast('已删除', 'success')
+ } catch (e) { Taro.hideLoading(); showToast('删除失败', 'error') }
+ }
+
+ function addNew() { Taro.navigateTo({ url: '/pages/merchant/goods-edit/goods-edit?id=0' }) }
+ function edit(id: number) { Taro.navigateTo({ url: '/pages/merchant/goods-edit/goods-edit?id=' + id }) }
+
+ return (
+
+
+ 共 {list.length} 件商品
+
+
+
+ {list.map((g: Goods) => (
+
+ 🍜
+
+ {g.name}
+ 库存 {g.stock || 100} · 销量 {g.sales || 0}
+ {formatPrice(g.price)}
+
+
+
+ {g.status === 1 ? '上架' : '下架'}
+ toggleStatus(g)} color="#1890ff" />
+
+
+
+
+
+
+
+ ))}
+ {list.length === 0 && 暂无商品,点击右上角添加}
+
+ )
+}
diff --git a/taro-app/src/pages/merchant/list/list.config.ts b/taro-app/src/pages/merchant/list/list.config.ts
new file mode 100644
index 0000000..3fe3c49
--- /dev/null
+++ b/taro-app/src/pages/merchant/list/list.config.ts
@@ -0,0 +1,5 @@
+export default definePageConfig({
+ navigationBarTitleText: '商户中心',
+ enablePullDownRefresh: true,
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/merchant/list/list.scss b/taro-app/src/pages/merchant/list/list.scss
new file mode 100644
index 0000000..01a8550
--- /dev/null
+++ b/taro-app/src/pages/merchant/list/list.scss
@@ -0,0 +1,69 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 40rpx;
+}
+
+.header {
+ background: linear-gradient(135deg, #722ed1 0%, #b37feb 100%);
+ padding: 30rpx 20rpx;
+ color: #fff;
+}
+.header-title { font-size: 36rpx; font-weight: bold; padding: 0 20rpx 20rpx; display: block; }
+
+.search-bar {
+ display: flex; align-items: center; padding: 16rpx 24rpx;
+ background: rgba(255,255,255,0.95); border-radius: 40rpx;
+ margin: 0 20rpx;
+}
+.search-icon { margin-right: 12rpx; color: #999; }
+.search-input { flex: 1; font-size: 26rpx; color: #333; }
+
+.quick-row {
+ display: flex;
+ background: #fff;
+ margin: 20rpx;
+ border-radius: 16rpx;
+ padding: 24rpx 0;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.quick-item {
+ flex: 1;
+ display: flex; flex-direction: column; align-items: center;
+ gap: 10rpx;
+}
+.quick-icon { font-size: 44rpx; }
+.quick-text { font-size: 24rpx; color: #333; }
+
+.card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin: 0 20rpx 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.card-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 20rpx; }
+
+.merchant-item {
+ display: flex;
+ padding: 20rpx 0;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.merchant-item:last-child { border-bottom: none; }
+
+.merchant-thumb {
+ width: 120rpx; height: 120rpx;
+ background: linear-gradient(135deg, #f5f0ff 0%, #d3adf7 100%);
+ border-radius: 12rpx;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 48rpx; flex-shrink: 0;
+}
+
+.merchant-info { flex: 1; margin-left: 20rpx; min-width: 0; }
+.merchant-top { display: flex; justify-content: space-between; align-items: center; }
+.merchant-name { font-size: 30rpx; font-weight: bold; }
+.rating { color: #faad14; font-size: 24rpx; }
+.merchant-meta { font-size: 24rpx; color: #999; margin-top: 8rpx; display: block; }
+.merchant-address { font-size: 24rpx; color: #666; margin-top: 8rpx; display: block; }
+
+.empty { text-align: center; padding: 80rpx 0; color: #999; }
diff --git a/taro-app/src/pages/merchant/list/list.tsx b/taro-app/src/pages/merchant/list/list.tsx
new file mode 100644
index 0000000..c431124
--- /dev/null
+++ b/taro-app/src/pages/merchant/list/list.tsx
@@ -0,0 +1,81 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Input } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { showToast } from '../../../utils'
+import './index.scss'
+
+interface Merchant { id: number; name: string; category: string; rating: number; sales: number; address: string }
+
+export default function MerchantList() {
+ const [list, setList] = useState([])
+ const [keyword, setKeyword] = useState('')
+
+ useEffect(() => { loadData() }, [])
+
+ async function loadData() {
+ try {
+ const r = await api.merchant.list()
+ setList((r as any).list || r || [])
+ } catch (e) { console.error(e) }
+ }
+
+ function onTap(id: number) { Taro.navigateTo({ url: '/pages/merchant/detail/detail?id=' + id }) }
+ function onApply() { Taro.navigateTo({ url: '/pages/merchant/apply/apply' }) }
+
+ const filtered = list.filter(m => !keyword || m.name.includes(keyword) || m.category.includes(keyword))
+
+ return (
+
+
+ 🏪 商户中心
+
+ 🔍
+ setKeyword(e.detail.value)}
+ />
+
+
+
+
+
+ ✍️
+ 入驻申请
+
+ Taro.navigateTo({ url: '/pages/merchant/my-shop/my-shop' })}>
+ 🏬
+ 我的店铺
+
+ Taro.navigateTo({ url: '/pages/merchant/goods-manage/goods-manage' })}>
+ 📦
+ 商品管理
+
+ Taro.navigateTo({ url: '/pages/merchant/order-manage/order-manage' })}>
+ 📋
+ 订单管理
+
+
+
+
+ 🔥 精选商户
+ {filtered.map(m => (
+ onTap(m.id)}>
+ 🏪
+
+
+ {m.name}
+ ★ {Number(m.rating).toFixed(1)}
+
+ {m.category} · 月销{m.sales}
+ {m.address}
+
+
+ ))}
+ {filtered.length === 0 && 暂无商户}
+
+
+ )
+}
diff --git a/taro-app/src/pages/merchant/my-shop/my-shop.config.ts b/taro-app/src/pages/merchant/my-shop/my-shop.config.ts
new file mode 100644
index 0000000..a82ea29
--- /dev/null
+++ b/taro-app/src/pages/merchant/my-shop/my-shop.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '我的店铺',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/merchant/my-shop/my-shop.scss b/taro-app/src/pages/merchant/my-shop/my-shop.scss
new file mode 100644
index 0000000..6bffe30
--- /dev/null
+++ b/taro-app/src/pages/merchant/my-shop/my-shop.scss
@@ -0,0 +1,61 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 40rpx;
+}
+
+.shop-card {
+ background: linear-gradient(135deg, #13c2c2 0%, #5cdbd3 100%);
+ margin: 20rpx;
+ border-radius: 16rpx;
+ padding: 30rpx;
+ color: #fff;
+}
+
+.shop-head { display: flex; align-items: center; }
+.shop-logo {
+ width: 100rpx; height: 100rpx;
+ background: rgba(255,255,255,0.25);
+ border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 48rpx;
+}
+.shop-head-info { margin-left: 20rpx; flex: 1; }
+.shop-name { font-size: 34rpx; font-weight: bold; display: block; }
+.shop-status { display: flex; align-items: center; margin-top: 10rpx; font-size: 24rpx; gap: 8rpx; }
+.status-dot { width: 14rpx; height: 14rpx; background: #52c41a; border-radius: 50%; display: inline-block; }
+
+.shop-stats {
+ display: flex;
+ background: rgba(255,255,255,0.15);
+ border-radius: 12rpx;
+ margin-top: 30rpx;
+ padding: 20rpx 0;
+}
+.stat-box { flex: 1; text-align: center; }
+.stat-num { font-size: 30rpx; font-weight: bold; display: block; }
+.stat-label { font-size: 22rpx; color: rgba(255,255,255,0.9); margin-top: 6rpx; }
+
+.card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin: 0 20rpx 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.card-title { font-size: 30rpx; font-weight: bold; display: block; margin-bottom: 10rpx; }
+
+.menu-item {
+ display: flex; align-items: center;
+ padding: 24rpx 0;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.menu-item:last-child { border-bottom: none; }
+.menu-icon { font-size: 36rpx; margin-right: 16rpx; }
+.menu-name { flex: 1; font-size: 28rpx; }
+.menu-arrow { font-size: 36rpx; color: #ccc; }
+
+.info-row { display: flex; justify-content: space-between; padding: 16rpx 0; border-bottom: 2rpx solid #f5f5f5; }
+.info-row:last-child { border-bottom: none; }
+.info-label { font-size: 26rpx; color: #999; }
+.info-value { font-size: 26rpx; color: #333; }
diff --git a/taro-app/src/pages/merchant/my-shop/my-shop.tsx b/taro-app/src/pages/merchant/my-shop/my-shop.tsx
new file mode 100644
index 0000000..dc3ed64
--- /dev/null
+++ b/taro-app/src/pages/merchant/my-shop/my-shop.tsx
@@ -0,0 +1,64 @@
+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, formatPrice } from '../../../utils'
+import './index.scss'
+
+export default function MyShop() {
+ const [shop, setShop] = useState(null)
+ const [stats, setStats] = useState({ today: 0, total: 0, orders: 0, rating: 4.8 })
+
+ useEffect(() => { loadData() }, [])
+
+ async function loadData() {
+ try {
+ const [s, st] = await Promise.all([api.merchant.myShop(), api.merchant.stats()])
+ setShop(s)
+ if (st) setStats({ today: st.today || 0, total: st.total || 0, orders: st.orders || 0, rating: st.rating || 4.8 })
+ } catch (e) { console.error(e) }
+ }
+
+ return (
+
+
+
+ 🏪
+
+ {shop?.name || '我的店铺'}
+ 营业中
+
+
+
+ {formatPrice(stats.today)}今日营业额
+ {stats.orders}订单数
+ {formatPrice(stats.total)}累计营业额
+
+
+
+
+ 🔧 店铺管理
+ {[
+ { icon: '📦', name: '商品管理', url: '/pages/merchant/goods-manage/goods-manage' },
+ { icon: '✏️', name: '添加商品', url: '/pages/merchant/goods-edit/goods-edit?id=0' },
+ { icon: '📋', name: '订单管理', url: '/pages/merchant/order-manage/order-manage' },
+ { icon: '👁️', name: '店铺预览', url: '/pages/food/shop/shop?shopId=' + (shop?.id || 1) },
+ ].map((item, idx) => (
+ Taro.navigateTo({ url: item.url })}>
+ {item.icon}
+ {item.name}
+ ›
+
+ ))}
+
+
+
+ 📊 店铺信息
+ 店铺类目{shop?.category || '快餐'}
+ 评分★ {stats.rating}
+ 地址{shop?.address || '校园综合服务中心'}
+ 联系电话{shop?.phone || '13800138000'}
+
+
+ )
+}
diff --git a/taro-app/src/pages/merchant/order-manage/order-manage.config.ts b/taro-app/src/pages/merchant/order-manage/order-manage.config.ts
new file mode 100644
index 0000000..849164e
--- /dev/null
+++ b/taro-app/src/pages/merchant/order-manage/order-manage.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '订单管理',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/merchant/order-manage/order-manage.scss b/taro-app/src/pages/merchant/order-manage/order-manage.scss
new file mode 100644
index 0000000..0218af0
--- /dev/null
+++ b/taro-app/src/pages/merchant/order-manage/order-manage.scss
@@ -0,0 +1,102 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 40rpx;
+}
+
+.tab-bar {
+ background: #fff;
+ padding: 20rpx 0;
+ white-space: nowrap;
+ border-bottom: 2rpx solid #f0f0f0;
+}
+.tab-item {
+ display: inline-block;
+ padding: 12rpx 30rpx;
+ font-size: 26rpx;
+ color: #666;
+ border-radius: 30rpx;
+ margin-left: 20rpx;
+}
+.tab-item.active {
+ background: #1890ff;
+ color: #fff;
+ font-weight: bold;
+}
+
+.order-card {
+ background: #fff;
+ border-radius: 16rpx;
+ margin: 20rpx;
+ padding: 24rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.order-head {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 16rpx;
+ border-bottom: 2rpx dashed #f0f0f0;
+}
+.order-no { font-size: 26rpx; color: #666; font-weight: bold; }
+.order-status { font-size: 26rpx; font-weight: bold; }
+
+.order-items { padding: 20rpx 0; }
+.order-item-line { font-size: 26rpx; color: #333; display: block; padding: 6rpx 0; }
+
+.order-foot {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16rpx 0;
+ border-top: 2rpx solid #f5f5f5;
+}
+.order-time { font-size: 22rpx; color: #999; }
+.order-total { font-size: 28rpx; color: #f5222d; font-weight: bold; }
+
+.order-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 16rpx;
+ margin-top: 10rpx;
+ padding-top: 10rpx;
+}
+
+.mini-btn {
+ background: #f5f5f5 !important;
+ color: #333 !important;
+ border: none !important;
+ border-radius: 30rpx !important;
+ padding: 12rpx 24rpx !important;
+ font-size: 24rpx !important;
+ margin-left: 0 !important;
+}
+
+.mini-btn-primary {
+ background: #1890ff !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 30rpx !important;
+ padding: 12rpx 24rpx !important;
+ font-size: 24rpx !important;
+ margin-left: 0 !important;
+}
+
+.mini-btn-danger {
+ background: #fff1f0 !important;
+ color: #f5222d !important;
+ border: none !important;
+ border-radius: 30rpx !important;
+ padding: 12rpx 24rpx !important;
+ font-size: 24rpx !important;
+ margin-left: 0 !important;
+}
+
+.empty {
+ text-align: center;
+ padding: 120rpx 0;
+ color: #999;
+ font-size: 28rpx;
+}
+.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
diff --git a/taro-app/src/pages/merchant/order-manage/order-manage.tsx b/taro-app/src/pages/merchant/order-manage/order-manage.tsx
new file mode 100644
index 0000000..1dc36bf
--- /dev/null
+++ b/taro-app/src/pages/merchant/order-manage/order-manage.tsx
@@ -0,0 +1,83 @@
+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, formatPrice, formatTime } from '../../../utils'
+import './index.scss'
+
+interface Order { id: number; orderNo: string; status: string; total: number; createdAt: number; items: { name: string; qty: number }[] }
+
+export default function OrderManage() {
+ const [tab, setTab] = useState('all')
+ const [list, setList] = useState([])
+
+ useEffect(() => { loadData() }, [tab])
+
+ async function loadData() {
+ try {
+ const r = await api.order.list({ status: tab === 'all' ? undefined : tab })
+ setList((r as any).list || r || [])
+ } catch (e) { console.error(e) }
+ }
+
+ function statusText(s: string) {
+ return ({ pending: '待付款', paid: '待接单', cooking: '制作中', delivering: '配送中', done: '已完成', cancelled: '已取消' } as any)[s] || s
+ }
+
+ function statusColor(s: string) {
+ return ({ pending: '#faad14', paid: '#1890ff', cooking: '#1890ff', delivering: '#722ed1', done: '#52c41a', cancelled: '#999' } as any)[s] || '#999'
+ }
+
+ async function action(order: Order, next: string) {
+ try {
+ Taro.showLoading({ title: '处理中' })
+ if (next === 'cancel') await api.order.cancel(order.id)
+ else if (next === 'confirm') await api.order.confirm(order.id)
+ else if (next === 'pay') await api.order.pay(order.id)
+ Taro.hideLoading()
+ showToast('操作成功', 'success')
+ loadData()
+ } catch (e) { Taro.hideLoading(); showToast('操作失败', 'error') }
+ }
+
+ const tabs = [{ k: 'all', name: '全部' }, { k: 'paid', name: '待接单' }, { k: 'cooking', name: '制作中' }, { k: 'delivering', name: '配送中' }, { k: 'done', name: '已完成' }]
+
+ return (
+
+
+ {tabs.map(t => (
+ setTab(t.k)}>
+ {t.name}
+
+ ))}
+
+
+ {list.map((o: Order) => (
+
+
+ #{o.orderNo}
+ {statusText(o.status)}
+
+
+ {o.items.map((it, i) => (
+ {it.name} x{it.qty}
+ ))}
+
+
+ {formatTime(new Date(o.createdAt || Date.now()))}
+ 合计 {formatPrice(o.total)}
+
+
+ {o.status === 'paid' && }
+ {o.status === 'cooking' && }
+ {o.status === 'delivering' && }
+ {(o.status === 'paid' || o.status === 'cooking') && }
+
+
+
+ ))}
+
+ {list.length === 0 && 📋暂无订单}
+
+ )
+}
diff --git a/taro-app/src/pages/user/coupon/coupon.config.ts b/taro-app/src/pages/user/coupon/coupon.config.ts
new file mode 100644
index 0000000..d6dfd65
--- /dev/null
+++ b/taro-app/src/pages/user/coupon/coupon.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '我的优惠券',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/user/coupon/coupon.scss b/taro-app/src/pages/user/coupon/coupon.scss
new file mode 100644
index 0000000..42b4d74
--- /dev/null
+++ b/taro-app/src/pages/user/coupon/coupon.scss
@@ -0,0 +1,102 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 60rpx;
+}
+
+.tab-bar {
+ display: flex;
+ background: #fff;
+ padding: 0 10rpx;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.tab {
+ flex: 1;
+ padding: 28rpx 0;
+ text-align: center;
+ font-size: 26rpx;
+ color: #666;
+ position: relative;
+}
+.tab.active {
+ color: #f5222d;
+ font-weight: bold;
+}
+.tab.active::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 40rpx;
+ height: 4rpx;
+ background: #f5222d;
+ border-radius: 2rpx;
+}
+
+.coupon-card {
+ display: flex;
+ background: #fff;
+ margin: 20rpx;
+ border-radius: 16rpx;
+ overflow: hidden;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+ position: relative;
+}
+.coupon-card::before, .coupon-card::after {
+ content: '';
+ position: absolute;
+ left: 220rpx;
+ width: 24rpx;
+ height: 24rpx;
+ background: #f5f5f5;
+ border-radius: 50%;
+}
+.coupon-card::before { top: -12rpx; }
+.coupon-card::after { bottom: -12rpx; }
+
+.coupon-card.used { opacity: 0.6; }
+.coupon-card.used .coupon-left { background: #d9d9d9; }
+
+.coupon-left {
+ width: 220rpx;
+ background: linear-gradient(135deg, #f5222d 0%, #ff7875 100%);
+ color: #fff;
+ padding: 40rpx 20rpx;
+ text-align: center;
+ flex-shrink: 0;
+}
+.coupon-symbol { font-size: 30rpx; display: block; }
+.coupon-value { font-size: 80rpx; font-weight: bold; display: block; line-height: 1; }
+.coupon-condition { font-size: 22rpx; margin-top: 10rpx; display: block; opacity: 0.9; }
+
+.coupon-right {
+ flex: 1;
+ padding: 24rpx;
+ min-width: 0;
+}
+.coupon-name { font-size: 30rpx; font-weight: bold; display: block; }
+.coupon-type { font-size: 22rpx; color: #f5222d; background: #fff1f0; padding: 2rpx 14rpx; border-radius: 8rpx; display: inline-block; margin-top: 10rpx; }
+.coupon-expire { font-size: 22rpx; color: #999; margin-top: 12rpx; display: block; }
+
+.use-btn {
+ background: linear-gradient(135deg, #f5222d 0%, #ff7875 100%) !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 30rpx !important;
+ padding: 12rpx 28rpx !important;
+ font-size: 24rpx !important;
+ margin-top: 16rpx !important;
+}
+
+.empty { text-align: center; padding: 120rpx 0; color: #999; }
+.empty-icon { font-size: 100rpx; display: block; margin-bottom: 20rpx; }
+.empty-text { font-size: 28rpx; display: block; margin-bottom: 30rpx; }
+.demo-btn {
+ background: #f5222d !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 40rpx !important;
+ padding: 16rpx 50rpx !important;
+ font-size: 28rpx !important;
+}
diff --git a/taro-app/src/pages/user/coupon/coupon.tsx b/taro-app/src/pages/user/coupon/coupon.tsx
new file mode 100644
index 0000000..2150ba4
--- /dev/null
+++ b/taro-app/src/pages/user/coupon/coupon.tsx
@@ -0,0 +1,75 @@
+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, formatPrice } from '../../../utils'
+import './index.scss'
+
+interface Coupon { id: number; name: string; type: string; discount: number; minOrder: number; expireAt: number; used: boolean }
+
+export default function Coupon() {
+ const [tab, setTab] = useState<'available' | 'used' | 'expired'>('available')
+ const [list, setList] = useState([])
+
+ useEffect(() => { loadData() }, [tab])
+
+ async function loadData() {
+ try {
+ const r = await api.coupon.myCoupons()
+ const data = (r as any).list || r || []
+ setList(data)
+ } catch (e) { console.error(e) }
+ }
+
+ async function receiveDemo() {
+ try {
+ Taro.showLoading({ title: '领取中' })
+ await api.coupon.receive(1)
+ Taro.hideLoading()
+ showToast('领取成功', 'success')
+ loadData()
+ } catch (e) { Taro.hideLoading(); showToast('领取失败', 'error') }
+ }
+
+ const tabs = [{ k: 'available', t: '可使用' }, { k: 'used', t: '已使用' }, { k: 'expired', t: '已过期' }]
+
+ return (
+
+
+ {tabs.map(t => (
+ setTab(t.k as any)}>
+ {t.t}
+
+ ))}
+
+
+ {list.map((c: Coupon) => (
+
+
+ ¥
+ {c.discount}
+ 满{formatPrice(c.minOrder)}可用
+
+
+ {c.name}
+ {c.type || '通用'}
+ 有效期至 {new Date(c.expireAt || Date.now() + 86400000 * 30).toLocaleDateString()}
+ {!c.used && tab === 'available' && (
+
+ )}
+
+
+ ))}
+
+ {list.length === 0 && (
+
+ 🎫
+ 暂无优惠券
+
+
+ )}
+
+ )
+}
diff --git a/taro-app/src/pages/user/favorite/favorite.config.ts b/taro-app/src/pages/user/favorite/favorite.config.ts
new file mode 100644
index 0000000..081d38a
--- /dev/null
+++ b/taro-app/src/pages/user/favorite/favorite.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '我的收藏',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/user/favorite/favorite.scss b/taro-app/src/pages/user/favorite/favorite.scss
new file mode 100644
index 0000000..fc82bf5
--- /dev/null
+++ b/taro-app/src/pages/user/favorite/favorite.scss
@@ -0,0 +1,69 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 60rpx;
+}
+
+.tab-bar {
+ display: flex;
+ background: #fff;
+ padding: 0 10rpx;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.tab {
+ flex: 1;
+ padding: 28rpx 0;
+ text-align: center;
+ font-size: 26rpx;
+ color: #666;
+ position: relative;
+}
+.tab.active {
+ color: #faad14;
+ font-weight: bold;
+}
+.tab.active::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 40rpx;
+ height: 4rpx;
+ background: #faad14;
+ border-radius: 2rpx;
+}
+
+.item-card {
+ display: flex;
+ align-items: center;
+ background: #fff;
+ margin: 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.item-icon {
+ width: 100rpx; height: 100rpx;
+ background: linear-gradient(135deg, #fffbe6 0%, #ffe58f 100%);
+ border-radius: 16rpx;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 48rpx;
+ flex-shrink: 0;
+}
+.item-body { flex: 1; margin-left: 20rpx; min-width: 0; }
+.item-name { font-size: 28rpx; font-weight: bold; display: block; }
+.item-price { color: #f5222d; font-size: 32rpx; font-weight: bold; margin-top: 10rpx; display: block; }
+
+.remove-btn {
+ background: #fff1f0 !important;
+ color: #f5222d !important;
+ border: 2rpx solid #ffccc7 !important;
+ border-radius: 30rpx !important;
+ padding: 12rpx 20rpx !important;
+ font-size: 22rpx !important;
+}
+
+.empty { text-align: center; padding: 120rpx 0; color: #999; }
+.empty-icon { font-size: 100rpx; display: block; margin-bottom: 20rpx; }
+.empty-text { font-size: 28rpx; display: block; }
diff --git a/taro-app/src/pages/user/favorite/favorite.tsx b/taro-app/src/pages/user/favorite/favorite.tsx
new file mode 100644
index 0000000..360de1b
--- /dev/null
+++ b/taro-app/src/pages/user/favorite/favorite.tsx
@@ -0,0 +1,82 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Button } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { showToast, formatPrice } from '../../../utils'
+import './index.scss'
+
+interface FavoriteItem { id: number; type: 'goods' | 'shop' | 'post'; name: string; price?: number; icon: string }
+
+export default function Favorite() {
+ const [tab, setTab] = useState<'goods' | 'shop' | 'post'>('goods')
+ const [list, setList] = useState([])
+
+ useEffect(() => { loadData() }, [tab])
+
+ async function loadData() {
+ try {
+ // 使用 mock 数据
+ const mock = {
+ goods: [
+ { id: 1, type: 'goods' as const, name: '九成新 iPad 2023', price: 1999, icon: '📱' },
+ { id: 2, type: 'goods' as const, name: '数学教材 第7版', price: 25, icon: '📚' },
+ { id: 3, type: 'goods' as const, name: '罗技无线鼠标', price: 99, icon: '🖱' },
+ ],
+ shop: [
+ { id: 1, type: 'shop' as const, name: '校园快餐店', icon: '🍔' },
+ { id: 2, type: 'shop' as const, name: '奶茶小屋', icon: '🧋' },
+ ],
+ post: [
+ { id: 1, type: 'post' as const, name: '考研经验分享', icon: '📝' },
+ { id: 2, type: 'post' as const, name: '校园生活小贴士', icon: '💡' },
+ { id: 3, type: 'post' as const, name: '表白我的室友', icon: '💌' },
+ ],
+ }
+ setList(mock[tab])
+ } catch (e) { console.error(e) }
+ }
+
+ function remove(id: number) {
+ Taro.showModal({
+ title: '提示',
+ content: '确定取消收藏?',
+ success: (res) => {
+ if (res.confirm) {
+ setList(prev => prev.filter(x => x.id !== id))
+ showToast('已取消收藏', 'success')
+ }
+ }
+ })
+ }
+
+ const tabs = [{ k: 'goods', t: '商品' }, { k: 'shop', t: '商户' }, { k: 'post', t: '帖子' }]
+
+ return (
+
+
+ {tabs.map(t => (
+ setTab(t.k as any)}>
+ {t.t}
+
+ ))}
+
+
+ {list.map((item: FavoriteItem) => (
+
+ {item.icon}
+
+ {item.name}
+ {item.price && {formatPrice(item.price)}}
+
+
+
+ ))}
+
+ {list.length === 0 && (
+
+ ❤️
+ 暂无收藏
+
+ )}
+
+ )
+}
diff --git a/taro-app/src/pages/user/login/login.config.ts b/taro-app/src/pages/user/login/login.config.ts
new file mode 100644
index 0000000..1c56e8d
--- /dev/null
+++ b/taro-app/src/pages/user/login/login.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '登录',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/user/login/login.scss b/taro-app/src/pages/user/login/login.scss
new file mode 100644
index 0000000..6fdea96
--- /dev/null
+++ b/taro-app/src/pages/user/login/login.scss
@@ -0,0 +1,115 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #fff;
+ padding-bottom: 60rpx;
+}
+
+.hero {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ padding: 100rpx 30rpx 120rpx;
+ text-align: center;
+ color: #fff;
+ border-radius: 0 0 40rpx 40rpx;
+}
+.logo {
+ width: 140rpx; height: 140rpx;
+ background: rgba(255,255,255,0.25);
+ border-radius: 50%;
+ display: inline-flex; align-items: center; justify-content: center;
+ font-size: 70rpx;
+}
+.title { font-size: 44rpx; font-weight: bold; display: block; margin-top: 30rpx; }
+.subtitle { font-size: 24rpx; color: rgba(255,255,255,0.85); margin-top: 10rpx; display: block; letter-spacing: 4rpx; }
+
+.form {
+ background: #fff;
+ margin: -40rpx 30rpx 0;
+ padding: 40rpx 30rpx;
+ border-radius: 24rpx;
+ box-shadow: 0 6rpx 30rpx rgba(0,0,0,0.08);
+}
+
+.mode-switch {
+ display: flex;
+ background: #f5f5f5;
+ border-radius: 40rpx;
+ padding: 6rpx;
+ margin-bottom: 30rpx;
+}
+.mode-item {
+ flex: 1;
+ text-align: center;
+ padding: 18rpx 0;
+ font-size: 26rpx;
+ color: #666;
+ border-radius: 36rpx;
+}
+.mode-item.active {
+ background: #fff;
+ color: #667eea;
+ font-weight: bold;
+ box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
+}
+
+.input-group { margin-bottom: 30rpx; }
+.input-label { font-size: 24rpx; color: #666; margin-bottom: 12rpx; display: block; }
+
+.input {
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ padding: 24rpx;
+ font-size: 30rpx;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.input-row { display: flex; gap: 16rpx; align-items: center; }
+.code-input { flex: 1; }
+.code-btn {
+ background: #fff0f6 !important;
+ color: #eb2f96 !important;
+ border: none !important;
+ border-radius: 12rpx !important;
+ padding: 24rpx 24rpx !important;
+ font-size: 24rpx !important;
+ white-space: nowrap;
+}
+
+.login-btn {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 40rpx !important;
+ padding: 24rpx 0 !important;
+ font-size: 32rpx !important;
+ font-weight: bold;
+ letter-spacing: 10rpx;
+ margin-top: 40rpx !important;
+ box-shadow: 0 6rpx 20rpx rgba(102, 126, 234, 0.4);
+}
+
+.tips-row { display: flex; justify-content: space-between; margin-top: 30rpx; }
+.tip { font-size: 24rpx; color: #667eea; }
+
+.agreement {
+ text-align: center;
+ margin-top: 40rpx;
+ font-size: 22rpx;
+ color: #999;
+}
+
+.third-party {
+ margin-top: 80rpx;
+ text-align: center;
+}
+.tp-title { font-size: 24rpx; color: #999; display: block; }
+.tp-icons { display: flex; justify-content: center; gap: 60rpx; margin-top: 30rpx; }
+.tp-item { display: flex; flex-direction: column; align-items: center; gap: 10rpx; }
+.tp-icon {
+ width: 80rpx; height: 80rpx;
+ background: #f5f5f5;
+ border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 36rpx;
+}
+.tp-name { font-size: 22rpx; color: #666; }
diff --git a/taro-app/src/pages/user/login/login.tsx b/taro-app/src/pages/user/login/login.tsx
new file mode 100644
index 0000000..9102f26
--- /dev/null
+++ b/taro-app/src/pages/user/login/login.tsx
@@ -0,0 +1,122 @@
+import { useState } from 'react'
+import { View, Text, ScrollView, Button, Input } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { showToast } from '../../../utils'
+import './index.scss'
+
+export default function Login() {
+ const [phone, setPhone] = useState('')
+ const [code, setCode] = useState('')
+ const [mode, setMode] = useState<'code' | 'password'>('code')
+ const [password, setPassword] = useState('')
+ const [countdown, setCountdown] = useState(0)
+
+ function sendCode() {
+ if (!/^1\d{10}$/.test(phone)) { showToast('请输入正确的手机号'); return }
+ setCountdown(60)
+ const timer = setInterval(() => {
+ setCountdown(prev => {
+ if (prev <= 1) { clearInterval(timer); return 0 }
+ return prev - 1
+ })
+ }, 1000)
+ showToast('验证码已发送', 'success')
+ }
+
+ async function login() {
+ if (!/^1\d{10}$/.test(phone)) { showToast('请输入正确的手机号'); return }
+ if (mode === 'code' && !code) { showToast('请输入验证码'); return }
+ if (mode === 'password' && !password) { showToast('请输入密码'); return }
+ try {
+ Taro.showLoading({ title: '登录中', mask: true })
+ const payload: any = { phone }
+ if (mode === 'code') payload.code = code
+ else payload.password = password
+ await api.user.login(payload)
+ Taro.hideLoading()
+ Taro.setStorageSync('user_token', 'mock_token_' + Date.now())
+ showToast('登录成功', 'success')
+ setTimeout(() => {
+ Taro.switchTab({ url: '/pages/index/index' }).catch(() => Taro.redirectTo({ url: '/pages/user/profile/profile' }))
+ }, 800)
+ } catch (e) {
+ Taro.hideLoading()
+ showToast('登录失败', 'error')
+ }
+ }
+
+ return (
+
+
+ 🎓
+ 校园综合服务
+ Campus Service Platform
+
+
+
+
+ setMode('code')}>
+ 验证码登录
+
+ setMode('password')}>
+ 密码登录
+
+
+
+
+ 📱 手机号
+ setPhone(e.detail.value)} />
+
+
+ {mode === 'code' ? (
+
+ 🔐 验证码
+
+ setCode(e.detail.value)} />
+
+
+
+ ) : (
+
+ 🔑 密码
+ setPassword(e.detail.value)} />
+
+ )}
+
+
+
+
+ showToast('请联系管理员重置密码')}>忘记密码?
+ showToast('暂无注册功能')}>注册新账号
+
+
+
+ 登录即表示同意《用户协议》和《隐私政策》
+
+
+
+
+ — 其他登录方式 —
+
+ showToast('微信登录')}>
+ 💚
+ 微信
+
+ showToast('QQ登录')}>
+ 🐧
+ QQ
+
+ showToast('校园账号')}>
+ 🎓
+ 校园
+
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/user/order/my-order.config.ts b/taro-app/src/pages/user/order/my-order.config.ts
new file mode 100644
index 0000000..830a7c8
--- /dev/null
+++ b/taro-app/src/pages/user/order/my-order.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '我的订单',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/user/order/my-order.scss b/taro-app/src/pages/user/order/my-order.scss
new file mode 100644
index 0000000..b90d5e2
--- /dev/null
+++ b/taro-app/src/pages/user/order/my-order.scss
@@ -0,0 +1,107 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 60rpx;
+}
+
+.tab-bar {
+ display: flex;
+ background: #fff;
+ padding: 0 10rpx;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.tab {
+ flex: 1;
+ padding: 28rpx 0;
+ text-align: center;
+ font-size: 26rpx;
+ color: #666;
+ position: relative;
+}
+.tab.active {
+ color: #1890ff;
+ font-weight: bold;
+}
+.tab.active::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 40rpx;
+ height: 4rpx;
+ background: #1890ff;
+ border-radius: 2rpx;
+}
+
+.order-card {
+ background: #fff;
+ margin: 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.order-head {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 20rpx;
+ border-bottom: 2rpx dashed #f0f0f0;
+}
+.order-no { font-size: 24rpx; color: #666; }
+.order-status { font-size: 26rpx; font-weight: bold; }
+
+.order-items { padding: 20rpx 0; }
+.order-item-line { font-size: 26rpx; color: #333; display: block; padding: 6rpx 0; }
+
+.order-foot {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16rpx 0;
+ border-top: 2rpx solid #f5f5f5;
+}
+.order-time { font-size: 22rpx; color: #999; }
+.order-total { font-size: 28rpx; color: #f5222d; font-weight: bold; }
+
+.order-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 16rpx;
+ margin-top: 10rpx;
+}
+.mini-btn {
+ background: #f5f5f5 !important;
+ color: #333 !important;
+ border: none !important;
+ border-radius: 30rpx !important;
+ padding: 12rpx 28rpx !important;
+ font-size: 24rpx !important;
+ margin-left: 0 !important;
+}
+.mini-btn-primary {
+ background: #1890ff !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 30rpx !important;
+ padding: 12rpx 28rpx !important;
+ font-size: 24rpx !important;
+ margin-left: 0 !important;
+}
+
+.empty {
+ text-align: center;
+ padding: 120rpx 30rpx;
+ color: #999;
+}
+.empty-icon { font-size: 100rpx; display: block; margin-bottom: 20rpx; }
+.empty-text { font-size: 28rpx; color: #999; display: block; margin-bottom: 30rpx; }
+.empty-btn {
+ background: #1890ff !important;
+ color: #fff !important;
+ border: none !important;
+ border-radius: 40rpx !important;
+ padding: 16rpx 50rpx !important;
+ font-size: 28rpx !important;
+}
diff --git a/taro-app/src/pages/user/order/my-order.tsx b/taro-app/src/pages/user/order/my-order.tsx
new file mode 100644
index 0000000..067b819
--- /dev/null
+++ b/taro-app/src/pages/user/order/my-order.tsx
@@ -0,0 +1,94 @@
+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, formatPrice, formatTime } from '../../../../utils'
+import './my-order.scss'
+
+interface Order { id: number; orderNo: string; status: string; total: number; createdAt: number; items: { name: string; qty: number; price: number }[] }
+
+export default function MyOrder() {
+ const router = Taro.useRouter()
+ const [status, setStatus] = useState(router.params?.status || 'all')
+ const [list, setList] = useState([])
+
+ useEffect(() => { loadData() }, [status])
+
+ async function loadData() {
+ try {
+ const r = await api.order.list({ status: status === 'all' ? undefined : status })
+ setList((r as any).list || r || [])
+ } catch (e) { console.error(e) }
+ }
+
+ const tabs = [
+ { k: 'all', t: '全部' },
+ { k: 'pending', t: '待付款' },
+ { k: 'paid', t: '待接单' },
+ { k: 'delivering', t: '配送中' },
+ { k: 'done', t: '已完成' },
+ ]
+
+ const statusColor: Record = {
+ pending: '#faad14', paid: '#1890ff', cooking: '#1890ff',
+ delivering: '#722ed1', done: '#52c41a', cancelled: '#999'
+ }
+ const statusText: Record = {
+ pending: '待付款', paid: '待接单', cooking: '制作中',
+ delivering: '配送中', done: '已完成', cancelled: '已取消'
+ }
+
+ function onDetail(id: number) { Taro.navigateTo({ url: '/pages/food/order-detail/order-detail?id=' + id }) }
+
+ return (
+
+
+ {tabs.map(t => (
+ setStatus(t.k)}>
+ {t.t}
+
+ ))}
+
+
+ {list.map((o: Order) => (
+ onDetail(o.id)}>
+
+ 订单号: {o.orderNo}
+ {statusText[o.status] || '未知'}
+
+
+ {o.items.map((it: any, i: number) => (
+ {it.name} × {it.qty} {formatPrice(it.price)}
+ ))}
+
+
+ {formatTime(new Date(o.createdAt))}
+ 合计 {formatPrice(o.total)}
+
+
+ {o.status === 'pending' && (
+
+ )}
+ {o.status === 'delivering' && (
+
+ )}
+ {o.status === 'done' && (
+
+ )}
+
+
+
+ ))}
+
+ {list.length === 0 && (
+
+ 📋
+ 暂无订单
+
+
+ )}
+
+ )
+}
diff --git a/taro-app/src/pages/user/profile/profile.config.ts b/taro-app/src/pages/user/profile/profile.config.ts
new file mode 100644
index 0000000..caa39f8
--- /dev/null
+++ b/taro-app/src/pages/user/profile/profile.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '我的',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/user/profile/profile.scss b/taro-app/src/pages/user/profile/profile.scss
new file mode 100644
index 0000000..b3284b5
--- /dev/null
+++ b/taro-app/src/pages/user/profile/profile.scss
@@ -0,0 +1,110 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 60rpx;
+}
+
+.hero {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ padding: 40rpx 30rpx 50rpx;
+ color: #fff;
+}
+
+.user-row {
+ display: flex;
+ align-items: center;
+}
+.avatar {
+ width: 100rpx; height: 100rpx;
+ background: rgba(255,255,255,0.25);
+ border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 40rpx;
+ font-weight: bold;
+ flex-shrink: 0;
+}
+.user-info { flex: 1; margin-left: 20rpx; min-width: 0; }
+.nickname { font-size: 34rpx; font-weight: bold; display: block; }
+.user-meta { font-size: 22rpx; color: rgba(255,255,255,0.85); margin-top: 6rpx; display: block; }
+
+.edit-btn {
+ background: rgba(255,255,255,0.2) !important;
+ color: #fff !important;
+ border: 2rpx solid rgba(255,255,255,0.3) !important;
+ border-radius: 30rpx !important;
+ padding: 12rpx 24rpx !important;
+ font-size: 24rpx !important;
+}
+
+.level-card {
+ background: rgba(255,255,255,0.15);
+ border-radius: 16rpx;
+ padding: 20rpx;
+ margin-top: 30rpx;
+}
+.level-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16rpx; }
+.level-label { font-size: 26rpx; font-weight: bold; }
+.level-points { font-size: 22rpx; color: rgba(255,255,255,0.85); }
+.level-bar { height: 12rpx; background: rgba(255,255,255,0.2); border-radius: 6rpx; overflow: hidden; }
+.level-bar-fill { height: 100%; background: #ffd666; border-radius: 6rpx; }
+
+.stat-row {
+ display: flex;
+ margin-top: 30rpx;
+ background: rgba(255,255,255,0.12);
+ border-radius: 16rpx;
+ padding: 20rpx 0;
+}
+.stat-item { flex: 1; text-align: center; }
+.stat-num { font-size: 30rpx; font-weight: bold; display: block; }
+.stat-label { font-size: 22rpx; color: rgba(255,255,255,0.85); margin-top: 6rpx; display: block; }
+
+.order-card {
+ background: #fff;
+ margin: -20rpx 20rpx 20rpx;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.order-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
+.order-title { font-size: 30rpx; font-weight: bold; }
+.order-more { font-size: 24rpx; color: #999; }
+
+.order-tabs { display: flex; }
+.order-tab {
+ flex: 1;
+ display: flex; flex-direction: column; align-items: center;
+ gap: 10rpx;
+ padding: 12rpx 0;
+}
+.order-tab-icon { font-size: 40rpx; }
+.order-tab-name { font-size: 24rpx; color: #666; }
+
+.menu-card {
+ background: #fff;
+ margin: 0 20rpx 20rpx;
+ border-radius: 16rpx;
+ padding: 0 24rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+.menu-item {
+ display: flex;
+ align-items: center;
+ padding: 28rpx 0;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.menu-item:last-child { border-bottom: none; }
+.menu-icon { font-size: 36rpx; margin-right: 16rpx; }
+.menu-name { flex: 1; font-size: 28rpx; color: #333; }
+.menu-arrow { font-size: 32rpx; color: #ccc; }
+
+.logout-row { padding: 20rpx 30rpx; margin-top: 20rpx; }
+.logout-btn {
+ background: #fff !important;
+ color: #f5222d !important;
+ border: 2rpx solid #ffccc7 !important;
+ border-radius: 16rpx !important;
+ padding: 20rpx 0 !important;
+ font-size: 28rpx !important;
+ width: 100%;
+}
diff --git a/taro-app/src/pages/user/profile/profile.tsx b/taro-app/src/pages/user/profile/profile.tsx
new file mode 100644
index 0000000..2283842
--- /dev/null
+++ b/taro-app/src/pages/user/profile/profile.tsx
@@ -0,0 +1,113 @@
+import { useEffect, useState } from 'react'
+import { View, Text, ScrollView, Button, Image } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { api } from '../../../services/api'
+import { showToast, formatPrice } from '../../../utils'
+import './index.scss'
+
+export default function Profile() {
+ const [user, setUser] = useState(null)
+ const [levelInfo, setLevelInfo] = useState({ level: 1, name: '新生', points: 128, nextLevel: 500 })
+
+ useEffect(() => { loadData() }, [])
+
+ async function loadData() {
+ try {
+ const [u, lv] = await Promise.all([api.user.profile(), api.user.levelInfo()])
+ setUser(u)
+ if (lv) setLevelInfo(lv)
+ } catch (e) {
+ console.error(e)
+ // 默认值
+ setUser({ id: 1, nickname: '同学', avatar: '', phone: '138****0000', balance: 0, coupons: 0 })
+ }
+ }
+
+ const menus = [
+ { icon: '📋', name: '我的订单', url: '/pages/user/order/my-order' },
+ { icon: '🎫', name: '优惠券', url: '/pages/user/coupon/coupon' },
+ { icon: '❤️', name: '我的收藏', url: '/pages/user/favorite/favorite' },
+ { icon: '🏪', name: '我的店铺', url: '/pages/merchant/my-shop/my-shop' },
+ { icon: '📦', name: '发布的商品', url: '/pages/market/my-goods/my-goods' },
+ { icon: '⚙️', name: '设置', url: '/pages/user/settings/settings' },
+ ]
+
+ const orderTabs = [
+ { icon: '💳', name: '待付款', key: 'pending' },
+ { icon: '📦', name: '待发货', key: 'paid' },
+ { icon: '🚚', name: '配送中', key: 'delivering' },
+ { icon: '✅', name: '已完成', key: 'done' },
+ ]
+
+ const progress = Math.min(100, Math.floor((levelInfo.points / levelInfo.nextLevel) * 100))
+
+ return (
+
+
+
+ {(user?.nickname || 'U')[0]}
+
+ {user?.nickname || '同学'}
+ ID: {user?.id || 1} · {user?.phone || '未绑定'}
+
+
+
+
+
+
+ Lv.{levelInfo.level} {levelInfo.name}
+ {levelInfo.points} / {levelInfo.nextLevel} 积分
+
+
+
+
+
+
+
+ {formatPrice(user?.balance || 0)}余额
+ {user?.coupons || 0}优惠券
+ {levelInfo.points}积分
+
+
+
+
+
+ 📋 我的订单
+ Taro.navigateTo({ url: '/pages/user/order/my-order' })}>全部订单 ›
+
+
+ {orderTabs.map((t, i) => (
+ Taro.navigateTo({ url: '/pages/user/order/my-order?status=' + t.key })}>
+ {t.icon}
+ {t.name}
+
+ ))}
+
+
+
+
+ {menus.map((m, i) => (
+ Taro.navigateTo({ url: m.url })}>
+ {m.icon}
+ {m.name}
+ ›
+
+ ))}
+
+
+
+
+
+
+ )
+}
diff --git a/taro-app/src/pages/user/settings/settings.config.ts b/taro-app/src/pages/user/settings/settings.config.ts
new file mode 100644
index 0000000..95c1b05
--- /dev/null
+++ b/taro-app/src/pages/user/settings/settings.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '设置',
+ backgroundColor: '#f5f5f5'
+})
diff --git a/taro-app/src/pages/user/settings/settings.scss b/taro-app/src/pages/user/settings/settings.scss
new file mode 100644
index 0000000..f6209f4
--- /dev/null
+++ b/taro-app/src/pages/user/settings/settings.scss
@@ -0,0 +1,64 @@
+.page-scroll {
+ min-height: 100vh;
+ background: #f5f5f5;
+ padding-bottom: 60rpx;
+}
+
+.section-title {
+ padding: 30rpx 40rpx 10rpx;
+ font-size: 24rpx;
+ color: #999;
+}
+
+.card {
+ background: #fff;
+ margin: 0 20rpx 20rpx;
+ border-radius: 16rpx;
+ padding: 0 24rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.menu-item {
+ display: flex;
+ align-items: center;
+ padding: 28rpx 0;
+ border-bottom: 2rpx solid #f5f5f5;
+}
+.menu-item:last-child { border-bottom: none; }
+
+.menu-icon {
+ width: 60rpx; height: 60rpx;
+ background: #f5f5f5;
+ border-radius: 12rpx;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 30rpx;
+ flex-shrink: 0;
+ text-align: center;
+}
+
+.menu-body { flex: 1; margin-left: 20rpx; min-width: 0; }
+.menu-name { font-size: 28rpx; color: #333; display: block; }
+.menu-desc { font-size: 22rpx; color: #999; margin-top: 6rpx; display: block; }
+
+.menu-arrow { font-size: 36rpx; color: #ccc; }
+
+.menu-switch { flex-shrink: 0; }
+
+.logout { padding: 40rpx 30rpx 20rpx; }
+
+.logout-btn {
+ background: #fff !important;
+ color: #f5222d !important;
+ border: 2rpx solid #ffccc7 !important;
+ border-radius: 16rpx !important;
+ padding: 24rpx 0 !important;
+ font-size: 30rpx !important;
+ width: 100%;
+}
+
+.version {
+ text-align: center;
+ padding: 40rpx 0 60rpx;
+ font-size: 22rpx;
+ color: #999;
+}
diff --git a/taro-app/src/pages/user/settings/settings.tsx b/taro-app/src/pages/user/settings/settings.tsx
new file mode 100644
index 0000000..9f07ead
--- /dev/null
+++ b/taro-app/src/pages/user/settings/settings.tsx
@@ -0,0 +1,104 @@
+import { useState } from 'react'
+import { View, Text, ScrollView, Switch, Button } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { showToast } from '../../../utils'
+import './index.scss'
+
+export default function Settings() {
+ const [notif, setNotif] = useState(true)
+ const [darkMode, setDarkMode] = useState(false)
+ const [autoUpdate, setAutoUpdate] = useState(true)
+
+ const accountMenus = [
+ { icon: '👤', name: '个人资料', desc: '已绑定', action: 'profile' },
+ { icon: '📱', name: '手机号', desc: '138****0000', action: 'phone' },
+ { icon: '🔐', name: '修改密码', desc: '', action: 'password' },
+ { icon: '🎓', name: '学生认证', desc: '已认证', action: 'verify' },
+ ]
+
+ const settingMenus = [
+ { icon: '🔔', name: '消息通知', switch: true, value: notif, setter: setNotif },
+ { icon: '🌙', name: '深色模式', switch: true, value: darkMode, setter: setDarkMode },
+ { icon: '📲', name: '自动更新', switch: true, value: autoUpdate, setter: setAutoUpdate },
+ ]
+
+ const otherMenus = [
+ { icon: '📦', name: '缓存清理', desc: '2.3 MB' },
+ { icon: '📜', name: '关于我们', desc: 'v1.0.0' },
+ { icon: '📋', name: '用户协议', desc: '' },
+ { icon: '🛡', name: '隐私政策', desc: '' },
+ { icon: '💬', name: '意见反馈', desc: '' },
+ { icon: '📞', name: '联系客服', desc: '400-888-0000' },
+ ]
+
+ function clearCache() {
+ Taro.showModal({
+ title: '清理缓存',
+ content: '确定清除应用缓存?',
+ success: (res) => { if (res.confirm) showToast('清理完成', 'success') }
+ })
+ }
+
+ return (
+
+ 账户
+
+ {accountMenus.map((m, i) => (
+ showToast('功能开发中')}>
+ {m.icon}
+
+ {m.name}
+ {m.desc && {m.desc}}
+
+ ›
+
+ ))}
+
+
+ 设置
+
+ {settingMenus.map((m, i) => (
+
+ {m.icon}
+ {m.name}
+
+ m.setter(e.detail.value)} color="#1890ff" />
+
+
+ ))}
+
+
+ 其他
+
+ {otherMenus.map((m, i) => (
+ {
+ if (m.name === '缓存清理') clearCache()
+ else if (m.name === '意见反馈') showToast('感谢反馈', 'success')
+ else showToast('敬请期待')
+ }}>
+ {m.icon}
+
+ {m.name}
+ {m.desc && {m.desc}}
+
+ ›
+
+ ))}
+
+
+
+
+
+
+ 校园综合服务平台 v1.0.0
+
+ )
+}
diff --git a/taro-app/src/services/api.ts b/taro-app/src/services/api.ts
new file mode 100644
index 0000000..4f3f4be
--- /dev/null
+++ b/taro-app/src/services/api.ts
@@ -0,0 +1,83 @@
+// src/services/api.ts - 业务 API 聚合
+import { http } from '../utils/request'
+
+export const api = {
+ user: {
+ profile: () => http.get('/api/user/profile'),
+ login: (data: any) => http.post('/api/user/login', data),
+ update: (data: any) => http.put('/api/user/profile', data),
+ levelInfo: () => http.get('/api/user/level-info')
+ },
+ merchant: {
+ list: (p?: any) => http.get('/api/merchant/list', p),
+ detail: (id: number | string) => http.get('/api/merchant/detail/' + id),
+ apply: (data: any) => http.post('/api/merchant/apply', data),
+ myShop: () => http.get('/api/merchant/my-shop'),
+ stats: () => http.get('/api/merchant/stats')
+ },
+ goods: {
+ list: (p?: any) => http.get('/api/goods/list', p),
+ detail: (id: number | string) => http.get('/api/goods/detail/' + id),
+ create: (data: any) => http.post('/api/goods', data),
+ update: (id: number, data: any) => http.put('/api/goods/' + id, data),
+ remove: (id: number) => http.delete('/api/goods/' + id),
+ toggleStatus: (id: number, status: number) => http.put('/api/goods/' + id + '/status', { status })
+ },
+ order: {
+ list: (p?: any) => http.get('/api/order/list', p),
+ detail: (id: number | string) => http.get('/api/order/detail/' + id),
+ create: (data: any) => http.post('/api/order', data),
+ pay: (id: number | string) => http.post('/api/order/' + id + '/pay'),
+ cancel: (id: number | string) => http.post('/api/order/' + id + '/cancel'),
+ confirm: (id: number | string) => http.post('/api/order/' + id + '/confirm')
+ },
+ coupon: {
+ list: () => http.get('/api/coupon/list'),
+ myCoupons: () => http.get('/api/coupon/my'),
+ receive: (id: number) => http.post('/api/coupon/' + id + '/receive')
+ },
+ community: {
+ list: (p?: any) => http.get('/api/community/list', p),
+ detail: (id: number) => http.get('/api/community/detail/' + id),
+ create: (data: any) => http.post('/api/community', data),
+ join: (id: number) => http.post('/api/community/' + id + '/join'),
+ posts: (id: number, p?: any) => http.get('/api/community/' + id + '/posts', p),
+ activities: (p?: any) => http.get('/api/community/activities', p)
+ },
+ forum: {
+ boards: () => http.get('/api/forum/boards'),
+ list: (p?: any) => http.get('/api/forum/list', p),
+ detail: (id: number) => http.get('/api/forum/detail/' + id),
+ create: (data: any) => http.post('/api/forum', data),
+ like: (id: number) => http.post('/api/forum/' + id + '/like'),
+ favorite: (id: number) => http.post('/api/forum/' + id + '/favorite'),
+ reply: (id: number, data: any) => http.post('/api/forum/' + id + '/reply', data)
+ },
+ confession: {
+ list: (p?: any) => http.get('/api/confession/list', p),
+ detail: (id: number) => http.get('/api/confession/detail/' + id),
+ create: (data: any) => http.post('/api/confession', data),
+ comment: (id: number, data: any) => http.post('/api/confession/' + id + '/comment', data),
+ like: (id: number) => http.post('/api/confession/' + id + '/like')
+ },
+ market: {
+ list: (p?: any) => http.get('/api/market/list', p),
+ detail: (id: number) => http.get('/api/market/detail/' + id),
+ create: (data: any) => http.post('/api/market', data),
+ myGoods: (p?: any) => http.get('/api/market/my-goods', p),
+ favorite: (id: number) => http.post('/api/market/' + id + '/favorite')
+ },
+ errand: {
+ list: (p?: any) => http.get('/api/errand/list', p),
+ detail: (id: number) => http.get('/api/errand/detail/' + id),
+ create: (data: any) => http.post('/api/errand', data),
+ accept: (id: number) => http.post('/api/errand/' + id + '/accept'),
+ complete: (id: number) => http.post('/api/errand/' + id + '/complete')
+ },
+ diy: {
+ list: () => http.get('/api/diy/list'),
+ detail: (id: number) => http.get('/api/diy/detail/' + id)
+ }
+}
+
+export default api
diff --git a/taro-app/src/utils/index.ts b/taro-app/src/utils/index.ts
new file mode 100644
index 0000000..abffa66
--- /dev/null
+++ b/taro-app/src/utils/index.ts
@@ -0,0 +1,112 @@
+// src/utils/index.ts - 通用工具函数(多端兼容)
+import Taro from '@tarojs/taro'
+
+export function formatTime(date: Date | number): string {
+ if (typeof date === 'number') date = new Date(date)
+ const y = date.getFullYear()
+ const m = (date.getMonth() + 1).toString().padStart(2, '0')
+ const d = date.getDate().toString().padStart(2, '0')
+ const h = date.getHours().toString().padStart(2, '0')
+ const min = date.getMinutes().toString().padStart(2, '0')
+ return `${y}-${m}-${d} ${h}:${min}`
+}
+
+export function timeAgo(ts: number): string {
+ const diff = Date.now() - ts
+ if (diff < 60 * 1000) return '刚刚'
+ if (diff < 3600 * 1000) return Math.floor(diff / 60000) + '分钟前'
+ if (diff < 86400 * 1000) return Math.floor(diff / 3600000) + '小时前'
+ if (diff < 86400 * 1000 * 7) return Math.floor(diff / 86400000) + '天前'
+ return formatTime(ts)
+}
+
+export function formatPrice(price: number | string): string {
+ return '¥' + Number(price || 0).toFixed(2)
+}
+
+export function showToast(title: string, icon: 'success' | 'error' | 'loading' | 'none' = 'none', duration = 2000) {
+ Taro.showToast({ title, icon, duration })
+}
+
+export function showLoading(title = '加载中...') {
+ Taro.showLoading({ title, mask: true })
+}
+
+export function hideLoading() {
+ Taro.hideLoading()
+}
+
+export async function confirm(title: string, content: string): Promise {
+ try {
+ const res = await Taro.showModal({ title, content, confirmText: '确定', cancelText: '取消' })
+ return !!res.confirm
+ } catch {
+ return false
+ }
+}
+
+export function navigateTo(url: string) {
+ Taro.navigateTo({ url })
+}
+
+export function switchTab(url: string) {
+ Taro.switchTab({ url })
+}
+
+export function redirectTo(url: string) {
+ Taro.redirectTo({ url })
+}
+
+export function navigateBack(delta = 1) {
+ Taro.navigateBack({ delta })
+}
+
+export function getStorage(key: string, defaultValue: T | null = null): T | null {
+ try {
+ const v = Taro.getStorageSync(key)
+ return v || defaultValue
+ } catch {
+ return defaultValue
+ }
+}
+
+export function setStorage(key: string, value: any) {
+ try {
+ Taro.setStorageSync(key, value)
+ } catch (e) {
+ console.warn('setStorage failed', e)
+ }
+}
+
+export function removeStorage(key: string) {
+ try { Taro.removeStorageSync(key) } catch {}
+}
+
+// 防抖
+export function debounce any>(fn: T, wait = 300) {
+ let timer: any
+ return (...args: Parameters) => {
+ if (timer) clearTimeout(timer)
+ timer = setTimeout(() => fn(...args), wait)
+ }
+}
+
+// 节流
+export function throttle any>(fn: T, wait = 300) {
+ let last = 0
+ return (...args: Parameters) => {
+ const now = Date.now()
+ if (now - last >= wait) { last = now; fn(...args) }
+ }
+}
+
+// 深拷贝
+export function deepClone(obj: T): T {
+ return JSON.parse(JSON.stringify(obj))
+}
+
+// 获取当前环境
+export function getPlatform(): string {
+ // eslint-disable-next-line no-undef
+ return process.env.TARO_ENV || 'weapp'
+}
diff --git a/taro-app/src/utils/printer.ts b/taro-app/src/utils/printer.ts
new file mode 100644
index 0000000..4553fd9
--- /dev/null
+++ b/taro-app/src/utils/printer.ts
@@ -0,0 +1,106 @@
+// src/utils/printer.ts - 通用云打印机(飞鹅/易联云/商米/自定义)
+import Taro from '@tarojs/taro'
+
+export interface PrinterDevice {
+ id: number
+ vendor: 'feie' | 'yilianyun' | 'sunmi' | 'xprinter' | 'generic'
+ name: string
+ sn: string
+ user?: string
+ ukey?: string
+ token?: string
+ apiUrl?: string
+}
+
+const STORAGE_KEY = 'printers_config'
+
+export function getPrinters(): PrinterDevice[] {
+ try {
+ return (Taro.getStorageSync(STORAGE_KEY) as PrinterDevice[]) || []
+ } catch {
+ return []
+ }
+}
+
+export function savePrinter(printer: PrinterDevice) {
+ const list = getPrinters()
+ const idx = list.findIndex(p => p.id === printer.id)
+ if (idx >= 0) list[idx] = printer
+ else list.push({ ...printer, id: printer.id || Date.now() })
+ Taro.setStorageSync(STORAGE_KEY, list)
+}
+
+export function removePrinter(id: number) {
+ Taro.setStorageSync(STORAGE_KEY, getPrinters().filter(p => p.id !== id))
+}
+
+// 小票模板构建
+export function buildReceipt(order: any, shop?: any): string {
+ const lines: string[] = []
+ lines.push('' + (shop?.name || '店铺') + ' 小票打印')
+ lines.push('订单号: ' + (order.id || 'OD' + Date.now()))
+ lines.push('时间: ' + new Date().toLocaleString())
+ lines.push('地址: ' + (order.address || ''))
+ lines.push('电话: ' + (order.phone || ''))
+ lines.push('----------------------------')
+ ;(order.items || order.goods || []).forEach((it: any) => {
+ lines.push(it.name + ' × ' + (it.qty || it.count || 1) + ' ¥' + Number(it.price || 0).toFixed(2))
+ })
+ lines.push('----------------------------')
+ lines.push('配送费: ¥' + Number(order.deliveryFee || 0).toFixed(2))
+ lines.push('合计: ¥' + Number(order.totalPrice || order.payPrice || 0).toFixed(2) + '')
+ if (order.remark) lines.push('备注: ' + order.remark)
+ lines.push('order_' + (order.id || Date.now()) + '')
+ lines.push('')
+ return lines.join('\n')
+}
+
+// 调用各平台云打印接口
+export async function printOrder(printer: PrinterDevice, content: string) {
+ try {
+ const base = { sn: printer.sn }
+ switch (printer.vendor) {
+ case 'feie':
+ // 飞鹅云
+ return await Taro.request({
+ url: printer.apiUrl || 'https://api.feieyun.cn/Api/Open/',
+ method: 'POST',
+ data: { user: printer.user, ukey: printer.ukey, sn: printer.sn, content, times: 1, sig: Date.now() }
+ })
+ case 'yilianyun':
+ return await Taro.request({
+ url: printer.apiUrl || 'https://open-api.10ss.net/print/index',
+ method: 'POST',
+ data: { machine_code: printer.sn, content, sign: printer.ukey, time: Math.floor(Date.now() / 1000) }
+ })
+ case 'sunmi':
+ return await Taro.request({
+ url: printer.apiUrl || 'https://api.sunmi.com/printer/print',
+ method: 'POST',
+ header: { 'Authorization': 'Bearer ' + (printer.token || '') },
+ data: { ...base, content }
+ })
+ case 'xprinter':
+ return await Taro.request({
+ url: printer.apiUrl || 'https://api.xprinter.cn/print',
+ method: 'POST',
+ data: { ...base, content, key: printer.ukey }
+ })
+ case 'generic':
+ default:
+ if (printer.apiUrl) {
+ return await Taro.request({
+ url: printer.apiUrl,
+ method: 'POST',
+ data: { sn: printer.sn, content, token: printer.token }
+ })
+ }
+ // 模拟打印
+ console.log('[Printer Mock]', content)
+ return { statusCode: 200, data: { code: 0, msg: '模拟打印成功' } }
+ }
+ } catch (e: any) {
+ console.log('[Printer fallback]', content)
+ return { statusCode: 200, data: { code: 0, msg: '网络不可达,已转为模拟打印' } }
+ }
+}
diff --git a/taro-app/src/utils/request.ts b/taro-app/src/utils/request.ts
new file mode 100644
index 0000000..8b6d6dd
--- /dev/null
+++ b/taro-app/src/utils/request.ts
@@ -0,0 +1,178 @@
+// src/utils/request.ts - 统一请求库(多端兼容)
+import Taro from '@tarojs/taro'
+import { getStorage, showToast, showLoading, hideLoading, navigateTo } from './index'
+
+const BASE_URL = 'https://api.campus.example.com'
+// 开发环境可切换为本地服务
+// const BASE_URL = 'http://localhost:3000'
+
+interface RequestOptions {
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTION'
+ data?: any
+ header?: Record
+ mock?: boolean
+ loading?: boolean
+ silent?: boolean
+}
+
+// Mock 数据池(离线演示用)
+const MOCK_DB: Record = {
+ user: { id: 10001, nickname: '校园用户', avatar: '', role: 'user', level: 1, points: 100, balance: 50.5 },
+ merchants: Array.from({ length: 6 }, (_, i) => ({
+ id: i + 1,
+ name: ['麦香汉堡', '鲜饮茶铺', '校园便利', '学霸文具', '快剪理发', '烧烤大师'][i],
+ category: ['餐饮', '餐饮', '零售', '零售', '服务', '餐饮'][i],
+ rating: 4.3 + Math.random() * 0.5,
+ sales: Math.floor(100 + Math.random() * 1000),
+ address: ['东门', '商业街', '宿舍区', '教学楼', '服务区', '西门'][i],
+ phone: '138****8888',
+ openTime: '09:00-22:00',
+ deliveryFee: [3, 2, 2, 0, 0, 4][i],
+ minOrder: [15, 10, 8, 5, 0, 20][i],
+ description: '精选好货,品质保证'
+ })),
+ coupons: [
+ { 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 }
+ ]
+}
+
+function genGoods(shopId: number): any[] {
+ const names = [
+ ['招牌牛肉堡', '薯条', '可乐', '鸡米花', '鸡腿堡'],
+ ['珍珠奶茶', '杨枝甘露', '柠檬茶', '芋泥波波', '芝士乌龙'],
+ ['泡面', '矿泉水', '薯片', '饼干', '辣条'],
+ ['中性笔', '笔记本', '文具盒', '橡皮', '文件夹'],
+ ['剪发', '洗发', '造型', '烫染', '护理'],
+ ['烤串', '烤翅', '烤韭菜', '烤茄子', '炒饭']
+ ]
+ const prices = [[28, 12, 5, 15, 22], [15, 18, 12, 16, 20], [6, 2, 8, 10, 5], [18, 15, 25, 5, 8], [25, 15, 30, 128, 88], [3, 8, 5, 10, 15]]
+ return names[shopId - 1].map((name, idx) => ({
+ id: shopId * 100 + idx,
+ shopId,
+ name,
+ price: prices[shopId - 1][idx],
+ originalPrice: prices[shopId - 1][idx] + 5,
+ sales: Math.floor(Math.random() * 500),
+ stock: Math.floor(Math.random() * 100),
+ description: '新鲜制作,品质保证',
+ attrs: [
+ { name: '口味', type: 'select', options: ['原味', '香辣', '黑椒'], required: true },
+ { name: '加料', type: 'multiselect', options: ['芝士', '培根', '鸡蛋', '番茄'], pricePerAddon: 3, maxSelect: 4 }
+ ]
+ }))
+}
+
+function handleMock(url: string, method: string, data: any): any {
+ if (url.includes('/api/user/profile')) return MOCK_DB.user
+ if (url.includes('/api/user/login')) return { token: 'mock_' + Date.now(), info: { ...MOCK_DB.user, nickname: data?.nickname || '校园用户' } }
+ if (url.includes('/api/merchant/list')) return { list: MOCK_DB.merchants, total: MOCK_DB.merchants.length, page: 1, hasMore: false }
+ if (url.includes('/api/merchant/detail')) {
+ const id = parseInt(url.split('/').pop() || '1')
+ const shop = MOCK_DB.merchants.find(m => m.id === id) || MOCK_DB.merchants[0]
+ return { ...shop, goods: genGoods(shop.id) }
+ }
+ if (url.includes('/api/goods/list')) {
+ const shopId = data?.shopId || 1
+ return { list: genGoods(shopId), total: 5, page: 1, hasMore: false }
+ }
+ if (url.includes('/api/coupon')) return MOCK_DB.coupons
+ if (url.includes('/api/order')) {
+ return { id: 'OD' + Date.now(), status: 1, totalPrice: data?.totalPrice || 28, createdAt: Date.now() }
+ }
+ if (url.includes('/api/community/list')) return { list: [
+ { id: 1, name: '校园篮球社', category: '运动', members: 328, posts: 1280, description: '篮球爱好者聚集地' },
+ { id: 2, name: '编程与算法', category: '科技', members: 512, posts: 2100, description: '代码改变世界' },
+ { id: 3, name: '摄影爱好社', category: '艺术', members: 156, posts: 680, description: '用镜头记录美好' },
+ { id: 4, name: '校园吉他社', category: '音乐', members: 208, posts: 450, description: '音乐无国界' }
+ ], total: 4, page: 1, hasMore: false }
+ if (url.includes('/api/forum/boards')) return [
+ { id: 1, name: '校园生活', posts: 5200 },
+ { id: 2, name: '学习交流', posts: 3100 },
+ { id: 3, name: '失物招领', posts: 890 },
+ { id: 4, name: '求职招聘', posts: 420 }
+ ]
+ if (url.includes('/api/forum/list')) return { list: [
+ { id: 1, title: '图书馆新增自习区域开放啦', author: '同学A', likes: 120, comments: 35, views: 1200, createdAt: Date.now() - 3600000 },
+ { id: 2, title: '分享一份考研数学复习笔记', author: '学霸君', likes: 280, comments: 60, views: 3200, createdAt: Date.now() - 7200000 },
+ { id: 3, title: '在食堂捡到一张校园卡', author: '好心人', likes: 5, comments: 2, views: 80, createdAt: Date.now() - 86400000 }
+ ], total: 3, page: 1, hasMore: false }
+ if (url.includes('/api/confession/list')) return { list: [
+ { id: 1, content: '图书馆三楼靠窗第三排的女生,每天都能看到你专注的样子,你笑起来真的好好看。', likes: 128, comments: 32, author: '匿名', createdAt: Date.now() - 3600000 },
+ { id: 2, content: '计算机学院的学长,上次帮我修电脑的那个,你说下次有问题再找我,我想...我可能又有问题了。', likes: 256, comments: 48, author: '匿名', createdAt: Date.now() - 7200000 },
+ { id: 3, content: '致篮球场上12号球衣的男生:你的后仰跳投真的很帅!', likes: 88, comments: 12, author: '匿名', createdAt: Date.now() - 86400000 }
+ ], total: 3, page: 1, hasMore: false }
+ if (url.includes('/api/market/list')) return { list: [
+ { id: 1, title: '九成新 MacBook Pro 2023', price: 6800, category: '数码', seller: '毕业学长', views: 320, createdAt: Date.now() - 86400000 },
+ { id: 2, title: '高数教材 + 习题册一套', price: 30, category: '书籍', seller: '学姐', views: 120, createdAt: Date.now() - 172800000 },
+ { id: 3, title: 'Nike 运动鞋 42码', price: 280, category: '服饰', seller: '同学D', views: 80, createdAt: Date.now() - 259200000 },
+ { id: 4, title: '台灯 - 可调光护眼', price: 50, category: '生活用品', seller: '学长A', views: 60, createdAt: Date.now() - 345600000 }
+ ], total: 4, page: 1, hasMore: false }
+ if (url.includes('/api/errand')) return { list: [
+ { id: 1, type: '取快递', title: '顺丰快递代取', fee: 5, publisher: '用户A', address: '南门菜鸟驿站', createdAt: Date.now() - 1800000 },
+ { id: 2, type: '代买', title: '帮我买份晚饭', fee: 8, publisher: '用户B', address: '东门麦香汉堡', createdAt: Date.now() - 3600000 },
+ { id: 3, type: '其他', title: '文件打印', fee: 10, publisher: '用户C', address: '打印店', createdAt: Date.now() - 86400000 }
+ ], total: 3, page: 1, hasMore: false }
+ return null
+}
+
+export async function request(url: string, options: RequestOptions = {}): Promise {
+ const { method = 'GET', data, header, mock = !BASE_URL.startsWith('http'), loading, silent } = options
+
+ if (loading) showLoading()
+
+ // 优先使用 mock
+ if (mock) {
+ await new Promise(resolve => setTimeout(resolve, 300))
+ const mockRes = handleMock(url, method, data)
+ if (loading) hideLoading()
+ return mockRes as T
+ }
+
+ try {
+ const token = getStorage('token', '')
+ const res = await Taro.request({
+ url: BASE_URL + url,
+ method,
+ data,
+ header: Object.assign(
+ { 'Content-Type': 'application/json', Authorization: token ? 'Bearer ' + token : '' },
+ header || {}
+ ),
+ timeout: 15000
+ })
+
+ if (loading) hideLoading()
+
+ // 鉴权失败
+ if (res.statusCode === 401) {
+ showToast('请先登录')
+ navigateTo('/pages/user/login/login')
+ throw new Error('Unauthorized')
+ }
+
+ const body: any = res.data
+ if (body.code === 0 || body.code === 200 || body.list) {
+ return (body.data || body || null) as T
+ }
+ if (!silent) showToast(body.msg || '请求失败')
+ throw new Error(body.msg || 'Request Failed')
+ } catch (e: any) {
+ if (loading) hideLoading()
+ // 网络失败降级到 mock
+ if (!silent) console.warn('[network fallback to mock]', url, e.message)
+ return handleMock(url, method, data) as T
+ }
+}
+
+export const http = {
+ get: (url: string, data?: any, opts: RequestOptions = {}): Promise =>
+ request(url, { method: 'GET', data, ...opts }),
+ post: (url: string, data?: any, opts: RequestOptions = {}): Promise =>
+ request(url, { method: 'POST', data, ...opts }),
+ put: (url: string, data?: any, opts: RequestOptions = {}): Promise =>
+ request(url, { method: 'PUT', data, ...opts }),
+ delete: (url: string, data?: any, opts: RequestOptions = {}): Promise =>
+ request(url, { method: 'DELETE', data, ...opts })
+}
diff --git a/taro-app/tsconfig.json b/taro-app/tsconfig.json
new file mode 100644
index 0000000..20a2541
--- /dev/null
+++ b/taro-app/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "target": "es2017",
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "noImplicitAny": false,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "typeRoots": [
+ "./types",
+ "./node_modules/@types"
+ ],
+ "lib": ["esnext", "dom"]
+ },
+ "include": [
+ "src/**/*",
+ "config/**/*"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist"
+ ]
+}
diff --git a/utils/api.js b/utils/api.js
new file mode 100644
index 0000000..71adf7f
--- /dev/null
+++ b/utils/api.js
@@ -0,0 +1,136 @@
+// utils/api.js - API 统一封装
+const req = require('./request.js')
+
+module.exports = {
+ // ========== 用户模块 ==========
+ user: {
+ profile: () => req.get('/api/user/profile'),
+ update: (data) => req.put('/api/user/profile', data),
+ login: (data) => req.post('/api/user/login', data),
+ register: (data) => req.post('/api/user/register', data),
+ levelInfo: () => req.get('/api/user/level'),
+ pointsLog: (p) => req.get('/api/user/points', p)
+ },
+
+ // ========== 商户模块 ==========
+ merchant: {
+ apply: (data) => req.post('/api/merchant/apply', data),
+ applyStatus: () => req.get('/api/merchant/apply/status'),
+ list: (p) => req.get('/api/merchant/list', p),
+ detail: (id) => req.get('/api/merchant/detail/' + id),
+ myShop: () => req.get('/api/merchant/my-shop'),
+ updateShop: (data) => req.put('/api/merchant/my-shop', data),
+ stats: () => req.get('/api/merchant/stats')
+ },
+
+ // ========== 商品模块 ==========
+ goods: {
+ list: (p) => req.get('/api/goods/list', p),
+ detail: (id) => req.get('/api/goods/detail/' + id),
+ create: (data) => req.post('/api/goods', data),
+ update: (id, data) => req.put('/api/goods/' + id, data),
+ delete: (id) => req.delete('/api/goods/' + id),
+ toggleStatus: (id, status) => req.put('/api/goods/' + id + '/status', { status }),
+ categories: () => req.get('/api/goods/categories')
+ },
+
+ // ========== 外卖订单 ==========
+ order: {
+ list: (p) => req.get('/api/order/list', p),
+ detail: (id) => req.get('/api/order/detail/' + id),
+ create: (data) => req.post('/api/order', data),
+ pay: (id) => req.post('/api/order/' + id + '/pay'),
+ cancel: (id) => req.post('/api/order/' + id + '/cancel'),
+ confirm: (id) => req.post('/api/order/' + id + '/confirm'),
+ refund: (id, reason) => req.post('/api/order/' + id + '/refund', { reason }),
+ print: (id) => req.post('/api/order/' + id + '/print'),
+ merchantList: (p) => req.get('/api/merchant/order/list', p)
+ },
+
+ // ========== 跑腿 ==========
+ errand: {
+ list: (p) => req.get('/api/errand/list', p),
+ detail: (id) => req.get('/api/errand/detail/' + id),
+ create: (data) => req.post('/api/errand', data),
+ accept: (id) => req.post('/api/errand/' + id + '/accept'),
+ complete: (id) => req.post('/api/errand/' + id + '/complete'),
+ location: (id) => req.get('/api/errand/' + id + '/location')
+ },
+
+ // ========== 社区 ==========
+ community: {
+ list: (p) => req.get('/api/community/list', p),
+ detail: (id) => req.get('/api/community/detail/' + id),
+ create: (data) => req.post('/api/community', data),
+ join: (id) => req.post('/api/community/' + id + '/join'),
+ posts: (id, p) => req.get('/api/community/' + id + '/posts', p),
+ postCreate: (id, data) => req.post('/api/community/' + id + '/posts', data),
+ activities: (p) => req.get('/api/community/activities', p),
+ activitySignUp: (id) => req.post('/api/community/activity/' + id + '/signup'),
+ activityCheckIn: (id) => req.post('/api/community/activity/' + id + '/checkin')
+ },
+
+ // ========== 论坛 ==========
+ forum: {
+ boards: () => req.get('/api/forum/boards'),
+ list: (p) => req.get('/api/forum/list', p),
+ detail: (id) => req.get('/api/forum/detail/' + id),
+ create: (data) => req.post('/api/forum', data),
+ reply: (id, data) => req.post('/api/forum/' + id + '/reply'),
+ like: (id) => req.post('/api/forum/' + id + '/like'),
+ favorite: (id) => req.post('/api/forum/' + id + '/favorite'),
+ search: (kw) => req.get('/api/forum/search', { kw }),
+ vote: (id, optionId) => req.post('/api/forum/' + id + '/vote', { optionId })
+ },
+
+ // ========== 表白墙 ==========
+ confession: {
+ list: (p) => req.get('/api/confession/list', p),
+ hot: (p) => req.get('/api/confession/hot', p),
+ detail: (id) => req.get('/api/confession/detail/' + id),
+ create: (data) => req.post('/api/confession', data),
+ comment: (id, data) => req.post('/api/confession/' + id + '/comment'),
+ like: (id) => req.post('/api/confession/' + id + '/like'),
+ report: (id, reason) => req.post('/api/confession/' + id + '/report', { reason })
+ },
+
+ // ========== 二手市场 ==========
+ market: {
+ list: (p) => req.get('/api/market/list', p),
+ detail: (id) => req.get('/api/market/detail/' + id),
+ create: (data) => req.post('/api/market', data),
+ myGoods: (p) => req.get('/api/market/my-goods', p),
+ offline: (id) => req.put('/api/market/' + id + '/offline'),
+ favorite: (id) => req.post('/api/market/' + id + '/favorite'),
+ categories: () => req.get('/api/market/categories')
+ },
+
+ // ========== 优惠券 ==========
+ coupon: {
+ list: () => req.get('/api/coupon/list'),
+ myCoupons: () => req.get('/api/coupon/my'),
+ receive: (id) => req.post('/api/coupon/' + id + '/receive'),
+ use: (id, orderId) => req.post('/api/coupon/' + id + '/use', { orderId })
+ },
+
+ // ========== DIY 页面 ==========
+ diy: {
+ list: () => req.get('/api/diy/list'),
+ detail: (id) => req.get('/api/diy/detail/' + id)
+ },
+
+ // ========== 支付 ==========
+ payment: {
+ wxPay: (orderId) => req.post('/api/payment/wxpay', { orderId })
+ },
+
+ // ========== 通用云打印机 ==========
+ printer: {
+ list: () => req.get('/api/printer/list'),
+ add: (data) => req.post('/api/printer', data),
+ print: (printerId, content) => req.post('/api/printer/' + printerId + '/print', { content }),
+ status: (printerId) => req.get('/api/printer/' + printerId + '/status'),
+ // 通用云打印:飞鹅、易联云、商米等
+ supported: ['feie', 'yilianyun', 'shangmi', 'xprinter']
+ }
+}
diff --git a/utils/auth.js b/utils/auth.js
new file mode 100644
index 0000000..b318c80
--- /dev/null
+++ b/utils/auth.js
@@ -0,0 +1,51 @@
+// utils/auth.js - 登录授权封装
+const storage = require('./storage.js')
+
+module.exports = {
+ // 模拟微信登录
+ mockLogin(code) {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ token: 'mock_token_' + Date.now(),
+ info: {
+ id: 10001,
+ nickname: '校园用户',
+ avatar: '/images/default-avatar.png',
+ phone: '138****8888',
+ role: 'user',
+ level: 1,
+ points: 100,
+ balance: 0
+ }
+ })
+ }, 300)
+ })
+ },
+
+ // 获取微信用户信息
+ getUserProfile() {
+ return new Promise((resolve, reject) => {
+ wx.getUserProfile({
+ desc: '用于完善会员资料',
+ success: (res) => resolve(res.userInfo),
+ fail: reject
+ })
+ })
+ },
+
+ // 检查是否有登录
+ isLogin() {
+ return !!storage.get('token')
+ },
+
+ // 切换角色(用于演示商户/管理员)
+ switchRole(role) {
+ const userInfo = storage.get('userInfo') || {}
+ userInfo.role = role
+ storage.set('userInfo', userInfo)
+ const app = getApp()
+ app.globalData.role = role
+ app.globalData.userInfo = userInfo
+ }
+}
diff --git a/utils/mock.js b/utils/mock.js
new file mode 100644
index 0000000..6ce6104
--- /dev/null
+++ b/utils/mock.js
@@ -0,0 +1,483 @@
+// utils/mock.js - Mock 数据服务
+const storage = require('./storage.js')
+
+// 内存数据池
+let DB = {
+ merchants: [
+ { id: 1, name: '麦香汉堡', category: '餐饮', categoryId: 1, rating: 4.8, sales: 1200, address: '校园东门1号', phone: '13811110001', openTime: '09:00-22:00', deliveryFee: 3, minOrder: 15, logo: '', images: [], status: 1, owner: '张老板', description: '正宗汉堡薯条,校园第一味' },
+ { id: 2, name: '鲜饮茶铺', category: '餐饮', categoryId: 1, rating: 4.6, sales: 850, address: '商业街B12', phone: '13811110002', openTime: '10:00-21:30', deliveryFee: 2, minOrder: 10, logo: '', images: [], status: 1, owner: '李老板', description: '新鲜果茶、奶茶饮品' },
+ { id: 3, name: '校园便利超市', category: '零售', categoryId: 2, rating: 4.7, sales: 2100, address: '宿舍区5号楼', phone: '13811110003', openTime: '07:00-23:30', deliveryFee: 2, minOrder: 8, logo: '', images: [], status: 1, owner: '王老板', description: '日用品、零食、饮料' },
+ { id: 4, name: '学霸文具', category: '零售', categoryId: 2, rating: 4.5, sales: 320, address: '教学楼A座', phone: '13811110004', openTime: '08:30-20:00', deliveryFee: 0, minOrder: 5, logo: '', images: [], status: 1, owner: '陈老板', description: '学习用品、笔记本' },
+ { id: 5, name: '快剪理发店', category: '服务', categoryId: 3, rating: 4.4, sales: 180, address: '生活服务中心', phone: '13811110005', openTime: '10:00-21:00', deliveryFee: 0, minOrder: 0, logo: '', images: [], status: 1, owner: '刘师傅', description: '快剪、造型' }
+ ],
+
+ goods: [
+ { id: 1, shopId: 1, name: '经典牛肉汉堡套餐', price: 28, originalPrice: 35, sales: 520, stock: 80, categoryId: 101, images: [], description: '牛肉汉堡+薯条+可乐', status: 1,
+ attrs: [
+ {
+ name: '口味',
+ type: 'select', // 多选
+ options: ['原味', '香辣', '黑椒'],
+ maxSelect: 2,
+ required: true
+ },
+ {
+ name: '加料',
+ type: 'multiselect',
+ options: ['芝士', '培根', '鸡蛋', '番茄', '生菜'],
+ pricePerAddon: 3,
+ maxSelect: 5
+ },
+ {
+ name: '辣度(1-10)',
+ type: 'number',
+ min: 1,
+ max: 10,
+ step: 1,
+ default: 5
+ },
+ {
+ name: '备注',
+ type: 'text',
+ maxLength: 50
+ }
+ ]
+ },
+ { id: 2, shopId: 1, name: '双层芝士汉堡', price: 22, originalPrice: 26, sales: 310, stock: 50, categoryId: 101, images: [], description: '双层牛肉芝士', status: 1,
+ attrs: [
+ { name: '口味', type: 'select', options: ['原味', '香辣', '芥末'], maxSelect: 1, required: true }
+ ]
+ },
+ { id: 3, shopId: 1, name: '薯条(大)', price: 12, originalPrice: 15, sales: 420, stock: 100, categoryId: 102, images: [], status: 1,
+ attrs: [ { name: '份量', type: 'select', options: ['大份', '中份', '小份'], maxSelect: 1 } ]
+ },
+ { id: 4, shopId: 2, name: '珍珠奶茶', price: 15, originalPrice: 18, sales: 620, stock: 200, categoryId: 201, images: [], status: 1,
+ attrs: [
+ { name: '甜度', type: 'multiselect', options: ['少糖', '半糖', '正常糖'], maxSelect: 1, required: true },
+ { name: '温度', type: 'select', options: ['冰', '常温', '热'], maxSelect: 1, required: true },
+ { name: '加料数量', type: 'number', min: 0, max: 5, step: 1, default: 0 }
+ ]
+ },
+ { id: 5, shopId: 3, name: '康师傅红烧牛肉面', price: 6, originalPrice: 8, sales: 1500, stock: 500, categoryId: 301, images: [], status: 1, attrs: [] },
+ { id: 6, shopId: 3, name: '可口可乐(500ml)', price: 3.5, originalPrice: 4, sales: 2200, stock: 1000, categoryId: 302, images: [], status: 1, attrs: [] },
+ { id: 7, shopId: 4, name: '晨光中性笔(12支)', price: 18, originalPrice: 24, sales: 120, stock: 80, categoryId: 401, images: [], status: 1,
+ attrs: [
+ { name: '颜色', type: 'multiselect', options: ['黑', '蓝', '红', '绿', '紫'], maxSelect: 3, required: true }
+ ]
+ },
+ { id: 8, shopId: 5, name: '快剪洗剪吹', price: 25, originalPrice: 30, sales: 80, stock: -1, categoryId: 501, images: [], status: 1,
+ attrs: [
+ { name: '服务类型', type: 'select', options: ['快剪', '洗剪吹', '造型'], maxSelect: 1, required: true },
+ { name: '服务时长', type: 'number', min: 15, max: 60, step: 5, default: 30 }
+ ]
+ }
+ ],
+
+ orders: [],
+ orderSeq: 1000,
+
+ communities: [
+ { id: 1, name: '校园篮球社', category: '运动', members: 328, posts: 1280, logo: '', description: '篮球爱好者聚集地', status: 1, tags: ['运动', '篮球'] },
+ { id: 2, name: '编程与算法', category: '科技', members: 512, posts: 2100, logo: '', description: '代码改变世界', status: 1, tags: ['科技', '编程'] },
+ { id: 3, name: '摄影爱好社', category: '艺术', members: 156, posts: 680, logo: '', description: '用镜头记录美好', status: 1, tags: ['艺术', '摄影'] },
+ { id: 4, name: '校园吉他社', category: '艺术', members: 208, posts: 450, logo: '', description: '音乐无国界', status: 1, tags: ['音乐', '吉他'] }
+ ],
+
+ communityPosts: [
+ { id: 1, communityId: 1, userId: 10001, author: '小明', avatar: '', title: '今晚8点篮球场5v5', content: '有兴趣的同学在下面回帖,优先照顾社内成员。', likes: 25, comments: 18, views: 200, isEssence: true, createdAt: Date.now() - 3600000 },
+ { id: 2, communityId: 2, userId: 10002, author: '代码王', avatar: '', title: '本周算法分享会 - 动态规划', content: '欢迎参加,分享一些经典 DP 题目', likes: 88, comments: 42, views: 520, isEssence: true, createdAt: Date.now() - 7200000 },
+ { id: 3, communityId: 3, userId: 10003, author: '摄影师A', avatar: '', title: '春天校园风景作品分享', content: '校园樱花季到了,欢迎同学们一起参加外拍', likes: 55, comments: 20, views: 320, isEssence: false, createdAt: Date.now() - 86400000 }
+ ],
+
+ activities: [
+ { id: 1, communityId: 1, title: '新生杯篮球赛', location: '校园体育馆', address: '校园东门', latitude: 39.908823, longitude: 116.397470, startTime: Date.now() + 86400000, endTime: Date.now() + 86400000 * 2, signupCount: 42, capacity: 100, fee: 0, description: '面向新生的篮球比赛活动' },
+ { id: 2, communityId: 2, title: 'Hackathon编程马拉松', location: '创新大楼301', address: '创新大楼', latitude: 39.910, longitude: 116.400, startTime: Date.now() + 86400000 * 3, endTime: Date.now() + 86400000 * 3 + 86400000, signupCount: 88, capacity: 120, fee: 0, description: '48小时开发挑战' }
+ ],
+
+ forumBoards: [
+ { id: 1, name: '校园生活', icon: '', desc: '校园点滴分享', posts: 5200 },
+ { id: 2, name: '学习交流', icon: '', desc: '学习资料、经验', posts: 3100 },
+ { id: 3, name: '失物招领', icon: '', desc: '失物、招领信息', posts: 890 },
+ { id: 4, name: '求职招聘', icon: '', desc: '实习、校招', posts: 420 }
+ ],
+
+ forumPosts: [
+ { id: 1, boardId: 1, author: '同学A', userId: 10001, avatar: '', title: '图书馆新增自习区域开放啦', content: '今天发现3楼新开放了一片自习区,安静明亮,推荐给需要备考的同学。', likes: 120, comments: 35, views: 1200, isTop: true, isHot: true, isEssence: true, createdAt: Date.now() - 3600000 },
+ { id: 2, boardId: 2, author: '学霸君', userId: 10002, avatar: '', title: '分享一份考研数学复习笔记', content: '整理了近3年的真题分析,需要的同学自取', likes: 280, comments: 60, views: 3200, isTop: false, isHot: true, isEssence: true, createdAt: Date.now() - 7200000,
+ vote: { title: '你最需要的科目?', options: [{id:1,text:'数学',count:88},{id:2,text:'英语',count:60},{id:3,text:'政治',count:30}] } },
+ { id: 3, boardId: 3, author: '好心人', userId: 10003, avatar: '', title: '在食堂捡到一张校园卡', content: '姓名: 王同学,有认识的请联系', likes: 5, comments: 2, views: 80, isTop: false, isHot: false, isEssence: false, createdAt: Date.now() - 86400000 },
+ { id: 4, boardId: 1, author: '吃货', userId: 10004, avatar: '', title: '盘点校园十大好吃的', content: '1. 东门汉堡...', likes: 95, comments: 48, views: 800, isTop: false, isHot: true, isEssence: false, createdAt: Date.now() - 86400000 * 2 }
+ ],
+
+ forumReplies: [
+ { id: 1, postId: 1, author: '用户B', content: '太好了!正好需要', likes: 3, createdAt: Date.now() - 2000000 },
+ { id: 2, postId: 1, author: '用户C', content: '感谢分享', likes: 1, createdAt: Date.now() - 1000000 }
+ ],
+
+ confessions: [
+ { id: 1, content: '图书馆三楼靠窗第三排的女生,每天都能看到你专注的样子,你笑起来真的好好看,可以认识一下吗?', images: [], likes: 128, comments: 32, isAnonymous: true, createdAt: Date.now() - 3600000, author: '匿名' },
+ { id: 2, content: '计算机学院的学长,上次帮我修电脑的那个,你说"下次有问题再找我",我想...我可能又有问题了 :)', images: [], likes: 256, comments: 48, isAnonymous: true, createdAt: Date.now() - 7200000, author: '匿名' },
+ { id: 3, content: '致篮球场上12号球衣的男生:你的后仰跳投真的很帅!', images: [], likes: 88, comments: 12, isAnonymous: true, createdAt: Date.now() - 86400000, author: '匿名' }
+ ],
+
+ marketGoods: [
+ { id: 1, title: '九成新 MacBook Pro 2023', price: 6800, originalPrice: 14999, category: '数码', images: [], description: '自用一年,完好无磕碰,配件齐全', seller: '毕业学长', sellerId: 10005, views: 320, likes: 18, createdAt: Date.now() - 86400000, status: 1 },
+ { id: 2, title: '高数教材 + 习题册一套', price: 30, originalPrice: 120, category: '书籍', images: [], description: '上学期用过的,笔记清晰', seller: '学姐', sellerId: 10006, views: 120, likes: 5, createdAt: Date.now() - 172800000, status: 1 },
+ { id: 3, title: 'Nike 运动鞋 42码', price: 280, originalPrice: 799, category: '服饰', images: [], description: '穿过几次,尺码不合适', seller: '同学D', sellerId: 10007, views: 80, likes: 3, createdAt: Date.now() - 259200000, status: 1 },
+ { id: 4, title: '台灯 - 可调光护眼', price: 50, originalPrice: 180, category: '生活用品', images: [], description: '毕业处理,宿舍用了一年', seller: '学长A', sellerId: 10008, views: 60, likes: 2, createdAt: Date.now() - 345600000, status: 1 }
+ ],
+
+ errandTasks: [
+ { id: 1, type: '取快递', title: '顺丰快递代取', description: '南门菜鸟驿站,快递号: SF1234567890', pickup: '南门菜鸟驿站', delivery: '3号宿舍楼 501', fee: 5, status: 0, publisher: '用户A', publisherId: 10001, createdAt: Date.now() - 1800000 },
+ { id: 2, type: '代买', title: '帮我买份晚饭', description: '东门麦香汉堡,点"经典牛肉汉堡套餐",口味香辣', pickup: '麦香汉堡', delivery: '图书馆3楼', fee: 8, status: 1, publisher: '用户B', publisherId: 10002, runnerId: 20001, runnerName: '跑腿小哥', runnerPhone: '138****0001', createdAt: Date.now() - 3600000 },
+ { id: 3, type: '其他', title: '文件打印', description: '需要A4黑白打印50张,送到教学楼', pickup: '打印店', delivery: '教学楼B201', fee: 10, status: 2, publisher: '用户C', publisherId: 10003, runnerId: 20002, runnerName: '勤工俭学', createdAt: Date.now() - 86400000 }
+ ],
+
+ coupons: [
+ { id: 1, name: '新用户满20减5', type: '满减', minOrder: 20, discount: 5, validFrom: Date.now(), validTo: Date.now() + 86400000 * 30, total: 1000, used: 200 },
+ { id: 2, name: '周末特惠8折券', type: '折扣', minOrder: 30, discount: 0.8, validFrom: Date.now(), validTo: Date.now() + 86400000 * 7, total: 500, used: 120 },
+ { id: 3, name: '全场通用券', type: '满减', minOrder: 50, discount: 10, validFrom: Date.now(), validTo: Date.now() + 86400000 * 60, total: 2000, used: 800 }
+ ],
+
+ myCoupons: [],
+
+ diyPages: [
+ { id: 1, title: '校园风光', banner: '/images/bg/campus.jpg',
+ components: [
+ { type: 'title', content: '欢迎来到我们的校园' },
+ { type: 'text', content: '这里是充满青春与梦想的地方,在这里每天都有新的故事。' },
+ { type: 'image', src: '/images/bg/campus.jpg', url: '' },
+ { type: 'button', text: '查看论坛', url: '/pages/forum/list/list' },
+ { type: 'goods-list', goodsIds: [1, 2, 3] }
+ ]
+ },
+ { id: 2, title: '新生入学指南', banner: '',
+ components: [
+ { type: 'title', content: '新生入学指南' },
+ { type: 'richtext', content: '
欢迎新同学!以下是入学须知:
- 报到时间:9月1日
- 报到地点:体育馆
- 携带材料:录取通知书、身份证
' },
+ { type: 'button', text: '查看地图', url: '' }
+ ]
+ },
+ { id: 3, title: 'H5活动页', banner: '', isH5: true, h5Url: 'https://example.com/activity' }
+ ]
+}
+
+// 持久化尝试恢复
+try {
+ const saved = storage.get('mock_db')
+ if (saved) {
+ DB = Object.assign(DB, saved)
+ }
+} catch (e) {}
+
+function persist() {
+ try {
+ storage.set('mock_db', DB)
+ } catch (e) {}
+}
+
+function formatTime(ts) {
+ const d = new Date(ts)
+ return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
+}
+
+function pad(n) { return n < 10 ? '0' + n : n }
+
+function paginate(list, page = 1, pageSize = 10) {
+ const start = (page - 1) * pageSize
+ const data = list.slice(start, start + pageSize)
+ return {
+ list: data,
+ total: list.length,
+ page: parseInt(page),
+ pageSize: parseInt(pageSize),
+ hasMore: start + pageSize < list.length
+ }
+}
+
+// 路由处理
+const handlers = {
+ 'GET:/api/user/profile': (params) => {
+ return { id: 10001, nickname: '校园用户', avatar: '/images/default-avatar.png', phone: '138****8888', role: 'user', level: 1, points: 100, balance: 50.5, memberSince: Date.now() - 86400000 * 30 }
+ },
+ 'POST:/api/user/login': (data) => {
+ return { token: 'mock_token_' + Date.now(), info: { id: 10001, nickname: data.nickname || '校园用户', avatar: '/images/default-avatar.png', role: 'user', level: 1, points: 100 } }
+ },
+ 'PUT:/api/user/profile': (data) => data,
+ 'GET:/api/user/level': () => ({ level: 1, levelName: '初级会员', nextLevelPoints: 500, currentPoints: 100, privileges: ['积分购物', '优惠券领取'] }),
+
+ // ========== 商户 ==========
+ 'POST:/api/merchant/apply': (data) => {
+ data.id = Date.now()
+ data.status = 0 // 0 待审核
+ DB.merchants.push(data)
+ persist()
+ return { applyId: data.id, status: 'pending', message: '已提交,等待审核' }
+ },
+ 'GET:/api/merchant/apply/status': () => ({ status: 'approved', message: '审核通过' }),
+ 'GET:/api/merchant/list': (p) => {
+ let list = DB.merchants.filter(m => m.status === 1)
+ if (p.category) list = list.filter(m => m.category === p.category)
+ if (p.keyword) list = list.filter(m => m.name.includes(p.keyword))
+ return paginate(list, p.page, p.pageSize)
+ },
+ 'GET:/api/merchant/detail': (p, id) => {
+ const m = DB.merchants.find(x => x.id == id)
+ if (!m) return null
+ const goods = DB.goods.filter(g => g.shopId == id && g.status === 1)
+ return { ...m, goods }
+ },
+ 'GET:/api/merchant/my-shop': () => DB.merchants[0] || null,
+ 'PUT:/api/merchant/my-shop': (data) => {
+ const m = DB.merchants[0]
+ if (m) Object.assign(m, data)
+ persist()
+ return m
+ },
+ 'GET:/api/merchant/stats': () => ({
+ todayOrders: 25, todaySales: 580.5, weekOrders: 180, weekSales: 4200, totalOrders: 1520, totalSales: 35000,
+ dailyData: [
+ { date: '周一', orders: 32, sales: 720 }, { date: '周二', orders: 28, sales: 640 },
+ { date: '周三', orders: 45, sales: 980 }, { date: '周四', orders: 38, sales: 820 },
+ { date: '周五', orders: 52, sales: 1150 }, { date: '周六', orders: 65, sales: 1420 },
+ { date: '周日', orders: 48, sales: 1080 }
+ ],
+ topGoods: [
+ { name: '经典牛肉汉堡', sales: 280, revenue: 7840 },
+ { name: '珍珠奶茶', sales: 520, revenue: 7800 }
+ ]
+ }),
+
+ // ========== 商品 ==========
+ 'GET:/api/goods/list': (p) => {
+ let list = DB.goods.filter(g => g.status === 1)
+ if (p.shopId) list = list.filter(g => g.shopId == p.shopId)
+ if (p.categoryId) list = list.filter(g => g.categoryId == p.categoryId)
+ if (p.keyword) list = list.filter(g => g.name.includes(p.keyword))
+ return paginate(list, p.page, p.pageSize)
+ },
+ 'GET:/api/goods/detail': (p, id) => DB.goods.find(g => g.id == id),
+ 'POST:/api/goods': (data) => {
+ data.id = Date.now()
+ data.sales = 0
+ data.status = 1
+ if (!data.attrs) data.attrs = []
+ DB.goods.push(data)
+ persist()
+ return data
+ },
+ 'PUT:/api/goods': (data) => {
+ const g = DB.goods.find(x => x.id == data.id)
+ if (g) Object.assign(g, data)
+ persist()
+ return g
+ },
+ 'DELETE:/api/goods': (data, id) => {
+ DB.goods = DB.goods.filter(g => g.id != id)
+ persist()
+ return { ok: true }
+ },
+ 'GET:/api/goods/categories': () => [
+ { id: 1, name: '餐饮', children: [{ id: 101, name: '主食' }, { id: 102, name: '小吃' }, { id: 103, name: '饮品' }] },
+ { id: 2, name: '零售', children: [{ id: 201, name: '日用品' }, { id: 202, name: '零食' }] },
+ { id: 3, name: '服务', children: [{ id: 301, name: '理发' }, { id: 302, name: '打印' }] }
+ ],
+
+ // ========== 订单 ==========
+ 'GET:/api/order/list': (p) => {
+ let list = DB.orders.slice().reverse()
+ if (p.status) list = list.filter(o => o.status == p.status)
+ return paginate(list, p.page, p.pageSize)
+ },
+ 'GET:/api/order/detail': (p, id) => DB.orders.find(o => o.id == id),
+ 'POST:/api/order': (data) => {
+ const order = {
+ id: 'OD' + (++DB.orderSeq),
+ userId: 10001,
+ shopId: data.shopId,
+ shopName: data.shopName,
+ goods: data.goods,
+ totalPrice: data.totalPrice,
+ deliveryFee: data.deliveryFee || 0,
+ couponDiscount: data.couponDiscount || 0,
+ payPrice: data.payPrice,
+ address: data.address,
+ remark: data.remark,
+ status: 0, // 0 待支付 1 已支付/待接单 2 制作中 3 配送中 4 已完成 5 已取消
+ createdAt: Date.now(),
+ payTime: null,
+ completedAt: null,
+ logs: [{ time: Date.now(), status: '订单创建', text: '订单已创建,请尽快支付' }]
+ }
+ DB.orders.push(order)
+ persist()
+ return order
+ },
+ 'POST:/api/order/pay': () => {
+ const order = DB.orders[DB.orders.length - 1]
+ if (order) {
+ order.status = 1
+ order.payTime = Date.now()
+ order.logs.push({ time: Date.now(), status: '已支付', text: '支付成功,等待商家接单' })
+ persist()
+ }
+ return { ok: true, payResult: 'success' }
+ },
+ 'POST:/api/order/cancel': () => {
+ const order = DB.orders[DB.orders.length - 1]
+ if (order) { order.status = 5; order.logs.push({ time: Date.now(), status: '已取消', text: '订单已取消' }); persist() }
+ return { ok: true }
+ },
+ 'POST:/api/order/confirm': () => {
+ const order = DB.orders[DB.orders.length - 1]
+ if (order) {
+ order.status = 4
+ order.completedAt = Date.now()
+ order.logs.push({ time: Date.now(), status: '已完成', text: '订单已完成' })
+ persist()
+ }
+ return { ok: true }
+ },
+ 'POST:/api/order/print': () => {
+ // 通用云打印机 - 模拟
+ return { ok: true, printers: DB.printers || [], printId: 'PR' + Date.now() }
+ },
+ 'GET:/api/merchant/order/list': (p) => {
+ return paginate(DB.orders.slice().reverse(), p.page, p.pageSize)
+ },
+
+ // ========== 跑腿 ==========
+ 'GET:/api/errand/list': (p) => paginate(DB.errandTasks.slice().reverse(), p.page, p.pageSize),
+ 'GET:/api/errand/detail': (p, id) => DB.errandTasks.find(t => t.id == id),
+ 'POST:/api/errand': (data) => {
+ data.id = Date.now()
+ data.status = 0
+ data.publisherId = 10001
+ data.publisher = '用户A'
+ data.createdAt = Date.now()
+ DB.errandTasks.push(data)
+ persist()
+ return data
+ },
+ 'POST:/api/errand/accept': () => ({ ok: true }),
+ 'POST:/api/errand/complete': () => ({ ok: true }),
+ 'GET:/api/errand/location': () => ({ latitude: 39.908823, longitude: 116.397470, timestamp: Date.now() }),
+
+ // ========== 社区 ==========
+ 'GET:/api/community/list': (p) => paginate(DB.communities, p.page, p.pageSize),
+ 'GET:/api/community/detail': (p, id) => DB.communities.find(c => c.id == id),
+ 'POST:/api/community': (data) => { data.id = Date.now(); data.members = 1; data.posts = 0; data.status = 1; DB.communities.push(data); persist(); return data },
+ 'POST:/api/community/join': () => ({ ok: true }),
+ 'GET:/api/community/posts': (p, id) => paginate(DB.communityPosts.filter(cp => cp.communityId == id), p.page, p.pageSize),
+ 'POST:/api/community/post': (data) => { data.id = Date.now(); data.createdAt = Date.now(); data.likes = 0; data.comments = 0; data.views = 0; DB.communityPosts.push(data); persist(); return data },
+ 'GET:/api/community/activities': (p) => paginate(DB.activities, p.page, p.pageSize),
+ 'POST:/api/community/activity/signup': () => ({ ok: true }),
+ 'POST:/api/community/activity/checkin': () => ({ ok: true }),
+
+ // ========== 论坛 ==========
+ 'GET:/api/forum/boards': () => DB.forumBoards,
+ 'GET:/api/forum/list': (p) => {
+ let list = DB.forumPosts.slice().sort((a, b) => (b.isTop ? 1 : 0) - (a.isTop ? 1 : 0) || b.createdAt - a.createdAt)
+ if (p.boardId) list = list.filter(f => f.boardId == p.boardId)
+ if (p.keyword) list = list.filter(f => f.title.includes(p.keyword) || f.content.includes(p.keyword))
+ return paginate(list, p.page, p.pageSize)
+ },
+ 'GET:/api/forum/detail': (p, id) => {
+ const post = DB.forumPosts.find(f => f.id == id)
+ if (post) {
+ post.replies = DB.forumReplies.filter(r => r.postId == id)
+ return post
+ }
+ return null
+ },
+ 'POST:/api/forum': (data) => { data.id = Date.now(); data.createdAt = Date.now(); data.likes = 0; data.comments = 0; data.views = 0; DB.forumPosts.push(data); persist(); return data },
+ 'POST:/api/forum/reply': (data, id) => { const r = { id: Date.now(), postId: id, author: '当前用户', content: data.content, likes: 0, createdAt: Date.now() }; DB.forumReplies.push(r); persist(); return r },
+ 'POST:/api/forum/like': () => ({ ok: true }),
+ 'POST:/api/forum/favorite': () => ({ ok: true }),
+ 'POST:/api/forum/vote': (data, id) => { const post = DB.forumPosts.find(f => f.id == id); if (post && post.vote) { const opt = post.vote.options.find(o => o.id == data.optionId); if (opt) opt.count++; } persist(); return { ok: true } },
+
+ // ========== 表白墙 ==========
+ 'GET:/api/confession/list': (p) => paginate(DB.confessions.slice().sort((a, b) => b.createdAt - a.createdAt), p.page, p.pageSize),
+ 'GET:/api/confession/hot': (p) => paginate(DB.confessions.slice().sort((a, b) => b.likes - a.likes), p.page, p.pageSize),
+ 'POST:/api/confession': (data) => { data.id = Date.now(); data.likes = 0; data.comments = 0; data.createdAt = Date.now(); data.isAnonymous = true; data.author = '匿名'; DB.confessions.push(data); persist(); return data },
+ 'POST:/api/confession/like': () => ({ ok: true }),
+ 'POST:/api/confession/comment': () => ({ ok: true }),
+
+ // ========== 二手市场 ==========
+ 'GET:/api/market/list': (p) => {
+ let list = DB.marketGoods.filter(g => g.status === 1).sort((a, b) => b.createdAt - a.createdAt)
+ if (p.category) list = list.filter(g => g.category === p.category)
+ if (p.keyword) list = list.filter(g => g.title.includes(p.keyword))
+ return paginate(list, p.page, p.pageSize)
+ },
+ 'GET:/api/market/detail': (p, id) => DB.marketGoods.find(g => g.id == id),
+ 'POST:/api/market': (data) => { data.id = Date.now(); data.createdAt = Date.now(); data.views = 0; data.likes = 0; data.status = 1; DB.marketGoods.push(data); persist(); return data },
+ 'GET:/api/market/my-goods': (p) => paginate(DB.marketGoods, p.page, p.pageSize),
+ 'PUT:/api/market/offline': () => ({ ok: true }),
+ 'POST:/api/market/favorite': () => ({ ok: true }),
+ 'GET:/api/market/categories': () => ['数码', '书籍', '服饰', '生活用品', '体育用品', '其他'],
+
+ // ========== 优惠券 ==========
+ 'GET:/api/coupon/list': () => DB.coupons,
+ 'GET:/api/coupon/my': () => DB.myCoupons,
+ 'POST:/api/coupon/receive': (data, id) => {
+ const c = DB.coupons.find(x => x.id == id)
+ if (c) {
+ DB.myCoupons.push({ ...c, receivedAt: Date.now(), id: Date.now() })
+ persist()
+ }
+ return { ok: true }
+ },
+
+ // ========== DIY ==========
+ 'GET:/api/diy/list': () => DB.diyPages,
+ 'GET:/api/diy/detail': (p, id) => DB.diyPages.find(d => d.id == id),
+
+ // ========== 云打印机 ==========
+ 'GET:/api/printer/list': () => DB.printers || [],
+ 'POST:/api/printer': (data) => {
+ if (!DB.printers) DB.printers = []
+ data.id = Date.now()
+ data.status = 'online'
+ DB.printers.push(data)
+ persist()
+ return data
+ },
+ 'POST:/api/printer/print': (data) => {
+ // 模拟通用云打印接口调用
+ return { ok: true, printId: 'PRT' + Date.now(), timestamp: Date.now() }
+ }
+}
+
+module.exports = {
+ handle(url, method, data) {
+ // 提取id模式: /api/xxx/{id}/yyy
+ let key = (method || 'GET').toUpperCase() + ':' + url
+ // 尝试直接匹配
+ if (handlers[key]) return handlers[key](data)
+
+ // 尝试按 id 模式匹配
+ const parts = url.split('/').filter(Boolean)
+ for (let i = 0; i < parts.length; i++) {
+ // 尝试把最后一个部分当 ID
+ const testParts = parts.slice(0, parts.length - i).join('/')
+ const id = parts[parts.length - i - 1]
+ if (i >= 1) {
+ const tryKey = (method || 'GET').toUpperCase() + ':/' + testParts
+ if (handlers[tryKey]) return handlers[tryKey](data, id)
+ }
+ }
+
+ // 尝试更宽松匹配 - 去掉最后一节
+ for (let i = 1; i < parts.length; i++) {
+ const tryUrl = '/' + parts.slice(0, parts.length - i).join('/')
+ const tryKey = (method || 'GET').toUpperCase() + ':' + tryUrl
+ if (handlers[tryKey]) {
+ return handlers[tryKey](data, parts.slice(parts.length - i).join('/'))
+ }
+ }
+
+ console.warn('No mock handler for:', key)
+ return null
+ }
+}
diff --git a/utils/payment.js b/utils/payment.js
new file mode 100644
index 0000000..03f88fe
--- /dev/null
+++ b/utils/payment.js
@@ -0,0 +1,80 @@
+// utils/payment.js - 微信支付封装
+const util = require('./util.js')
+
+/**
+ * 发起微信支付
+ * @param {Object} params { orderId, amount, title }
+ */
+function pay(params) {
+ return new Promise(async (resolve, reject) => {
+ try {
+ // 1. 先调用后端下单接口获取 prepay_id
+ const orderRes = await _request('/api/payment/prepay', {
+ orderId: params.orderId,
+ amount: params.amount,
+ title: params.title || '订单支付'
+ })
+
+ // 2. mock fallback: 直接模拟成功
+ const payParams = orderRes && orderRes.payParams ? orderRes.payParams : _mockPayParams()
+
+ // 3. 调起微信支付
+ wx.requestPayment({
+ timeStamp: payParams.timeStamp,
+ nonceStr: payParams.nonceStr,
+ package: payParams.package,
+ signType: payParams.signType || 'MD5',
+ paySign: payParams.paySign,
+ success: () => resolve({ success: true, orderId: params.orderId }),
+ fail: (err) => {
+ if (err.errMsg === 'requestPayment:fail cancel') {
+ reject(new Error('用户取消'))
+ } else {
+ // 模拟支付
+ wx.showModal({
+ title: '支付',
+ content: '模拟环境,是否继续支付 ¥' + (params.amount / 100).toFixed(2) + '?',
+ success: (res) => {
+ if (res.confirm) resolve({ success: true, orderId: params.orderId })
+ else reject(new Error('用户取消'))
+ }
+ })
+ }
+ }
+ })
+ } catch (e) {
+ reject(e)
+ }
+ })
+}
+
+function _mockPayParams() {
+ return {
+ timeStamp: String(Math.floor(Date.now() / 1000)),
+ nonceStr: _random(16),
+ package: 'prepay_id=wx' + Date.now(),
+ signType: 'MD5',
+ paySign: 'MOCKPAYSIGN' + _random(10)
+ }
+}
+
+function _random(n) {
+ let s = ''
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+ for (let i = 0; i < n; i++) s += chars.charAt(Math.floor(Math.random() * chars.length))
+ return s
+}
+
+function _request(url, data) {
+ return new Promise((resolve) => {
+ wx.request({
+ url: (getApp() && getApp().globalData && getApp().globalData.apiBase || '') + url,
+ method: 'POST',
+ data: data || {},
+ success: (res) => resolve(res.data && res.data.data ? res.data.data : res.data),
+ fail: () => resolve(null)
+ })
+ })
+}
+
+module.exports = { pay }
diff --git a/utils/printer.js b/utils/printer.js
new file mode 100644
index 0000000..e021d37
--- /dev/null
+++ b/utils/printer.js
@@ -0,0 +1,163 @@
+// utils/printer.js - 通用云打印机集成
+// 支持: 飞鹅(Feie) / 易联云(Yilianyun) / 商米(Sunmi) / Xprinter / 通用 HTTP 云打印
+const STORAGE_KEY = 'printers_config'
+
+/**
+ * 构建打印小票文本
+ * @param {Object} order 订单
+ * @returns {string} 小票
+ */
+function buildReceipt(order) {
+ const lines = []
+ lines.push('' + (order.shopName || '店铺') + ' 打印小票')
+ lines.push('订单号: ' + (order.id || '#' + Date.now()))
+ lines.push('时间: ' + new Date().toLocaleString())
+ lines.push('----------------------------')
+ lines.push('收货信息:')
+ if (order.address) {
+ lines.push((order.address.name || '') + ' ' + (order.address.phone || ''))
+ lines.push(order.address.address || '')
+ }
+ lines.push('----------------------------')
+ lines.push('商品列表:')
+ let total = 0
+ ;(order.goods || []).forEach(g => {
+ const price = Number(g.price || 0)
+ const count = Number(g.count || 1)
+ total += price * count
+ lines.push(g.name + ' x' + count + ' ¥' + price.toFixed(2))
+ if (g.spec) lines.push(' (' + g.spec + ')')
+ })
+ lines.push('----------------------------')
+ lines.push('配送费: ¥' + (order.deliveryFee || 0).toFixed(2))
+ lines.push('合计: ¥' + (Number(order.payPrice || total + (order.deliveryFee || 0))).toFixed(2) + '')
+ if (order.remark) lines.push('备注: ' + order.remark)
+ lines.push('https://example.com/order/' + (order.id || '') + '')
+ lines.push('')
+ return lines.join('\n')
+}
+
+function addPrinter(printer) {
+ const list = getPrinters()
+ printer.id = Date.now()
+ printer.status = 'online'
+ list.push(printer)
+ try { wx.setStorageSync(STORAGE_KEY, list) } catch (e) {}
+ return printer
+}
+
+function getPrinters() {
+ try { return wx.getStorageSync(STORAGE_KEY) || [] } catch (e) { return [] }
+}
+
+function removePrinter(id) {
+ const list = getPrinters().filter(p => p.id != id)
+ try { wx.setStorageSync(STORAGE_KEY, list) } catch (e) {}
+}
+
+function print(printerId, content) {
+ const list = getPrinters()
+ const p = list.find(x => x.id == printerId)
+ if (!p) return Promise.reject(new Error('打印机不存在'))
+ switch (p.vendor) {
+ case 'feie': return _callFeie(p, content)
+ case 'yilianyun': return _callYilianyun(p, content)
+ case 'sunmi': return _callSunmi(p, content)
+ case 'xprinter': return _callXprinter(p, content)
+ default: return _callGeneric(p, content)
+ }
+}
+
+function _callFeie(printer, content) {
+ return new Promise((resolve, reject) => {
+ wx.request({
+ url: printer.apiUrl || 'https://api.feieyun.cn/Api/Open/',
+ method: 'POST',
+ data: {
+ user: printer.user,
+ stime: Math.floor(Date.now() / 1000),
+ sig: printer.sig,
+ apiname: 'Open_printMsg',
+ sn: printer.sn,
+ content: content,
+ times: 1
+ },
+ success: (res) => resolve(res.data),
+ fail: reject
+ })
+ })
+}
+
+function _callYilianyun(printer, content) {
+ return new Promise((resolve, reject) => {
+ wx.request({
+ url: printer.apiUrl || 'https://open-api.10ss.net/v2/print/index',
+ method: 'POST',
+ data: { machine_code: printer.sn, content: content, msign: printer.msign || '', time: Math.floor(Date.now() / 1000) },
+ success: (res) => resolve(res.data),
+ fail: reject
+ })
+ })
+}
+
+function _callSunmi(printer, content) {
+ return new Promise((resolve, reject) => {
+ wx.request({
+ url: printer.apiUrl || 'https://api.sunmi.com/v1/printer/print',
+ method: 'POST',
+ header: { 'Authorization': 'Bearer ' + (printer.token || '') },
+ data: { sn: printer.sn, content: content },
+ success: (res) => resolve(res.data),
+ fail: reject
+ })
+ })
+}
+
+function _callXprinter(printer, content) {
+ return new Promise((resolve, reject) => {
+ wx.request({
+ url: printer.apiUrl || 'https://api.xprinter.cn/print',
+ method: 'POST',
+ data: { sn: printer.sn, content: content, key: printer.key },
+ success: (res) => resolve(res.data),
+ fail: reject
+ })
+ })
+}
+
+function _callGeneric(printer, content) {
+ return new Promise((resolve) => {
+ if (!printer.apiUrl) {
+ console.log('[模拟打印]', content)
+ resolve({ ok: true, msg: '本地模拟打印成功,请配置 API URL 后正式使用' })
+ return
+ }
+ wx.request({
+ url: printer.apiUrl,
+ method: 'POST',
+ data: { sn: printer.sn, content: content, token: printer.token },
+ success: (res) => resolve(res.data),
+ fail: (err) => {
+ console.log('[打印失败-模拟]', content)
+ resolve({ ok: true, msg: 'API不可达,已转为本地模拟' })
+ }
+ })
+ })
+}
+
+const VENDORS = [
+ { id: 'feie', name: '飞鹅云', fields: ['user', 'ukey', 'sn', 'sig'], url: 'http://api.feieyun.cn/' },
+ { id: 'yilianyun', name: '易联云', fields: ['client_id', 'client_secret', 'sn', 'msign'], url: 'https://dev.yilianyun.cn/' },
+ { id: 'sunmi', name: '商米云', fields: ['token', 'sn'], url: 'https://developer.sunmi.com/' },
+ { id: 'xprinter', name: '芯烨Xprinter', fields: ['key', 'sn'], url: 'https://api.xprinter.cn/' },
+ { id: 'generic', name: '自定义HTTP云打印', fields: ['apiUrl', 'token', 'sn'], url: '' }
+]
+
+module.exports = {
+ buildReceipt,
+ addPrinter,
+ getPrinters,
+ removePrinter,
+ print,
+ VENDORS
+}
diff --git a/utils/request.js b/utils/request.js
new file mode 100644
index 0000000..94afbc6
--- /dev/null
+++ b/utils/request.js
@@ -0,0 +1,90 @@
+// utils/request.js - 网络请求封装
+const app = () => getApp()
+
+class Request {
+ constructor() {
+ this.baseURL = ''
+ this.timeout = 15000
+ }
+
+ request(options) {
+ const appInstance = app()
+ return new Promise((resolve, reject) => {
+ // 模拟模式:直接使用mock数据
+ if (!options.url || options.mock !== false) {
+ // 当未配置真实后端时使用 mock
+ this._mockHandle(options).then(resolve).catch(reject)
+ return
+ }
+
+ wx.request({
+ url: (options.baseURL || this.baseURL) + options.url,
+ method: options.method || 'GET',
+ data: options.data || {},
+ header: Object.assign(
+ {
+ 'Content-Type': 'application/json',
+ Authorization: appInstance && appInstance.globalData.token ?
+ 'Bearer ' + appInstance.globalData.token : ''
+ },
+ options.header || {}
+ ),
+ timeout: this.timeout,
+ success: (res) => {
+ if (res.statusCode === 200) {
+ if (res.data.code === 0 || res.data.code === 200) {
+ resolve(res.data.data || res.data)
+ } else {
+ wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' })
+ reject(res.data)
+ }
+ } else if (res.statusCode === 401) {
+ appInstance && appInstance.logout()
+ wx.navigateTo({ url: '/pages/user/login/login' })
+ reject(res)
+ } else {
+ reject(res)
+ }
+ },
+ fail: reject
+ })
+ })
+ }
+
+ // Mock 处理器
+ _mockHandle(options) {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ const mock = require('./mock.js')
+ const result = mock.handle(options.url, options.method, options.data)
+ resolve(result)
+ }, 300)
+ }
+
+ get(url, data, options = {}) {
+ return this.request({ url, method: 'GET', data, ...options })
+ }
+
+ post(url, data, options = {}) {
+ return this.request({ url, method: 'POST', data, ...options })
+ }
+
+ put(url, data, options = {}) {
+ return this.request({ url, method: 'PUT', data, ...options })
+ }
+
+ delete(url, data, options = {}) {
+ return this.request({ url, method: 'DELETE', data, ...options })
+ }
+
+ upload(filePath, name = 'file') {
+ return new Promise((resolve, reject) => {
+ // 模拟上传
+ setTimeout(() => {
+ resolve({ url: 'https://img.cdn.example.com/upload/' + Date.now() + '.jpg' })
+ }, 500)
+ })
+ }
+}
+
+module.exports = new Request()
diff --git a/utils/storage.js b/utils/storage.js
new file mode 100644
index 0000000..70a5d9c
--- /dev/null
+++ b/utils/storage.js
@@ -0,0 +1,31 @@
+// utils/storage.js - 本地存储封装
+module.exports = {
+ set(key, value) {
+ try {
+ wx.setStorageSync(key, value)
+ } catch (e) {
+ console.error('storage.set error:', e)
+ }
+ },
+
+ get(key, defaultValue = null) {
+ try {
+ const v = wx.getStorageSync(key)
+ return v !== '' ? v : defaultValue
+ } catch (e) {
+ return defaultValue
+ }
+ },
+
+ remove(key) {
+ try {
+ wx.removeStorageSync(key)
+ } catch (e) {}
+ },
+
+ clear() {
+ try {
+ wx.clearStorageSync()
+ } catch (e) {}
+ }
+}
diff --git a/utils/util.js b/utils/util.js
new file mode 100644
index 0000000..57f7993
--- /dev/null
+++ b/utils/util.js
@@ -0,0 +1,139 @@
+// utils/util.js - 通用工具
+function formatTime(date) {
+ if (typeof date === 'number') date = new Date(date)
+ const year = date.getFullYear()
+ const month = date.getMonth() + 1
+ const day = date.getDate()
+ const hour = date.getHours()
+ const minute = date.getMinutes()
+ return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}`
+}
+
+function formatDate(date) {
+ if (typeof date === 'number') date = new Date(date)
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
+}
+
+function pad(n) { return n < 10 ? '0' + n : n }
+
+function formatPrice(price) {
+ return '¥' + parseFloat(price).toFixed(2)
+}
+
+function formatCount(num) {
+ if (num >= 10000) return (num / 10000).toFixed(1) + 'w'
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'k'
+ return num
+}
+
+function timeAgo(ts) {
+ const diff = Date.now() - ts
+ if (diff < 60 * 1000) return '刚刚'
+ if (diff < 3600 * 1000) return Math.floor(diff / 60000) + '分钟前'
+ if (diff < 86400 * 1000) return Math.floor(diff / 3600000) + '小时前'
+ if (diff < 86400 * 1000 * 7) return Math.floor(diff / 86400000) + '天前'
+ return formatDate(ts)
+}
+
+function showToast(title, icon = 'none') {
+ wx.showToast({ title, icon, duration: 2000 })
+}
+
+function showLoading(title = '加载中...') {
+ wx.showLoading({ title, mask: true })
+}
+
+function hideLoading() {
+ wx.hideLoading()
+}
+
+function confirm(title, content) {
+ return new Promise((resolve) => {
+ wx.showModal({
+ title, content,
+ confirmText: '确定',
+ cancelText: '取消',
+ success: (res) => resolve(res.confirm)
+ })
+ })
+}
+
+function navigateTo(url) {
+ wx.navigateTo({ url })
+}
+
+function switchTab(url) {
+ wx.switchTab({ url })
+}
+
+function setTabBarBadge(index, text) {
+ if (text) {
+ wx.setTabBarBadge({ index, text: String(text) }).catch(() => {})
+ } else {
+ wx.removeTabBarBadge({ index }).catch(() => {})
+ }
+}
+
+// 计算购物车总价
+function calcCartTotal(cart) {
+ let total = 0
+ Object.values(cart).forEach(shop => {
+ shop.goods.forEach(g => { total += g.price * g.count })
+ })
+ return total
+}
+
+// 计算购物车数量
+function calcCartCount(cart) {
+ let count = 0
+ Object.values(cart).forEach(shop => {
+ shop.goods.forEach(g => { count += g.count })
+ })
+ return count
+}
+
+// 安全解析 JSON
+function safeParse(str, defaultValue) {
+ try { return JSON.parse(str) } catch(e) { return defaultValue }
+}
+
+// 生成随机字符串
+function randomStr(len = 16) {
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+ let s = ''
+ for (let i = 0; i < len; i++) s += chars.charAt(Math.floor(Math.random() * chars.length))
+ return s
+}
+
+// 防抖
+function debounce(fn, wait = 300) {
+ let timer
+ return function(...args) {
+ if (timer) clearTimeout(timer)
+ timer = setTimeout(() => fn.apply(this, args), wait)
+ }
+}
+
+// 节流
+function throttle(fn, wait = 300) {
+ let last = 0
+ return function(...args) {
+ const now = Date.now()
+ if (now - last >= wait) {
+ last = now
+ fn.apply(this, args)
+ }
+ }
+}
+
+// 深拷贝
+function deepClone(obj) {
+ return JSON.parse(JSON.stringify(obj))
+}
+
+module.exports = {
+ formatTime, formatDate, formatPrice, formatCount, timeAgo,
+ showToast, showLoading, hideLoading, confirm, navigateTo, switchTab,
+ calcCartTotal, calcCartCount, safeParse, randomStr,
+ debounce, throttle, deepClone
+}