feat(fun): 添加 AI 剧本生成功能
- 在 素材录入页面添加 AI 生成剧本按钮 - 新增 AiDrama 页面用于与 AI 助手交互 - 实现与 DeepSeek API 的流式和非流式通信 - 添加 Markdown 渲染功能 - 优化聊天界面布局和交互
This commit is contained in:
parent
4e6414d8c8
commit
0d06fe8fb6
|
@ -656,6 +656,12 @@
|
||||||
"navigationBarTitleText": "视频预览",
|
"navigationBarTitleText": "视频预览",
|
||||||
"navigationBarBackgroundColor": "#fff"
|
"navigationBarBackgroundColor": "#fff"
|
||||||
}
|
}
|
||||||
|
},{
|
||||||
|
"path": "AiDrama",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "AI 剧本助手",
|
||||||
|
"navigationBarBackgroundColor": "#fff"
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,634 @@
|
||||||
|
<template>
|
||||||
|
<view class="chat-container">
|
||||||
|
<!-- 聊天内容区域 -->
|
||||||
|
<scroll-view
|
||||||
|
class="chat-content"
|
||||||
|
scroll-y
|
||||||
|
:scroll-top="scrollTop"
|
||||||
|
:scroll-with-animation="true"
|
||||||
|
@scrolltoupper="loadMoreMessages"
|
||||||
|
:refresher-enabled="false"
|
||||||
|
ref="scrollView"
|
||||||
|
>
|
||||||
|
<view class="message-wrapper-box" >
|
||||||
|
<view v-for="(message, index) in messages" :key="index" class="message-wrapper">
|
||||||
|
<!-- 系统消息或AI消息 -->
|
||||||
|
<block v-if="message.role === 'system' || message.role === 'assistant'">
|
||||||
|
<view class="message-container ai-message-container">
|
||||||
|
<view class="message ai-message">
|
||||||
|
<!-- 使用渲染后的Markdown内容 -->
|
||||||
|
<rich-text class="message-content markdown-body" :nodes="message.renderedContent"></rich-text>
|
||||||
|
</view>
|
||||||
|
<text class="message-time">{{ message.time }}</text>
|
||||||
|
</view>
|
||||||
|
<!-- 在每条AI消息后显示确认按钮(除了系统消息) -->
|
||||||
|
<view v-if="message.role === 'assistant' && message.endStatus" class="accept-button-container">
|
||||||
|
<u-button
|
||||||
|
type="success"
|
||||||
|
size="mini"
|
||||||
|
@click="acceptScript(message.content)"
|
||||||
|
:custom-style="{marginTop: '10px'}"
|
||||||
|
>
|
||||||
|
<u-icon name="checkmark" size="14" class="button-icon"></u-icon>
|
||||||
|
确认使用此剧本
|
||||||
|
</u-button>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<!-- 用户消息 -->
|
||||||
|
<block v-else-if="message.role === 'user'">
|
||||||
|
<view class="message-container user-message-container">
|
||||||
|
<view class="message user-message">
|
||||||
|
<text class="message-content">{{ message.content }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="message-time">{{ message.time }}</text>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载中提示 -->
|
||||||
|
<view v-if="loading" class="loading-container">
|
||||||
|
<u-loading mode="circle" size="24"></u-loading>
|
||||||
|
<text class="loading-text">正在生成剧本...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用于滚动到底部的空白元素 -->
|
||||||
|
<view id="scroll-bottom-anchor" style="height: 1px;"></view>
|
||||||
|
|
||||||
|
<!-- 底部占位,确保内容不被输入框遮挡 -->
|
||||||
|
<view class="bottom-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 输入区域 - 固定在底部 -->
|
||||||
|
<view class="chat-input-container">
|
||||||
|
<view class="chat-input">
|
||||||
|
<u-input
|
||||||
|
v-model="inputMessage"
|
||||||
|
placeholder="请输入剧本相关描述,AI将为您生成专业剧本..."
|
||||||
|
:disabled="loading"
|
||||||
|
type='textarea'
|
||||||
|
:auto-height="true"
|
||||||
|
:custom-style="{flex: 1, minHeight: '80rpx', maxHeight: '200rpx'}"
|
||||||
|
@confirm="sendMessage"
|
||||||
|
></u-input>
|
||||||
|
<u-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="loading || !inputMessage.trim()"
|
||||||
|
@click="sendMessage"
|
||||||
|
:custom-style="{marginLeft: '10px'}"
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { renderMarkdown } from './markdown.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// 聊天消息列表
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: '你好!我是你的AI剧本助手。请告诉我你想创作的剧本类型和主要情节,我会为你生成专业的剧本内容。',
|
||||||
|
renderedContent: '',
|
||||||
|
time: this.formatTime(new Date()),
|
||||||
|
endStatus: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// 输入框内容
|
||||||
|
inputMessage: '',
|
||||||
|
// 加载状态
|
||||||
|
loading: false,
|
||||||
|
// 滚动位置
|
||||||
|
scrollTop: 0,
|
||||||
|
// URL参数
|
||||||
|
orderId: '',
|
||||||
|
// API密钥
|
||||||
|
apiKey: 'sk-uobtjwyzastkxcnrrbllxpftqxdumkafnitdqqhoermmioru',
|
||||||
|
// 当前AI消息索引
|
||||||
|
currentAiMessageIndex: -1,
|
||||||
|
// 是否是H5环境
|
||||||
|
isH5: false,
|
||||||
|
// 是否需要滚动到底部
|
||||||
|
needScrollToBottom: true,
|
||||||
|
// 滚动计时器
|
||||||
|
scrollTimer: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
// 从URL获取参数
|
||||||
|
this.orderId = options.orderId || '';
|
||||||
|
|
||||||
|
// 检测是否是H5环境
|
||||||
|
// #ifdef H5
|
||||||
|
this.isH5 = true;
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
this.initPage();
|
||||||
|
|
||||||
|
// 渲染初始消息的Markdown内容
|
||||||
|
this.renderAllMarkdown();
|
||||||
|
},
|
||||||
|
onReady() {
|
||||||
|
// 页面加载完成后滚动到底部
|
||||||
|
this.throttledScrollToBottom();
|
||||||
|
},
|
||||||
|
onUnload() {
|
||||||
|
// 清除滚动计时器
|
||||||
|
if (this.scrollTimer) {
|
||||||
|
clearTimeout(this.scrollTimer);
|
||||||
|
this.scrollTimer = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// 监听消息变化,自动滚动到底部
|
||||||
|
messages: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
if (this.needScrollToBottom) {
|
||||||
|
this.throttledScrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 初始化页面
|
||||||
|
initPage() {
|
||||||
|
// 设置导航栏标题
|
||||||
|
uni.setNavigationBarTitle({
|
||||||
|
title: 'AI 剧本助手'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 渲染所有消息的Markdown内容
|
||||||
|
renderAllMarkdown() {
|
||||||
|
this.messages.forEach((message, index) => {
|
||||||
|
if (message.role === 'system' || message.role === 'assistant') {
|
||||||
|
this.messages[index].renderedContent = renderMarkdown(message.content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 格式化时间为 HH:MM 格式
|
||||||
|
formatTime(date) {
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
async sendMessage() {
|
||||||
|
if (!this.inputMessage.trim() || this.loading) return;
|
||||||
|
|
||||||
|
// 添加用户消息
|
||||||
|
const userMessage = {
|
||||||
|
role: 'user',
|
||||||
|
content: this.inputMessage,
|
||||||
|
time: this.formatTime(new Date())
|
||||||
|
};
|
||||||
|
this.messages.push(userMessage);
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
const userInput = this.inputMessage;
|
||||||
|
this.inputMessage = '';
|
||||||
|
|
||||||
|
// 设置加载状态
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
this.throttledScrollToBottom();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 准备发送给API的消息
|
||||||
|
const apiMessages = [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: '你好!我是你的AI剧本助手。请告诉我你想创作的剧本类型和主要情节,我会为你生成专业的剧本内容。请使用Markdown格式来组织你的回答,可以使用标题、列表、粗体、斜体等Markdown语法来增强可读性。'
|
||||||
|
},
|
||||||
|
...this.messages
|
||||||
|
.filter(msg => msg.role !== 'system')
|
||||||
|
.map(msg => ({
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
// 在H5环境中使用fetch API进行流式请求
|
||||||
|
if (this.isH5) {
|
||||||
|
await this.fetchStreamResponse(apiMessages);
|
||||||
|
} else {
|
||||||
|
// 在非H5环境中使用普通请求
|
||||||
|
await this.fetchNormalResponse(apiMessages);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API请求失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: '生成剧本失败,请稍后重试',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
// 滚动到底部
|
||||||
|
this.throttledScrollToBottom();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 使用fetch API进行流式请求(仅H5环境)
|
||||||
|
async fetchStreamResponse(apiMessages) {
|
||||||
|
try {
|
||||||
|
// 添加一个空的AI回复,用于流式更新
|
||||||
|
this.currentAiMessageIndex = this.messages.length;
|
||||||
|
this.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
renderedContent: '',
|
||||||
|
time: this.formatTime(new Date()),
|
||||||
|
endStatus: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch('https://api.siliconflow.cn/v1/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B',
|
||||||
|
messages: apiMessages,
|
||||||
|
stream: true,
|
||||||
|
max_tokens: 8192,
|
||||||
|
stop: '',
|
||||||
|
temperature: 0.6,
|
||||||
|
top_p: 0.7,
|
||||||
|
top_k: 50,
|
||||||
|
frequency_penalty: 0
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API请求失败: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error('响应体为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value);
|
||||||
|
const lines = chunk.split('\n').filter(line => line.trim() !== '');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
const data = line.substring(6).trim();
|
||||||
|
if (data === '[DONE]') {
|
||||||
|
// 设置消息已完成状态
|
||||||
|
if (this.currentAiMessageIndex >= 0 && this.currentAiMessageIndex < this.messages.length) {
|
||||||
|
this.messages[this.currentAiMessageIndex].endStatus = true;
|
||||||
|
// 最后一次滚动到底部
|
||||||
|
this.throttledScrollToBottom();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
// 检查parsed和choices是否存在
|
||||||
|
if (!parsed || !parsed.choices || !Array.isArray(parsed.choices) || parsed.choices.length === 0) {
|
||||||
|
console.warn('无效的响应格式:', data);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全地访问delta属性
|
||||||
|
const delta = parsed.choices[0].delta || {};
|
||||||
|
|
||||||
|
// 根据您提供的格式解析内容
|
||||||
|
const content = delta.content;
|
||||||
|
// 有些响应可能包含reasoning_content而不是content
|
||||||
|
const reasoningContent = delta.reasoning_content || '';
|
||||||
|
|
||||||
|
// 只有当content不是null且有值,或者reasoningContent有值时才添加
|
||||||
|
const textToAdd = (content !== null && content !== undefined) ? content : reasoningContent;
|
||||||
|
|
||||||
|
if (textToAdd) {
|
||||||
|
result += textToAdd;
|
||||||
|
// 确保messages[currentAiMessageIndex]存在
|
||||||
|
if (this.currentAiMessageIndex >= 0 && this.currentAiMessageIndex < this.messages.length) {
|
||||||
|
this.messages[this.currentAiMessageIndex].content = result;
|
||||||
|
this.messages[this.currentAiMessageIndex].renderedContent = renderMarkdown(result);
|
||||||
|
|
||||||
|
// 滚动到底部,但限制频率以提高性能
|
||||||
|
this.throttledScrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析流数据失败:', e, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('调用DeepSeek流式API失败:', error);
|
||||||
|
// 如果出错,确保移除空的AI消息
|
||||||
|
if (this.messages.length > 0 && this.messages[this.messages.length - 1].content === '') {
|
||||||
|
this.messages.pop();
|
||||||
|
this.currentAiMessageIndex = -1;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 使用普通请求(非H5环境)
|
||||||
|
async fetchNormalResponse(apiMessages) {
|
||||||
|
try {
|
||||||
|
// 添加一个空的AI回复
|
||||||
|
this.currentAiMessageIndex = this.messages.length;
|
||||||
|
this.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
renderedContent: '',
|
||||||
|
time: this.formatTime(new Date()),
|
||||||
|
endStatus: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await uni.request({
|
||||||
|
url: 'https://api.siliconflow.cn/v1/chat/completions',
|
||||||
|
method: 'POST',
|
||||||
|
header: {
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
model: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B',
|
||||||
|
messages: apiMessages,
|
||||||
|
stream: false,
|
||||||
|
max_tokens: 8192,
|
||||||
|
stop: '',
|
||||||
|
temperature: 0.6,
|
||||||
|
top_p: 0.7,
|
||||||
|
top_k: 50,
|
||||||
|
frequency_penalty: 0
|
||||||
|
},
|
||||||
|
dataType: 'json'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
throw new Error(`API请求失败: ${response.statusCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取响应内容
|
||||||
|
const result = response.data.choices[0].message.content;
|
||||||
|
|
||||||
|
// 更新消息内容
|
||||||
|
this.messages[this.currentAiMessageIndex].content = result;
|
||||||
|
this.messages[this.currentAiMessageIndex].renderedContent = renderMarkdown(result);
|
||||||
|
this.messages[this.currentAiMessageIndex].endStatus = true;
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
this.throttledScrollToBottom();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('调用DeepSeek API失败:', error);
|
||||||
|
// 如果出错,确保移除空的AI消息
|
||||||
|
if (this.messages.length > 0 && this.messages[this.messages.length - 1].content === '') {
|
||||||
|
this.messages.pop();
|
||||||
|
this.currentAiMessageIndex = -1;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 接受剧本
|
||||||
|
acceptScript(scriptContent) {
|
||||||
|
// // 使用全局方法返回选择的剧本
|
||||||
|
// getApp().globalData.selectedScript = {
|
||||||
|
// orderId: this.orderId,
|
||||||
|
// content: scriptContent
|
||||||
|
// };
|
||||||
|
// const pages = getCurrentPages();
|
||||||
|
// // 上一个页面实例(假设是 pageA)
|
||||||
|
// const prevPage = pages[pages.length - 2];
|
||||||
|
// console.log("prevPage",prevPage)
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '已确认使用此剧本',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
// setTimeout(() => {
|
||||||
|
// uni.navigateBack();
|
||||||
|
// }, 1500);
|
||||||
|
|
||||||
|
// 当前页面(假设是 pageB)
|
||||||
|
uni.navigateBack({
|
||||||
|
delta: 1, // 返回的页面层数,1 表示返回上一个页面
|
||||||
|
success: function() {
|
||||||
|
// 获取页面栈
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
// 上一个页面实例(假设是 pageA)
|
||||||
|
const prevPage = pages[pages.length - 2];
|
||||||
|
console.log("prevPage",prevPage)
|
||||||
|
// 调用上一个页面的方法
|
||||||
|
prevPage.getAiDramaTxt(scriptContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 节流滚动到底部函数,避免频繁滚动
|
||||||
|
throttledScrollToBottom() {
|
||||||
|
// if (this.scrollTimer) return;
|
||||||
|
|
||||||
|
// this.scrollTimer = setTimeout(() => {
|
||||||
|
// this.scrollToBottom();
|
||||||
|
// this.scrollTimer = null;
|
||||||
|
// }, 1000); // 300ms内只执行一次滚动
|
||||||
|
this.$u.throttle(this.scrollToBottom, 300)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
scrollToBottom() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// 方法1: 使用scrollTop设置滚动位置
|
||||||
|
// const query = uni.createSelectorQuery().in(this);
|
||||||
|
// query.select('.chat-content').boundingClientRect(data => {
|
||||||
|
// if (data) {
|
||||||
|
// // 设置一个非常大的值确保滚动到底部
|
||||||
|
// this.scrollTop = 999999;
|
||||||
|
// }
|
||||||
|
// }).exec();
|
||||||
|
|
||||||
|
// 方法2: 使用选择器定位到底部锚点元素
|
||||||
|
setTimeout(() => {
|
||||||
|
const query = uni.createSelectorQuery().in(this);
|
||||||
|
query.select('#scroll-bottom-anchor').boundingClientRect(data => {
|
||||||
|
if (data) {
|
||||||
|
// 滚动到锚点元素位置
|
||||||
|
uni.pageScrollTo({
|
||||||
|
selector: '#scroll-bottom-anchor',
|
||||||
|
duration: 100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).exec();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载更多消息(上拉加载)
|
||||||
|
loadMoreMessages() {
|
||||||
|
// 这里可以实现加载历史消息的逻辑
|
||||||
|
// 目前不需要,保留接口
|
||||||
|
this.needScrollToBottom = false; // 上拉加载时禁用自动滚动
|
||||||
|
|
||||||
|
// 加载完成后恢复自动滚动
|
||||||
|
setTimeout(() => {
|
||||||
|
this.needScrollToBottom = true;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.chat-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - var(--window-top));
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
// padding: 20rpx;
|
||||||
|
// padding-bottom: 180rpx; /* 为底部输入框留出空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-wrapper-box{
|
||||||
|
padding: 20rpx;
|
||||||
|
// padding-bottom: 180rpx; /* 为底部输入框留出空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-wrapper {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-container {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message-container {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
max-width: calc(80% - 40rpx);
|
||||||
|
padding: 24rpx 30rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message {
|
||||||
|
background-color: #4e6ef2;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(78, 110, 242, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown样式 */
|
||||||
|
.markdown-body {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message-container .message-time {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 固定在底部的输入区域 */
|
||||||
|
.chat-input-container {
|
||||||
|
// position: fixed;
|
||||||
|
// bottom: 0;
|
||||||
|
// left: 0;
|
||||||
|
// right: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
z-index: 100;
|
||||||
|
// min-height: 170rpx;
|
||||||
|
margin-top: auto;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-placeholder {
|
||||||
|
height: 40rpx; /* 确保内容不被输入框遮挡 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept-button-container {
|
||||||
|
// display: flex;
|
||||||
|
// justify-content: flex-start;
|
||||||
|
// margin-top: 20rpx;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 28rpx;
|
||||||
|
margin: 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-icon {
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,64 @@
|
||||||
|
// 简单的Markdown渲染函数
|
||||||
|
export function renderMarkdown(content) {
|
||||||
|
if (!content) return '';
|
||||||
|
|
||||||
|
// 由于UniApp的rich-text组件支持HTML,我们将Markdown转换为HTML
|
||||||
|
// 这是一个简化版的Markdown解析器,仅支持基本语法
|
||||||
|
|
||||||
|
let html = content
|
||||||
|
// 转义HTML特殊字符
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
.replace(/^# (.*$)/gm, '<h1>$1</h1>')
|
||||||
|
.replace(/^## (.*$)/gm, '<h2>$1</h2>')
|
||||||
|
.replace(/^### (.*$)/gm, '<h3>$1</h3>')
|
||||||
|
.replace(/^#### (.*$)/gm, '<h4>$1</h4>')
|
||||||
|
.replace(/^##### (.*$)/gm, '<h5>$1</h5>')
|
||||||
|
.replace(/^###### (.*$)/gm, '<h6>$1</h6>')
|
||||||
|
|
||||||
|
// 粗体和斜体
|
||||||
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||||
|
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||||
|
.replace(/__(.*?)__/g, '<strong>$1</strong>')
|
||||||
|
.replace(/_(.*?)_/g, '<em>$1</em>')
|
||||||
|
|
||||||
|
// 链接
|
||||||
|
.replace(/\[([^\]]+)\]$$([^)]+)$$/g, '<a href="$2">$1</a>')
|
||||||
|
|
||||||
|
// 图片
|
||||||
|
.replace(/!\[([^\]]+)\]$$([^)]+)$$/g, '<img src="$2" alt="$1">')
|
||||||
|
|
||||||
|
// 无序列表
|
||||||
|
.replace(/^\s*[-*+]\s+(.*$)/gm, '<li>$1</li>')
|
||||||
|
|
||||||
|
// 有序列表
|
||||||
|
.replace(/^\s*\d+\.\s+(.*$)/gm, '<li>$1</li>')
|
||||||
|
|
||||||
|
// 代码块
|
||||||
|
.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
|
||||||
|
|
||||||
|
// 行内代码
|
||||||
|
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||||
|
|
||||||
|
// 引用
|
||||||
|
.replace(/^\> (.*$)/gm, '<blockquote>$1</blockquote>')
|
||||||
|
|
||||||
|
// 段落
|
||||||
|
.replace(/\n\s*\n/g, '</p><p>')
|
||||||
|
|
||||||
|
// 换行
|
||||||
|
.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
// 包装在段落标签中
|
||||||
|
html = '<p>' + html + '</p>';
|
||||||
|
|
||||||
|
// 修复列表
|
||||||
|
html = html.replace(/<li>.*?<\/li>/g, function(match) {
|
||||||
|
return '<ul>' + match + '</ul>';
|
||||||
|
});
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
|
@ -8,6 +8,16 @@
|
||||||
<u-icon name="arrow-down-fill" color="#999" size="28"></u-icon>
|
<u-icon name="arrow-down-fill" color="#999" size="28"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="form-label">剧本 <text class="form-hint">视频的剧本</text> </text>
|
||||||
|
<!-- 文本输入 -->
|
||||||
|
<template>
|
||||||
|
<u-input style="max-height: 500rpx;overflow-y: auto;" :disabled="getDisabledStatus()" maxlength="9999" type='textarea' class="bg-gray" v-model="scriptContent" :placeholder="`请输入剧本`" />
|
||||||
|
</template>
|
||||||
|
<u-button @click="openAiDrama" type="primary" :disabled="getDisabledStatus()">Ai生成剧本</u-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view v-for="(item,index) in materialTemplateList" :key="index" class="form-item">
|
<view v-for="(item,index) in materialTemplateList" :key="index" class="form-item">
|
||||||
<text class="form-label">{{ item.attrTitle }} <text class="form-hint">{{ item.attrHint ? '(' +item.attrHint + ')' : '' }}</text> </text>
|
<text class="form-label">{{ item.attrTitle }} <text class="form-hint">{{ item.attrHint ? '(' +item.attrHint + ')' : '' }}</text> </text>
|
||||||
<!-- 文本输入 -->
|
<!-- 文本输入 -->
|
||||||
|
@ -100,6 +110,7 @@
|
||||||
audioType: ['.wav', '.mp3', '.aac', '.flac', '.ogg'],
|
audioType: ['.wav', '.mp3', '.aac', '.flac', '.ogg'],
|
||||||
audioH5Type: ['wav', 'mp3', 'aac', 'flac', 'ogg'],
|
audioH5Type: ['wav', 'mp3', 'aac', 'flac', 'ogg'],
|
||||||
buttonLoading:false,
|
buttonLoading:false,
|
||||||
|
scriptContent:'',
|
||||||
// videoType: ['.avi', '.mp4', '.mkv', '.flv', '.mov', '.wmv', '.mpeg', '.3gp'],
|
// videoType: ['.avi', '.mp4', '.mkv', '.flv', '.mov', '.wmv', '.mpeg', '.3gp'],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -112,6 +123,14 @@
|
||||||
this.getProductDetail();
|
this.getProductDetail();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getAiDramaTxt(txt){
|
||||||
|
this.scriptContent = txt;
|
||||||
|
},
|
||||||
|
openAiDrama(){
|
||||||
|
uni.navigateTo({
|
||||||
|
url:'./AiDrama'
|
||||||
|
})
|
||||||
|
},
|
||||||
getDisabledStatus(){
|
getDisabledStatus(){
|
||||||
if(this.commitState !== 'add' && this.commitState !== 'draft' && this.commitState !== 'back'){
|
if(this.commitState !== 'add' && this.commitState !== 'draft' && this.commitState !== 'back'){
|
||||||
return true;
|
return true;
|
||||||
|
|
Loading…
Reference in New Issue