Browse Source

aichat升级

master
xueyinfei 4 days ago
parent
commit
62d9ef33b6
  1. 2
      vue-fastapi-backend/module_admin/service/aichat_service.py
  2. 8
      vue-fastapi-frontend/src/api/aichat/aichat.js
  3. 4
      vue-fastapi-frontend/src/layout/components/AppMain.vue
  4. 87
      vue-fastapi-frontend/src/views/aichat/Interrupt.vue
  5. 25
      vue-fastapi-frontend/src/views/aichat/OperationButton.vue
  6. 180
      vue-fastapi-frontend/src/views/aichat/aichat.vue
  7. 4
      vue-fastapi-frontend/src/views/aichat/index.vue
  8. 34
      vue-fastapi-frontend/src/views/aichat/markdown.vue

2
vue-fastapi-backend/module_admin/service/aichat_service.py

@ -38,7 +38,7 @@ class AiChatService:
@classmethod
async def delete_chat_list(cls, result_db: AsyncSession, chatId: str,
current_user: Optional[CurrentUserModel] = None):
chat = AiChatDao.get_ai_chat_by_id(chatId, result_db)
chat = await AiChatDao.get_ai_chat_by_id(chatId, result_db)
await AiChatDao.delete_chat_with_session_and_time(result_db, chat.sessionId, chat.time)
await result_db.commit()
return CrudResponseModel(is_success=True, message='删除成功')

8
vue-fastapi-frontend/src/api/aichat/aichat.js

