123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508 |
- <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 @click="switchToNormal()" class="primary" src="../../static/makedetail/primary.png"></image>
- <image @click="newChar()" 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" v-if="!msg.isStartGenerating">
- <template v-if="idx === lastAiIndex && isLoading">
- <text>{{ displayText }}</text>
- <text v-if="isLoading" class="loading-dot">...</text>
- </template>
- <template v-else>
- <text>{{ msg.content }}</text>
- </template>
- </view>
- <view v-else class="ai-bubble-content">
- <text>OK!~小萌正在根据你的描述生成形象中</text><br>
- <div v-if="msg.isStartGenerating && msg.startGeneratingId == 0" class="btn-box">
- 点击查看
- ({{ countdown }}) s</div>
- <div v-else
- @click="goPage(`/pages/makedetail/makeImgDetail?id=${msg.startGeneratingId}`)"
- class="btn-box"> 点击查看
- </div>
- </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="bom-box-bg">
- <c-lottie ref="cLottieRef" :src='"/static/lottie/xiaomeng.json"' class="icon-img" height="108rpx"
- width="112rpx" :loop="true" :autoPlay="false"></c-lottie>
-
- </view>
- <!-- 底部输入区 -->
- <view class="input-bar">
- <!-- <image class="icon-img" src="../../static/makedetail/characterProfilePicture.png" mode="aspectFill">
- </image> -->
- <textarea :autoHeight="true" class="input-box" :adjust-position="false" v-model="question"
- :disabled="isLoading" placeholder="给我发送消息吧..." maxlength="400" @input="onTextareaInput"
- confirm-type="search" @confirm="onSend" rows="1"
- style="overflow-y:auto;max-height:176rpx;min-height:44rpx;" />
- <!-- <button class="send-btn" :disabled="isLoading || !question.trim()" @click="onSend">{{ isLoading ?
- '生成中...' :
- '发送' }}</button> -->
- <image v-if="isLoading" class="stop" src="../../static/makedetail/stop.png" mode=""
- @click="stopStreamAnswer" />
- <image v-else-if="!isLoading && !question.trim() && keyboardHeight === 0" class="keyboard"
- src="../../static/makedetail/keyboard.png" mode="" />
- <image v-else-if="!isLoading && question.trim() && keyboardHeight !== 0" class="send"
- src="../../static/makedetail/send.png" mode="" @click="onSend" />
- </view>
- <!-- 底部提示 -->
- <view class="footer-tip">内容由AI生成,禁用相关功能请联系管理员。</view>
- </view>
- </view>
- <DialogBox ref="DialogBox"></DialogBox>
- </view>
- </template>
- <script>
- import websocket from '@/common/websocket.js';
- 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,
- messages: [],
- lastAiIndex: -1,
- keyboardHeight: 0,
- statusBarHeight: 0,
- windowBottom: 0,
- toView: '',
- showToBottomBtn: false,
- textareaHeight: 0,
- isConnected: false,
- isStartGenerating: false,
- countdown: 0,
- };
- },
- methods: {
- scrollToBottom() {
- this.toView = '';
- this.$nextTick(() => {
- this.toView = 'bottom-anchor';
- });
- },
- async initWebSocket() {
- if (this.isConnected) return;
- try {
- await websocket.connect('wss://e.zhichao.art/Gapi/Work/streamAnswer', {
- uuid: getApp().globalData.uuid
- });
- this.isConnected = true;
- // 设置消息处理回调
- websocket.onMessage((data) => {
- console.log("data:", data);
- if (this.isLoading) {
- if (data.includes('[DONE]')) {
- this.completeAnswer();
- } else if (data == 'PING' || data == 'PONG') {
- // 心跳消息 忽略
- } else if (data.includes('ID:')) {
- // 提取ID
- const aiMsg = this.messages[this.lastAiIndex];
- if (aiMsg) {
- aiMsg.startGeneratingId = data.split(':')[1];
- }
- } else if (data.includes('OKOKOK')) {
- const aiMsg = this.messages[this.lastAiIndex];
- if (aiMsg) {
- aiMsg.isStartGenerating = true;
- }
- this.isStartGenerating = true;
- this.countdownFun(20);
- } else {
- const aiMsg = this.messages[this.lastAiIndex];
- if (aiMsg) {
- aiMsg.isStartGenerating = false;
- this.displayText += data;
- this.answer += data;
- aiMsg.content += data;
- this.retryCount = 0;
- this.lastResponseTime = Date.now();
- this.scrollToBottom();
- }
- }
- }
- });
- // 设置错误处理回调
- websocket.onError((error) => {
- console.error('WebSocket错误:', error);
- this.isConnected = false;
- this.handleError(error);
- });
- // 设置关闭处理回调
- websocket.onClose(() => {
- console.log('WebSocket已关闭');
- this.isConnected = false;
- this.isLoading = false;
- });
- } catch (error) {
- console.error('WebSocket初始化失败:', error);
- this.isConnected = false;
- this.handleError(error);
- }
- },
- async startStreamAnswer(content) {
- if (!content.trim()) {
- uni.showToast({
- title: '请输入问题',
- icon: 'none'
- });
- return;
- }
- // 检查连接状态,如果断开则重连
- if (!this.isConnected) {
- try {
- await this.initWebSocket();
- } catch (error) {
- console.error('重连失败:', error);
- uni.showToast({
- title: '连接已断开,请重试',
- icon: 'none'
- });
- return;
- }
- }
- this.resetState();
- this.messages.push({ role: 'user', content });
- try {
- this.isLoading = true;
- let aiMsg = {
- role: 'ai',
- content: '',
- isStartGenerating: false,
- startGeneratingId: 0
- };
- this.messages.push(aiMsg);
- this.lastAiIndex = this.messages.length - 1;
- // 发送消息
- websocket.send(content);
- } catch (error) {
- console.error('发送消息失败:', 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();
- },
- 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;
- },
- stopStreamAnswer() {
- if (this.timer) {
- clearTimeout(this.timer);
- this.timer = null;
- }
- this.isLoading = false;
- },
- 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) {
- console.log(e.detail);
- // 获取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" && res.data.list) {
- this.messages = res.data.list
- if (res && res.data && res.data.list && res.data.list.length > 0) {
- this.showToBottomBtn = true
- }
- } else {
- this.messages = []
- }
- },
- 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();
- },
- countdownFun(n) {
- if (this.timer) {
- clearInterval(this.timer);
- }
- this.countdown = n;
- // 倒计时
- this.timer = setInterval(() => {
- this.countdown--;
- if (this.countdown <= 0) {
- clearInterval(this.timer);
- }
- }, 1000);
- },
- goPage(page) {
- uni.navigateTo({
- url: page,
- });
- },
- newChar() {
- this.$refs["DialogBox"]
- .confirm({
- title: "是否创建新对话",
- content: "立即结束当前对话内容,开启新的对话?",
- DialogType: "inquiry",
- btn1: "取消",
- btn2: "确定",
- animation: 0,
- })
- .then(() => {
- websocket.send('clear');
- this.retrieveHistoricalRecords()
- });
- },
- switchToNormal() {
- this.$refs["DialogBox"]
- .confirm({
- title: "是否切回常规模式",
- content: "切换至普通常规模式进行创作?",
- DialogType: "inquiry",
- btn1: "取消",
- btn2: "确定",
- animation: 0,
- })
- .then(() => {
- this.goPage('/pages/makedetail/makeImgDetail')
- });
- }
- },
- async created() {
- await this.initWebSocket();
- this.retrieveHistoricalRecords();
- this.timer = setInterval(() => {
- if (this.isLoading) {
- this.checkTimeout();
- }
- }, 1000);
- uni.onKeyboardHeightChange(res => {
- this.keyboardHeight = res.height;
- if (res.height === 0) {
- this.$refs.cLottieRef.call('stop')
- } else {
- this.$refs.cLottieRef.call('play')
- }
- this.$nextTick(() => {
- this.recalculateHeights();
- });
- });
- const systemInfo = uni.getSystemInfoSync();
- this.statusBarHeight = systemInfo.statusBarHeight;
- this.windowBottom = systemInfo.safeAreaInsets ? systemInfo.safeAreaInsets.bottom : 0;
- },
- beforeDestroy() {
- websocket.close();
- this.stopStreamAnswer();
- }
- }
- </script>
- <style lang="scss">
- @import './intelligentLifeChart.scss';
- .to-bottom-btn {
- position: fixed;
- right: 50%;
- transform: translateX(50%);
- bottom: 20rpx;
- width: 60rpx;
- height: 60rpx;
- bottom: 220rpx;
- z-index: 999;
- opacity: .75;
- transition: opacity 0.2s;
- }
- .ai-bubble-row {
- display: flex;
- align-items: flex-start;
- }
- .ai-avatar {
- width: 60rpx;
- height: 60rpx;
- border-radius: 50%;
- margin-right: 20rpx;
- border: solid 2rpx rgb(238, 238, 238, .3);
- display: flex;
- align-items: flex-end;
- justify-content: center;
- image {
- width: 52rpx;
- height: 52rpx;
- }
- }
- .ai-bubble-content {
- flex: 1;
- }
- </style>tyle>
|