mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-02-02 08:35:25 +08:00
v3.9.1 发布(AI应用首页,加入 Chat2BI 和 AI绘图)
This commit is contained in:
@ -25,8 +25,8 @@
|
||||
:historyData="historyData"
|
||||
type="view"
|
||||
:formState="appData"
|
||||
:prologue="appData.prologue"
|
||||
:presetQuestion="appData.presetQuestion"
|
||||
:prologue="appData?.prologue"
|
||||
:presetQuestion="appData?.presetQuestion"
|
||||
@reload-message-title="reloadMessageTitle"
|
||||
:chatTitle="chatTitle"
|
||||
:conversationSettings="getCurrentSettings"
|
||||
|
||||
@ -108,6 +108,21 @@
|
||||
name: '看图说话',
|
||||
icon: 'https://jeecgdev.oss-cn-beijing.aliyuncs.com/temp/工具-图片解析_1743065064801.png',
|
||||
prologue: '上传一张图片,我来为你讲述图片中的故事',
|
||||
},
|
||||
{
|
||||
id: '2008448202536456193',
|
||||
name: 'Chat2BI',
|
||||
icon: 'https://minio.jeecg.com/otatest/chatShow_1769395642452.png',
|
||||
prologue: '你好,我是图表生成智能体。',
|
||||
flowId: '2008379264947519489',
|
||||
type: 'chatFLow',
|
||||
presetQuestion: '[{"key":1,"descr":"用户性别比例","update":true}]'
|
||||
},
|
||||
{
|
||||
id: '2008090512835629057',
|
||||
name: 'AI绘画',
|
||||
icon: 'https://minio.jeecg.com/otatest/AiWrite_1769395779558.png',
|
||||
prologue: '请输入文本,并选择图像生成,我来为你生成图片',
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@ -16,7 +16,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="prompt-left-textarea">
|
||||
<div class="command">指令</div>
|
||||
<div class="command">
|
||||
<span style="margin-right: 5px">指令</span>
|
||||
<a-tooltip title="提示词库">
|
||||
<span @click="openPromptApps" style="color:#1890ff;cursor: pointer">
|
||||
<Icon icon="ant-design:bulb-outlined" color="#1890ff"></Icon>词库选择
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-textarea v-model:value="prompt" :autoSize="{ minRows: 8, maxRows: 8 }"></a-textarea>
|
||||
</div>
|
||||
<a-button @click="generatedPrompt" class="prompt-left-btn" type="primary" :loading="loading">
|
||||
@ -50,17 +57,21 @@
|
||||
</div>
|
||||
</BasicModal>
|
||||
</div>
|
||||
<!-- Ai提示词选择弹窗 -->
|
||||
<AiAppPromptMarketModal @register="registerAiPromptSelectModal" @ok="handleAiAppPromptOk"></AiAppPromptMarketModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, unref } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModalInner } from '@/components/Modal';
|
||||
import {useModal, useModalInner} from '@/components/Modal';
|
||||
import { promptGenerate } from '@/views/super/airag/aiapp/AiApp.api';
|
||||
import AiAppPromptMarketModal from "@/views/super/airag/aiapp/components/AiAppPromptMarketModal.vue";
|
||||
|
||||
export default {
|
||||
name: 'AiAppGeneratedPrompt',
|
||||
components: {
|
||||
AiAppPromptMarketModal,
|
||||
BasicModal,
|
||||
},
|
||||
emits: ['ok', 'register'],
|
||||
@ -93,6 +104,8 @@
|
||||
linux: '你是一个linux专家,擅长解决各种linux相关的问题。',
|
||||
content: '你是一个阅读理解大师,可以阅读用户提供的文章,并提炼主要内容输出给用户。',
|
||||
});
|
||||
//注册提示词modal
|
||||
const [registerAiPromptSelectModal, { openModal: aiPromptSelectModalOpen }] = useModal();
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
content.value = '';
|
||||
@ -197,6 +210,20 @@
|
||||
closeModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开提示词库弹窗
|
||||
*/
|
||||
function openPromptApps() {
|
||||
aiPromptSelectModalOpen(true,{});
|
||||
}
|
||||
/**
|
||||
* 提示词回调
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function handleAiAppPromptOk(value) {
|
||||
content.value = value;
|
||||
}
|
||||
return {
|
||||
registerModal,
|
||||
handleOk,
|
||||
@ -207,6 +234,9 @@
|
||||
loading,
|
||||
instructionsClick,
|
||||
content,
|
||||
openPromptApps,
|
||||
registerAiPromptSelectModal,
|
||||
handleAiAppPromptOk,
|
||||
};
|
||||
},
|
||||
};
|
||||
@ -250,6 +280,8 @@
|
||||
.prompt-left-textarea {
|
||||
margin-top: 25px;
|
||||
.command {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #101828;
|
||||
line-height: 15px;
|
||||
font-weight: 500;
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="card-content">
|
||||
<p class="description">{{ item.description || item.desc }}</p>
|
||||
<p class="description" :title="item.description || item.desc">{{ item.description || item.desc }}</p>
|
||||
<div class="card-footer" >
|
||||
<span class="create-time">
|
||||
{{ formatTime(item.createTime) }}
|
||||
@ -315,7 +315,7 @@
|
||||
line-height: 1.5;
|
||||
margin: 8px 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
flex: 1; // 让描述区域自适应
|
||||
|
||||
@ -81,8 +81,7 @@
|
||||
<div style="align-items: center;display:flex;justify-content: center" v-if="!isRelease">
|
||||
<a-button size="middle" ghost>
|
||||
<span style="align-items: center;display:flex" @click="openPromptApps">
|
||||
<Icon icon="ant-design:appstore-outlined"></Icon>
|
||||
<span style="margin-left: 4px">模版</span>
|
||||
<Icon icon="ant-design:database-outlined"></Icon>提示词库
|
||||
</span>
|
||||
</a-button>
|
||||
<a-button size="middle" ghost>
|
||||
|
||||
@ -91,14 +91,14 @@
|
||||
<div class="variable-container">
|
||||
<div class="variable-container-header">
|
||||
<Icon icon="ant-design:file-text-outlined" class="output-format-icon" />
|
||||
<span class="variable-format-title">评估器内容变量要求</span>
|
||||
<span class="variable-format-title">评估器内容变量要求(点击变量插入到评估器内容)</span>
|
||||
</div>
|
||||
<div class="variable-container-content">
|
||||
<div class="variable-tag-wrapper">
|
||||
<a-tooltip title="评估的输入内容变量">
|
||||
<a-tooltip title="评估的输入内容变量(必填)">
|
||||
<a-tag color="blue" class="variable-tag required-tag" @click="handleTagClick('input')">input</a-tag>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="评估的输出内容变量">
|
||||
<a-tooltip title="评估的输出内容变量(必填)">
|
||||
<a-tag color="blue" class="variable-tag required-tag" @click="handleTagClick('output')">output</a-tag>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="评估的参考内容变量">
|
||||
@ -141,7 +141,7 @@
|
||||
</a-row>
|
||||
</a-form>
|
||||
<a-button v-if="showTest" class="mt-10 ml" style="float: right" @click="showTest = false">取消</a-button>
|
||||
<a-button class="mt-10" style="float: right" @click="showTest = true" type="primary">调试</a-button>
|
||||
<!-- <a-button class="mt-10" style="float: right" @click="showTest = true" type="primary">调试</a-button>-->
|
||||
</a-col>
|
||||
<a-col :span="12" class="setting-right" v-if="showTest">
|
||||
<EvaluatorDebug ref="debugRef" :content="formState.dataValue" @run="debugRun"></EvaluatorDebug>
|
||||
@ -180,7 +180,7 @@
|
||||
//uuid
|
||||
const uuid = ref(randomString(16));
|
||||
//showTest 显示调试器
|
||||
const showTest = ref(false);
|
||||
const showTest = ref(true);
|
||||
//debugRef 调试器引用
|
||||
const debugRef = ref(null);
|
||||
//form表单数据
|
||||
@ -263,8 +263,10 @@
|
||||
* @param type
|
||||
*/
|
||||
function handleTagClick(type) {
|
||||
if(formState.dataValue){
|
||||
let label = type=='input'?'## 输入参数':type=='output'?'## 输出参数':'## 参考参数';
|
||||
let label = type=='input'?'## 输入参数':type=='output'?'## 输出参数':'## 参考参数';
|
||||
if(!formState.dataValue){
|
||||
formState.dataValue = `${label}:{{${type}}}`;
|
||||
}else{
|
||||
formState.dataValue += `\r\n\r\n${label}:{{${type}}}`;
|
||||
// 获取textarea元素并滚动到底部
|
||||
setTimeout(() => {
|
||||
@ -390,6 +392,8 @@
|
||||
debugRef.value.loading = false;
|
||||
if(res.success){
|
||||
debugRef.value.result = res.result
|
||||
}else{
|
||||
message.error(res.message);
|
||||
}
|
||||
console.log("debugEvaluator",res)
|
||||
}
|
||||
|
||||
@ -5,39 +5,112 @@
|
||||
</div>
|
||||
<div class="preview" ref="previewRef">
|
||||
<div class="preview-header">
|
||||
<span>预览</span>
|
||||
<a-tooltip title="复制内容">
|
||||
<CopyOutlined class="copy-btn" @click="handleCopy" />
|
||||
</a-tooltip>
|
||||
<div class="preview-header-left">
|
||||
<span class="preview-title-text">{{ isEditing ? '编辑' : '预览' }}</span>
|
||||
<a-select
|
||||
v-if="historyData && historyData.length"
|
||||
v-model:value="activeVersion"
|
||||
size="small"
|
||||
class="version-select"
|
||||
@change="handleVersionChange"
|
||||
>
|
||||
<a-select-option :value="CURRENT_VERSION_KEY">当前内容</a-select-option>
|
||||
<a-select-option v-for="item in historyData" :key="item.version" :value="item.version">
|
||||
{{ item.version }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-button
|
||||
v-if="historyData && historyData.length && activeVersion !== CURRENT_VERSION_KEY"
|
||||
type="link"
|
||||
size="small"
|
||||
class="preview-action-btn version-delete-btn"
|
||||
@click="handleDeleteVersion"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="preview-actions">
|
||||
<a-button v-if="!generating" type="link" size="small" class="preview-action-btn custom-save-btn" @click="toggleEdit">
|
||||
<FormOutlined v-if="!isEditing" />
|
||||
<CheckOutlined v-else />
|
||||
{{ isEditing ? '完成' : '编辑' }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!generating"
|
||||
type="link"
|
||||
size="small"
|
||||
class="preview-action-btn custom-save-btn"
|
||||
:loading="polishLoading"
|
||||
@click="handlePolish"
|
||||
>
|
||||
<ThunderboltOutlined class="preview-actions-icon" style="position: relative; top: 1px" />
|
||||
润色
|
||||
</a-button>
|
||||
<a-tooltip title="保存草稿">
|
||||
<a-button v-if="!generating" type="link" size="small" class="preview-action-btn custom-save-btn" :loading="saving" @click="handleSave">
|
||||
<SaveOutlined class="preview-actions-icon" />
|
||||
保存
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="复制内容">
|
||||
<a-button type="link" size="small" class="preview-action-btn custom-save-btn" @click="handleCopy">
|
||||
<CopyOutlined class="preview-actions-icon" />
|
||||
复制
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isEditing" v-html="previewMd" class="markdown-container" @click="!generating" />
|
||||
<div v-else class="markdown-editor-container">
|
||||
<JMarkdownEditor v-model:value="writeText" height="100vh" :preview="{ mode: 'view', action: [] }" />
|
||||
</div>
|
||||
<a-spin :spinning="writerLoading">
|
||||
<div v-html="previewMd" class="markdown-container" />
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick } from 'vue';
|
||||
import { ref, computed, nextTick, onMounted } from 'vue';
|
||||
import AiWriterLeft from './AiWriterLeft.vue';
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
import { CopyOutlined } from '@ant-design/icons-vue';
|
||||
import { CopyOutlined, ThunderboltOutlined, FormOutlined, CheckOutlined, SaveOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { copyTextToClipboard } from '/@/hooks/web/useCopyToClipboard';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import mdKatex from '@traptitech/markdown-it-katex';
|
||||
import mila from 'markdown-it-link-attributes';
|
||||
import hljs from 'highlight.js';
|
||||
import JMarkdownEditor from '/@/components/Form/src/jeecg/components/JMarkdownEditor.vue';
|
||||
import '/@/views/super/airag/aiapp/chat/style/github-markdown.less';
|
||||
import '/@/views/super/airag/aiapp/chat/style/highlight.less';
|
||||
import '/@/views/super/airag/aiapp/chat/style/style.less';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
//加载
|
||||
const writerLoading = ref<boolean>(false);
|
||||
const writeText = ref<string>('');
|
||||
const previewRef = ref<HTMLElement | null>(null);
|
||||
//生成的loading
|
||||
const generating = ref<boolean>(false);
|
||||
//是否编辑
|
||||
const isEditing = ref<boolean>(false);
|
||||
//润色的loading
|
||||
const polishLoading = ref<boolean>(false);
|
||||
//保存loading
|
||||
const saving = ref<boolean>(false);
|
||||
//左侧的内容
|
||||
const leftData = ref<any>();
|
||||
//历史数据
|
||||
const historyData = ref<any>([]);
|
||||
//ai提示文本
|
||||
const aiText = ref<string>('');
|
||||
//当期版本的key
|
||||
const CURRENT_VERSION_KEY = 'CURRENT';
|
||||
//当前选中的本本
|
||||
const activeVersion = ref<string>(CURRENT_VERSION_KEY);
|
||||
//原始内容
|
||||
const originalContent = ref<string | null>(null);
|
||||
//第一个回复节点之后的内容
|
||||
const afterNodeFinished = ref<boolean>(false);
|
||||
const mdi = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
@ -55,13 +128,27 @@
|
||||
|
||||
//返回文本(生成中在末尾追加打点)
|
||||
const previewMd = computed(() => {
|
||||
const html = mdi.render(writeText.value);
|
||||
let content = writeText.value || aiText.value;
|
||||
if (generating.value) {
|
||||
return html + '<span class=\"typing-dot\"></span><span class=\"typing-dot\"></span><span class=\"typing-dot\"></span>';
|
||||
content +=
|
||||
' <span class="typing-dot"></span><span class="typing-dot" style="animation-delay: 0.2s"></span><span class="typing-dot" style="animation-delay: 0.4s"></span>';
|
||||
}
|
||||
return html;
|
||||
return mdi.render(content);
|
||||
});
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
function toggleEdit() {
|
||||
if (generating.value) {
|
||||
return;
|
||||
}
|
||||
isEditing.value = !isEditing.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制
|
||||
*/
|
||||
function handleCopy() {
|
||||
if (!writeText.value) {
|
||||
createMessage.warning('暂无内容可复制');
|
||||
@ -85,26 +172,35 @@
|
||||
* 生成
|
||||
*
|
||||
* @param data
|
||||
* @param type
|
||||
*/
|
||||
async function onGenerate(data) {
|
||||
async function onGenerate(data, type = '') {
|
||||
isEditing.value = false;
|
||||
data.responseMode = 'streaming';
|
||||
writerLoading.value = true;
|
||||
writeText.value = '';
|
||||
generating.value = true;
|
||||
activeVersion.value = CURRENT_VERSION_KEY;
|
||||
if (!type) {
|
||||
leftData.value = data;
|
||||
}
|
||||
|
||||
try {
|
||||
let readableStream = await defHttp.post(
|
||||
{
|
||||
url: '/airag/chat/genAiWriter',
|
||||
params: { ...data },
|
||||
timeout: 5 * 60 * 1000,
|
||||
adapter: 'fetch',
|
||||
responseType: 'stream',
|
||||
},
|
||||
{
|
||||
isTransformResponse: false,
|
||||
}
|
||||
);
|
||||
let readableStream = await defHttp
|
||||
.post(
|
||||
{
|
||||
url: '/airag/chat/genAiWriter',
|
||||
params: { ...data },
|
||||
timeout: 5 * 60 * 1000,
|
||||
adapter: 'fetch',
|
||||
responseType: 'stream',
|
||||
},
|
||||
{
|
||||
isTransformResponse: false,
|
||||
}
|
||||
)
|
||||
.catch((res) => {
|
||||
createMessage.warn(res.message ? res.message : '请求出错,请稍后重试!');
|
||||
});
|
||||
|
||||
const reader = readableStream.getReader();
|
||||
const decoder = new TextDecoder('UTF-8');
|
||||
@ -122,40 +218,37 @@
|
||||
const lines = buffer.split('\n\n');
|
||||
// 保留最后一个片段(可能不完整)
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (!trimmedLine) continue;
|
||||
|
||||
if (trimmedLine.startsWith('data:')) {
|
||||
const content = trimmedLine.replace('data:', '').trim();
|
||||
if (content) {
|
||||
renderText(content);
|
||||
if (line.startsWith('data:')) {
|
||||
const content = line.replace('data:', '').trim();
|
||||
if(!content){
|
||||
continue;
|
||||
}
|
||||
if(!content.endsWith('}')){
|
||||
buffer = buffer + line;
|
||||
continue;
|
||||
}
|
||||
buffer = "";
|
||||
renderText(content)
|
||||
} else {
|
||||
// 尝试直接解析(兼容非SSE格式或异常格式)
|
||||
renderText(trimmedLine);
|
||||
if(!line) {
|
||||
continue;
|
||||
}
|
||||
if(!line.endsWith('}')) {
|
||||
buffer = buffer + line;
|
||||
continue;
|
||||
}
|
||||
buffer = "";
|
||||
renderText(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理剩余的buffer
|
||||
if (buffer && buffer.trim()) {
|
||||
const trimmedLine = buffer.trim();
|
||||
if (trimmedLine.startsWith('data:')) {
|
||||
renderText(trimmedLine.replace('data:', '').trim());
|
||||
} else {
|
||||
renderText(trimmedLine);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Generation error:', e);
|
||||
writeText.value += '\n\n[生成出错]';
|
||||
} finally {
|
||||
writerLoading.value = false;
|
||||
// 若服务端结束未触发 MESSAGE_END,兜底关闭生成状态
|
||||
generating.value = false;
|
||||
nextTick(scrollToBottom);
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,25 +260,52 @@
|
||||
function renderText(item) {
|
||||
try {
|
||||
let parse = JSON.parse(item);
|
||||
if(parse.event == 'NODE_FINISHED'){
|
||||
afterNodeFinished.value = true;
|
||||
return;
|
||||
}
|
||||
if (parse.event == 'MESSAGE') {
|
||||
writeText.value += parse.data.message;
|
||||
if (writerLoading.value) {
|
||||
writerLoading.value = false;
|
||||
aiText.value = '';
|
||||
if (afterNodeFinished.value) {
|
||||
writeText.value = parse.data.message;
|
||||
afterNodeFinished.value = false;
|
||||
} else {
|
||||
writeText.value += parse.data.message;
|
||||
}
|
||||
generating.value = true;
|
||||
polishLoading.value = true;
|
||||
nextTick(scrollToBottom);
|
||||
}
|
||||
if (parse.event == 'MESSAGE_END') {
|
||||
writerLoading.value = false;
|
||||
generating.value = false;
|
||||
if (activeVersion.value === CURRENT_VERSION_KEY) {
|
||||
originalContent.value = writeText.value;
|
||||
}
|
||||
nextTick(scrollToBottom);
|
||||
}
|
||||
if (parse.event == 'ERROR') {
|
||||
writeText.value = parse.data.message ? parse.data.message : '生成失败,请稍后重试!';
|
||||
writerLoading.value = false;
|
||||
generating.value = false;
|
||||
polishLoading.value = false;
|
||||
nextTick(scrollToBottom);
|
||||
}
|
||||
|
||||
//开始加点
|
||||
if (parse.event === 'NODE_STARTED') {
|
||||
if (!parse.data || parse.data.type !== 'end') {
|
||||
if (parse.data.type === 'llm' || parse.data.type === 'reply') {
|
||||
aiText.value = '正在构建响应内容';
|
||||
}
|
||||
}
|
||||
}
|
||||
//流程结束节点
|
||||
if (parse.event == 'FLOW_FINISHED') {
|
||||
if (parse.data && !parse.data.success) {
|
||||
writeText.value = parse.data.message ? parse.data.message : '生成失败,请稍后重试!';
|
||||
}
|
||||
generating.value = false;
|
||||
polishLoading.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error parsing update:', error);
|
||||
}
|
||||
@ -199,6 +319,127 @@
|
||||
function highlightBlock(str: string, lang?: string) {
|
||||
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">复制代码</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 润色
|
||||
*/
|
||||
async function handlePolish() {
|
||||
if (!writeText.value) {
|
||||
createMessage.warning('暂无内容可润色');
|
||||
return;
|
||||
}
|
||||
if (generating.value || polishLoading.value) {
|
||||
return;
|
||||
}
|
||||
polishLoading.value = true;
|
||||
const data: any = {
|
||||
prompt: writeText.value,
|
||||
originalContent: '',
|
||||
length: leftData.value.length,
|
||||
format: leftData.value.format,
|
||||
tone: leftData.value.tone,
|
||||
language: leftData.value.language,
|
||||
activeMode: 'polish',
|
||||
};
|
||||
try {
|
||||
await onGenerate(data, 'polish');
|
||||
} finally {
|
||||
polishLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
async function handleSave() {
|
||||
if (!writeText.value) {
|
||||
createMessage.warning('暂无内容可保存');
|
||||
return;
|
||||
}
|
||||
if (saving.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
saving.value = true;
|
||||
await defHttp.post({ url: '/airag/app/save/article/write', params: { content: writeText.value } });
|
||||
if (activeVersion.value === CURRENT_VERSION_KEY) {
|
||||
originalContent.value = writeText.value;
|
||||
}
|
||||
initHistoryData();
|
||||
} catch (e) {
|
||||
createMessage.error('保存失败,请稍后重试');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化历史信息
|
||||
*/
|
||||
function initHistoryData() {
|
||||
historyData.value = [];
|
||||
defHttp.get({ url: "/airag/app/list/article/write" }, { isTransformResponse: false }).then((res)=>{
|
||||
if(res.success){
|
||||
historyData.value = res.result;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本删除
|
||||
*/
|
||||
async function handleDeleteVersion() {
|
||||
if (activeVersion.value === CURRENT_VERSION_KEY) {
|
||||
return;
|
||||
}
|
||||
const target = historyData.value.find((item) => item.version === activeVersion.value);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '删除版本',
|
||||
content: '是否确认删除该版本?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
try {
|
||||
await defHttp.delete(
|
||||
{ url: '/airag/app/delete/article/write', params: { version: target.version } },
|
||||
{ joinParamsToUrl: true }
|
||||
);
|
||||
historyData.value = historyData.value.filter((item) => item.version !== target.version);
|
||||
activeVersion.value = CURRENT_VERSION_KEY;
|
||||
writeText.value = originalContent.value ?? '';
|
||||
} catch (e) {
|
||||
createMessage.error('删除失败,请稍后重试');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本切换事件
|
||||
* @param value
|
||||
*/
|
||||
function handleVersionChange(value: string) {
|
||||
if (value === CURRENT_VERSION_KEY) {
|
||||
activeVersion.value = value;
|
||||
writeText.value = originalContent.value ?? '';
|
||||
return;
|
||||
}
|
||||
const target = historyData.value.find((item) => item.version === value);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
activeVersion.value = value;
|
||||
writeText.value = target.content;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
//初始化的时候加载历史版本
|
||||
initHistoryData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@ -222,13 +463,15 @@
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
/*begin 头部样式 */
|
||||
.preview {
|
||||
flex: 1;
|
||||
border: 1px solid #eef0f5;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
padding: 16px 20px;
|
||||
overflow: auto;
|
||||
background: #fafbff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
@ -239,32 +482,94 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #eef0f5;
|
||||
padding-bottom: 8px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.preview-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
transition: color 0.3s;
|
||||
.preview-title-text {
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
.preview-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.version-select {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.preview-action-btn {
|
||||
padding: 0 14px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
height: 26px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.custom-green-btn {
|
||||
background-color: @primary-color;
|
||||
border-color: @primary-color;
|
||||
color: #ffffff;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: @primary-color;
|
||||
border-color: @primary-color;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-actions-icon {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.custom-save-btn, .version-delete-btn {
|
||||
background-color: #ffffff;
|
||||
border-color: @primary-color;
|
||||
color: @primary-color;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: fade(@primary-color, 10%);
|
||||
border-color: @primary-color;
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
.preview-actions-icon {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
/*end 头部样式 */
|
||||
|
||||
/*begin 编辑器的样式*/
|
||||
.markdown-container {
|
||||
min-height: 300px;
|
||||
/* 缩小图片宽度 */
|
||||
padding: 8px 4px 16px 4px;
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
cursor: text;
|
||||
:deep(img) {
|
||||
width: 60% !important;
|
||||
width: 40% !important;
|
||||
max-width: 280px;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.typing-dot {
|
||||
.markdown-editor-container {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
:deep(.typing-dot) {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@ -287,4 +592,5 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
/*end 编辑器的样式*/
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user