@ -26,6 +26,14 @@ export function postChatMessage(data,signal) {
return postStream('/aichat-api/datagov_assist',data,signal)
}
export function cancelJob(data){
return request({
url: '/aichat-api/cancel_job',
method: 'post',
data: data
})
}
export function DeleteChatSession(sessionId) {
return request({
url: '/default-api/aichat/delete/session/'+sessionId,

4
vue-fastapi-frontend/src/layout/components/AppMain.vue

@ -124,10 +124,10 @@ function closeChatDiv(){
cursor: pointer;
max-height: 500px;
max-width: 500px;
z-index: 10000;
z-index: 1000;
}
.ai_chat_div {
z-index: 10000;
z-index: 1000;
border-radius: 8px;
border: 1px solid #ffffff;
background: linear-gradient(188deg, rgba(235, 241, 255, 0.20) 39.6%, rgba(231, 249, 255, 0.20) 94.3%), #EFF0F1;

87
vue-fastapi-frontend/src/views/aichat/Interrupt.vue

@ -1,21 +1,21 @@
<template>
<template v-if="formData && formData.block">
<el-form :rules="formRules" ref="dynamicForm" >
<template v-for="(item, index) in formData.block">
<template v-if="source && source.block">
<el-form ref="dynamicForm" :model="formData" :rules="formRules">
<template v-for="(item, index) in source.block">
<el-form-item :key="'span'+ index" v-if="item.d_type === 'text' && item.ct_type === 'span'">
<span style="font-size: 14px">{{item.default_value}}</span>
</el-form-item>
<el-form-item v-else :key="'comp'+ index" :label="item.name" :prop="'field_' + index" :rules="item.required ? [{ required: true, message: `${item.name}必填` }] : []">
<el-date-picker v-if="item.d_type === 'date' && item.ct_type === 'datePicker'" v-model="item.default_value" type="date" placeholder="请输入日期" value-format="YYYY-MM-DD" :disabled="item.read_only"/>
<el-date-picker v-if="item.d_type === 'date' && item.ct_type === 'dateRangePicker'" v-model="item.default_value" type="daterange" range-separator="" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD" :disabled="item.read_only"/>
<el-input v-if="item.d_type === 'text' && item.ct_type === 'input'" v-model="item.default_value" :disabled="item.read_only"></el-input>
<el-radio-group v-if="item.d_type === 'enum' && item.ct_type === 'radioGroup'" v-model="item.default_value" :disabled="item.read_only">
<el-form-item v-else :key="'comp'+ index" :label="item.name" :prop="item.name" :rules="getValidationRules(item)">
<el-date-picker popper-class="custom-dropdown" v-if="item.d_type === 'date' && item.ct_type === 'datePicker'" v-model="formData[item.name]" type="date" placeholder="请输入日期" value-format="YYYY-MM-DD" :disabled="item.read_only || !isLastChat"/>
<el-date-picker popper-class="custom-dropdown" v-if="item.d_type === 'date' && item.ct_type === 'dateRangePicker'" v-model="formData[item.name]" type="daterange" range-separator="" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD" :disabled="item.read_only || !isLastChat"/>
<el-input v-if="item.d_type === 'text' && item.ct_type === 'input'" v-model="formData[item.name]" :disabled="item.read_only || !isLastChat"></el-input>
<el-radio-group v-if="item.d_type === 'enum' && item.ct_type === 'radioGroup'" v-model="formData[item.name]" :disabled="item.read_only || !isLastChat">
<el-radio v-for="radio in item.options" :value="radio">{{radio}}</el-radio>
</el-radio-group>
<el-checkbox-group v-if="item.d_type === 'enum' && item.ct_type === 'checkboxGroup'" v-model="item.default_value" :disabled="item.read_only">
<el-checkbox-group v-if="item.d_type === 'enum' && item.ct_type === 'checkboxGroup'" v-model="formData[item.name]" :disabled="item.read_only || !isLastChat">
<el-checkbox v-for="checkItem in item.options" :label="checkItem" :value="checkItem" />
</el-checkbox-group>
<el-select v-if="item.d_type === 'enum' && item.ct_type === 'select'" v-model="item.default_value" :disabled="item.read_only" :filterable="item.allow_create" :allow-create="item.allow_create">
<el-select class="interrupt-select" v-if="item.d_type === 'enum' && item.ct_type === 'select'" v-model="formData[item.name]" :disabled="item.read_only || !isLastChat" :filterable="item.allow_create" :allow-create="item.allow_create">
<el-option
v-for="selectItem in item.options"
:key="selectItem"
@ -23,7 +23,7 @@
:value="selectItem"
/>
</el-select>
<el-select v-if="item.d_type === 'enum' && item.ct_type === 'multiselect'" multiple v-model="item.default_value" :disabled="item.read_only" :filterable="item.allow_create" :allow-create="item.allow_create">
<el-select popper-class="custom-dropdown" v-if="item.d_type === 'enum' && item.ct_type === 'multiselect'" multiple v-model="formData[item.name]" :disabled="item.read_only || !isLastChat" :filterable="item.allow_create" :allow-create="item.allow_create">
<el-option
v-for="selectItem in item.options"
:key="selectItem"
@ -34,29 +34,53 @@
</el-form-item>
</template>
<el-form-item>
<el-button :disabled="!isLastChat" v-for="item in source.action" :type="item.style" @click="click(item)">{{item.label}}</el-button>
<div style="text-align: center;width: 100%">
<el-button :disabled="!isLastChat" v-for="item in source.action" :type="item.style" @click="click(item)">
<el-icon v-if="!isLastChat && action === item.action"><CircleCheckFilled /></el-icon>
{{item.label}}
</el-button>
</div>
</el-form-item>
</el-form>
</template>
</template>
<script setup>
import { ref, nextTick, computed, watch, reactive, onMounted } from 'vue'
const emit = defineEmits(['processAuth'])
const { proxy } = getCurrentInstance();
const props = defineProps({
source: Object,
checkpointer: Object,
action: String,
chatId: String,
isLastChat: Boolean
})
const formData = ref({})
const formRules = ref({})
onMounted(()=>{
formData.value = props.source
})
const formData = reactive({});
const formRules = computed(() => {
const rules = {};
props.source.block.forEach((item) => {
rules[item.name] = getValidationRules(item);
});
return rules;
});
function getValidationRules(item) {
const baseRules = [];
//
if (item.required) {
baseRules.push({
required: true,
message: `${item.name}必填`,
// trigger: 'blur'
});
}
return [...baseRules];
}
watch(() => props.source?.block, (blocks) => {
if (!blocks) return;
//
blocks.forEach((item) => {
formData[item.name] = item.default_value
});
}, { immediate: true });
function click(item){
if (item.style === 'primary'){
@ -64,25 +88,28 @@ function click(item){
if (valid) {
let data = {
'chatId':props.chatId,
'checkpointer': props.style.checkpointer,
'interrupt': formData.value,
'action': item.action
'checkpointer': props.checkpointer,
'interrupt': props.source,
'action': item.action,
'formData': formData
}
emit('processAuth',data)
}
})
}else{
let data = {
'checkpointer': props.style.checkpointer,
'block': formData.value.block,
'action': item.action
'checkpointer': props.checkpointer,
'block': props.source.block,
'interrupt': props.source,
'action': item.action,
'formData': formData
}
emit('processAuth',data)
}
}
</script>
<style scoped lang="scss">
:deep(.interrupt-select .el-select-dropdown){
z-index: 2000 !important;
}
</style>

25
vue-fastapi-frontend/src/views/aichat/OperationButton.vue

@ -6,12 +6,12 @@
</div>
<div>
<span>
<el-tooltip effect="dark" content="换个答案" placement="top" popper-class="operate-tooltip">
<el-button text @click="regeneration" style="padding: 0">
<el-icon><RefreshRight /></el-icon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
<!-- <el-tooltip effect="dark" content="换个答案" placement="top" popper-class="operate-tooltip">-->
<!-- <el-button text @click="regeneration" style="padding: 0">-->
<!-- <el-icon><RefreshRight /></el-icon>-->
<!-- </el-button>-->
<!-- </el-tooltip>-->
<!-- <el-divider direction="vertical" />-->
<el-tooltip effect="dark" content="复制" placement="top" popper-class="operate-tooltip">
<el-button text @click="copyClick(data)" style="padding: 0">
<el-icon><CopyDocument /></el-icon>
@ -45,7 +45,7 @@
:visible="visible"
v-if="data.operate === null || data.operate === ''"
placement="left"
popper-style="z-index:99999;"
popper-style="z-index:2000;"
:show-arrow=false
title="反对原因"
:width="260">
@ -105,11 +105,12 @@ const props = defineProps({
const thumbDownReason = ref("")
const visible = ref(false)
const emit = defineEmits(['update:data', 'regeneration', 'changeThumb'])
const emit = defineEmits(['update:data', 'changeThumb'])
// const emit = defineEmits(['update:data', 'regeneration', 'changeThumb'])
function regeneration() {
emit('regeneration')
}
// function regeneration() {
// emit('regeneration')
// }
function closeThumb(){
visible.value = false
@ -235,6 +236,6 @@ function ToPlainText(md) {
</script>
<style lang="scss">
.operate-tooltip {
z-index: 99999 !important;
z-index: 2000 !important;
}
</style>

180
vue-fastapi-frontend/src/views/aichat/aichat.vue

@ -30,7 +30,7 @@
</div>
<!-- 回答 -->
<div v-if="item.type === 'answer'" class="item-content mb-16 lighter" style="font-weight: 400">
<el-popconfirm popper-style="z-index:99999" title="确定要回到这一步吗?" @confirm="confirmReturn(item,index)">
<el-popconfirm popper-style="z-index:2000" title="确定要回到这一步吗?" @confirm="confirmReturn(item,index)">
<template #reference>
<div class="returnToHere" style="margin-top: 5px;margin-bottom: 5px">
<i class="ri-bookmark-2-fill returnToHereIcon"></i>
@ -46,27 +46,44 @@
<!-- </div>-->
<div class="content">
<MdRenderer :is_large="is_large" :chatIndex="index" :source="item.content" @fullscreenG6="fullscreen"></MdRenderer>
<Interrupt :isLastChat="index === (chatList.length - 1)" :chatId="item.chatId" :source="item.interrupt" :checkpointer="item.checkpointer" @processAuth="processAuth"></Interrupt>
<div class="flex-between mt-8" style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px">
<div>
<el-button
type="primary"
v-if="item.isStop && !item.isEnd"
@click="startChat(index)"
link
>重新生成
</el-button>
<el-button type="primary" v-else-if="!item.isEnd" @click="stopChat(index)" link
>停止回答
</el-button>
</div>
</div>
<Interrupt :isLastChat="index === (chatList.length - 1)" :chatId="item.chatId" :action="item.action" :source="item.interrupt" :checkpointer="item.checkpointer" @processAuth="processAuth"></Interrupt>
<div v-if="item.isEnd" class="flex-between" style="display: flex; justify-content: space-between; align-items: center;">
<OperationButton :data="item" :index="index" @regeneration="regenerationChart(index)" @changeThumb="changeThumb" />
<OperationButton :data="item" :index="index" @changeThumb="changeThumb" />
<!-- <OperationButton :data="item" :index="index" @regeneration="regenerationChart(index)" @changeThumb="changeThumb" />-->
</div>
</div>
</div>
</template>
<div class="flex-between mt-8" style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px">
<div>
<div v-if="currentJobId !== ''">
<i class="ri-loader-2-line loading-icon"></i>
<span style="font-size: 14px;color: #409EFF">回复中...</span>
</div>
<!-- <el-button-->
<!-- type="primary"-->
<!-- v-if="chatList.length>0 && chatList[chatList.length-1].isStop && !chatList[chatList.length-1].isEnd"-->
<!-- @click="startChat(chatList.length-1)"-->
<!-- link-->
<!-- >重新生成-->
<!-- </el-button>-->
<!-- <el-button type="primary" v-else-if="chatList.length>0 && !chatList[chatList.length-1].isEnd" @click="stopChat(chatList.length-1)" link-->
<!-- >停止回答-->
<!-- </el-button>-->
<el-button type="primary" v-if="currentJobId !== ''" link @click="stopChat(chatList.length-1)"
>停止回答
</el-button>
</div>
</div>
<div v-if="currentError !== ''">
<div class="returnToHere" style="margin-top: 5px;margin-bottom: 5px">
<i class="ri-bookmark-2-fill returnToHereIcon"></i>
<el-divider class="returnToHereDivider">
</el-divider>
</div>
<span style="font-size: 14px">{{currentError}}</span>
<el-link :underline="false" icon="Refresh" type="primary" @click="refreshModal">重试</el-link>
</div>
</div>
</el-scrollbar>
<div class="ai-chat__operate p-24" style="padding: 24px">
@ -91,7 +108,7 @@
:visible="popoverVisible"
placement="top"
:width="is_large?'400px':'860px'"
popper-style="max-width:860px;z-index:99999;margin-left:35px"
popper-style="max-width:860px;z-index:2000;margin-left:35px"
:show-arrow=false
:offset=1
>
@ -161,7 +178,7 @@
<img v-show="!isDisabledChart && !loading" src="@/assets/aichat/icon_send_colorful.svg" alt="" />
</el-button>
<el-popover
popper-style="z-index:99999;width:300px"
popper-style="z-index:2000;width:300px"
title="自动审批配置"
placement="left-end"
trigger="click"
@ -199,7 +216,7 @@
</div>
</div>
<!-- 文件导入对话框 -->
<el-dialog :z-index="99999" title="文件导入" v-model="upload.open" width="400px" append-to-body>
<el-dialog :z-index="2000" title="文件导入" v-model="upload.open" width="400px" append-to-body>
<el-upload v-if="upload.open"
ref="uploadRef"
:headers="upload.headers"
@ -233,7 +250,7 @@ import OperationButton from './OperationButton.vue'
import MdRenderer from '@/views/aichat/MdRenderer.vue'
import fullscreenG6 from '@/views/aichat/fullscreenG6.vue'
import {getToken} from "@/utils/auth.js";
import {addChat, DeleteChatList, postChatMessage, updateChatProcessData} from "@/api/aichat/aichat.js"
import {addChat, cancelJob, DeleteChatList, postChatMessage, updateChatProcessData} from "@/api/aichat/aichat.js"
import cache from "@/plugins/cache.js";
import Cookies from "js-cookie";
import Interrupt from "@/views/aichat/Interrupt.vue";
@ -279,8 +296,8 @@ const inputValue = ref('')
const currentQuestion = ref({})
const chartOpenId = ref('')
const chatList = ref([])
const answerList = ref([])
const controller = ref(null)
const currentChatData = ref({})
const autoProcess = ref({
checkAll: false,
isIndeterminate: false,
@ -290,6 +307,9 @@ const autoProcess = ref({
const popoverVisible = ref(false)
const currentMachine = ref([])
const currentFiles = ref([])
const currentError = ref('')
const currentJobId = ref('')
const lastQuestion = ref({})
const upload = reactive({
//
open: false,
@ -415,24 +435,30 @@ watch(
watch(() => props.cookieSessionId, value => upload.data = {sessionId:value})
function regenerationChart(index){
let question = chatList.value[index - 1]
let chat = {
"chatId":uuidv4(),
"type":"question",
"content":question.content,
"time": formatDate(new Date()),
"file": question.file}
chatList.value.push(chat)
let data = {
"query": chat.content,
"user_id": cache.local.get("username"),
"robot": currentMachine.value.length>0?currentMachine.value[0]:"",
"session_id": Cookies.get("chatSessionId"),
"doc": chat.file,
}
sendChatMessage(data)
}
// function regenerationChart(index){
//
// if (currentQuestion.value.query){
// sendChatMessage(currentQuestion.value)
// }else{
// // let question = chatList.value[index - 1]
// // let chat = {
// // "chatId":uuidv4(),
// // "type":"question",
// // "content":question.content,
// // "time": formatDate(new Date()),
// // "file": question.file}
// // chatList.value.push(chat)
// // let data = {
// // "query": chat.content,
// // "user_id": cache.local.get("username"),
// // "robot": currentMachine.value.length>0?currentMachine.value[0]:"",
// // "session_id": Cookies.get("chatSessionId"),
// // "doc": chat.file,
// // }
// sendChatMessage(data)
// }
//
// }
function changeThumb(index,chat){
chatList.value[index].operate = chat.operate
@ -503,6 +529,13 @@ function downloadFile(file,bucket,sessionId){
}
function processAuth(data){
for (let i = 0; i < data.interrupt.block.length; i++) {
for (let key in data.formData){
if (key && key === data.interrupt.block[i].name){
data.interrupt.block[i].default_value = data.formData[key]
}
}
}
let updateData = {
chatId: data.chatId,
action: data.action,
@ -517,10 +550,15 @@ function processAuth(data){
"resume": true,
"block": data.interrupt.block
}
currentChatData.value = reqData
sendChatMessage(reqData)
})
}
function refreshModal(){
sendChatMessage(currentChatData.value)
}
async function sendChatHandle(event) {
if (!event.ctrlKey) {
// ctrl
@ -570,8 +608,11 @@ function sendChatMessage(data){
controller.value = new AbortController()
postChatMessage(data,{signal:controller.value.signal}).then(res=>{
if (res.status !== 200){
chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[{"type":"text","content":"服务异常,错误码:"+res.status}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":'',"thumbDownReason":''})
// chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[{"type":"text","content":":"+res.status}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":'',"thumbDownReason":''})
currentError.value = "服务异常,错误码:"+res.status +",请联系管理员!"
}else {
currentError.value = ''
currentChatData.value = {}
currentFiles.value = []
chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[],"isEnd":false,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName, "operate":'',"thumbDownReason":''})
const reader = res.body.getReader()
@ -579,7 +620,7 @@ function sendChatMessage(data){
reader.read().then(write).then(()=> {
let answer = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1]))
answer.content = JSON.stringify(answer.content)
answer.interrupt = JSON.stringify(answer.interrupt)? answer.interrupt: null
answer.interrupt = answer.interrupt ? JSON.stringify(answer.interrupt): null
answer.checkpointer = JSON.stringify(answer.checkpointer)
addChat(answer)
}).then(()=>{
@ -631,7 +672,7 @@ function sendChatMessage(data){
})
}
}).catch((e) => {
chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[{"type":"text","content":"服务异常"}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":"","thumbDownReason":""})
currentError.value = "服务异常,请联系系统管理员!"
})
}
@ -653,13 +694,11 @@ watch(
const getWrite = (reader) => {
// 1tempResult
let tempResult = '';
const write_stream = ({ done, value }) => {
if (done) return;
const decoder = new TextDecoder('utf-8');
let str = decoder.decode(value, { stream: true });
tempResult += str;
// 2使
const split = tempResult.match(/data:.*?}\r\n/g);
@ -684,6 +723,7 @@ const getWrite = (reader) => {
};
const processChunk = (chunk) => {
currentJobId.value = chunk.job_id;
const lastMsg = chatList.value[chatList.value.length - 1];
// 5
if (chunk.docs?.length) {
@ -700,12 +740,13 @@ const getWrite = (reader) => {
// 6
// const text = chunk.choices[0].delta.content.replace(/\n/g, "\n\n");
//
const content = chunk.choices[0].delta.content;
const isNewParagraph = content.startsWith('\n\n');
const text = chunk.choices[0].delta.content;
// const isNewParagraph = content.startsWith('\n\n');
//
const text = isNewParagraph
? content.replace(/\n{2,}/g, '\n\n')
: content.replace(/\n/g, ' ');
// const text = content;
// const text = isNewParagraph
// ? content.replace(/\n{2,}/g, '\n\n')
// : content.replace(/\n/g, '\n\n');
const lastContent = lastMsg.content[lastMsg.content.length - 1];
if (lastContent?.type === "text") {
lastContent.content += text;
@ -719,6 +760,7 @@ const getWrite = (reader) => {
// 7
if (chunk.checkpointer) {
lastMsg.checkpointer = chunk.checkpointer
currentJobId.value = ''
lastMsg.isEnd = true;
lastMsg.time = formatDate(new Date());
}
@ -729,6 +771,9 @@ const getWrite = (reader) => {
};
const stopChat = (index) => {
if (currentJobId.value !== ''){
cancelJob({job_id:currentJobId.value})
}
chatList.value[index].isStop = true
if (controller.value !== null){
controller.value.abort()
@ -736,11 +781,12 @@ const stopChat = (index) => {
let answer = JSON.parse(JSON.stringify(chatList.value[index]))
answer.content = JSON.stringify(answer.content)
addChat(answer)
currentJobId.value = ''
}
const startChat = (index) => {
regenerationChart(index)
}
// const startChat = (index) => {
// regenerationChart(index)
// }
defineExpose({
setScrollBottom
@ -945,5 +991,33 @@ defineExpose({
:deep(.el-divider__line) {
border-top-color: var(--divider-border-color) !important;
}
/* 定义旋转动画 */
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 应用动画到图标 */
//.ri-loader-2-line {
// display: inline-block; /* transform */
// animation: rotate 1s linear infinite; /* 1 */
// transform-origin: 50% 50%;
//}
/* 可选:添加渐隐效果 */
//.ri-loader-2-line.fade {
// opacity: 0.7;
// transition: opacity 0.3s;
//}
.loading-icon {
display: inline-block; /* 确保 transform 生效 */
animation: rotate 1s linear infinite; /* 1秒一圈,匀速旋转,无限循环 */
transform-origin: 50% 50%;
color: #409EFF; /* 修改颜色 */
//animation: rotate 1s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite;
/* 使用非匀速旋转增加动感 */
}
</style>

4
vue-fastapi-frontend/src/views/aichat/index.vue

@ -226,7 +226,7 @@ onMounted(
top: 56px;
background: #ffffff;
padding-bottom: 24px;
z-index: 2009;
z-index: 1009;
}
.chat-popover-button {
z-index: 100;
@ -244,7 +244,7 @@ onMounted(
position: absolute;
right: 0;
top: 56px;
z-index: 2008;
z-index: 1008;
}
.gradient-divider {
position: relative;

34
vue-fastapi-frontend/src/views/aichat/markdown.vue

@ -8,10 +8,19 @@ const props = defineProps({
markdownString: String
})
const compiledMarkdown = ref('')
const md = new MarkdownIt()
const md = new MarkdownIt({
breaks: true, // <br>
html: true, // HTML
//
configure: (md) => {
md.renderer.rules.hardbreak = (tokens, idx) => {
return '<br class="custom-break">';
};
}
})
watch(() => props.markdownString, (newValue) => {
if (newValue){
compiledMarkdown.value = md.render(newValue.replace("\n","\n\n"));
compiledMarkdown.value = md.render(newValue);
}
}, { immediate: true });
</script>
@ -20,4 +29,25 @@ watch(() => props.markdownString, (newValue) => {
text-decoration: underline;
color: blue
}
.markdown-content {
//
line-height: 1.8;
//
p {
margin: 0; //
//margin-bottom: 0.5em; //
}
// <br>
br {
display: block;
content: ''; //
margin-top: 0.5em; //
}
//
word-break: break-word;
overflow-wrap: break-word;
}
</style>
Loading…
Cancel
Save