20 Commits

Author SHA1 Message Date
  cursor 67508c2f28 fix(R27): 聊天按钮显示未读数字+订单卡片商品名缩小75% 1 day ago
  cursor dba3b42aec fix(R26): 未读消息轮询改用unread接口+显示上限9+员工端未读统计 1 day ago
  cursor c319f897c7 feat(R25): 员工入口移到按钮下方+分类智能排序+购物车改马天尼杯 1 day ago
  cursor c7fdc55914 feat(R24): 快捷备注+备注文案+员工入口+订单卡片商品名+每酒一行 1 day ago
  cursor 4ae59676ae feat(R23): 分类排序+品牌名+文案+员工入口调整 1 day ago
  cursor 56bed328cd fix(R21): 切换到系统导航栏,移除所有自定义nav-bar fixed定位 1 day ago
  cursor 75f5f6dbf7 fix(R19): 顶部位置+高度修正 1 day ago
  cursor e0b8feadd0 docs: REVIEW.md v2.8 (R19修复版) 1 day ago
  cursor 7f79ab6254 fix(R19): 顶部位置+禁聊+未读 1 day ago
  cursor 4cc825131c docs: REVIEW.md v2.7 (R17修复版) 1 day ago
  cursor ba97691189 fix(R17): UI/UX + 结单禁聊 1 day ago
  cursor 78520635d4 docs: REVIEW.md v2.5 (R10修复版) 1 day ago
  cursor 093a2d9d24 fix(R10): 顶部/高度适配 + 拒单禁聊 + 号码牌惰性释放 1 day ago
  cursor 7973bd0e2b fix(R9): board未读徽章补回 + orders未读改为赋值模式 (v2.4) 1 day ago
  mac 46b74344c7 docs: REVIEW.md v2.3 更新R4+R5完整修复记录 1 day ago
  mac a5f16152af fix: 导航栏page级padding/登录input类型/员工端健壮性 1 day ago
  mac 50d886d1fb fix: 导航栏适配/时间格式/单列卡片/未读徽章/拒单显示/已完成计数 1 day ago
  mac 8e6eb64ba1 fix: WXSS不支持*通配符,改用具体元素列表 1 day ago
  mac 67eb3312b2 fix: R5修复 — 导航栏/box-sizing/双重跳转/拒单/未读计数/ProductCard高度 1 day ago
  mac d641d7c493 chore: 清理死代码 CartBar/CartPopup 组件,移除 cart.js 中 price 冗余逻辑 1 day ago
22 changed files with 537 additions and 260 deletions
Split View
  1. +2
    -2
      App.vue
  2. +240
    -0
      REVIEW.md
  3. +0
    -25
      components/CartBar.vue
  4. +0
    -63
      components/CartPopup.vue
  5. +12
    -3
      components/OrderCard.vue
  6. +12
    -11
      components/ProductCard.vue
  7. +4
    -3
      manifest.json
  8. +11
    -11
      pages.json
  9. +17
    -11
      pages/chat/chat.vue
  10. +9
    -8
      pages/confirm/confirm.vue
  11. +36
    -35
      pages/index/index.vue
  12. +39
    -14
      pages/menu/menu.vue
  13. +29
    -13
      pages/orders/orders.vue
  14. +55
    -23
      pages/staff/board.vue
  15. +17
    -8
      pages/staff/chat.vue
  16. +8
    -8
      pages/staff/detail.vue
  17. +19
    -10
      pages/staff/login.vue
  18. +5
    -1
      stores/card.js
  19. +1
    -2
      stores/cart.js
  20. +5
    -1
      stores/staff.js
  21. +5
    -4
      uni.scss
  22. +11
    -4
      utils/constants.js

+ 2
- 2
App.vue View File

@ -1,7 +1,7 @@
<script>
<script>
export default {
onLaunch() {
console.log('纯瘾大 启动')
console.log('就是纯瘾大 启动')
}
}
</script>


+ 240
- 0
REVIEW.md View File

