123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- <template>
- <view class="dialog-generation">
- <view class="subject-matter-ofText" :style="{ paddingBottom: keyboardHeight + 'px' }">
- <!-- 顶部导航栏 -->
- <div class="navbar">
- <view class="navbar-left">
- <image @click="goBack()" class="back" src="../../static/vip/hy_icon_jiantou.png"></image>
- <view class="elf-name">精灵·小萌</view>
- <image class="deepseek" src="../../static/makedetail/deepseek-logo.png"></image>
- </view>
- <view class="navbar-right">
- <image class="primary" src="../../static/makedetail/primary.png"></image>
- <image class="createChat" src="../../static/makedetail/createChat.png"></image>
- </view>
- </div>
- <!-- <view class="navbar-reserveASeat"> </view> -->
- <!-- 聊天内容区 -->
- <scroll-view class="chat-content" :scroll-into-view="toView" scroll-y
- :style="{ height: `calc(100% - ${370 + textareaHeight}rpx)` }" @scroll="onChatScroll">
- <template v-if="messages && messages.length > 0">
- <!-- <scroll-view class="chat-content" scroll-y> -->
- <view v-for="(msg, idx) in messages" :key="idx" :class="['chat-bubble', msg.role]">
- <template v-if="msg.role === 'user'">
- {{ msg.content }}
- </template>
- <template v-else-if="msg.role === 'ai'">
- <view class="ai-bubble-row">
- <view class="ai-avatar">
- <image src="../../static/makedetail/characterProfilePicture.png" mode="aspectFill">
- </image>
- </view>
- <view class="ai-bubble-content">
- <template v-if="idx === lastAiIndex && isLoading">
- <text>{{ displayText }}</text>
- <text v-if="isLoading" class="loading-dot">...</text>
- </template>
- <template v-else>
- <uv-text size="32rpx" color="#fff" :text="msg.content"></uv-text>
- </template>
- </view>
- </view>
- </template>
- </view>
- <view id="bottom-anchor"></view>
- <view v-if="error" class="chat-bubble ai error">{{ error }}</view>
- <image v-if="showToBottomBtn && keyboardHeight === 0" class="to-bottom-btn" @click="scrollToBottom"
- src="../../static/makedetail/toBottomBtn.png"></image>
- </template>
- <template v-else>
- <view class="chat-content-empty">
- <image src="../../static/makedetail/characterProfilePicture.png" mode="aspectFill"></image>
- <view class="chat-content-empty-title">嗨!我是创梦精灵</view>
- <view class="chat-content-empty-desc">与我聊聊你想要生成的角色吧!</view>
- </view>
- </template>
- </scroll-view>
- <!-- <view class="bom-reserveASeat"></view> -->
- <view class="bom-box" :style="{ bottom: 0 + 'px', height: `${190 + textareaHeight}rpx` }">
- <!-- 底部输入区 -->
- <view class="input-bar">
- <textarea :autoHeight="true" class="input-box" :adjust-position="false" v-model="question"
- :disabled="isLoading" placeholder="给我发送消息吧..." @keyup.enter="onSend" maxlength="400"
- confirm-type="send" @input="onTextareaInput" rows="1"
- style="overflow-y:auto;max-height:176rpx;min-height:44rpx;" />
- <button class="send-btn" :disabled="isLoading || !question.trim()" @click="onSend">{{ isLoading ?
- '生成中...' :
- '发送' }}</button>
- </view>
- <!-- 底部提示 -->
- <view class="footer-tip">内容由AI生成,禁用相关功能请联系管理员。</view>
- </view>
- </view>
- </view>
- </template>
- <script>
- export default {
- data() {
- return {
- question: '',
- answer: '',
- displayText: '',
- isComplete: false,
- isLoading: false,
- error: null,
- retryCount: 0,
- maxRetries: 3,
- timer: null,
- lastResponseTime: 0,
- timeout: 30000,
- typingSpeed: 50,
- socketTask: null,
- messages: [
- ],
- lastAiIndex: -1,
- keyboardHeight: 0, // 存储键盘高度
- statusBarHeight: 0,
- windowBottom: 0,
- toView: '',
- showToBottomBtn: false,
- textareaHeight: 0, // 新增:textarea高度
- };
- },
- methods: {
- scrollToBottom() {
- this.toView = '';
- this.$nextTick(() => {
- this.toView = 'bottom-anchor';
- });
- },
- async startStreamAnswer(content) {
- if (!content.trim()) {
- uni.showToast({
- title: '请输入问题',
- icon: 'none'
- });
- return;
- }
- this.resetState();
- // 用户消息入队
- this.messages.push({ role: 'user', content });
- try {
- const response = await fetch(this.$apiHost + '/Work/streamAnswer?content=' + encodeURIComponent(content) + "&uuid=" + getApp().globalData.uuid);
- this.isLoading = true;
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let aiMsg = { role: 'ai', content: '' };
- this.messages.push(aiMsg);
- console.log("this.messages", this.messages);
- this.lastAiIndex = this.messages.length - 1;
- while (true) {
- const { value, done } = await reader.read();
- if (done) break;
- const text = decoder.decode(value);
- console.log("收到的消息", text);
- if (text && text.includes('[DONE]')) {
- this.completeAnswer();
- break;
- }
- this.displayText += text;
- this.answer += text;
- aiMsg.content += text;
- this.retryCount = 0;
- this.lastResponseTime = Date.now();
- this.scrollToBottom();
- }
- } catch (error) {
- this.handleError(error);
- }
- },
- onSend() {
- if (!this.question.trim() || this.isLoading) return;
- this.startStreamAnswer(this.question);
- this.question = '';
- },
- resetState() {
- this.answer = '';
- this.displayText = '';
- this.isComplete = false;
- this.error = null;
- this.retryCount = 0;
- this.lastResponseTime = Date.now();
- this.stopStreamAnswer();
- },
- handleError(error) {
- if (this.retryCount < this.maxRetries) {
- this.retryCount++;
- this.retryRequest();
- } else {
- this.error = '请求失败,请稍后重试';
- this.isLoading = false;
- uni.showToast({
- title: this.error,
- icon: 'none'
- });
- }
- },
- retryRequest() {
- uni.showToast({
- title: `正在重试 (${this.retryCount}/${this.maxRetries})`,
- icon: 'none'
- });
- setTimeout(() => {
- this.startStreamAnswer(this.question);
- }, 1000 * this.retryCount);
- },
- completeAnswer() {
- this.isComplete = true;
- this.isLoading = false;
- this.stopStreamAnswer();
- },
- stopStreamAnswer() {
- if (this.timer) {
- clearTimeout(this.timer);
- this.timer = null;
- }
- if (this.socketTask) {
- this.socketTask.close();
- this.socketTask = null;
- }
- },
- checkTimeout() {
- if (Date.now() - this.lastResponseTime > this.timeout) {
- this.handleError(new Error('请求超时'));
- }
- },
- goBack() {
- uni.navigateBack({
- delta: 1
- });
- },
- onChatScroll(e) {
- const threshold = 600; // 离底部100px以内不显示按钮
- const { scrollHeight, scrollTop } = e.detail;
- const clientHeight = e.detail.clientHeight || e.detail.height || 0;
- if (scrollHeight - scrollTop - clientHeight > threshold) {
- this.showToBottomBtn = true;
- } else {
- this.showToBottomBtn = false;
- }
- },
- onTextareaInput(e) {
- // 获取textarea的实际高度
- const query = uni.createSelectorQuery().in(this);
- query.select('.input-box').boundingClientRect(data => {
- if (data) {
- // 将px转换为rpx (假设设计稿是750rpx宽度)
- const height = (data.height * 750) / uni.getSystemInfoSync().windowWidth;
- // 减去基础高度90rpx,得到额外增加的高度
- this.textareaHeight = Math.max(0, height - 90);
- // 滚动到底部
- this.scrollToBottom();
- }
- }).exec();
- },
- retrieveHistoricalRecords(){
- uni.request({
- url: this.$apiHost + '/Work/streamAnswerLast',
- method: 'GET',
- header: {
- 'content-type': 'application/json',
- 'sign': getApp().globalData.headerSign
- },
- data: {
- uuid: getApp().globalData.uuid,
- task_type: 2
- },
- success: (res) => {
- console.log("获取历史记录:", res.data.list);
- if (res.data.success === "yes") {
- this.messages = res.data.list
- if (res&&res.data&&res.data.list&&res.data.list.length>0) {
- this.showToBottomBtn = true
- }
- }
- },
- fail: (err) => {
- console.log('获取历史记录失败', err);
- uni.showToast({
- title: '获取历史记录失败',
- icon: 'none'
- });
- }
- })
- },
- // 重新计算元素高度
- recalculateHeights() {
- // 重新计算textarea高度
- const query = uni.createSelectorQuery().in(this);
- query.select('.input-box').boundingClientRect(data => {
- if (data) {
- const height = (data.height * 750) / uni.getSystemInfoSync().windowWidth;
- this.textareaHeight = Math.max(0, height - 90);
- }
- }).exec();
- }
- },
- created() {
- this.retrieveHistoricalRecords();
- this.timer = setInterval(() => {
- if (this.isLoading) {
- this.checkTimeout();
- }
- }, 1000);
- // 监听键盘高度变化
- uni.onKeyboardHeightChange(res => {
- this.keyboardHeight = res.height;
- console.log('键盘高度变化:', res.height);
- // 键盘高度变化时重新计算
- this.$nextTick(() => {
- this.recalculateHeights();
- });
- });
- const systemInfo = uni.getSystemInfoSync();
- this.statusBarHeight = systemInfo.statusBarHeight; // 状态栏高度
- this.windowBottom = systemInfo.safeAreaInsets ? systemInfo.safeAreaInsets.bottom : 0;
- console.log("状态栏高度", this.statusBarHeight, "底部安全区域高度", this.windowBottom);
- },
- beforeDestroy() {
- this.stopStreamAnswer();
- }
- }
- </script>
- <style lang="scss">
- @import './dialogGeneration.scss';
- .to-bottom-btn {
- position: fixed;
- right: 50%;
- transform: translateX(50%);
- bottom: 20rpx;
- width: 60rpx;
- height: 60rpx;
- bottom: 220rpx;
- z-index: 999;
- transition: opacity 0.2s;
- }
- .ai-bubble-row {
- display: flex;
- align-items: flex-start;
- }
- .ai-avatar {
- width: 60rpx;
- height: 60rpx;
- border-radius: 50%;
- margin-right: 32rpx;
- border: solid 2rpx #999;
- display: flex;
- align-items: flex-end;
- justify-content: center;
- image {
- width: 52rpx;
- height: 52rpx;
- }
- }
- .ai-bubble-content {
- flex: 1;
- }
- </style>
|