Files
beauty-miniapp-uni/pages/ai/chat.vue
T
2026-06-29 10:54:33 +08:00

191 lines
4.5 KiB
Vue

<template>
<view class="page">
<scroll-view class="msgs" scroll-y :scroll-top="scrollTop">
<view class="container">
<view v-for="m in messages" :key="m.id" class="m" :class="{ me: m.role === 'user' }">
<view class="bubble" :class="{ bme: m.role === 'user' }">
<text class="txt">{{ m.text }}</text>
</view>
</view>
<view class="chips" v-if="showChips">
<view class="chip" @tap="send('推荐爆款项目')">推荐爆款项目</view>
<view class="chip" @tap="send('我想预约今天')">我想预约今天</view>
<view class="chip" @tap="send('敏感肌适合做什么')">敏感肌适合做什么</view>
</view>
</view>
</scroll-view>
<view class="quick row">
<view class="q" @tap="send('查档期')">查档期</view>
<view class="q" @tap="send('看价格')">看价格</view>
<view class="q" @tap="send('我要预约')">我要预约</view>
</view>
<view class="bar">
<view class="voice" @tap="mockVoice">按住说话</view>
<input class="ipt" v-model="text" confirm-type="send" @confirm="send(text)" placeholder="说出你的需求,例如:想做补水/我想预约今天 14:30" />
<view class="send" @tap="send(text)">发送</view>
</view>
</view>
</template>
<script>
import { aiReply } from '@/common/aiRules'
import { projects } from '@/common/mockData'
function mid() {
return `${Date.now()}_${Math.floor(Math.random() * 10000)}`
}
export default {
data() {
return {
projectId: '',
messages: [],
text: '',
scrollTop: 0
}
},
computed: {
showChips() {
return this.messages.length < 4
}
},
onLoad(query) {
this.projectId = query.projectId || ''
const base =
'我是 AI 预约助手。你可以直接说“预约 + 时间 + 项目/需求”,我会把你带到预约/购买流程。'
const ctx = this.projectId ? projects.find((x) => x.id === this.projectId) : null
const intro = ctx ? `当前项目:${ctx.name}。你想预约哪个日期和时段?` : base
this.messages = [
{ id: mid(), role: 'assistant', text: intro }
]
this.bump()
},
methods: {
bump() {
this.$nextTick(() => {
this.scrollTop = this.scrollTop + 99999
})
},
send(raw) {
const v = (raw || '').trim()
if (!v) return
this.messages.push({ id: mid(), role: 'user', text: v })
this.text = ''
const r = aiReply(v)
this.messages.push({ id: mid(), role: 'assistant', text: r.text })
this.bump()
if (r.action?.type === 'go_booking') {
setTimeout(() => {
uni.navigateTo({ url: `/pages/booking/create?projectId=${r.action.projectId}` })
}, 450)
}
},
mockVoice() {
uni.showToast({ title: '语音输入(原型演示)', icon: 'none' })
}
}
}
</script>
<style lang="scss" scoped>
.page {
height: 100vh;
display: flex;
flex-direction: column;
}
.msgs {
flex: 1;
}
.m {
display: flex;
margin: 14rpx 0;
}
.me {
justify-content: flex-end;
}
.bubble {
max-width: 620rpx;
padding: 18rpx 18rpx;
border-radius: 22rpx;
background: #fff;
border: 1rpx solid rgba(17, 24, 39, 0.08);
box-shadow: 0 10rpx 28rpx rgba(17, 24, 39, 0.06);
}
.bme {
background: rgba(59, 130, 246, 0.12);
border-color: rgba(59, 130, 246, 0.22);
}
.txt {
font-size: 28rpx;
line-height: 1.5;
}
.quick {
padding: 12rpx 18rpx 6rpx;
background: #fff;
border-top: 1rpx solid rgba(17, 24, 39, 0.08);
gap: 12rpx;
}
.q {
padding: 12rpx 16rpx;
border-radius: 999rpx;
background: rgba(17, 24, 39, 0.06);
font-size: 26rpx;
font-weight: 800;
}
.bar {
padding: 18rpx;
background: #fff;
display: flex;
gap: 12rpx;
}
.voice {
width: 160rpx;
height: 82rpx;
border-radius: 18rpx;
background: rgba(17, 24, 39, 0.06);
font-size: 24rpx;
font-weight: 900;
color: rgba(17, 24, 39, 0.78);
display: flex;
align-items: center;
justify-content: center;
}
.ipt {
flex: 1;
height: 82rpx;
padding: 0 16rpx;
border-radius: 18rpx;
background: rgba(17, 24, 39, 0.05);
font-size: 28rpx;
}
.send {
width: 140rpx;
height: 82rpx;
border-radius: 18rpx;
background: linear-gradient(135deg, #111827 0%, #3b82f6 100%);
color: #fff;
font-weight: 900;
display: flex;
align-items: center;
justify-content: center;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin: 8rpx 0 12rpx;
}
.chip {
padding: 12rpx 16rpx;
border-radius: 999rpx;
background: rgba(17, 24, 39, 0.06);
font-size: 26rpx;
font-weight: 700;
}
</style>