Showing
6 changed files
with
1255 additions
and
2 deletions
miniprogram/pages/customer/aiBook.js
0 → 100644
| 1 | +const { | ||
| 2 | + mockReply | ||
| 3 | +} = require('../../services/ai/mock') | ||
| 4 | + | ||
| 5 | +const HINTS = [ | ||
| 6 | + '今晚还有空桌吗?', | ||
| 7 | + '热销菜品有哪些?', | ||
| 8 | + '今天预订情况怎么样?', | ||
| 9 | + '帮我查一下订单', | ||
| 10 | +] | ||
| 11 | + | ||
| 12 | +let typingTimer = null | ||
| 13 | +let recTimer = null | ||
| 14 | +let recStart = 0 | ||
| 15 | +let touchStartY = 0 | ||
| 16 | +let stopped = false | ||
| 17 | +let recordToken = 0 | ||
| 18 | +let recorderBound = false | ||
| 19 | +const recorder = wx.getRecorderManager() | ||
| 20 | + | ||
| 21 | +function bindRecorder() { | ||
| 22 | + if (recorderBound) return | ||
| 23 | + recorderBound = true | ||
| 24 | + recorder.onStop((res) => { | ||
| 25 | + const pages = getCurrentPages() | ||
| 26 | + const page = pages[pages.length - 1] | ||
| 27 | + if (page && typeof page.handleRecordStop === 'function') { | ||
| 28 | + page.handleRecordStop(res) | ||
| 29 | + } | ||
| 30 | + }) | ||
| 31 | + recorder.onError((err) => { | ||
| 32 | + const pages = getCurrentPages() | ||
| 33 | + const page = pages[pages.length - 1] | ||
| 34 | + if (page && typeof page.handleRecordError === 'function') { | ||
| 35 | + page.handleRecordError(err) | ||
| 36 | + } | ||
| 37 | + }) | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +function pad2(n) { | ||
| 41 | + return n < 10 ? `0${n}` : `${n}` | ||
| 42 | +} | ||
| 43 | + | ||
| 44 | +function timeStr(date) { | ||
| 45 | + date = date || new Date() | ||
| 46 | + return `${pad2(date.getHours())}:${pad2(date.getMinutes())}` | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +function uid() { | ||
| 50 | + return `${Date.now()}_${Math.random().toString(36).slice(2, 8)}` | ||
| 51 | +} | ||
| 52 | + | ||
| 53 | +function clearTimers() { | ||
| 54 | + if (typingTimer) { | ||
| 55 | + clearInterval(typingTimer) | ||
| 56 | + typingTimer = null | ||
| 57 | + } | ||
| 58 | + if (recTimer) { | ||
| 59 | + clearInterval(recTimer) | ||
| 60 | + recTimer = null | ||
| 61 | + } | ||
| 62 | +} | ||
| 63 | + | ||
| 64 | +Page({ | ||
| 65 | + data: { | ||
| 66 | + messages: [], | ||
| 67 | + inputText: '', | ||
| 68 | + scrollIntoView: '', | ||
| 69 | + streaming: false, | ||
| 70 | + recording: false, | ||
| 71 | + cancelHint: false, | ||
| 72 | + recTime: '0:00', | ||
| 73 | + confirmVisible: false, | ||
| 74 | + confirmText: '', | ||
| 75 | + recognizing: false, | ||
| 76 | + showHints: true, | ||
| 77 | + hints: HINTS, | ||
| 78 | + storeName: '高新店', | ||
| 79 | + }, | ||
| 80 | + | ||
| 81 | + onLoad() { | ||
| 82 | + bindRecorder() | ||
| 83 | + this._recorderOn = false | ||
| 84 | + this._ignoreRecord = true | ||
| 85 | + this.seedWelcome() | ||
| 86 | + }, | ||
| 87 | + | ||
| 88 | + onUnload() { | ||
| 89 | + clearTimers() | ||
| 90 | + }, | ||
| 91 | + | ||
| 92 | + seedWelcome() { | ||
| 93 | + const welcome = { | ||
| 94 | + id: uid(), | ||
| 95 | + role: 'agent', | ||
| 96 | + text: '你好,我是经营助手。今天想问点什么?比如:"今天营业额多少?"(当前为本地演示数据)', | ||
| 97 | + meta: `经营助手 · ${timeStr()}`, | ||
| 98 | + } | ||
| 99 | + this.setData({ | ||
| 100 | + messages: [welcome] | ||
| 101 | + }, () => this.scrollBottom()) | ||
| 102 | + }, | ||
| 103 | + | ||
| 104 | + scrollBottom() { | ||
| 105 | + this.setData({ | ||
| 106 | + scrollIntoView: '' | ||
| 107 | + }, () => { | ||
| 108 | + this.setData({ | ||
| 109 | + scrollIntoView: 'msg-bottom' | ||
| 110 | + }) | ||
| 111 | + }) | ||
| 112 | + }, | ||
| 113 | + | ||
| 114 | + updateMessage(id, patch) { | ||
| 115 | + const messages = this.data.messages.map((m) => | ||
| 116 | + m.id === id ? Object.assign({}, m, patch) : m | ||
| 117 | + ) | ||
| 118 | + this.setData({ | ||
| 119 | + messages | ||
| 120 | + }) | ||
| 121 | + }, | ||
| 122 | + | ||
| 123 | + onInput(e) { | ||
| 124 | + this.setData({ | ||
| 125 | + inputText: e.detail.value | ||
| 126 | + }) | ||
| 127 | + }, | ||
| 128 | + | ||
| 129 | + onHintTap(e) { | ||
| 130 | + const text = e.currentTarget.dataset.text | ||
| 131 | + this.setData({ | ||
| 132 | + inputText: text, | ||
| 133 | + showHints: false | ||
| 134 | + }) | ||
| 135 | + this.send(text) | ||
| 136 | + }, | ||
| 137 | + | ||
| 138 | + onSend() { | ||
| 139 | + this.send(this.data.inputText) | ||
| 140 | + }, | ||
| 141 | + | ||
| 142 | + onStop() { | ||
| 143 | + stopped = true | ||
| 144 | + }, | ||
| 145 | + | ||
| 146 | + send(raw) { | ||
| 147 | + const q = (raw || '').trim() | ||
| 148 | + if (!q || this.data.streaming || this.data.recording) return | ||
| 149 | + | ||
| 150 | + const userMsg = { | ||
| 151 | + id: uid(), | ||
| 152 | + role: 'user', | ||
| 153 | + text: q, | ||
| 154 | + meta: timeStr(), | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + this.setData({ | ||
| 158 | + messages: this.data.messages.concat([userMsg]), | ||
| 159 | + inputText: '', | ||
| 160 | + showHints: false, | ||
| 161 | + confirmVisible: false, | ||
| 162 | + }, | ||
| 163 | + () => { | ||
| 164 | + this.scrollBottom() | ||
| 165 | + setTimeout(() => this.streamMock(mockReply(q)), 400) | ||
| 166 | + } | ||
| 167 | + ) | ||
| 168 | + }, | ||
| 169 | + | ||
| 170 | + streamMock(reply) { | ||
| 171 | + const id = uid() | ||
| 172 | + const holder = { | ||
| 173 | + id, | ||
| 174 | + role: 'agent', | ||
| 175 | + text: '', | ||
| 176 | + meta: `经营助手 · ${timeStr()}`, | ||
| 177 | + typing: true, | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + stopped = false | ||
| 181 | + this.setData({ | ||
| 182 | + messages: this.data.messages.concat([holder]), | ||
| 183 | + streaming: true, | ||
| 184 | + }, | ||
| 185 | + () => this.scrollBottom() | ||
| 186 | + ) | ||
| 187 | + | ||
| 188 | + let i = 0 | ||
| 189 | + const full = reply.text | ||
| 190 | + if (typingTimer) clearInterval(typingTimer) | ||
| 191 | + | ||
| 192 | + typingTimer = setInterval(() => { | ||
| 193 | + if (stopped) { | ||
| 194 | + clearInterval(typingTimer) | ||
| 195 | + typingTimer = null | ||
| 196 | + const current = this.data.messages.find((m) => m.id === id) | ||
| 197 | + this.updateMessage(id, { | ||
| 198 | + typing: false, | ||
| 199 | + showCaret: false, | ||
| 200 | + stopped: true, | ||
| 201 | + text: ((current && current.text) || '').replace(/\s+$/, '') || full.slice(0, Math.max(i, 2)), | ||
| 202 | + }) | ||
| 203 | + this.setData({ | ||
| 204 | + streaming: false | ||
| 205 | + }) | ||
| 206 | + this.scrollBottom() | ||
| 207 | + return | ||
| 208 | + } | ||
| 209 | + | ||
| 210 | + i += 2 | ||
| 211 | + this.updateMessage(id, { | ||
| 212 | + typing: false, | ||
| 213 | + showCaret: i < full.length, | ||
| 214 | + text: full.slice(0, i), | ||
| 215 | + }) | ||
| 216 | + this.scrollBottom() | ||
| 217 | + | ||
| 218 | + if (i >= full.length) { | ||
| 219 | + clearInterval(typingTimer) | ||
| 220 | + typingTimer = null | ||
| 221 | + this.updateMessage(id, { | ||
| 222 | + typing: false, | ||
| 223 | + showCaret: false, | ||
| 224 | + text: full, | ||
| 225 | + card: reply.card, | ||
| 226 | + }) | ||
| 227 | + this.setData({ | ||
| 228 | + streaming: false | ||
| 229 | + }) | ||
| 230 | + this.scrollBottom() | ||
| 231 | + } | ||
| 232 | + }, 24) | ||
| 233 | + }, | ||
| 234 | + | ||
| 235 | + onVoiceStart(e) { | ||
| 236 | + if (this.data.streaming || this.data.recognizing) return | ||
| 237 | + const touch = e.touches[0] | ||
| 238 | + touchStartY = touch ? touch.clientY : 0 | ||
| 239 | + recStart = Date.now() | ||
| 240 | + recordToken += 1 | ||
| 241 | + this._recordToken = recordToken | ||
| 242 | + this._ignoreRecord = false | ||
| 243 | + this.setData({ | ||
| 244 | + recording: true, | ||
| 245 | + cancelHint: false, | ||
| 246 | + recTime: '0:00', | ||
| 247 | + confirmVisible: false, | ||
| 248 | + confirmText: '', | ||
| 249 | + }) | ||
| 250 | + | ||
| 251 | + if (recTimer) clearInterval(recTimer) | ||
| 252 | + recTimer = setInterval(() => { | ||
| 253 | + const s = Math.floor((Date.now() - recStart) / 1000) | ||
| 254 | + this.setData({ | ||
| 255 | + recTime: `0:${pad2(s)}` | ||
| 256 | + }) | ||
| 257 | + }, 200) | ||
| 258 | + | ||
| 259 | + try { | ||
| 260 | + recorder.start({ | ||
| 261 | + duration: 60000, | ||
| 262 | + sampleRate: 16000, | ||
| 263 | + numberOfChannels: 1, | ||
| 264 | + encodeBitRate: 48000, | ||
| 265 | + format: 'mp3', | ||
| 266 | + }) | ||
| 267 | + this._recorderOn = true | ||
| 268 | + } catch (err) { | ||
| 269 | + this._recorderOn = false | ||
| 270 | + this._ignoreRecord = true | ||
| 271 | + this.setData({ | ||
| 272 | + recording: false | ||
| 273 | + }) | ||
| 274 | + wx.showToast({ | ||
| 275 | + title: '无法开始录音', | ||
| 276 | + icon: 'none' | ||
| 277 | + }) | ||
| 278 | + } | ||
| 279 | + }, | ||
| 280 | + | ||
| 281 | + onVoiceMove(e) { | ||
| 282 | + if (!this.data.recording) return | ||
| 283 | + const touch = e.touches[0] | ||
| 284 | + if (!touch) return | ||
| 285 | + const dy = touchStartY - touch.clientY | ||
| 286 | + const cancelHint = dy > 60 | ||
| 287 | + if (cancelHint !== this.data.cancelHint) { | ||
| 288 | + this.setData({ | ||
| 289 | + cancelHint | ||
| 290 | + }) | ||
| 291 | + } | ||
| 292 | + }, | ||
| 293 | + | ||
| 294 | + onVoiceCancel() { | ||
| 295 | + this.finishVoice(true) | ||
| 296 | + }, | ||
| 297 | + | ||
| 298 | + onVoiceEnd() { | ||
| 299 | + this.finishVoice(this.data.cancelHint) | ||
| 300 | + }, | ||
| 301 | + | ||
| 302 | + finishVoice(cancelled) { | ||
| 303 | + if (!this.data.recording) return | ||
| 304 | + if (recTimer) { | ||
| 305 | + clearInterval(recTimer) | ||
| 306 | + recTimer = null | ||
| 307 | + } | ||
| 308 | + const dur = Math.round((Date.now() - recStart) / 1000) | ||
| 309 | + const tooShort = !cancelled && dur < 1 | ||
| 310 | + this._ignoreRecord = cancelled || tooShort | ||
| 311 | + this._pendingToken = this._ignoreRecord ? 0 : this._recordToken | ||
| 312 | + this.setData({ | ||
| 313 | + recording: false, | ||
| 314 | + cancelHint: false | ||
| 315 | + }) | ||
| 316 | + | ||
| 317 | + if (this._recorderOn) { | ||
| 318 | + this._recorderOn = false | ||
| 319 | + try { | ||
| 320 | + recorder.stop() | ||
| 321 | + } catch (err) { | ||
| 322 | + // ignore | ||
| 323 | + } | ||
| 324 | + } | ||
| 325 | + | ||
| 326 | + if (cancelled) return | ||
| 327 | + if (tooShort) { | ||
| 328 | + wx.showToast({ | ||
| 329 | + title: '录音太短,请重说', | ||
| 330 | + icon: 'none' | ||
| 331 | + }) | ||
| 332 | + } | ||
| 333 | + }, | ||
| 334 | + | ||
| 335 | + handleRecordStop(res) { | ||
| 336 | + if (this._ignoreRecord || !this._pendingToken || this._pendingToken !== this._recordToken) return | ||
| 337 | + this._pendingToken = 0 | ||
| 338 | + if (!(res && res.tempFilePath)) { | ||
| 339 | + wx.showToast({ | ||
| 340 | + title: '没有录到声音', | ||
| 341 | + icon: 'none' | ||
| 342 | + }) | ||
| 343 | + return | ||
| 344 | + } | ||
| 345 | + // 演示阶段不接语音识别,后续接后端接口 | ||
| 346 | + wx.showToast({ | ||
| 347 | + title: '演示模式暂不支持语音', | ||
| 348 | + icon: 'none' | ||
| 349 | + }) | ||
| 350 | + }, | ||
| 351 | + | ||
| 352 | + handleRecordError(err) { | ||
| 353 | + this._recorderOn = false | ||
| 354 | + this._ignoreRecord = true | ||
| 355 | + if (recTimer) { | ||
| 356 | + clearInterval(recTimer) | ||
| 357 | + recTimer = null | ||
| 358 | + } | ||
| 359 | + this.setData({ | ||
| 360 | + recording: false, | ||
| 361 | + recognizing: false | ||
| 362 | + }) | ||
| 363 | + const msg = (err && err.errMsg) || '' | ||
| 364 | + if (/auth|deny|permission/i.test(msg)) { | ||
| 365 | + wx.showModal({ | ||
| 366 | + title: '需要麦克风权限', | ||
| 367 | + content: '请在设置中允许使用麦克风,再按住说话。', | ||
| 368 | + confirmText: '去设置', | ||
| 369 | + success(res) { | ||
| 370 | + if (res.confirm) wx.openSetting() | ||
| 371 | + }, | ||
| 372 | + }) | ||
| 373 | + return | ||
| 374 | + } | ||
| 375 | + if (this.data.recording || /start/i.test(msg)) { | ||
| 376 | + wx.showToast({ | ||
| 377 | + title: '录音失败,请重试', | ||
| 378 | + icon: 'none' | ||
| 379 | + }) | ||
| 380 | + } | ||
| 381 | + }, | ||
| 382 | + | ||
| 383 | + onConfirmInput(e) { | ||
| 384 | + this.setData({ | ||
| 385 | + confirmText: e.detail.value | ||
| 386 | + }) | ||
| 387 | + }, | ||
| 388 | + | ||
| 389 | + onCancelConfirm() { | ||
| 390 | + this.setData({ | ||
| 391 | + confirmVisible: false, | ||
| 392 | + confirmText: '' | ||
| 393 | + }) | ||
| 394 | + }, | ||
| 395 | + | ||
| 396 | + onOkConfirm() { | ||
| 397 | + const t = this.data.confirmText.trim() | ||
| 398 | + this.setData({ | ||
| 399 | + confirmVisible: false, | ||
| 400 | + confirmText: '' | ||
| 401 | + }) | ||
| 402 | + if (t) this.send(t) | ||
| 403 | + }, | ||
| 404 | +}) |
miniprogram/pages/customer/aiBook.json
0 → 100644
miniprogram/pages/customer/aiBook.wxml
0 → 100644
| 1 | +<view class="page"> | ||
| 2 | + <nav backgroundColor="#FFFFFF" color="#1A1B1C"> | ||
| 3 | + <view class="nav-title"> | ||
| 4 | + <text class="nav-title-main">AI 经营助手</text> | ||
| 5 | + <text class="nav-title-sub">{{storeName}}</text> | ||
| 6 | + </view> | ||
| 7 | + </nav> | ||
| 8 | + | ||
| 9 | + <scroll-view class="msg-list" scroll-y scroll-with-animation scroll-into-view="{{scrollIntoView}}" enhanced show-scrollbar="{{false}}"> | ||
| 10 | + <view class="msg-list-inner"> | ||
| 11 | + <block wx:for="{{messages}}" wx:key="id"> | ||
| 12 | + <view id="msg-{{item.id}}" class="msg {{item.role}}"> | ||
| 13 | + <view class="bubble"> | ||
| 14 | + <view wx:if="{{item.typing}}" class="typing"> | ||
| 15 | + <view class="dot"></view> | ||
| 16 | + <view class="dot"></view> | ||
| 17 | + <view class="dot"></view> | ||
| 18 | + </view> | ||
| 19 | + <block wx:else> | ||
| 20 | + <view class="bubble-body"> | ||
| 21 | + <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> | ||
| 22 | + </view> | ||
| 23 | + </block> | ||
| 24 | + | ||
| 25 | + <!-- KPI 卡片 --> | ||
| 26 | + <view wx:if="{{item.card && item.card.type === 'kpi'}}" class="card"> | ||
| 27 | + <view class="c-title">{{item.card.title}}</view> | ||
| 28 | + <view class="kpis"> | ||
| 29 | + <view class="kpi" wx:for="{{item.card.items}}" wx:for-item="kpi" wx:key="label"> | ||
| 30 | + <view class="k-label">{{kpi.label}}</view> | ||
| 31 | + <view class="k-val">{{kpi.value}}</view> | ||
| 32 | + <view class="k-delta {{kpi.trend}}">{{kpi.delta}}</view> | ||
| 33 | + </view> | ||
| 34 | + </view> | ||
| 35 | + </view> | ||
| 36 | + | ||
| 37 | + <!-- 行列表卡片(桌位 / 预订 / 订单) --> | ||
| 38 | + <view wx:if="{{item.card && item.card.type === 'rows'}}" class="card"> | ||
| 39 | + <view class="c-title">{{item.card.title}}</view> | ||
| 40 | + <view class="row" wx:for="{{item.card.rows}}" wx:for-item="row" wx:key="label"> | ||
| 41 | + <text class="row-label">{{row.label}}</text> | ||
| 42 | + <text class="row-status {{row.status}}">{{row.statusText}}</text> | ||
| 43 | + </view> | ||
| 44 | + </view> | ||
| 45 | + | ||
| 46 | + <!-- 排行卡片 --> | ||
| 47 | + <view wx:if="{{item.card && item.card.type === 'rank'}}" class="card"> | ||
| 48 | + <view class="c-title">{{item.card.title}}</view> | ||
| 49 | + <view class="rank" wx:for="{{item.card.items}}" wx:for-item="rank" wx:key="name"> | ||
| 50 | + <view class="no">{{rank.no}}</view> | ||
| 51 | + <text class="rank-name">{{rank.name}}</text> | ||
| 52 | + <view class="bar"> | ||
| 53 | + <view class="bar-fill" style="width: {{rank.percent}};"></view> | ||
| 54 | + </view> | ||
| 55 | + <text class="rank-val">{{rank.value}}</text> | ||
| 56 | + </view> | ||
| 57 | + </view> | ||
| 58 | + </view> | ||
| 59 | + <view class="meta">{{item.meta}}</view> | ||
| 60 | + </view> | ||
| 61 | + </block> | ||
| 62 | + <view id="msg-bottom" class="msg-bottom"></view> | ||
| 63 | + </view> | ||
| 64 | + </scroll-view> | ||
| 65 | + | ||
| 66 | + <!-- 快捷提问 --> | ||
| 67 | + <scroll-view wx:if="{{showHints}}" class="hints" scroll-x enable-flex show-scrollbar="{{false}}"> | ||
| 68 | + <view class="hint-btn" wx:for="{{hints}}" wx:key="*this" data-text="{{item}}" bindtap="onHintTap">{{item}}</view> | ||
| 69 | + </scroll-view> | ||
| 70 | + | ||
| 71 | + <!-- 语音转写确认 --> | ||
| 72 | + <view wx:if="{{confirmVisible}}" class="confirm-box"> | ||
| 73 | + <textarea class="confirm-text" value="{{confirmText}}" maxlength="-1" auto-height placeholder="{{recognizing ? '正在识别…' : '转写结果可在这里修改…'}}" disabled="{{recognizing}}" bindinput="onConfirmInput" /> | ||
| 74 | + <view class="confirm-bar"> | ||
| 75 | + <view class="cbtn cancel" bindtap="onCancelConfirm">取消</view> | ||
| 76 | + <view class="cbtn ok" bindtap="onOkConfirm">确认发送</view> | ||
| 77 | + </view> | ||
| 78 | + </view> | ||
| 79 | + | ||
| 80 | + <!-- 输入区 --> | ||
| 81 | + <view class="inputbar"> | ||
| 82 | + <input class="text-input" value="{{inputText}}" placeholder="问点什么…" confirm-type="send" bindinput="onInput" bindconfirm="onSend" disabled="{{recording}}" /> | ||
| 83 | + <view class="ibtn voice {{recording ? 'recording' : ''}}" catchtouchstart="onVoiceStart" catchtouchend="onVoiceEnd" catchtouchcancel="onVoiceCancel" catchtouchmove="onVoiceMove">🎤</view> | ||
| 84 | + <view wx:if="{{streaming}}" class="stop-btn" bindtap="onStop">停止</view> | ||
| 85 | + <view wx:else class="ibtn send" bindtap="onSend">➤</view> | ||
| 86 | + </view> | ||
| 87 | + | ||
| 88 | + <!-- 录音浮层 --> | ||
| 89 | + <view class="record-overlay {{recording ? 'show' : ''}} {{cancelHint ? 'cancel' : ''}}"> | ||
| 90 | + <view class="rec-time">{{recTime}}</view> | ||
| 91 | + <view class="wave"> | ||
| 92 | + <view class="wave-bar"></view> | ||
| 93 | + <view class="wave-bar"></view> | ||
| 94 | + <view class="wave-bar"></view> | ||
| 95 | + <view class="wave-bar"></view> | ||
| 96 | + <view class="wave-bar"></view> | ||
| 97 | + <view class="wave-bar"></view> | ||
| 98 | + <view class="wave-bar"></view> | ||
| 99 | + </view> | ||
| 100 | + <view class="rec-hint">{{cancelHint ? '松开取消' : '松开发送 · 上滑取消'}}</view> | ||
| 101 | + </view> | ||
| 102 | +</view> |
miniprogram/pages/customer/aiBook.wxss
0 → 100644
| 1 | +page { | ||
| 2 | + height: 100%; | ||
| 3 | + background: linear-gradient(160deg, #edf4f8 0%, #f4f3ee 100%); | ||
| 4 | +} | ||
| 5 | + | ||
| 6 | +.page { | ||
| 7 | + height: 100vh; | ||
| 8 | + display: flex; | ||
| 9 | + flex-direction: column; | ||
| 10 | + box-sizing: border-box; | ||
| 11 | + background: linear-gradient(160deg, #edf4f8 0%, #f4f3ee 100%); | ||
| 12 | + position: relative; | ||
| 13 | +} | ||
| 14 | + | ||
| 15 | +.nav-title { | ||
| 16 | + display: flex; | ||
| 17 | + flex-direction: column; | ||
| 18 | + align-items: center; | ||
| 19 | + justify-content: center; | ||
| 20 | + height: 100%; | ||
| 21 | + line-height: 1.15; | ||
| 22 | + white-space: normal; | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +.nav-title-main { | ||
| 26 | + font-size: 30rpx; | ||
| 27 | + font-weight: 600; | ||
| 28 | + color: #1a1b1c; | ||
| 29 | +} | ||
| 30 | + | ||
| 31 | +.nav-title-sub { | ||
| 32 | + font-size: 20rpx; | ||
| 33 | + color: #6b7280; | ||
| 34 | + margin-top: 2rpx; | ||
| 35 | + font-weight: 400; | ||
| 36 | +} | ||
| 37 | + | ||
| 38 | +.msg-list { | ||
| 39 | + flex: 1; | ||
| 40 | + height: 0; | ||
| 41 | + padding: 0 20rpx; | ||
| 42 | + box-sizing: border-box; | ||
| 43 | +} | ||
| 44 | + | ||
| 45 | +.msg-list-inner { | ||
| 46 | + padding: 24rpx 0 16rpx; | ||
| 47 | + display: flex; | ||
| 48 | + flex-direction: column; | ||
| 49 | + gap: 24rpx; | ||
| 50 | +} | ||
| 51 | + | ||
| 52 | +.msg { | ||
| 53 | + max-width: 88%; | ||
| 54 | + display: flex; | ||
| 55 | + flex-direction: column; | ||
| 56 | + animation: msgIn 0.4s cubic-bezier(0.22, 0.9, 0.3, 1) both; | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +.msg.user { | ||
| 60 | + align-self: flex-end; | ||
| 61 | +} | ||
| 62 | + | ||
| 63 | +.msg.agent { | ||
| 64 | + align-self: flex-start; | ||
| 65 | +} | ||
| 66 | + | ||
| 67 | +@keyframes msgIn { | ||
| 68 | + from { | ||
| 69 | + opacity: 0; | ||
| 70 | + transform: translateY(14px) scale(0.96); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + to { | ||
| 74 | + opacity: 1; | ||
| 75 | + transform: none; | ||
| 76 | + } | ||
| 77 | +} | ||
| 78 | + | ||
| 79 | +.bubble { | ||
| 80 | + padding: 18rpx 24rpx; | ||
| 81 | + font-size: 28rpx; | ||
| 82 | + line-height: 1.55; | ||
| 83 | + color: #1a1b1c; | ||
| 84 | + word-break: break-word; | ||
| 85 | +} | ||
| 86 | + | ||
| 87 | +.msg.user .bubble { | ||
| 88 | + background: #a3d5e8; | ||
| 89 | + border-radius: 28rpx 28rpx 6rpx 28rpx; | ||
| 90 | +} | ||
| 91 | + | ||
| 92 | +.msg.agent .bubble { | ||
| 93 | + background: rgba(26, 27, 28, 0.05); | ||
| 94 | + border-radius: 28rpx 28rpx 28rpx 6rpx; | ||
| 95 | +} | ||
| 96 | + | ||
| 97 | +.bubble-body { | ||
| 98 | + display: inline; | ||
| 99 | +} | ||
| 100 | + | ||
| 101 | +.bubble-text { | ||
| 102 | + white-space: pre-wrap; | ||
| 103 | +} | ||
| 104 | + | ||
| 105 | +.meta { | ||
| 106 | + font-size: 20rpx; | ||
| 107 | + color: #9ca3af; | ||
| 108 | + margin-top: 8rpx; | ||
| 109 | + padding: 0 8rpx; | ||
| 110 | +} | ||
| 111 | + | ||
| 112 | +.msg.user .meta { | ||
| 113 | + text-align: right; | ||
| 114 | +} | ||
| 115 | + | ||
| 116 | +.typing { | ||
| 117 | + display: inline-flex; | ||
| 118 | + gap: 8rpx; | ||
| 119 | + align-items: center; | ||
| 120 | + padding: 6rpx 4rpx; | ||
| 121 | + height: 28rpx; | ||
| 122 | +} | ||
| 123 | + | ||
| 124 | +.dot { | ||
| 125 | + width: 10rpx; | ||
| 126 | + height: 10rpx; | ||
| 127 | + border-radius: 50%; | ||
| 128 | + background: #6b7280; | ||
| 129 | + animation: blink 1.2s infinite; | ||
| 130 | +} | ||
| 131 | + | ||
| 132 | +.dot:nth-child(2) { | ||
| 133 | + animation-delay: 0.2s; | ||
| 134 | +} | ||
| 135 | + | ||
| 136 | +.dot:nth-child(3) { | ||
| 137 | + animation-delay: 0.4s; | ||
| 138 | +} | ||
| 139 | + | ||
| 140 | +@keyframes blink { | ||
| 141 | + | ||
| 142 | + 0%, | ||
| 143 | + 60%, | ||
| 144 | + 100% { | ||
| 145 | + opacity: 0.25; | ||
| 146 | + transform: translateY(0); | ||
| 147 | + } | ||
| 148 | + | ||
| 149 | + 30% { | ||
| 150 | + opacity: 1; | ||
| 151 | + transform: translateY(-6rpx); | ||
| 152 | + } | ||
| 153 | +} | ||
| 154 | + | ||
| 155 | +.caret { | ||
| 156 | + display: inline; | ||
| 157 | + color: #1a1b1c; | ||
| 158 | + font-weight: 400; | ||
| 159 | + margin-left: 2rpx; | ||
| 160 | + animation: caretBlink 0.8s step-end infinite; | ||
| 161 | +} | ||
| 162 | + | ||
| 163 | +@keyframes caretBlink { | ||
| 164 | + 50% { | ||
| 165 | + opacity: 0; | ||
| 166 | + } | ||
| 167 | +} | ||
| 168 | + | ||
| 169 | +.stopped-tag { | ||
| 170 | + color: #9ca3af; | ||
| 171 | + font-size: 20rpx; | ||
| 172 | +} | ||
| 173 | + | ||
| 174 | +.card { | ||
| 175 | + margin-top: 16rpx; | ||
| 176 | + background: #fff; | ||
| 177 | + border-radius: 20rpx; | ||
| 178 | + padding: 20rpx; | ||
| 179 | + border: 1rpx solid rgba(0, 0, 0, 0.06); | ||
| 180 | + animation: cardIn 0.5s cubic-bezier(0.22, 0.9, 0.3, 1) both; | ||
| 181 | +} | ||
| 182 | + | ||
| 183 | +@keyframes cardIn { | ||
| 184 | + from { | ||
| 185 | + opacity: 0; | ||
| 186 | + transform: translateY(8px) scale(0.97); | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + to { | ||
| 190 | + opacity: 1; | ||
| 191 | + transform: none; | ||
| 192 | + } | ||
| 193 | +} | ||
| 194 | + | ||
| 195 | +.c-title { | ||
| 196 | + font-size: 22rpx; | ||
| 197 | + color: #6b7280; | ||
| 198 | + margin-bottom: 16rpx; | ||
| 199 | +} | ||
| 200 | + | ||
| 201 | +.kpis { | ||
| 202 | + display: flex; | ||
| 203 | + gap: 12rpx; | ||
| 204 | + flex-wrap: wrap; | ||
| 205 | +} | ||
| 206 | + | ||
| 207 | +.kpi { | ||
| 208 | + flex: 1 1 140rpx; | ||
| 209 | + min-width: 0; | ||
| 210 | +} | ||
| 211 | + | ||
| 212 | +.k-label { | ||
| 213 | + font-size: 20rpx; | ||
| 214 | + color: #6b7280; | ||
| 215 | +} | ||
| 216 | + | ||
| 217 | +.k-val { | ||
| 218 | + font-size: 28rpx; | ||
| 219 | + font-weight: 700; | ||
| 220 | + color: #1a1b1c; | ||
| 221 | + margin-top: 4rpx; | ||
| 222 | +} | ||
| 223 | + | ||
| 224 | +.k-delta { | ||
| 225 | + font-size: 20rpx; | ||
| 226 | + margin-top: 2rpx; | ||
| 227 | +} | ||
| 228 | + | ||
| 229 | +.k-delta.up { | ||
| 230 | + color: #52c41a; | ||
| 231 | +} | ||
| 232 | + | ||
| 233 | +.k-delta.down { | ||
| 234 | + color: #ea6668; | ||
| 235 | +} | ||
| 236 | + | ||
| 237 | +.k-delta.flat { | ||
| 238 | + color: #6b7280; | ||
| 239 | +} | ||
| 240 | + | ||
| 241 | +.row { | ||
| 242 | + display: flex; | ||
| 243 | + justify-content: space-between; | ||
| 244 | + align-items: center; | ||
| 245 | + font-size: 24rpx; | ||
| 246 | + color: #3d4a5c; | ||
| 247 | + padding: 10rpx 0; | ||
| 248 | + border-bottom: 1rpx dashed rgba(0, 0, 0, 0.06); | ||
| 249 | +} | ||
| 250 | + | ||
| 251 | +.row:last-child { | ||
| 252 | + border-bottom: none; | ||
| 253 | +} | ||
| 254 | + | ||
| 255 | +.row-label { | ||
| 256 | + flex: 1; | ||
| 257 | + padding-right: 16rpx; | ||
| 258 | +} | ||
| 259 | + | ||
| 260 | +.row-status.ok { | ||
| 261 | + color: #52c41a; | ||
| 262 | +} | ||
| 263 | + | ||
| 264 | +.row-status.warn { | ||
| 265 | + color: #faad14; | ||
| 266 | +} | ||
| 267 | + | ||
| 268 | +.row-status.bad { | ||
| 269 | + color: #ea6668; | ||
| 270 | +} | ||
| 271 | + | ||
| 272 | +.rank { | ||
| 273 | + display: flex; | ||
| 274 | + align-items: center; | ||
| 275 | + gap: 12rpx; | ||
| 276 | + font-size: 24rpx; | ||
| 277 | + color: #3d4a5c; | ||
| 278 | + padding: 10rpx 0; | ||
| 279 | +} | ||
| 280 | + | ||
| 281 | +.no { | ||
| 282 | + width: 36rpx; | ||
| 283 | + height: 36rpx; | ||
| 284 | + border-radius: 50%; | ||
| 285 | + background: rgba(163, 213, 232, 0.35); | ||
| 286 | + color: #1a1b1c; | ||
| 287 | + font-size: 20rpx; | ||
| 288 | + font-weight: 700; | ||
| 289 | + display: flex; | ||
| 290 | + align-items: center; | ||
| 291 | + justify-content: center; | ||
| 292 | + flex: 0 0 auto; | ||
| 293 | +} | ||
| 294 | + | ||
| 295 | +.rank-name { | ||
| 296 | + width: 160rpx; | ||
| 297 | + flex-shrink: 0; | ||
| 298 | + overflow: hidden; | ||
| 299 | + text-overflow: ellipsis; | ||
| 300 | + white-space: nowrap; | ||
| 301 | +} | ||
| 302 | + | ||
| 303 | +.bar { | ||
| 304 | + flex: 1; | ||
| 305 | + height: 12rpx; | ||
| 306 | + border-radius: 6rpx; | ||
| 307 | + background: rgba(163, 213, 232, 0.25); | ||
| 308 | + overflow: hidden; | ||
| 309 | +} | ||
| 310 | + | ||
| 311 | +.bar-fill { | ||
| 312 | + display: block; | ||
| 313 | + height: 100%; | ||
| 314 | + border-radius: 6rpx; | ||
| 315 | + background: #a3d5e8; | ||
| 316 | + animation: barGrow 0.6s ease-out both; | ||
| 317 | +} | ||
| 318 | + | ||
| 319 | +@keyframes barGrow { | ||
| 320 | + from { | ||
| 321 | + width: 0 !important; | ||
| 322 | + } | ||
| 323 | +} | ||
| 324 | + | ||
| 325 | +.rank-val { | ||
| 326 | + flex: 0 0 auto; | ||
| 327 | + font-size: 22rpx; | ||
| 328 | + color: #3d4a5c; | ||
| 329 | +} | ||
| 330 | + | ||
| 331 | +.msg-bottom { | ||
| 332 | + height: 8rpx; | ||
| 333 | +} | ||
| 334 | + | ||
| 335 | +.hints { | ||
| 336 | + white-space: nowrap; | ||
| 337 | + padding: 0 20rpx 12rpx; | ||
| 338 | + height: 64rpx; | ||
| 339 | + box-sizing: border-box; | ||
| 340 | +} | ||
| 341 | + | ||
| 342 | +.hint-btn { | ||
| 343 | + display: inline-block; | ||
| 344 | + background: #a3d5e8; | ||
| 345 | + color: #1a1b1c; | ||
| 346 | + border-radius: 28rpx; | ||
| 347 | + padding: 10rpx 24rpx; | ||
| 348 | + font-size: 22rpx; | ||
| 349 | + margin-right: 12rpx; | ||
| 350 | + vertical-align: middle; | ||
| 351 | +} | ||
| 352 | + | ||
| 353 | +.confirm-box { | ||
| 354 | + padding: 12rpx 20rpx; | ||
| 355 | + background: rgba(255, 255, 255, 0.9); | ||
| 356 | + border-top: 1rpx solid rgba(0, 0, 0, 0.06); | ||
| 357 | + animation: msgIn 0.3s both; | ||
| 358 | +} | ||
| 359 | + | ||
| 360 | +.confirm-text { | ||
| 361 | + width: 100%; | ||
| 362 | + border: 1rpx solid rgba(0, 0, 0, 0.14); | ||
| 363 | + border-radius: 20rpx; | ||
| 364 | + padding: 16rpx 20rpx; | ||
| 365 | + font-size: 26rpx; | ||
| 366 | + color: #1a1b1c; | ||
| 367 | + line-height: 1.5; | ||
| 368 | + min-height: 100rpx; | ||
| 369 | + box-sizing: border-box; | ||
| 370 | + background: #fff; | ||
| 371 | +} | ||
| 372 | + | ||
| 373 | +.confirm-bar { | ||
| 374 | + display: flex; | ||
| 375 | + justify-content: flex-end; | ||
| 376 | + gap: 16rpx; | ||
| 377 | + margin-top: 12rpx; | ||
| 378 | +} | ||
| 379 | + | ||
| 380 | +.cbtn { | ||
| 381 | + border-radius: 28rpx; | ||
| 382 | + padding: 10rpx 28rpx; | ||
| 383 | + font-size: 24rpx; | ||
| 384 | +} | ||
| 385 | + | ||
| 386 | +.cbtn.cancel { | ||
| 387 | + background: rgba(26, 27, 28, 0.06); | ||
| 388 | + color: #3d4a5c; | ||
| 389 | +} | ||
| 390 | + | ||
| 391 | +.cbtn.ok { | ||
| 392 | + background: #1a1b1c; | ||
| 393 | + color: #fff; | ||
| 394 | +} | ||
| 395 | + | ||
| 396 | +.inputbar { | ||
| 397 | + display: flex; | ||
| 398 | + align-items: center; | ||
| 399 | + gap: 16rpx; | ||
| 400 | + padding: 16rpx 20rpx calc(16rpx + env(safe-area-inset-bottom)); | ||
| 401 | + border-top: 1rpx solid rgba(0, 0, 0, 0.08); | ||
| 402 | + background: rgba(255, 255, 255, 0.85); | ||
| 403 | + backdrop-filter: blur(12px); | ||
| 404 | +} | ||
| 405 | + | ||
| 406 | +.text-input { | ||
| 407 | + flex: 1; | ||
| 408 | + border: 1rpx solid rgba(0, 0, 0, 0.14); | ||
| 409 | + border-radius: 40rpx; | ||
| 410 | + padding: 16rpx 28rpx; | ||
| 411 | + font-size: 26rpx; | ||
| 412 | + color: #1a1b1c; | ||
| 413 | + background: #fff; | ||
| 414 | + height: 72rpx; | ||
| 415 | + box-sizing: border-box; | ||
| 416 | +} | ||
| 417 | + | ||
| 418 | +.ibtn { | ||
| 419 | + width: 72rpx; | ||
| 420 | + height: 72rpx; | ||
| 421 | + border-radius: 50%; | ||
| 422 | + display: flex; | ||
| 423 | + align-items: center; | ||
| 424 | + justify-content: center; | ||
| 425 | + font-size: 30rpx; | ||
| 426 | + flex-shrink: 0; | ||
| 427 | +} | ||
| 428 | + | ||
| 429 | +.ibtn.voice { | ||
| 430 | + background: #a3d5e8; | ||
| 431 | +} | ||
| 432 | + | ||
| 433 | +.ibtn.voice.recording { | ||
| 434 | + background: #ea6668; | ||
| 435 | + animation: pulse 1s infinite; | ||
| 436 | +} | ||
| 437 | + | ||
| 438 | +@keyframes pulse { | ||
| 439 | + | ||
| 440 | + 0%, | ||
| 441 | + 100% { | ||
| 442 | + box-shadow: 0 0 0 0 rgba(234, 102, 104, 0.5); | ||
| 443 | + } | ||
| 444 | + | ||
| 445 | + 50% { | ||
| 446 | + box-shadow: 0 0 0 16rpx rgba(234, 102, 104, 0); | ||
| 447 | + } | ||
| 448 | +} | ||
| 449 | + | ||
| 450 | +.ibtn.send { | ||
| 451 | + background: #1a1b1c; | ||
| 452 | + color: #fff; | ||
| 453 | + font-size: 26rpx; | ||
| 454 | +} | ||
| 455 | + | ||
| 456 | +.stop-btn { | ||
| 457 | + background: #f4f3ee; | ||
| 458 | + border: 1rpx solid rgba(0, 0, 0, 0.1); | ||
| 459 | + color: #3d4a5c; | ||
| 460 | + font-size: 22rpx; | ||
| 461 | + border-radius: 32rpx; | ||
| 462 | + padding: 0 24rpx; | ||
| 463 | + height: 72rpx; | ||
| 464 | + display: flex; | ||
| 465 | + align-items: center; | ||
| 466 | + justify-content: center; | ||
| 467 | + flex-shrink: 0; | ||
| 468 | +} | ||
| 469 | + | ||
| 470 | +.record-overlay { | ||
| 471 | + position: absolute; | ||
| 472 | + inset: 0; | ||
| 473 | + background: rgba(255, 255, 255, 0.92); | ||
| 474 | + backdrop-filter: blur(6px); | ||
| 475 | + display: none; | ||
| 476 | + flex-direction: column; | ||
| 477 | + align-items: center; | ||
| 478 | + justify-content: center; | ||
| 479 | + gap: 28rpx; | ||
| 480 | + z-index: 20; | ||
| 481 | +} | ||
| 482 | + | ||
| 483 | +.record-overlay.show { | ||
| 484 | + display: flex; | ||
| 485 | + animation: fadeIn 0.2s both; | ||
| 486 | +} | ||
| 487 | + | ||
| 488 | +.record-overlay.cancel .wave-bar { | ||
| 489 | + background: #ea6668; | ||
| 490 | +} | ||
| 491 | + | ||
| 492 | +.record-overlay.cancel .rec-time, | ||
| 493 | +.record-overlay.cancel .rec-hint { | ||
| 494 | + color: #ea6668; | ||
| 495 | +} | ||
| 496 | + | ||
| 497 | +@keyframes fadeIn { | ||
| 498 | + from { | ||
| 499 | + opacity: 0; | ||
| 500 | + } | ||
| 501 | + | ||
| 502 | + to { | ||
| 503 | + opacity: 1; | ||
| 504 | + } | ||
| 505 | +} | ||
| 506 | + | ||
| 507 | +.rec-time { | ||
| 508 | + font-size: 28rpx; | ||
| 509 | + color: #ea6668; | ||
| 510 | + font-weight: 600; | ||
| 511 | +} | ||
| 512 | + | ||
| 513 | +.wave { | ||
| 514 | + display: flex; | ||
| 515 | + gap: 8rpx; | ||
| 516 | + align-items: center; | ||
| 517 | + height: 88rpx; | ||
| 518 | +} | ||
| 519 | + | ||
| 520 | +.wave-bar { | ||
| 521 | + width: 8rpx; | ||
| 522 | + border-radius: 4rpx; | ||
| 523 | + background: #a3d5e8; | ||
| 524 | + animation: waveJump 1s ease-in-out infinite; | ||
| 525 | +} | ||
| 526 | + | ||
| 527 | +.wave-bar:nth-child(1) { | ||
| 528 | + height: 28rpx; | ||
| 529 | +} | ||
| 530 | + | ||
| 531 | +.wave-bar:nth-child(2) { | ||
| 532 | + height: 60rpx; | ||
| 533 | + animation-delay: 0.1s; | ||
| 534 | +} | ||
| 535 | + | ||
| 536 | +.wave-bar:nth-child(3) { | ||
| 537 | + height: 44rpx; | ||
| 538 | + animation-delay: 0.2s; | ||
| 539 | +} | ||
| 540 | + | ||
| 541 | +.wave-bar:nth-child(4) { | ||
| 542 | + height: 76rpx; | ||
| 543 | + animation-delay: 0.3s; | ||
| 544 | +} | ||
| 545 | + | ||
| 546 | +.wave-bar:nth-child(5) { | ||
| 547 | + height: 52rpx; | ||
| 548 | + animation-delay: 0.15s; | ||
| 549 | +} | ||
| 550 | + | ||
| 551 | +.wave-bar:nth-child(6) { | ||
| 552 | + height: 68rpx; | ||
| 553 | + animation-delay: 0.25s; | ||
| 554 | +} | ||
| 555 | + | ||
| 556 | +.wave-bar:nth-child(7) { | ||
| 557 | + height: 36rpx; | ||
| 558 | + animation-delay: 0.35s; | ||
| 559 | +} | ||
| 560 | + | ||
| 561 | +@keyframes waveJump { | ||
| 562 | + | ||
| 563 | + 0%, | ||
| 564 | + 100% { | ||
| 565 | + transform: scaleY(0.5); | ||
| 566 | + } | ||
| 567 | + | ||
| 568 | + 50% { | ||
| 569 | + transform: scaleY(1); | ||
| 570 | + } | ||
| 571 | +} | ||
| 572 | + | ||
| 573 | +.rec-hint { | ||
| 574 | + font-size: 22rpx; | ||
| 575 | + color: #6b7280; | ||
| 576 | +} |
miniprogram/services/ai/mock.js
0 → 100644
| 1 | +/** 无 API Key 时的本地演示回复 */ | ||
| 2 | +function mockReply(q) { | ||
| 3 | + if (/桌位|空桌|座位/.test(q)) { | ||
| 4 | + return { | ||
| 5 | + text: '已查到今晚 20:00 空闲 4 人桌还有 5 张(共 12 张,其中 2 桌待确认预订)。', | ||
| 6 | + card: { | ||
| 7 | + type: 'rows', | ||
| 8 | + title: '今晚 20:00 · 空桌情况', | ||
| 9 | + rows: [{ | ||
| 10 | + label: '桌 A12 · 4人桌', | ||
| 11 | + statusText: '空', | ||
| 12 | + status: 'ok' | ||
| 13 | + }, | ||
| 14 | + { | ||
| 15 | + label: '桌 B06 · 4人桌', | ||
| 16 | + statusText: '空', | ||
| 17 | + status: 'ok' | ||
| 18 | + }, | ||
| 19 | + { | ||
| 20 | + label: '桌 A18 · 4人桌', | ||
| 21 | + statusText: '待确认', | ||
| 22 | + status: 'warn' | ||
| 23 | + }, | ||
| 24 | + { | ||
| 25 | + label: '桌 C03 · 6人桌', | ||
| 26 | + statusText: '空', | ||
| 27 | + status: 'ok' | ||
| 28 | + }, | ||
| 29 | + ], | ||
| 30 | + }, | ||
| 31 | + } | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + if (/菜品|热销|销量/.test(q)) { | ||
| 35 | + return { | ||
| 36 | + text: '今日热销菜品排行(按销售额):', | ||
| 37 | + card: { | ||
| 38 | + type: 'rank', | ||
| 39 | + title: '今日菜品销售排行', | ||
| 40 | + items: [{ | ||
| 41 | + no: 1, | ||
| 42 | + name: '招牌酸菜鱼', | ||
| 43 | + percent: 92, | ||
| 44 | + value: '¥6,420' | ||
| 45 | + }, | ||
| 46 | + { | ||
| 47 | + no: 2, | ||
| 48 | + name: '口水鸡', | ||
| 49 | + percent: 74, | ||
| 50 | + value: '¥4,180' | ||
| 51 | + }, | ||
| 52 | + { | ||
| 53 | + no: 3, | ||
| 54 | + name: '干锅肥肠', | ||
| 55 | + percent: 58, | ||
| 56 | + value: '¥3,260' | ||
| 57 | + }, | ||
| 58 | + { | ||
| 59 | + no: 4, | ||
| 60 | + name: '时蔬拼盘', | ||
| 61 | + percent: 35, | ||
| 62 | + value: '¥1,940' | ||
| 63 | + }, | ||
| 64 | + ], | ||
| 65 | + }, | ||
| 66 | + } | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + if (/预订/.test(q)) { | ||
| 70 | + return { | ||
| 71 | + text: '今日预订共 18 单:已确认 13 单,未确认 5 单。近 3 单如下:', | ||
| 72 | + card: { | ||
| 73 | + type: 'rows', | ||
| 74 | + title: '今日预订(近3单)', | ||
| 75 | + rows: [{ | ||
| 76 | + label: '王先生 · 18:30 · 4人', | ||
| 77 | + statusText: '已确认', | ||
| 78 | + status: 'ok' | ||
| 79 | + }, | ||
| 80 | + { | ||
| 81 | + label: '李女士 · 19:00 · 6人包间', | ||
| 82 | + statusText: '已确认', | ||
| 83 | + status: 'ok' | ||
| 84 | + }, | ||
| 85 | + { | ||
| 86 | + label: '张先生 · 20:15 · 2人', | ||
| 87 | + statusText: '未确认', | ||
| 88 | + status: 'warn' | ||
| 89 | + }, | ||
| 90 | + ], | ||
| 91 | + }, | ||
| 92 | + } | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + if (/订单/.test(q)) { | ||
| 96 | + return { | ||
| 97 | + text: '最近 3 笔订单:', | ||
| 98 | + card: { | ||
| 99 | + type: 'rows', | ||
| 100 | + title: '近期订单', | ||
| 101 | + rows: [{ | ||
| 102 | + label: '#2026091718 · 桌A12', | ||
| 103 | + statusText: '已支付 ¥386', | ||
| 104 | + status: 'ok' | ||
| 105 | + }, | ||
| 106 | + { | ||
| 107 | + label: '#2026091719 · 桌B06', | ||
| 108 | + statusText: '已支付 ¥258', | ||
| 109 | + status: 'ok' | ||
| 110 | + }, | ||
| 111 | + { | ||
| 112 | + label: '#2026091720 · 桌C03', | ||
| 113 | + statusText: '待支付 ¥412', | ||
| 114 | + status: 'warn' | ||
| 115 | + }, | ||
| 116 | + ], | ||
| 117 | + }, | ||
| 118 | + } | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + if (/营业额|概况|概览/.test(q) || (/今天/.test(q) && !/预订|订单|菜品|热销|空桌/.test(q))) { | ||
| 122 | + return { | ||
| 123 | + text: '今日(9月17日)营业额 28,600 元,环比昨日 +12.3%。', | ||
| 124 | + card: { | ||
| 125 | + type: 'kpi', | ||
| 126 | + title: '今日经营概览', | ||
| 127 | + items: [{ | ||
| 128 | + label: '营业额', | ||
| 129 | + value: '¥28,600', | ||
| 130 | + delta: '↑ 12.3%', | ||
| 131 | + trend: 'up' | ||
| 132 | + }, | ||
| 133 | + { | ||
| 134 | + label: '订单量', | ||
| 135 | + value: '186', | ||
| 136 | + delta: '↑ 8.1%', | ||
| 137 | + trend: 'up' | ||
| 138 | + }, | ||
| 139 | + { | ||
| 140 | + label: '人均消费', | ||
| 141 | + value: '¥154', | ||
| 142 | + delta: '较上周 +4', | ||
| 143 | + trend: 'flat' | ||
| 144 | + }, | ||
| 145 | + ], | ||
| 146 | + }, | ||
| 147 | + } | ||
| 148 | + } | ||
| 149 | + | ||
| 150 | + if (/同比|环比/.test(q)) { | ||
| 151 | + return { | ||
| 152 | + text: '同比去年今日 +8.6%(去年同期 26,335 元);环比昨日 +12.3%。已按今日·高新店口径计算。', | ||
| 153 | + } | ||
| 154 | + } | ||
| 155 | + | ||
| 156 | + return { | ||
| 157 | + text: `收到,正在按「高新店 · 今日」口径查询:${q}。目前我支持经营数据、桌位、预订、订单、菜品销售这几类查询,你可以直接问具体数字。`, | ||
| 158 | + } | ||
| 159 | +} | ||
| 160 | + | ||
| 161 | +module.exports = { | ||
| 162 | + mockReply, | ||
| 163 | +} |
| @@ -340,10 +340,11 @@ module.exports = { | @@ -340,10 +340,11 @@ module.exports = { | ||
| 340 | } | 340 | } |
| 341 | }); | 341 | }); |
| 342 | }, | 342 | }, |
| 343 | - toast: function (title, icon = 'success') { | 343 | + toast: function (title, icon = 'success',duration) { |
| 344 | wx.showToast({ | 344 | wx.showToast({ |
| 345 | title: title, | 345 | title: title, |
| 346 | - icon: icon | 346 | + icon: icon, |
| 347 | + duration, | ||
| 347 | }) | 348 | }) |
| 348 | }, | 349 | }, |
| 349 | showLoading: function (title) { | 350 | showLoading: function (title) { |
-
Please register or login to post a comment