Commit 56f5feef041a98a311a8bd3a0d1f36bb86b0bf58

Authored by pengye
1 parent 84224915

aiBook页面

const {
mockReply
} = require('../../services/ai/mock')
const HINTS = [
'今晚还有空桌吗?',
'热销菜品有哪些?',
'今天预订情况怎么样?',
'帮我查一下订单',
]
let typingTimer = null
let recTimer = null
let recStart = 0
let touchStartY = 0
let stopped = false
let recordToken = 0
let recorderBound = false
const recorder = wx.getRecorderManager()
function bindRecorder() {
if (recorderBound) return
recorderBound = true
recorder.onStop((res) => {
const pages = getCurrentPages()
const page = pages[pages.length - 1]
if (page && typeof page.handleRecordStop === 'function') {
page.handleRecordStop(res)
}
})
recorder.onError((err) => {
const pages = getCurrentPages()
const page = pages[pages.length - 1]
if (page && typeof page.handleRecordError === 'function') {
page.handleRecordError(err)
}
})
}
function pad2(n) {
return n < 10 ? `0${n}` : `${n}`
}
function timeStr(date) {
date = date || new Date()
return `${pad2(date.getHours())}:${pad2(date.getMinutes())}`
}
function uid() {
return `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
}
function clearTimers() {
if (typingTimer) {
clearInterval(typingTimer)
typingTimer = null
}
if (recTimer) {
clearInterval(recTimer)
recTimer = null
}
}
Page({
data: {
messages: [],
inputText: '',
scrollIntoView: '',
streaming: false,
recording: false,
cancelHint: false,
recTime: '0:00',
confirmVisible: false,
confirmText: '',
recognizing: false,
showHints: true,
hints: HINTS,
storeName: '高新店',
},
onLoad() {
bindRecorder()
this._recorderOn = false
this._ignoreRecord = true
this.seedWelcome()
},
onUnload() {
clearTimers()
},
seedWelcome() {
const welcome = {
id: uid(),
role: 'agent',
text: '你好,我是经营助手。今天想问点什么?比如:"今天营业额多少?"(当前为本地演示数据)',
meta: `经营助手 · ${timeStr()}`,
}
this.setData({
messages: [welcome]
}, () => this.scrollBottom())
},
scrollBottom() {
this.setData({
scrollIntoView: ''
}, () => {
this.setData({
scrollIntoView: 'msg-bottom'
})
})
},
updateMessage(id, patch) {
const messages = this.data.messages.map((m) =>
m.id === id ? Object.assign({}, m, patch) : m
)
this.setData({
messages
})
},
onInput(e) {
this.setData({
inputText: e.detail.value
})
},
onHintTap(e) {
const text = e.currentTarget.dataset.text
this.setData({
inputText: text,
showHints: false
})
this.send(text)
},
onSend() {
this.send(this.data.inputText)
},
onStop() {
stopped = true
},
send(raw) {
const q = (raw || '').trim()
if (!q || this.data.streaming || this.data.recording) return
const userMsg = {
id: uid(),
role: 'user',
text: q,
meta: timeStr(),
}
this.setData({
messages: this.data.messages.concat([userMsg]),
inputText: '',
showHints: false,
confirmVisible: false,
},
() => {
this.scrollBottom()
setTimeout(() => this.streamMock(mockReply(q)), 400)
}
)
},
streamMock(reply) {
const id = uid()
const holder = {
id,
role: 'agent',
text: '',
meta: `经营助手 · ${timeStr()}`,
typing: true,
}
stopped = false
this.setData({
messages: this.data.messages.concat([holder]),
streaming: true,
},
() => this.scrollBottom()
)
let i = 0
const full = reply.text
if (typingTimer) clearInterval(typingTimer)
typingTimer = setInterval(() => {
if (stopped) {
clearInterval(typingTimer)
typingTimer = null
const current = this.data.messages.find((m) => m.id === id)
this.updateMessage(id, {
typing: false,
showCaret: false,
stopped: true,
text: ((current && current.text) || '').replace(/\s+$/, '') || full.slice(0, Math.max(i, 2)),
})
this.setData({
streaming: false
})
this.scrollBottom()
return
}
i += 2
this.updateMessage(id, {
typing: false,
showCaret: i < full.length,
text: full.slice(0, i),
})
this.scrollBottom()
if (i >= full.length) {
clearInterval(typingTimer)
typingTimer = null
this.updateMessage(id, {
typing: false,
showCaret: false,
text: full,
card: reply.card,
})
this.setData({
streaming: false
})
this.scrollBottom()
}
}, 24)
},
onVoiceStart(e) {
if (this.data.streaming || this.data.recognizing) return
const touch = e.touches[0]
touchStartY = touch ? touch.clientY : 0
recStart = Date.now()
recordToken += 1
this._recordToken = recordToken
this._ignoreRecord = false
this.setData({
recording: true,
cancelHint: false,
recTime: '0:00',
confirmVisible: false,
confirmText: '',
})
if (recTimer) clearInterval(recTimer)
recTimer = setInterval(() => {
const s = Math.floor((Date.now() - recStart) / 1000)
this.setData({
recTime: `0:${pad2(s)}`
})
}, 200)
try {
recorder.start({
duration: 60000,
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
format: 'mp3',
})
this._recorderOn = true
} catch (err) {
this._recorderOn = false
this._ignoreRecord = true
this.setData({
recording: false
})
wx.showToast({
title: '无法开始录音',
icon: 'none'
})
}
},
onVoiceMove(e) {
if (!this.data.recording) return
const touch = e.touches[0]
if (!touch) return
const dy = touchStartY - touch.clientY
const cancelHint = dy > 60
if (cancelHint !== this.data.cancelHint) {
this.setData({
cancelHint
})
}
},
onVoiceCancel() {
this.finishVoice(true)
},
onVoiceEnd() {
this.finishVoice(this.data.cancelHint)
},
finishVoice(cancelled) {
if (!this.data.recording) return
if (recTimer) {
clearInterval(recTimer)
recTimer = null
}
const dur = Math.round((Date.now() - recStart) / 1000)
const tooShort = !cancelled && dur < 1
this._ignoreRecord = cancelled || tooShort
this._pendingToken = this._ignoreRecord ? 0 : this._recordToken
this.setData({
recording: false,
cancelHint: false
})
if (this._recorderOn) {
this._recorderOn = false
try {
recorder.stop()
} catch (err) {
// ignore
}
}
if (cancelled) return
if (tooShort) {
wx.showToast({
title: '录音太短,请重说',
icon: 'none'
})
}
},
handleRecordStop(res) {
if (this._ignoreRecord || !this._pendingToken || this._pendingToken !== this._recordToken) return
this._pendingToken = 0
if (!(res && res.tempFilePath)) {
wx.showToast({
title: '没有录到声音',
icon: 'none'
})
return
}
// 演示阶段不接语音识别,后续接后端接口
wx.showToast({
title: '演示模式暂不支持语音',
icon: 'none'
})
},
handleRecordError(err) {
this._recorderOn = false
this._ignoreRecord = true
if (recTimer) {
clearInterval(recTimer)
recTimer = null
}
this.setData({
recording: false,
recognizing: false
})
const msg = (err && err.errMsg) || ''
if (/auth|deny|permission/i.test(msg)) {
wx.showModal({
title: '需要麦克风权限',
content: '请在设置中允许使用麦克风,再按住说话。',
confirmText: '去设置',
success(res) {
if (res.confirm) wx.openSetting()
},
})
return
}
if (this.data.recording || /start/i.test(msg)) {
wx.showToast({
title: '录音失败,请重试',
icon: 'none'
})
}
},
onConfirmInput(e) {
this.setData({
confirmText: e.detail.value
})
},
onCancelConfirm() {
this.setData({
confirmVisible: false,
confirmText: ''
})
},
onOkConfirm() {
const t = this.data.confirmText.trim()
this.setData({
confirmVisible: false,
confirmText: ''
})
if (t) this.send(t)
},
})
\ No newline at end of file
... ...
{
"usingComponents": {
"nav": "/components/nav/nav"
},
"navigationStyle": "custom",
"disableScroll": true
}
... ...
<view class="page">
<nav backgroundColor="#FFFFFF" color="#1A1B1C">
<view class="nav-title">
<text class="nav-title-main">AI 经营助手</text>
<text class="nav-title-sub">{{storeName}}</text>
</view>
</nav>
<scroll-view class="msg-list" scroll-y scroll-with-animation scroll-into-view="{{scrollIntoView}}" enhanced show-scrollbar="{{false}}">
<view class="msg-list-inner">
<block wx:for="{{messages}}" wx:key="id">
<view id="msg-{{item.id}}" class="msg {{item.role}}">
<view class="bubble">
<view wx:if="{{item.typing}}" class="typing">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
<block wx:else>
<view class="bubble-body">
<text class="bubble-text" user-select>{{item.text}}</text><text wx:if="{{item.showCaret}}" class="caret">|</text><text wx:if="{{item.stopped}}" class="stopped-tag"> (已停止)</text>
</view>
</block>
<!-- KPI 卡片 -->
<view wx:if="{{item.card && item.card.type === 'kpi'}}" class="card">
<view class="c-title">{{item.card.title}}</view>
<view class="kpis">
<view class="kpi" wx:for="{{item.card.items}}" wx:for-item="kpi" wx:key="label">
<view class="k-label">{{kpi.label}}</view>
<view class="k-val">{{kpi.value}}</view>
<view class="k-delta {{kpi.trend}}">{{kpi.delta}}</view>
</view>
</view>
</view>
<!-- 行列表卡片(桌位 / 预订 / 订单) -->
<view wx:if="{{item.card && item.card.type === 'rows'}}" class="card">
<view class="c-title">{{item.card.title}}</view>
<view class="row" wx:for="{{item.card.rows}}" wx:for-item="row" wx:key="label">
<text class="row-label">{{row.label}}</text>
<text class="row-status {{row.status}}">{{row.statusText}}</text>
</view>
</view>
<!-- 排行卡片 -->
<view wx:if="{{item.card && item.card.type === 'rank'}}" class="card">
<view class="c-title">{{item.card.title}}</view>
<view class="rank" wx:for="{{item.card.items}}" wx:for-item="rank" wx:key="name">
<view class="no">{{rank.no}}</view>
<text class="rank-name">{{rank.name}}</text>
<view class="bar">
<view class="bar-fill" style="width: {{rank.percent}};"></view>
</view>
<text class="rank-val">{{rank.value}}</text>
</view>
</view>
</view>
<view class="meta">{{item.meta}}</view>
</view>
</block>
<view id="msg-bottom" class="msg-bottom"></view>
</view>
</scroll-view>
<!-- 快捷提问 -->
<scroll-view wx:if="{{showHints}}" class="hints" scroll-x enable-flex show-scrollbar="{{false}}">
<view class="hint-btn" wx:for="{{hints}}" wx:key="*this" data-text="{{item}}" bindtap="onHintTap">{{item}}</view>
</scroll-view>
<!-- 语音转写确认 -->
<view wx:if="{{confirmVisible}}" class="confirm-box">
<textarea class="confirm-text" value="{{confirmText}}" maxlength="-1" auto-height placeholder="{{recognizing ? '正在识别…' : '转写结果可在这里修改…'}}" disabled="{{recognizing}}" bindinput="onConfirmInput" />
<view class="confirm-bar">
<view class="cbtn cancel" bindtap="onCancelConfirm">取消</view>
<view class="cbtn ok" bindtap="onOkConfirm">确认发送</view>
</view>
</view>
<!-- 输入区 -->
<view class="inputbar">
<input class="text-input" value="{{inputText}}" placeholder="问点什么…" confirm-type="send" bindinput="onInput" bindconfirm="onSend" disabled="{{recording}}" />
<view class="ibtn voice {{recording ? 'recording' : ''}}" catchtouchstart="onVoiceStart" catchtouchend="onVoiceEnd" catchtouchcancel="onVoiceCancel" catchtouchmove="onVoiceMove">🎤</view>
<view wx:if="{{streaming}}" class="stop-btn" bindtap="onStop">停止</view>
<view wx:else class="ibtn send" bindtap="onSend">➤</view>
</view>
<!-- 录音浮层 -->
<view class="record-overlay {{recording ? 'show' : ''}} {{cancelHint ? 'cancel' : ''}}">
<view class="rec-time">{{recTime}}</view>
<view class="wave">
<view class="wave-bar"></view>
<view class="wave-bar"></view>
<view class="wave-bar"></view>
<view class="wave-bar"></view>
<view class="wave-bar"></view>
<view class="wave-bar"></view>
<view class="wave-bar"></view>
</view>
<view class="rec-hint">{{cancelHint ? '松开取消' : '松开发送 · 上滑取消'}}</view>
</view>
</view>
\ No newline at end of file
... ...
page {
height: 100%;
background: linear-gradient(160deg, #edf4f8 0%, #f4f3ee 100%);
}
.page {
height: 100vh;
display: flex;
flex-direction: column;
box-sizing: border-box;
background: linear-gradient(160deg, #edf4f8 0%, #f4f3ee 100%);
position: relative;
}
.nav-title {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
line-height: 1.15;
white-space: normal;
}
.nav-title-main {
font-size: 30rpx;
font-weight: 600;
color: #1a1b1c;
}
.nav-title-sub {
font-size: 20rpx;
color: #6b7280;
margin-top: 2rpx;
font-weight: 400;
}
.msg-list {
flex: 1;
height: 0;
padding: 0 20rpx;
box-sizing: border-box;
}
.msg-list-inner {
padding: 24rpx 0 16rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.msg {
max-width: 88%;
display: flex;
flex-direction: column;
animation: msgIn 0.4s cubic-bezier(0.22, 0.9, 0.3, 1) both;
}
.msg.user {
align-self: flex-end;
}
.msg.agent {
align-self: flex-start;
}
@keyframes msgIn {
from {
opacity: 0;
transform: translateY(14px) scale(0.96);
}
to {
opacity: 1;
transform: none;
}
}
.bubble {
padding: 18rpx 24rpx;
font-size: 28rpx;
line-height: 1.55;
color: #1a1b1c;
word-break: break-word;
}
.msg.user .bubble {
background: #a3d5e8;
border-radius: 28rpx 28rpx 6rpx 28rpx;
}
.msg.agent .bubble {
background: rgba(26, 27, 28, 0.05);
border-radius: 28rpx 28rpx 28rpx 6rpx;
}
.bubble-body {
display: inline;
}
.bubble-text {
white-space: pre-wrap;
}
.meta {
font-size: 20rpx;
color: #9ca3af;
margin-top: 8rpx;
padding: 0 8rpx;
}
.msg.user .meta {
text-align: right;
}
.typing {
display: inline-flex;
gap: 8rpx;
align-items: center;
padding: 6rpx 4rpx;
height: 28rpx;
}
.dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: #6b7280;
animation: blink 1.2s infinite;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes blink {
0%,
60%,
100% {
opacity: 0.25;
transform: translateY(0);
}
30% {
opacity: 1;
transform: translateY(-6rpx);
}
}
.caret {
display: inline;
color: #1a1b1c;
font-weight: 400;
margin-left: 2rpx;
animation: caretBlink 0.8s step-end infinite;
}
@keyframes caretBlink {
50% {
opacity: 0;
}
}
.stopped-tag {
color: #9ca3af;
font-size: 20rpx;
}
.card {
margin-top: 16rpx;
background: #fff;
border-radius: 20rpx;
padding: 20rpx;
border: 1rpx solid rgba(0, 0, 0, 0.06);
animation: cardIn 0.5s cubic-bezier(0.22, 0.9, 0.3, 1) both;
}
@keyframes cardIn {
from {
opacity: 0;
transform: translateY(8px) scale(0.97);
}
to {
opacity: 1;
transform: none;
}
}
.c-title {
font-size: 22rpx;
color: #6b7280;
margin-bottom: 16rpx;
}
.kpis {
display: flex;
gap: 12rpx;
flex-wrap: wrap;
}
.kpi {
flex: 1 1 140rpx;
min-width: 0;
}
.k-label {
font-size: 20rpx;
color: #6b7280;
}
.k-val {
font-size: 28rpx;
font-weight: 700;
color: #1a1b1c;
margin-top: 4rpx;
}
.k-delta {
font-size: 20rpx;
margin-top: 2rpx;
}
.k-delta.up {
color: #52c41a;
}
.k-delta.down {
color: #ea6668;
}
.k-delta.flat {
color: #6b7280;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 24rpx;
color: #3d4a5c;
padding: 10rpx 0;
border-bottom: 1rpx dashed rgba(0, 0, 0, 0.06);
}
.row:last-child {
border-bottom: none;
}
.row-label {
flex: 1;
padding-right: 16rpx;
}
.row-status.ok {
color: #52c41a;
}
.row-status.warn {
color: #faad14;
}
.row-status.bad {
color: #ea6668;
}
.rank {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 24rpx;
color: #3d4a5c;
padding: 10rpx 0;
}
.no {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: rgba(163, 213, 232, 0.35);
color: #1a1b1c;
font-size: 20rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
}
.rank-name {
width: 160rpx;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bar {
flex: 1;
height: 12rpx;
border-radius: 6rpx;
background: rgba(163, 213, 232, 0.25);
overflow: hidden;
}
.bar-fill {
display: block;
height: 100%;
border-radius: 6rpx;
background: #a3d5e8;
animation: barGrow 0.6s ease-out both;
}
@keyframes barGrow {
from {
width: 0 !important;
}
}
.rank-val {
flex: 0 0 auto;
font-size: 22rpx;
color: #3d4a5c;
}
.msg-bottom {
height: 8rpx;
}
.hints {
white-space: nowrap;
padding: 0 20rpx 12rpx;
height: 64rpx;
box-sizing: border-box;
}
.hint-btn {
display: inline-block;
background: #a3d5e8;
color: #1a1b1c;
border-radius: 28rpx;
padding: 10rpx 24rpx;
font-size: 22rpx;
margin-right: 12rpx;
vertical-align: middle;
}
.confirm-box {
padding: 12rpx 20rpx;
background: rgba(255, 255, 255, 0.9);
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
animation: msgIn 0.3s both;
}
.confirm-text {
width: 100%;
border: 1rpx solid rgba(0, 0, 0, 0.14);
border-radius: 20rpx;
padding: 16rpx 20rpx;
font-size: 26rpx;
color: #1a1b1c;
line-height: 1.5;
min-height: 100rpx;
box-sizing: border-box;
background: #fff;
}
.confirm-bar {
display: flex;
justify-content: flex-end;
gap: 16rpx;
margin-top: 12rpx;
}
.cbtn {
border-radius: 28rpx;
padding: 10rpx 28rpx;
font-size: 24rpx;
}
.cbtn.cancel {
background: rgba(26, 27, 28, 0.06);
color: #3d4a5c;
}
.cbtn.ok {
background: #1a1b1c;
color: #fff;
}
.inputbar {
display: flex;
align-items: center;
gap: 16rpx;
padding: 16rpx 20rpx calc(16rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid rgba(0, 0, 0, 0.08);
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
}
.text-input {
flex: 1;
border: 1rpx solid rgba(0, 0, 0, 0.14);
border-radius: 40rpx;
padding: 16rpx 28rpx;
font-size: 26rpx;
color: #1a1b1c;
background: #fff;
height: 72rpx;
box-sizing: border-box;
}
.ibtn {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
flex-shrink: 0;
}
.ibtn.voice {
background: #a3d5e8;
}
.ibtn.voice.recording {
background: #ea6668;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(234, 102, 104, 0.5);
}
50% {
box-shadow: 0 0 0 16rpx rgba(234, 102, 104, 0);
}
}
.ibtn.send {
background: #1a1b1c;
color: #fff;
font-size: 26rpx;
}
.stop-btn {
background: #f4f3ee;
border: 1rpx solid rgba(0, 0, 0, 0.1);
color: #3d4a5c;
font-size: 22rpx;
border-radius: 32rpx;
padding: 0 24rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.record-overlay {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(6px);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28rpx;
z-index: 20;
}
.record-overlay.show {
display: flex;
animation: fadeIn 0.2s both;
}
.record-overlay.cancel .wave-bar {
background: #ea6668;
}
.record-overlay.cancel .rec-time,
.record-overlay.cancel .rec-hint {
color: #ea6668;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.rec-time {
font-size: 28rpx;
color: #ea6668;
font-weight: 600;
}
.wave {
display: flex;
gap: 8rpx;
align-items: center;
height: 88rpx;
}
.wave-bar {
width: 8rpx;
border-radius: 4rpx;
background: #a3d5e8;
animation: waveJump 1s ease-in-out infinite;
}
.wave-bar:nth-child(1) {
height: 28rpx;
}
.wave-bar:nth-child(2) {
height: 60rpx;
animation-delay: 0.1s;
}
.wave-bar:nth-child(3) {
height: 44rpx;
animation-delay: 0.2s;
}
.wave-bar:nth-child(4) {
height: 76rpx;
animation-delay: 0.3s;
}
.wave-bar:nth-child(5) {
height: 52rpx;
animation-delay: 0.15s;
}
.wave-bar:nth-child(6) {
height: 68rpx;
animation-delay: 0.25s;
}
.wave-bar:nth-child(7) {
height: 36rpx;
animation-delay: 0.35s;
}
@keyframes waveJump {
0%,
100% {
transform: scaleY(0.5);
}
50% {
transform: scaleY(1);
}
}
.rec-hint {
font-size: 22rpx;
color: #6b7280;
}
\ No newline at end of file
... ...
/** 无 API Key 时的本地演示回复 */
function mockReply(q) {
if (/桌位|空桌|座位/.test(q)) {
return {
text: '已查到今晚 20:00 空闲 4 人桌还有 5 张(共 12 张,其中 2 桌待确认预订)。',
card: {
type: 'rows',
title: '今晚 20:00 · 空桌情况',
rows: [{
label: '桌 A12 · 4人桌',
statusText: '空',
status: 'ok'
},
{
label: '桌 B06 · 4人桌',
statusText: '空',
status: 'ok'
},
{
label: '桌 A18 · 4人桌',
statusText: '待确认',
status: 'warn'
},
{
label: '桌 C03 · 6人桌',
statusText: '空',
status: 'ok'
},
],
},
}
}
if (/菜品|热销|销量/.test(q)) {
return {
text: '今日热销菜品排行(按销售额):',
card: {
type: 'rank',
title: '今日菜品销售排行',
items: [{
no: 1,
name: '招牌酸菜鱼',
percent: 92,
value: '¥6,420'
},
{
no: 2,
name: '口水鸡',
percent: 74,
value: '¥4,180'
},
{
no: 3,
name: '干锅肥肠',
percent: 58,
value: '¥3,260'
},
{
no: 4,
name: '时蔬拼盘',
percent: 35,
value: '¥1,940'
},
],
},
}
}
if (/预订/.test(q)) {
return {
text: '今日预订共 18 单:已确认 13 单,未确认 5 单。近 3 单如下:',
card: {
type: 'rows',
title: '今日预订(近3单)',
rows: [{
label: '王先生 · 18:30 · 4人',
statusText: '已确认',
status: 'ok'
},
{
label: '李女士 · 19:00 · 6人包间',
statusText: '已确认',
status: 'ok'
},
{
label: '张先生 · 20:15 · 2人',
statusText: '未确认',
status: 'warn'
},
],
},
}
}
if (/订单/.test(q)) {
return {
text: '最近 3 笔订单:',
card: {
type: 'rows',
title: '近期订单',
rows: [{
label: '#2026091718 · 桌A12',
statusText: '已支付 ¥386',
status: 'ok'
},
{
label: '#2026091719 · 桌B06',
statusText: '已支付 ¥258',
status: 'ok'
},
{
label: '#2026091720 · 桌C03',
statusText: '待支付 ¥412',
status: 'warn'
},
],
},
}
}
if (/营业额|概况|概览/.test(q) || (/今天/.test(q) && !/预订|订单|菜品|热销|空桌/.test(q))) {
return {
text: '今日(9月17日)营业额 28,600 元,环比昨日 +12.3%。',
card: {
type: 'kpi',
title: '今日经营概览',
items: [{
label: '营业额',
value: '¥28,600',
delta: '↑ 12.3%',
trend: 'up'
},
{
label: '订单量',
value: '186',
delta: '↑ 8.1%',
trend: 'up'
},
{
label: '人均消费',
value: '¥154',
delta: '较上周 +4',
trend: 'flat'
},
],
},
}
}
if (/同比|环比/.test(q)) {
return {
text: '同比去年今日 +8.6%(去年同期 26,335 元);环比昨日 +12.3%。已按今日·高新店口径计算。',
}
}
return {
text: `收到,正在按「高新店 · 今日」口径查询:${q}。目前我支持经营数据、桌位、预订、订单、菜品销售这几类查询,你可以直接问具体数字。`,
}
}
module.exports = {
mockReply,
}
\ No newline at end of file
... ...
... ... @@ -340,10 +340,11 @@ module.exports = {
}
});
},
toast: function (title, icon = 'success') {
toast: function (title, icon = 'success',duration) {
wx.showToast({
title: title,
icon: icon
icon: icon,
duration,
})
},
showLoading: function (title) {
... ...