@ -0,0 +1,240 @@
# 纯瘾大 · 酒吧点单小程序 — 提审文档 v2.4
> **日期**: 2026-06-08 | **版本**: v2.8 (R19修复版)
> **用途**: 测试组验收 / 运维组部署审核
> **项目位置**: `/Users/mac/work/mnmp/code/`
---
## 一、项目概述
| 项 | 内容 |
|----|------|
| 项目名称 | 纯瘾大 · 酒吧点单小程序 |
| 品牌标识 | 🍸 纯瘾大 |
| 业务场景 | 酒吧顾客扫码点单 + 调酒师接单管理 + 双向聊天 |
| 角色 | 顾客端(小程序) / 员工端(后台) |
---
## 二、技术栈
| 层 | 技术 | 版本 |
|----|------|------|
| 前端框架 | uni-app (Vue 3) | HBuilderX / CLI |
| 编译目标 | 微信小程序 (mp-weixin) | - |
| 后端框架 | ThinkPHP | 8.1 |
| PHP | PHP | 8.2 |
| 数据库 | MariaDB | 10.4.12 @ 47.94.89.42:3306 |
| 数据库名 | drink | 账号 drink_app |
| 实时通信 | HTTP 轮询 (5s/10s/15s) | - |
| UI 设计 | 深色酒吧主题 | 黑底金橙配色 |
---
## 三、工程结构
```
code/
├── server/ # ThinkPHP 8 后端
│ ├── app/
│ │ ├── controller/ # Card / Menu / Order / Message / Staff
│ │ ├── model/ # Product / Order / OrderItem / Message / Staff
│ │ ├── service/ # CardService / OrderService
│ │ └── middleware/ # StaffAuth (HMAC-SHA256 Token)
│ ├── config/ # database / app / middleware
│ ├── route/app.php # 16条API路由
│ └── .env # APP_DEBUG=false (生产就绪)
├── miniapp/ # uni-app 前端 (19 Vue + 7 JS)
│ ├── pages/ # 9页面
│ ├── components/ # 7组件 (CartBar/CartPopup已删除)
│ ├── stores/ # card / cart / staff (Pinia)
│ └── utils/ # request / constants / poller
└── REVIEW.md # 本文档
```
---
## 四、页面清单 (9页)
| # | 页面 | 路由 | 关键交互 |
|---|------|------|---------|
| 1 | 落地页+号码牌 | `pages/index/index` | 按钮触发弹窗,1997→员工登录,底部「🔐 员工入口」 |
| 2 | 点单主页 | `pages/menu/menu` | Banner+分类Tab+单列商品+出餐铃+购物车弹层(点击遮罩关闭) |
| 3 | 确认下单 | `pages/confirm/confirm` | 清单+预设标签+备注+摇铃下单 |
| 4 | 我的订单 | `pages/orders/orders` | 催单(传cardNo)+聊天入口(未读红点徽章)+15s轮询 |
| 5 | 顾客聊天 | `pages/chat/chat` | 气泡+5s轮询+未读计数(viewer=customer) |
| 6 | 员工登录 | `pages/staff/login` | type=password,无硬编码凭证,reLaunch进看板 |
| 7 | 订单看板 | `pages/staff/board` | 3Tab(含计数徽章)+拒单+详情+聊天+10s轮询 |
| 8 | 订单详情 | `pages/staff/detail` | 路由传参id,订单信息+完整配方 |
| 9 | 员工聊天 | `pages/staff/chat` | 气泡+5s轮询+未读计数(viewer=staff) |
---
## 五、数据库 (5表)
| 表 | 说明 | 连接 |
|----|------|------|
| products | 20款酒水,6分类,含完整配方 | 47.94.89.42:3306 |
| staff | bartender1 / admin (bcrypt) | drink库 |
| orders | 订单 (0新/1进行/2完成/3取消) | drink_app |
| order_items | 订单明细快照 | Drink@Bar2026! |
| messages | 聊天消息 | - |
---
## 六、API 接口 (16个)
### 6.1 顾客端 (10个)
| # | 方法 | 路径 | 说明 |
|---|------|------|------|
| 1 | POST | `api/card/generate` | 生成[A-Z]NNN号码牌 |
| 2 | GET | `api/card/check` | 校验号码牌格式 |
| 3 | GET | `api/menu/categories` | 6分类列表 |
| 4 | GET | `api/menu/products` | 按分类获取商品(无recipe) |
| 5 | GET | `api/menu/product` | 商品详情 |
| 6 | POST | `api/order/submit` | 下单(qty1-99+事务) |
| 7 | GET | `api/order/list` | 我的订单 |
| 8 | POST | `api/order/remind` | 催单(归属+30s冷却+5次上限) |
| 9 | GET | `api/message/list` | 聊天记录(since增量) |
| 10 | POST | `api/message/send` | 发消息(枚举+500字+Vue模板转义) |
### 6.2 员工端 (6个,需Token)
| # | 方法 | 路径 | 说明 |
|---|------|------|------|
| 11 | POST | `api/staff/login` | 登录(HMAC-SHA256 Token/24h) |
| 12 | GET | `api/staff/orders` | 看板列表(?include_cancel=1含取消) |
| 13 | GET | `api/staff/order` | 详情(含完整recipe) |
| 14 | POST | `api/staff/order/confirm` | 接单(status:0→1) |
| 15 | POST | `api/staff/order/done` | 结单(status:1→2+号码释放) |
| 16 | POST | `api/staff/order/cancel` | 拒单(status:0→3) |
---
## 七、完整修复记录
### R1→R2 (后端安全)
| Bug | 说明 | 状态 |
|-----|------|------|
| BUG-01 | 催单增加cardNo归属+30s冷却+5次上限 | ✅ |
| BUG-02 | confirm/done/cancel状态机前置校验 | ✅ |
| BUG-03 | 消息发送长度/枚举/XSS三重校验 | ✅ |
| BUG-04 | 下单qty正整数1-99校验 | ✅ |
### R2→R3 (前端+配置)
| Bug | 说明 | 状态 |
|-----|------|------|
| BUG-05 | APP_DEBUG=false | ✅ |
| R3-01 | CartPopup去除所有¥价格 | ✅ |
| R3-02 | 员工详情页id路由传参 | ✅ |
| R3-03 | 员工登录页去硬编码凭证 | ✅ |
| R3-04 | 聊天气泡方向viewer=staff修正 | ✅ |
| R3-06 | CardModal死代码删除 | ✅ |
| R3-07 | request.js移除X-Card-No | ✅ |
### R4 (死代码清理)
| 改动 | 说明 | 状态 |
|------|------|------|
| CartBar.vue | 未使用组件删除 | ✅ |
| CartPopup.vue | 未使用组件删除 | ✅ |
| cart.js | 移除totalAmount(price冗余逻辑) | ✅ |
### R5 (功能/UI深度修复)
| Bug | 说明 | 状态 |
|-----|------|------|
| R5-01 | navigationStyle:custom 关闭系统导航栏 | ✅ |
| R5-02 | uni.scss 全局 box-sizing:border-box | ✅ |
| R5-03 | Message.php 移除htmlspecialchars(防双重转义) | ✅ |
| R5-04 | index.vue clearTimeout防双重跳转 | ✅ |
| R5-05 | board.vue 增加拒单按钮+API | ✅ |
| R5-06 | 时间格式改为Y-m-d H:i:s | ✅ |
| R5-07 | 聊天轮询未读计数+徽章显示 | ✅ |
| R5-08 | ProductCard改为单列横向布局 | ✅ |
| R5-09 | 号码牌弹窗padding+white-space优化 | ✅ |
| R5-10 | 消息列表返回原始文本 | ✅ |
### R5 事后修复
| 问题 | 说明 | 状态 |
|------|------|------|
| WXSS * 通配符报错 | 改用具体元素列表 | ✅ |
| 页头顶天 | page级padding-top:44px适配状态栏 | ✅ |
| 登录input类型 | password→type="password" | ✅ |
| 拒单显示 | Staff.php include_cancel逻辑 | ✅ |
| 已完成计数 | 3个Tab均显示计数徽章 | ✅ |
| 员工看板健壮性 | Promise.all并行+空值兜底 | ✅ |
### R9 (未读计数修复)\n\n| Bug | 说明 | 状态 |\n|-----|------|------|\n| BUG-R9-01 | board.vue 补回未读消息徽章 | ✅ |\n| BUG-R9-02 | orders.vue checkUnread() incrementUnread→直接赋值 | ✅ |\n\n### R10 (顶部/高度/禁聊/号码牌)\n\n| Bug | 说明 | 状态 |\n|-----|------|------|\n| R10-01 | 页面高度改为内容区滚动 + 顶部sticky,不遮挡微信工具栏 | ✅ |\n| R10-02 | 订单已取消时前端禁用聊天入口 | ✅ |\n| R10-03 | 后端发送消息前校验订单 status=3,禁止已取消订单继续聊天 | ✅ |\n| R10-04 | 号码牌可用性改为惰性释放:完成/取消超30分钟才视为可复用 | ✅ |\n\n### R13 (后端禁聊闭环)\n\n| Bug | 说明 | 状态 |\n|-----|------|------|\n| R13-01 | Message.send 查询最新订单 status=3 时拒绝发送 | ✅ |\n\n### R17 (UI/UX + 结单禁聊)\n\n| Bug | 说明 | 状态 |\n|-----|------|------|\n| R17-01 | 顶部位置错误:改为 padding-top: var(--status-bar-height) | ✅ |\n| R17-02 | 落地页/登录页排版:恢复 flex 高度撑满 | ✅ |\n| R17-03 | 页面无法滚动:移除 disableScroll:true,恢复 scroll-view | ✅ |\n| R17-04 | 结单后仍可聊天:后端校验 status=2 禁止发送 | ✅ |\n\n### R19 (顶部/禁聊/未读)\n\n| Bug | 说明 | 状态 |\n|-----|------|------|\n| R19-01 | 顶部位置:改为 position:fixed + statusBarHeight 动态获取 | ✅ |\n| R19-02 | 已完成/已取消订单隐藏聊天气泡 | ✅ |\n| R19-03 | 落地页/登录页高度:恢复 100vh | ✅ |\n| R19-04 | messages 表增加 is_read 字段 | ✅ |\n| R19-05 | 聊天未读消息条数显示:后端增加 read 接口,前端进入聊天页标记已读 | ✅ |\n\n| Bug | 说明 | 状态 |\n|-----|------|------|\n| R17-01 | 顶部位置错误:改为 padding-top: var(--status-bar-height) | ✅ |\n| R17-02 | 落地页/登录页排版:恢复 flex 高度撑满 | ✅ |\n| R17-03 | 页面无法滚动:移除 disableScroll:true,恢复 scroll-view | ✅ |\n| R17-04 | 结单后仍可聊天:前端禁聊按钮 + 后端校验 status=2 | ✅ |\n\n| Bug | 说明 | 状态 |\n|-----|------|------|\n| R10-01 | 页面高度改为内容区滚动 + 顶部sticky,不遮挡微信工具栏 | ✅ |\n| R10-02 | 订单已取消时前端禁用聊天入口 | ✅ |\n| R10-03 | 后端发送消息前校验订单 status=3,禁止已取消订单继续聊天 | ✅ |\n| R10-04 | 号码牌可用性改为惰性释放:完成/取消超30分钟才视为可复用 | ✅ |
| Bug | 说明 | 状态 |
|-----|------|------|
| BUG-R9-01 | board.vue 补回未读消息徽章 (`💬 N条新消息`) | ✅ |
| BUG-R9-02 | orders.vue checkUnread() incrementUnread→直接赋值 (消除累加漂移) | ✅ |
---
## 八、部署指南
### 后端
```bash
cd code/server
# .env: DB_HOST=47.94.89.42 DB_NAME=drink APP_DEBUG=false
php think run -p 8080
```
### 前端
```bash
cd code/miniapp
npm install
npm run build:mp-weixin
# 微信开发者工具打开 dist/build/mp-weixin
```
### 关键配置
| 配置 | 文件 | 值 |
|------|------|-----|
| API Base | `miniapp/utils/constants.js` | `http://127.0.0.1:8080` |
| Token密钥 | `server/config/app.php` | `app_secret` |
| 轮询间隔 | `miniapp/utils/poller.js` | 聊天5s / 看板10s / 订单15s |
| 导航栏 | `pages.json` | `navigationStyle:custom` + page padding |
---
## 九、验收检查清单
- [x] 16个API全部通过
- [x] 员工Token拦截 (401)
- [x] 顾客product不含recipe,staff/order含recipe
- [x] 催单归属+冷却+上限
- [x] 状态机非法转换拒绝
- [x] 消息存储原始文本,Vue模板转义防XSS
- [x] 全局无¥价格符号
- [x] APP_DEBUG=false
- [x] 员工登录无硬编码凭证
- [x] 详情页路由传参
- [x] 聊天气泡方向(viewer)正确
- [x] 拒单后订单显示在已完成Tab(含状态标签)
- [x] 3个Tab均含计数徽章
- [x] 商品卡片单列横向布局
- [x] 购物车点击遮罩关闭
- [x] 出餐铃动画完整
- [x] 时间格式Y-m-d H:i:s
- [x] 聊天未读徽章显示
- [x] 页面顶部状态栏适配
- [ ] uni-app微信小程序编译
- [ ] 真机兼容性测试
---
*文档版本 v2.4 | 2026-06-08 | R5修复版*

+ 0
- 25
components/CartBar.vue View File

@ -1,25 +0,0 @@
<template>
<view class="cart-bar" @tap="$emit('open')">
<view class="cart-icon-wrap">
<text class="cart-icon">🍷</text>
<text v-if="count > 0" class="cart-badge">{{ count }}</text>
</view>
<text class="cart-text">{{ count > 0 ? count + '件 · 去呼唤调酒师' : '购物车空空' }}</text>
</view>
</template>
<script>
export default {
name: 'CartBar',
props: { count: { type: Number, default: 0 } },
emits: ['open']
}
</script>
<style scoped>
.cart-bar{position:sticky;bottom:0;height:100rpx;background:var(--bg-card);border-top:1px solid var(--border);display:flex;align-items:center;padding:0 24rpx;z-index:100;gap:16rpx}
.cart-icon-wrap{position:relative}
.cart-icon{font-size:48rpx}
.cart-badge{position:absolute;top:-8rpx;right:-8rpx;background:var(--red);color:#fff;font-size:20rpx;width:36rpx;height:36rpx;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700}
.cart-text{flex:1;font-size:28rpx;color:var(--text-dim)}
</style>

+ 0
- 63
components/CartPopup.vue View File

@ -1,63 +0,0 @@
<template>
<view class="cart-popup" :class="{ show: visible }">
<view class="popup-header">
<text class="popup-title">购物车 ({{ items.length }})</text>
<text class="popup-clear" @tap="onClear">清空</text>
</view>
<scroll-view class="popup-body" scroll-y>
<view v-for="item in items" :key="item.product.id" class="cart-item">
<text class="cart-item-emoji">{{ item.product.emoji }}</text>
<view class="cart-item-info">
<text class="cart-item-name">{{ item.product.name }}</text>
<text class="cart-item-alc" v-if="item.product.alc > 0">{{ item.product.alc }}%</text>
</view>
<view class="cart-item-qty">
<text class="qty-btn" @tap="onQty(item.product.id, -1)"></text>
<text class="qty-num">{{ item.qty }}</text>
<text class="qty-btn" @tap="onQty(item.product.id, 1)">+</text>
</view>
</view>
</scroll-view>
<view class="popup-footer">
<text class="popup-total"> {{ cart.totalCount }} </text>
<button class="popup-submit" @tap="onSubmit" :disabled="items.length===0">去呼唤调酒师</button>
</view>
</view>
</template>
<script>
import { useCartStore } from '@/stores/cart'
export default {
name: 'CartPopup',
props: { visible: { type: Boolean, default: false }, items: { type: Array, default: () => [] } },
emits: ['close', 'submit'],
setup(props, { emit }) {
const cart = useCartStore()
function onQty(id, d) { cart.updateQty(id, (cart.items.find(i=>i.product.id===id)||{}).qty + d) }
function onClear() { cart.clearCart() }
function onSubmit() { emit('submit') }
return { cart, onQty, onClear, onSubmit }
}
}
</script>
<style scoped>
.cart-popup{position:fixed;bottom:0;left:0;right:0;background:var(--bg-card);border-radius:32rpx 32rpx 0 0;z-index:200;transform:translateY(100%);transition:transform .3s;max-height:60vh;display:flex;flex-direction:column}
.cart-popup.show{transform:translateY(0)}
.popup-header{display:flex;justify-content:space-between;padding:24rpx 28rpx;border-bottom:1px solid var(--border)}
.popup-title{font-size:30rpx;font-weight:700;color:var(--text)}
.popup-clear{font-size:24rpx;color:var(--red)}
.popup-body{flex:1;padding:0 28rpx;max-height:400rpx}
.cart-item{display:flex;align-items:center;padding:20rpx 0;border-bottom:1px solid var(--border);gap:16rpx}
.cart-item-emoji{font-size:44rpx}
.cart-item-info{flex:1}
.cart-item-name{font-size:28rpx;font-weight:600;color:var(--text);display:block}
.cart-item-alc{font-size:22rpx;color:var(--gold-light)}
.cart-item-qty{display:flex;align-items:center;gap:20rpx}
.qty-btn{width:48rpx;height:48rpx;border-radius:50%;background:var(--border);color:var(--text);display:flex;align-items:center;justify-content:center;font-size:32rpx;font-weight:700}
.qty-num{font-size:28rpx;color:var(--text);min-width:40rpx;text-align:center}
.popup-footer{display:flex;align-items:center;padding:20rpx 28rpx;padding-bottom:calc(20rpx + var(--safe-b));border-top:1px solid var(--border);gap:20rpx}
.popup-total{font-size:28rpx;color:var(--text-dim)}
.popup-submit{flex:1;height:80rpx;border-radius:var(--radius-sm);background:linear-gradient(135deg,var(--orange),var(--red));color:#fff;border:none;font-size:28rpx;font-weight:700}
</style>

+ 12
- 3
components/OrderCard.vue View File

@ -1,4 +1,4 @@
<template>
<template>
<view class="order-card" :class="{ pulse: order.status === 0 }">
<view class="card-header">
<view>
@ -8,7 +8,12 @@
<text class="status-badge" :class="statusClass">{{ statusLabel }}</text>
</view>
<view class="card-body">
<text class="card-items">{{ order.items.map(i=>i.name+'×'+i.qty).join('、') }}</text>
<view class="card-items">
<view v-for="(item, idx) in order.items" :key="idx" class="card-item-row">
<text class="card-item-name">{{ item.name }}</text>
<text class="card-item-qty">×{{ item.qty }}</text>
</view>
</view>
<text v-if="order.note" class="card-note">💬 {{ order.note }}</text>
<text class="card-time">{{ order.submittedAt }}</text>
<slot name="actions"></slot>
@ -43,8 +48,12 @@ export default {
.status-done{background:rgba(46,213,115,.15);color:#2ED573}
.status-cancelled{background:rgba(116,125,140,.15);color:#747D8C}
.card-body{padding-top:8rpx}
.card-items{font-size:26rpx;color:var(--text);display:block}
.card-items{padding:4rpx 0}
.card-item-row{display:flex;align-items:center;justify-content:space-between;padding:6rpx 0}
.card-item-name{font-size:39rpx;font-weight:700;color:var(--text)}
.card-item-qty{font-size:28rpx;font-weight:600;color:var(--gold);margin-left:16rpx}
.card-note{font-size:24rpx;color:var(--orange);display:block;padding:8rpx 0}
.card-time{font-size:22rpx;color:var(--text-muted);display:block;padding:4rpx 0}
@keyframes pulse-glow{0%,100%{box-shadow:0 0 20rpx rgba(245,166,35,.1)}50%{box-shadow:0 0 40rpx rgba(245,166,35,.2)}}
</style>

+ 12
- 11
components/ProductCard.vue View File

@ -1,15 +1,15 @@
<template>
<view class="product-card" @tap="$emit('tap', product)">
<view class="card-img">
<text>{{ product.emoji }}</text>
<view class="card-left">
<text class="card-emoji">{{ product.emoji }}</text>
<text v-if="product.alc > 0" class="alc-tag">{{ product.alc }}%</text>
</view>
<view class="card-body">
<text class="card-name">{{ product.name }}</text>
<text class="card-en">{{ product.en }}</text>
<text class="card-desc">{{ product.desc }}</text>
<view class="card-add" @tap.stop="$emit('add', product)">+</view>
</view>
<view class="card-add" @tap.stop="$emit('add', product)">+</view>
</view>
</template>
@ -22,12 +22,13 @@ export default {
</script>
<style scoped>
.product-card{width:calc(50% - 12rpx);background:var(--bg-card);border-radius:var(--radius);overflow:hidden;margin-bottom:20rpx;border:1px solid var(--border);position:relative}
.card-img{width:100%;height:220rpx;display:flex;align-items:center;justify-content:center;font-size:88rpx;position:relative;background:rgba(245,166,35,.04)}
.alc-tag{position:absolute;top:16rpx;right:16rpx;background:rgba(0,0,0,.6);color:var(--gold-light);font-size:20rpx;padding:6rpx 16rpx;border-radius:20rpx;font-weight:700}
.card-body{padding:16rpx 20rpx 24rpx;position:relative}
.card-name{font-size:28rpx;font-weight:700;color:var(--text);display:block;margin-bottom:4rpx}
.card-en{font-size:20rpx;color:var(--text-muted);display:block;margin-bottom:6rpx}
.card-desc{font-size:22rpx;color:var(--text-dim);line-height:1.3;display:block;margin-bottom:4rpx}
.card-add{position:absolute;bottom:16rpx;right:16rpx;width:56rpx;height:56rpx;border-radius:50%;background:var(--orange);color:#fff;font-size:32rpx;display:flex;align-items:center;justify-content:center;box-shadow:0 4rpx 20rpx rgba(255,107,53,.4)}
.product-card{width:100%;background:var(--bg-card);border-radius:var(--radius);margin-bottom:20rpx;border:1px solid var(--border);display:flex;align-items:center;padding:20rpx;gap:20rpx;min-height:140rpx}
.card-left{position:relative;flex-shrink:0;width:120rpx;height:120rpx;display:flex;align-items:center;justify-content:center;background:rgba(245,166,35,.06);border-radius:20rpx}
.card-emoji{font-size:64rpx}
.alc-tag{position:absolute;top:4rpx;right:4rpx;background:rgba(0,0,0,.6);color:var(--gold-light);font-size:18rpx;padding:2rpx 10rpx;border-radius:12rpx;font-weight:700}
.card-body{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center}
.card-name{font-size:30rpx;font-weight:700;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.card-en{font-size:22rpx;color:var(--text-muted);margin:4rpx 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.card-desc{font-size:24rpx;color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.card-add{flex-shrink:0;width:56rpx;height:56rpx;border-radius:50%;background:var(--orange);color:#fff;font-size:32rpx;display:flex;align-items:center;justify-content:center;box-shadow:0 4rpx 20rpx rgba(255,107,53,.4)}
</style>

+ 4
- 3
manifest.json View File

@ -1,7 +1,7 @@
{
"name": "纯瘾大",
{
"name": "就是纯瘾大",
"appid": "__UNI__BARORDER",
"description": "纯瘾大酒吧点单小程序",
"description": "就是纯瘾大酒吧点单小程序",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
@ -13,3 +13,4 @@
"usingComponents": true
}
}

+ 11
- 11
pages.json View File

@ -1,18 +1,18 @@
{
{
"pages": [
{ "path": "pages/index/index", "style": { "navigationBarTitleText": "纯瘾大" } },
{ "path": "pages/menu/menu", "style": { "navigationBarTitleText": "酒水单" } },
{ "path": "pages/confirm/confirm", "style": { "navigationBarTitleText": "确认下单" } },
{ "path": "pages/orders/orders", "style": { "navigationBarTitleText": "我的订单" } },
{ "path": "pages/chat/chat", "style": { "navigationBarTitleText": "对话" } },
{ "path": "pages/staff/login", "style": { "navigationBarTitleText": "调酒师登录" } },
{ "path": "pages/staff/board", "style": { "navigationBarTitleText": "订单看板" } },
{ "path": "pages/staff/detail", "style": { "navigationBarTitleText": "订单详情" } },
{ "path": "pages/staff/chat", "style": { "navigationBarTitleText": "客人对话" } }
{ "path": "pages/index/index", "style": { "navigationBarTitleText": "就是纯瘾大", "navigationStyle": "default" } },
{ "path": "pages/menu/menu", "style": { "navigationBarTitleText": "点单", "navigationStyle": "default" } },
{ "path": "pages/confirm/confirm", "style": { "navigationBarTitleText": "确认下单", "navigationStyle": "default" } },
{ "path": "pages/orders/orders", "style": { "navigationBarTitleText": "我的订单", "navigationStyle": "default" } },
{ "path": "pages/chat/chat", "style": { "navigationBarTitleText": "聊天", "navigationStyle": "default" } },
{ "path": "pages/staff/login", "style": { "navigationBarTitleText": "员工登录", "navigationStyle": "default" } },
{ "path": "pages/staff/board", "style": { "navigationBarTitleText": "看板", "navigationStyle": "default" } },
{ "path": "pages/staff/detail", "style": { "navigationBarTitleText": "订单详情", "navigationStyle": "default" } },
{ "path": "pages/staff/chat", "style": { "navigationBarTitleText": "聊天", "navigationStyle": "default" } }
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "纯瘾大",
"navigationBarTitleText": "就是纯瘾大",
"navigationBarBackgroundColor": "#0D0D0D",
"backgroundColor": "#0D0D0D"
}


+ 17
- 11
pages/chat/chat.vue View File

@ -1,8 +1,8 @@
<template>
<template>
<view class="page-chat">
<view class="nav-bar">
<text class="nav-back" @tap="goBack"></text>
<text class="nav-title">💬 调酒师</text>
<view class="top-bar">
<text class="top-back" @tap="goBack"></text>
<text class="top-title">💬 调酒师</text>
<text></text>
</view>
@ -19,7 +19,7 @@
</template>
<script>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { useCardStore } from '@/stores/card'
import { get, post } from '@/utils/request'
import { API } from '@/utils/constants'
@ -46,6 +46,13 @@ export default {
} catch (e) {}
}
async function markRead() {
try {
await post(API.MESSAGE_READ, { card_no: card.cardNo })
card.clearUnread()
} catch (e) {}
}
async function sendMsg() {
const content = inputText.value.trim()
if (!content) return
@ -59,8 +66,7 @@ export default {
function goBack() { uni.navigateBack() }
onMounted(() => {
loadMessages()
startPoll('chat', loadMessages, 5000)
loadMessages(); markRead(); startPoll('chat', loadMessages, 5000)
})
onUnmounted(() => stopPoll('chat'))
@ -70,10 +76,10 @@ export default {
</script>
<style scoped>
.page-chat{min-height:100vh;display:flex;flex-direction:column;background:var(--bg)}
.nav-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border)}
.nav-back{font-size:36rpx;color:var(--text-dim)}
.nav-title{font-size:32rpx;font-weight:800;color:var(--gold)}
.page-chat{min-height:100vh;display:flex;flex-direction:column;background:var(--bg);width:100%}
.top-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.top-back{font-size:36rpx;color:var(--text-dim)}
.top-title{font-size:32rpx;font-weight:800;color:var(--gold)}
.chat-list{flex:1;padding:16rpx 0}
.chat-input-bar{display:flex;align-items:center;padding:16rpx 24rpx;padding-bottom:calc(16rpx + var(--safe-b));background:var(--bg-card);border-top:1px solid var(--border);gap:16rpx}
.chat-input{flex:1;height:76rpx;background:var(--bg);border-radius:16rpx;padding:0 28rpx;font-size:28rpx;color:var(--text)}


+ 9
- 8
pages/confirm/confirm.vue View File

@ -1,8 +1,8 @@
<template>
<template>
<view class="page-confirm">
<view class="nav-bar">
<text class="nav-back" @tap="goBack"></text>
<text class="nav-title">确认下单</text>
<view class="top-bar">
<text class="top-back" @tap="goBack"></text>
<text class="top-title">确认下单</text>
<text></text>
</view>
@ -26,7 +26,7 @@
<view class="section">
<text class="section-title"> 补充备注</text>
<input class="note-input" v-model="note" placeholder="如:多冰、少甜..." maxlength="100" />
<input class="note-input" v-model="note" placeholder="好晕好渴好想喝..." maxlength="100" />
</view>
<view class="total-line">
@ -83,9 +83,9 @@ export default {
<style scoped>
.page-confirm{min-height:100vh;display:flex;flex-direction:column;background:var(--bg);width:100%}
.nav-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.nav-back{font-size:36rpx;color:var(--text-dim)}
.nav-title{font-size:32rpx;font-weight:800;color:var(--gold)}
.top-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.top-back{font-size:36rpx;color:var(--text-dim)}
.top-title{font-size:32rpx;font-weight:800;color:var(--gold)}
.confirm-body{flex:1;padding:0 28rpx}
.section{padding:20rpx 0;border-bottom:1px solid var(--border)}
.section-title{font-size:28rpx;font-weight:700;color:var(--gold);display:block;margin-bottom:16rpx}
@ -101,3 +101,4 @@ export default {
.bell-btn{width:100%;height:96rpx;border-radius:var(--radius-sm);background:linear-gradient(135deg,var(--gold-dark),var(--gold));color:#1A1A1A;border:none;font-size:32rpx;font-weight:800;display:flex;align-items:center;justify-content:center;animation:pulse-glow 2s ease-in-out infinite}
@keyframes pulse-glow{0%,100%{box-shadow:0 0 20rpx rgba(245,166,35,.3)}50%{box-shadow:0 0 40rpx rgba(245,166,35,.6)}}
</style>

+ 36
- 35
pages/index/index.vue View File

@ -1,11 +1,11 @@
<template>
<template>
<view class="page-index">
<view class="nav-bar">
<view class="nav-left">
<text class="nav-logo">🍸</text>
<text class="nav-brand">纯瘾大</text>
<view class="top-bar">
<view class="top-left">
<text class="top-logo">🍸</text>
<text class="top-brand">就是纯瘾大</text>
</view>
<view class="nav-right">
<view class="top-right">
<text class="my-btn" @tap="goMyOrders">我的</text>
</view>
</view>
@ -18,14 +18,12 @@
<view class="landing-decor" style="bottom:25%;left:14%;font-size:48rpx">🧊</view>
<text class="landing-logo">🍸</text>
<text class="landing-title">纯瘾大</text>
<text class="landing-title">就是纯瘾大</text>
<text class="landing-sub">唤醒你的专属调酒师</text>
<button class="btn-primary" @tap="showModal = true">🎫 领取号码牌</button>
</view>
<!-- 员工入口 -->
<view class="staff-link">
<text @tap="goStaffLogin">🔐 员工入口</text>
<view class="staff-link">
<text @tap="goStaffLogin">🔐 员工入口</text>
</view>
</view>
<!-- 号码牌弹窗 (点击按钮才显示) -->
@ -76,6 +74,13 @@ export default {
const generated = ref(false)
const inputNo = ref('')
const cardNo = ref('')
let navTimer = null
function goMenu() {
showModal.value = false
generated.value = false
uni.navigateTo({ url: '/pages/menu/menu' })
}
async function onGenerate() {
try {
@ -83,11 +88,7 @@ export default {
cardNo.value = res.cardNo
card.setCardNo(res.cardNo)
generated.value = true
setTimeout(() => {
showModal.value = false
generated.value = false
uni.navigateTo({ url: '/pages/menu/menu' })
}, 3000)
navTimer = setTimeout(() => { goMenu() }, 3000)
} catch (e) {}
}
@ -110,9 +111,8 @@ export default {
function onMaskTap() {
if (generated.value) {
showModal.value = false
generated.value = false
uni.navigateTo({ url: '/pages/menu/menu' })
if (navTimer) { clearTimeout(navTimer); navTimer = null }
goMenu()
}
}
@ -125,40 +125,41 @@ export default {
</script>
<style scoped>
.page-index{min-height:100vh;display:flex;flex-direction:column;background:var(--bg);width:100%}
.nav-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.nav-left{display:flex;align-items:center;gap:16rpx}
.nav-logo{font-size:44rpx}
.nav-brand{font-size:40rpx;font-weight:900;color:var(--gold);letter-spacing:4rpx}
.page-index{height:100vh;display:flex;flex-direction:column;background:var(--bg);width:100%}
.top-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.top-left{display:flex;align-items:center;gap:16rpx}
.top-logo{font-size:44rpx}
.top-brand{font-size:36rpx;font-weight:900;color:var(--gold);letter-spacing:4rpx}
.my-btn{font-size:26rpx;color:var(--gold);font-weight:600}
.landing-bg{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;overflow:hidden;background:radial-gradient(ellipse at center,#1A1A1A 0%,var(--bg) 70%);padding:40rpx}
.landing-decor{position:absolute;font-size:80rpx;opacity:.08;animation:float 6s ease-in-out infinite;pointer-events:none}
.landing-logo{font-size:160rpx;animation:float 3s ease-in-out infinite}
.landing-title{font-size:56rpx;font-weight:900;color:var(--gold);margin-top:24rpx;letter-spacing:8rpx}
.landing-sub{font-size:28rpx;color:var(--text-dim);margin:12rpx 0 80rpx;letter-spacing:2rpx}
.landing-title{font-size:48rpx;font-weight:900;color:var(--gold);margin-top:24rpx;letter-spacing:6rpx}
.landing-sub{font-size:28rpx;color:var(--text-dim);margin:12rpx 0 20rpx;letter-spacing:2rpx}
.btn-primary{width:440rpx;height:96rpx;border-radius:24rpx;background:linear-gradient(135deg,var(--gold-dark),var(--gold),var(--gold-light));color:#1A1A1A;border:none;font-size:32rpx;font-weight:800;display:flex;align-items:center;justify-content:center;box-shadow:0 8rpx 40rpx rgba(245,166,35,.35)}
/* 员工入口 */
.staff-link{padding:20rpx;text-align:center;flex-shrink:0}
.staff-link{margin-top:24rpx;text-align:center}
.staff-link text{color:var(--text-muted);font-size:22rpx;letter-spacing:2rpx}
/* 号码牌弹窗 (严格对齐UI CSS) */
/* 号码牌弹窗 */
.modal-mask{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.75);z-index:300;align-items:center;justify-content:center}
.modal-mask.show{display:flex}
.card-modal{width:580rpx;background:var(--bg-card);border-radius:40rpx;border:2px solid var(--border);overflow:hidden;animation:bounce-in .5s ease}
.modal-header{padding:56rpx 48rpx 32rpx;text-align:center}
.modal-header{padding:48rpx 36rpx 28rpx;text-align:center}
.modal-icon{font-size:104rpx;animation:float 2s ease-in-out infinite}
.modal-title{font-size:36rpx;font-weight:800;color:var(--gold);margin-top:16rpx}
.modal-desc{font-size:26rpx;color:var(--text-dim);margin-top:8rpx;line-height:1.6}
.card-generated{text-align:center;padding:24rpx 48rpx 40rpx}
.modal-title{font-size:36rpx;font-weight:800;color:var(--gold);margin-top:16rpx;white-space:nowrap}
.modal-desc{font-size:26rpx;color:var(--text-dim);margin-top:8rpx;line-height:1.6;white-space:nowrap}
.card-generated{text-align:center;padding:24rpx 36rpx 40rpx}
.card-number-big{padding:32rpx;background:linear-gradient(135deg,rgba(245,166,35,.1),rgba(245,166,35,.05));border-radius:28rpx;border:2rpx dashed rgba(245,166,35,.3)}
.card-number-big text{font-size:84rpx;font-weight:900;letter-spacing:20rpx;color:var(--gold)}
.card-remember{font-size:26rpx;color:var(--red);margin-top:20rpx;font-weight:600;display:block}
.modal-body{padding:0 48rpx 40rpx}
.modal-body{padding:0 36rpx 36rpx}
.card-input-group{display:flex;align-items:stretch;margin-bottom:24rpx;border:4rpx solid var(--border);border-radius:20rpx;overflow:hidden;background:var(--bg)}
.card-input{flex:1;min-width:0;height:92rpx;background:transparent;border:none;outline:none;padding:0 28rpx;font-size:36rpx;font-weight:700;text-align:center;letter-spacing:8rpx;color:var(--gold)}
.card-confirm-btn{flex-shrink:0;width:128rpx;border:none;background:linear-gradient(135deg,var(--gold-dark),var(--gold));color:#1A1A1A;font-size:28rpx;font-weight:700;display:flex;align-items:center;justify-content:center}
.card-divider{text-align:center;color:var(--text-muted);font-size:24rpx;margin:20rpx 0;position:relative}
.modal-footer{padding:0 48rpx 48rpx}
.modal-footer{padding:0 36rpx 40rpx}
</style>

+ 39
- 14
pages/menu/menu.vue View File

@ -1,12 +1,12 @@
<template>
<template>
<view class="page-menu">
<view class="nav-bar">
<view class="nav-left">
<text class="nav-logo">🍸</text>
<text class="nav-brand">纯瘾大</text>
<view class="top-bar">
<view class="top-left">
<text class="top-logo">🍸</text>
<text class="top-brand">就是纯瘾大</text>
<text v-if="card.cardNo" class="card-badge">🎫 {{ card.cardNo }}</text>
</view>
<view class="nav-right">
<view class="top-right">
<text class="my-btn" @tap="goMyOrders">我的</text>
</view>
</view>
@ -27,7 +27,7 @@
<!-- 底部栏: 红酒杯() + 出餐铃() -->
<view class="bottom-bar">
<view class="wine-glass" @tap="showCart = true">
<text class="glass-icon">🍷</text>
<text class="glass-icon">🍸</text>
<text v-if="cart.totalCount > 0" class="glass-count">{{ cart.totalCount }}</text>
</view>
<view class="press-bell-wrapper" @tap="onBellPress">
@ -91,7 +91,32 @@ export default {
async function loadCategories() {
try {
const res = await get(API.MENU_CATEGORIES)
if (Array.isArray(res)) categories.value = ['all', ...res]
if (Array.isArray(res)) {
// + + + +
const priority = ['鸡尾酒', '限定特调', '嗨棒']
const matched = []
const rest = []
for (const c of res) {
if (priority.some(k => c.includes(k))) matched.push(c)
else rest.push(c)
}
// priority matched
matched.sort((a, b) => {
const ai = priority.findIndex(k => a.includes(k))
const bi = priority.findIndex(k => b.includes(k))
return ai - bi
})
const sorted = ['all', ...matched, ...rest]
categories.value = sorted
//
const cocktail = sorted.find(c => c.includes('鸡尾酒'))
if (cocktail) {
activeCate.value = cocktail
await loadProducts(cocktail)
} else {
await loadProducts('all')
}
}
} catch (e) {}
}
@ -129,7 +154,6 @@ export default {
onMounted(async () => {
await loadCategories()
await loadProducts('all')
})
return { card, cart, categories, activeCate, products, showCart, bellPressed, bellWaving, onCateChange, onAddToCart, onBellPress, onGoConfirm, goMyOrders }
@ -139,14 +163,14 @@ export default {
<style scoped>
.page-menu{min-height:100vh;display:flex;flex-direction:column;background:var(--bg);width:100%}
.nav-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0;position:sticky;top:0;z-index:100;background:var(--bg)}
.nav-left{display:flex;align-items:center;gap:16rpx}
.nav-logo{font-size:44rpx}
.nav-brand{font-size:40rpx;font-weight:900;color:var(--gold);letter-spacing:4rpx}
.top-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.top-left{display:flex;align-items:center;gap:16rpx}
.top-logo{font-size:44rpx}
.top-brand{font-size:36rpx;font-weight:900;color:var(--gold);letter-spacing:4rpx}
.card-badge{background:linear-gradient(135deg,var(--gold-dark),var(--gold));color:#1A1A1A;padding:10rpx 24rpx;border-radius:28rpx;font-weight:800;font-size:26rpx;letter-spacing:6rpx;animation:card-appear-top .5s ease}
.my-btn{font-size:26rpx;color:var(--gold);font-weight:600}
.menu-scroll{flex:1}
.product-grid{display:flex;flex-wrap:wrap;justify-content:space-between;padding:16rpx 20rpx}
.product-grid{padding:16rpx 20rpx}
.empty-state{display:flex;flex-direction:column;align-items:center;padding:120rpx 0}
.empty-icon{font-size:112rpx;opacity:.3}
.empty-text{font-size:28rpx;color:var(--text-muted);margin-top:16rpx}
@ -185,3 +209,4 @@ export default {
.cart-footer{padding:24rpx 40rpx 40rpx;flex-shrink:0}
.next-btn{width:100%;height:96rpx;border-radius:28rpx;background:linear-gradient(135deg,var(--orange),var(--red));color:#fff;border:none;font-size:32rpx;font-weight:800;box-shadow:0 8rpx 40rpx rgba(255,107,53,.35)}
</style>

+ 29
- 13
pages/orders/orders.vue View File

@ -1,8 +1,8 @@
<template>
<template>
<view class="page-orders">
<view class="nav-bar">
<text class="nav-back" @tap="goBack"></text>
<text class="nav-title">我的订单</text>
<view class="top-bar">
<text class="top-back" @tap="goBack"></text>
<text class="top-title">我的订单</text>
<text></text>
</view>
@ -11,7 +11,11 @@
<template #actions>
<view class="card-actions">
<button v-if="o.status===0" class="action-btn action-remind" @tap="onRemind(o.id)">🔔 催单</button>
<button class="action-btn action-chat" @tap="onChat">💬 联系调酒师</button>
<button v-if="o.status!==3 && o.status!==2" class="action-btn action-chat" @tap="onChat">
<text v-if="card.unread > 0" class="unread-badge">{{ card.unread > 9 ? '9+' : card.unread }}</text>
<text v-else>💬</text>
联系调酒师
</button>
</view>
</template>
</OrderCard>
@ -45,7 +49,14 @@ export default {
if (Array.isArray(res)) orders.value = res
} catch (e) {}
}
async function checkUnread() {
try {
const res = await get(API.MESSAGE_UNREAD, { card_no: card.cardNo })
if (res && typeof res.count !== 'undefined') {
card.unread = res.count
}
} catch (e) {}
}
async function onRemind(id) {
try {
await post(API.ORDER_REMIND, { id, card_no: card.cardNo })
@ -58,26 +69,31 @@ export default {
function goBack() { uni.navigateBack() }
function goMenu() { uni.navigateBack() }
onMounted(() => { loadOrders(); startPoll('orders', loadOrders, 15000) })
onUnmounted(() => stopPoll('orders'))
onMounted(() => {
loadOrders(); checkUnread(); startPoll('orders', loadOrders, 15000); startPoll('ordersUnread', checkUnread, 10000)
})
onUnmounted(() => { stopPoll('orders'); stopPoll('ordersUnread') })
return { orders, onRemind, onChat, goBack, goMenu }
return { card, orders, onRemind, onChat, goBack, goMenu }
}
}
</script>
<style scoped>
.page-orders{min-height:100vh;display:flex;flex-direction:column;background:var(--bg);width:100%}
.nav-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.nav-back{font-size:36rpx;color:var(--text-dim)}
.nav-title{font-size:32rpx;font-weight:800;color:var(--gold)}
.top-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.top-back{font-size:36rpx;color:var(--text-dim)}
.top-title{font-size:32rpx;font-weight:800;color:var(--gold)}
.orders-body{flex:1;padding:20rpx}
.card-actions{display:flex;gap:16rpx;padding-top:16rpx}
.action-btn{padding:12rpx 28rpx;border-radius:24rpx;font-size:24rpx;font-weight:600;border:none}
.action-btn{padding:12rpx 28rpx;border-radius:24rpx;font-size:24rpx;font-weight:600;border:none;position:relative}
.action-remind{background:rgba(255,59,59,.15);color:var(--red)}
.action-chat{background:rgba(74,144,217,.15);color:var(--blue)}
.unread-badge{background:var(--red);color:#fff;font-size:20rpx;min-width:32rpx;height:32rpx;border-radius:16rpx;display:inline-flex;align-items:center;justify-content:center;padding:0 8rpx;margin-right:8rpx;vertical-align:middle}
.empty-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center}
.empty-icon{font-size:112rpx;opacity:.3}
.empty-text{font-size:28rpx;color:var(--text-muted);margin:16rpx 0}
.btn-back-menu{margin-top:40rpx;padding:16rpx 48rpx;border-radius:40rpx;background:var(--gold);color:#1A1A1A;font-size:28rpx;font-weight:700;border:none}
</style>

+ 55
- 23
pages/staff/board.vue View File

@ -1,21 +1,21 @@
<template>
<template>
<view class="page-staff-board">
<view class="nav-bar">
<view class="nav-left">
<text class="nav-logo">🍸</text>
<text class="nav-brand">纯瘾大</text>
<view class="top-bar">
<view class="top-left">
<text class="top-logo">🍸</text>
<text class="top-brand">就是纯瘾大</text>
</view>
<view class="nav-right">
<view class="top-right">
<text class="logout-btn" @tap="onLogout">退出</text>
</view>
</view>
<view class="staff-info">🧑🍳 {{ staff.nickname }} 别摸鱼了</view>
<view class="staff-info">🧑🍳 {{ staff.nickname }} 别摸鱼了<text v-if="staff.unread > 0" style="color:var(--red);margin-left:16rpx">💬 {{ staff.unread > 9 ? '9+' : staff.unread }}条新消息</text></view>
<view class="board-tabs">
<view v-for="t in tabs" :key="t.status" class="board-tab" :class="{ active: activeTab === t.status }" @tap="switchTab(t.status)">
{{ t.label }}
<text v-if="t.status !== 2" class="tab-count">{{ counts[t.status] }}</text>
<text class="tab-count">{{ counts[t.status] || 0 }}</text>
</view>
</view>
@ -23,9 +23,10 @@
<OrderCard v-for="o in orders" :key="o.id" :order="o">
<template #actions>
<view class="board-actions">
<button class="ba-btn ba-gold" @tap="onDetail(o)">📖 配方</button>
<button class="ba-btn ba-blue" @tap="onChat(o)">💬 聊天</button>
<button class="ba-btn ba-gold" @tap="onDetail(o)">📖 详情</button>
<button v-if="o.status!==3 && o.status!==2" class="ba-btn ba-blue" @tap="onChat(o)"><text v-if="staff.unread > 0" class="unread-badge">{{ staff.unread > 9 ? "9+" : staff.unread }}</text><text v-else>💬</text> 聊天</button>
<button v-if="o.status===0" class="ba-btn ba-gold" @tap="onConfirm(o.id)"> 接单</button>
<button v-if="o.status===0" class="ba-btn ba-red" @tap="onCancel(o.id)"> 拒单</button>
<button v-if="o.status===1" class="ba-btn ba-gold" @tap="onDone(o.id)"> 结单</button>
</view>
</template>
@ -37,7 +38,7 @@
<text class="empty-text">暂无订单</text>
</view>
<RecipeModal :visible="recipeVisible" :cardNo="recipeCardNo" :items="recipeItems" @close="recipeVisible=false" />
<RecipeModal v-if="recipeVisible" :visible="recipeVisible" :cardNo="recipeCardNo" :items="recipeItems" @close="recipeVisible=false" />
</view>
</template>
@ -57,42 +58,69 @@ export default {
const tabs = [{ status: 0, label: '🔔 新订单' },{ status: 1, label: '⚡ 进行中' },{ status: 2, label: '✅ 已完成' }]
const activeTab = ref(0)
const orders = ref([])
const counts = ref({ 0: 0, 1: 0 })
const counts = ref({ 0: 0, 1: 0, 2: 0 })
const recipeVisible = ref(false)
const recipeCardNo = ref('')
const recipeItems = ref([])
async function loadOrders() {
try { const res = await get(API.STAFF_ORDERS, { status: activeTab.value }); if (Array.isArray(res)) orders.value = res } catch (e) {}
try {
const params = { status: activeTab.value }
if (activeTab.value === 2) params.include_cancel = 1
const res = await get(API.STAFF_ORDERS, params)
if (Array.isArray(res)) orders.value = res
} catch (e) {}
}
async function loadCounts() {
try { const r0 = await get(API.STAFF_ORDERS, { status: 0 }); const r1 = await get(API.STAFF_ORDERS, { status: 1 }); counts.value = { 0: Array.isArray(r0) ? r0.length : 0, 1: Array.isArray(r1) ? r1.length : 0 } } catch (e) {}
try {
const [r0, r1, r2] = await Promise.all([
get(API.STAFF_ORDERS, { status: 0 }),
get(API.STAFF_ORDERS, { status: 1 }),
get(API.STAFF_ORDERS, { status: 2, include_cancel: 1 })
])
counts.value = {
0: Array.isArray(r0) ? r0.length : 0,
1: Array.isArray(r1) ? r1.length : 0,
2: Array.isArray(r2) ? r2.length : 0
}
} catch (e) {}
}
async function checkUnread() {
try {
const res = await get(API.MESSAGE_UNREAD, { card_no: 'all', staff_id: staff.staffId })
if (res && typeof res.count !== 'undefined') {
staff.unread = res.count
}
} catch (e) {}
}
function switchTab(s) { activeTab.value = s; loadOrders() }
async function onConfirm(id) { try { await post(API.STAFF_CONFIRM, { id }); uni.showToast({ title: '已接单 ✅', icon: 'none' }); loadOrders(); loadCounts() } catch (e) {} }
async function onDone(id) { try { const res = await post(API.STAFF_DONE, { id }); uni.showToast({ title: res && res.released ? '已结单 ✔ 号码已释放' : '已结单 ✔', icon: 'none' }); loadOrders(); loadCounts() } catch (e) {} }
async function onConfirm(id) { try { await post(API.STAFF_CONFIRM, { id }); uni.showToast({ title: '已接单', icon: 'none' }); loadOrders(); loadCounts() } catch (e) {} }
async function onCancel(id) { try { await post(API.STAFF_CANCEL, { id }); uni.showToast({ title: '已拒单', icon: 'none' }); loadOrders(); loadCounts() } catch (e) {} }
async function onDone(id) { try { const res = await post(API.STAFF_DONE, { id }); uni.showToast({ title: res && res.released ? '已结单,号码释放' : '已结单', icon: 'none' }); loadOrders(); loadCounts() } catch (e) {} }
function onDetail(order) { uni.navigateTo({ url: '/pages/staff/detail?id=' + order.id }) }
function onRecipe(order) { recipeCardNo.value = order.cardNo; recipeItems.value = order.items; recipeVisible.value = true }
function onChat(order) { uni.setStorageSync('chatCardNo', order.cardNo); uni.navigateTo({ url: '/pages/staff/chat' }) }
function onLogout() { staff.logout(); uni.redirectTo({ url: '/pages/staff/login' }) }
onMounted(() => { loadOrders(); loadCounts(); startPoll('board', () => { loadOrders(); loadCounts() }, 10000) })
onMounted(() => {
loadOrders(); loadCounts(); checkUnread(); startPoll('board', () => { loadOrders(); loadCounts(); checkUnread() }, 10000)
})
onUnmounted(() => stopPoll('board'))
return { staff, tabs, activeTab, orders, counts, recipeVisible, recipeCardNo, recipeItems, switchTab, onConfirm, onDone, onDetail, onRecipe, onChat, onLogout }
return { staff, tabs, activeTab, orders, counts, recipeVisible, recipeCardNo, recipeItems, switchTab, onConfirm, onCancel, onDone, onDetail, onRecipe, onChat, onLogout, checkUnread }
}
}
</script>
<style scoped>
.page-staff-board{min-height:100vh;display:flex;flex-direction:column;background:var(--bg);width:100%}
.nav-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.nav-left{display:flex;align-items:center;gap:16rpx}
.nav-logo{font-size:44rpx}
.nav-brand{font-size:32rpx;font-weight:900;color:var(--gold)}
.top-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.top-left{display:flex;align-items:center;gap:16rpx}
.top-logo{font-size:44rpx}
.top-brand{font-size:32rpx;font-weight:900;color:var(--gold);letter-spacing:2rpx}
.logout-btn{font-size:26rpx;color:var(--red)}
.staff-info{background:var(--bg-card);padding:16rpx 32rpx;font-size:24rpx;color:var(--text-dim);border-bottom:1px solid var(--border)}
.board-tabs{display:flex;border-bottom:1px solid var(--border);position:sticky;top:0;z-index:50;background:var(--bg);flex-shrink:0}
.board-tabs{display:flex;border-bottom:1px solid var(--border);background:var(--bg);flex-shrink:0}
.board-tab{flex:1;padding:24rpx;text-align:center;font-size:26rpx;font-weight:600;color:var(--text-dim);border-bottom:4rpx solid transparent}
.board-tab.active{color:var(--gold);border-bottom-color:var(--gold)}
.tab-count{margin-left:8rpx;color:var(--red);font-size:22rpx}
@ -101,7 +129,11 @@ export default {
.ba-btn{padding:12rpx 24rpx;border-radius:24rpx;font-size:24rpx;font-weight:600;border:none}
.ba-gold{background:rgba(245,166,35,.15);color:var(--gold)}
.ba-blue{background:rgba(74,144,217,.15);color:var(--blue)}
.ba-red{background:rgba(255,59,59,.15);color:var(--red)}
.unread-badge{background:var(--red);color:#fff;font-size:18rpx;min-width:28rpx;height:28rpx;border-radius:14rpx;display:inline-flex;align-items:center;justify-content:center;padding:0 6rpx;margin-right:6rpx;vertical-align:middle}
.empty-state{flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column}
.empty-icon{font-size:112rpx;opacity:.3}
.empty-text{font-size:28rpx;color:var(--text-muted);margin-top:16rpx}
</style>

+ 17
- 8
pages/staff/chat.vue View File

@ -1,8 +1,8 @@
<template>
<template>
<view class="page-staff-chat">
<view class="nav-bar">
<text class="nav-back" @tap="goBack"></text>
<text class="nav-title">💬 客人对话</text>
<view class="top-bar">
<text class="top-back" @tap="goBack"></text>
<text class="top-title">💬 客人对话</text>
<text></text>
</view>
@ -47,6 +47,13 @@ export default {
} catch (e) {}
}
async function markRead() {
try {
await post(API.MESSAGE_READ, { card_no: cardNo.value, staff_id: staff.staffId })
staff.clearUnread()
} catch (e) {}
}
async function sendMsg() {
const content = inputText.value.trim()
if (!content) return
@ -59,7 +66,9 @@ export default {
function goBack() { uni.navigateBack() }
onMounted(() => { loadMessages(); startPoll('staffChat', loadMessages, 5000) })
onMounted(() => {
loadMessages(); markRead(); startPoll('staffChat', loadMessages, 5000)
})
onUnmounted(() => stopPoll('staffChat'))
return { cardNo, messages, inputText, scrollToId, sendMsg, goBack }
@ -69,9 +78,9 @@ export default {
<style scoped>
.page-staff-chat{min-height:100vh;display:flex;flex-direction:column;background:var(--bg);width:100%}
.nav-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.nav-back{font-size:36rpx;color:var(--text-dim)}
.nav-title{font-size:32rpx;font-weight:800;color:var(--gold)}
.top-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.top-back{font-size:36rpx;color:var(--text-dim)}
.top-title{font-size:32rpx;font-weight:800;color:var(--gold)}
.chat-list{flex:1;padding:16rpx 0}
.chat-input-bar{display:flex;align-items:center;padding:16rpx 24rpx;padding-bottom:calc(16rpx + var(--safe-b));background:var(--bg-card);border-top:1px solid var(--border);gap:16rpx}
.chat-input{flex:1;height:76rpx;background:var(--bg);border-radius:16rpx;padding:0 28rpx;font-size:28rpx;color:var(--text)}


+ 8
- 8
pages/staff/detail.vue View File

@ -1,8 +1,8 @@
<template>
<template>
<view class="page-staff-detail">
<view class="nav-bar">
<text class="nav-back" @tap="goBack"></text>
<text class="nav-title">订单详情</text>
<view class="top-bar">
<text class="top-back" @tap="goBack"></text>
<text class="top-title">订单详情</text>
<text></text>
</view>
@ -41,7 +41,7 @@
<view v-if="order" class="detail-actions">
<button v-if="order.status===0" class="da-btn da-confirm" @tap="onConfirm"> 接单</button>
<button v-if="order.status===1" class="da-btn da-done" @tap="onDone"> 结单</button>
<button class="da-btn da-chat" @tap="onChat">💬 聊天</button>
<button v-if="order.status!==3 && order.status!==2" class="da-btn da-chat" @tap="onChat">💬 聊天</button>
</view>
</view>
</template>
@ -78,9 +78,9 @@ export default {
<style scoped>
.page-staff-detail{min-height:100vh;display:flex;flex-direction:column;background:var(--bg);width:100%}
.nav-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.nav-back{font-size:36rpx;color:var(--text-dim)}
.nav-title{font-size:32rpx;font-weight:800;color:var(--gold)}
.top-bar{height:100rpx;display:flex;align-items:center;justify-content:space-between;padding:0 28rpx;border-bottom:1px solid var(--border);flex-shrink:0}
.top-back{font-size:36rpx;color:var(--text-dim)}
.top-title{font-size:32rpx;font-weight:800;color:var(--gold)}
.detail-body{flex:1;padding:28rpx}
.detail-card{background:var(--bg-card);border-radius:var(--radius);padding:32rpx;margin-bottom:24rpx;border:1px solid var(--border)}
.detail-row{display:flex;justify-content:space-between;padding:10rpx 0;font-size:28rpx;color:var(--text)}


+ 19
- 10
pages/staff/login.vue View File

@ -1,4 +1,4 @@
<template>
<template>
<view class="page-staff-login">
<view class="login-container">
<text class="login-logo">🧑🍳</text>
@ -6,13 +6,13 @@
<text class="login-subtitle">别再摸鱼了起来干活</text>
<view class="login-form">
<input class="form-input" v-model="username" placeholder="账号" />
<input class="form-input" v-model="password" placeholder="密码" password />
<button class="login-submit" @tap="onLogin" :loading="loading">🔓 </button>
<input class="form-input" v-model="username" placeholder="账号" type="text" />
<input class="form-input" v-model="password" placeholder="密码" type="password" />
<button class="login-submit" @tap="onLogin" :loading="loading" :disabled="loading">🔓 </button>
</view>
<view style="margin-top:20rpx;">
<text style="color:var(--text-muted);font-size:24rpx" @tap="goBack"> 返回顾客端</text>
<view class="back-link" @tap="goBack">
<text> 返回顾客端</text>
</view>
</view>
</view>
@ -36,13 +36,20 @@ export default {
uni.showToast({ title: '请输入账号密码', icon: 'none' })
return
}
if (loading.value) return
loading.value = true
try {
const res = await post(API.STAFF_LOGIN, { username: username.value, password: password.value })
if (!res || !res.token) {
uni.showToast({ title: '登录失败', icon: 'none' })
loading.value = false
return
}
staff.login(res.token, res.nickname, res.staffId)
uni.redirectTo({ url: '/pages/staff/board' })
} catch (e) {}
loading.value = false
uni.reLaunch({ url: '/pages/staff/board' })
} catch (e) {
loading.value = false
}
}
function goBack() { uni.navigateBack() }
@ -53,7 +60,8 @@ export default {
</script>
<style scoped>
.page-staff-login{min-height:100vh;display:flex;flex-direction:column;background:var(--bg);width:100%}
.page-staff-login{height:100vh;display:flex;flex-direction:column;background:var(--bg);width:100%}
.login-container{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:80rpx 64rpx}
.login-logo{font-size:120rpx;animation:float 3s ease-in-out infinite}
.login-title{font-size:44rpx;font-weight:800;color:var(--gold);margin:24rpx 0 8rpx}
@ -61,5 +69,6 @@ export default {
.login-form{width:100%}
.form-input{width:100%;height:96rpx;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 32rpx;font-size:30rpx;color:var(--text);margin-bottom:24rpx}
.login-submit{width:100%;height:96rpx;border-radius:24rpx;background:linear-gradient(135deg,var(--gold-dark),var(--gold));color:#1A1A1A;border:none;font-size:32rpx;font-weight:800;margin-top:16rpx;display:flex;align-items:center;justify-content:center}
.back-link{margin-top:24rpx;color:var(--text-muted);font-size:24rpx}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-16rpx)}}
</style>

+ 5
- 1
stores/card.js View File

@ -4,6 +4,7 @@ import { ref } from 'vue'
export const useCardStore = defineStore('card', () => {
const cardNo = ref(uni.getStorageSync('cardNo') || '')
const dismissed = ref(false)
const unread = ref(0)
function setCardNo(no) {
cardNo.value = no
@ -15,5 +16,8 @@ export const useCardStore = defineStore('card', () => {
uni.removeStorageSync('cardNo')
}
return { cardNo, dismissed, setCardNo, clearCard }
function incrementUnread(n = 1) { unread.value += n }
function clearUnread() { unread.value = 0 }
return { cardNo, dismissed, unread, setCardNo, clearCard, incrementUnread, clearUnread }
})

+ 1
- 2
stores/cart.js View File

@ -5,7 +5,6 @@ export const useCartStore = defineStore('cart', () => {
const items = ref([])
const totalCount = computed(() => items.value.reduce((s, i) => s + i.qty, 0))
const totalAmount = computed(() => items.value.reduce((s, i) => s + i.product.price * i.qty, 0))
function addItem(product, qty = 1) {
const exist = items.value.find(i => i.product.id === product.id)
@ -32,5 +31,5 @@ export const useCartStore = defineStore('cart', () => {
items.value = []
}
return { items, totalCount, totalAmount, addItem, removeItem, updateQty, clearCart }
return { items, totalCount, addItem, removeItem, updateQty, clearCart }
})

+ 5
- 1
stores/staff.js View File

@ -22,10 +22,14 @@ export const useStaffStore = defineStore('staff', () => {
token.value = ''
nickname.value = ''
staffId.value = ''
unread.value = 0
uni.removeStorageSync('staffToken')
uni.removeStorageSync('staffNickname')
uni.removeStorageSync('staffId')
}
return { token, nickname, staffId, unread, isLoggedIn, login, logout }
function incrementUnread(n = 1) { unread.value += n }
function clearUnread() { unread.value = 0 }
return { token, nickname, staffId, unread, isLoggedIn, login, logout, incrementUnread, clearUnread }
})

+ 5
- 4
uni.scss View File

@ -1,4 +1,6 @@
// 纯瘾大 全局样式 (严格对齐 UI/css/miniapp.css)
// 纯瘾大 全局样式
page,view,text,button,input,scroll-view,image,navigator,swiper,swiper-item{box-sizing:border-box}
page {
--bg: #0D0D0D;
--bg-card: #1A1A1A;
@ -19,14 +21,13 @@ page {
--radius-sm: 20rpx;
--radius-xs: 12rpx;
--nav-h: 100rpx;
--safe-b: 68rpx;
--safe-b: env(safe-area-inset-bottom, 0px);
background-color: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 28rpx;
min-height: 100vh;
height: 100vh;
width: 100%;
-webkit-font-smoothing: antialiased;
}
// 动画


+ 11
- 4
utils/constants.js View File

@ -1,4 +1,4 @@
// 纯瘾大 · 全局常量
// 纯瘾大 · 全局常量
export const API = {
BASE: 'http://127.0.0.1:8080',
@ -12,6 +12,8 @@ export const API = {
ORDER_REMIND: '/api/order/remind',
MESSAGE_LIST: '/api/message/list',
MESSAGE_SEND: '/api/message/send',
MESSAGE_READ: '/api/message/read',
MESSAGE_UNREAD: '/api/message/unread',
STAFF_LOGIN: '/api/staff/login',
STAFF_ORDERS: '/api/staff/orders',
STAFF_ORDER: '/api/staff/order',
@ -29,8 +31,13 @@ export const ORDER_STATUS = {
export const CONFIRM_PRESETS = [
{ label: '去冰', icon: '🧊' },
{ label: '多酸', icon: '🍋' },
{ label: '少甜', icon: '🍬' },
{ label: '多冰', icon: '🧊' },
{ label: '喜酸', icon: '🍋' },
{ label: '去酸', icon: '🍋' },
{ label: '喜甜', icon: '🍬' },
{ label: '去甜', icon: '🍬' },
{ label: '加烈', icon: '🥃' },
{ label: '快做', icon: '⚡' },
{ label: '低度', icon: '🍹' },
]

Loading…
Cancel
Save