<template>
<div class="math-content" ref="container" v-html="processedContent"></div>
<!-- <div v-if="fileUrls && fileUrls.length">
<div v-for="(val,index) in fileUrls" :key="index" class="flex-center-warp" @click="openUrl(val)">
<img src="../../../assets/item/ppt.png" alt="" style="width: 20px; height: 20px;margin-right:10px;" />{{getFileName(val)}}
</div>
</div> -->
<div class="flex">
<Popover>
<div @click="handleCopy" class="copys" v-if="is_loadin">
<img src="../../../assets/item/copy.png" alt="" style="width: 20px; height: 20px" />
</div>
<template #content>
<div> 复制 </div>
</template>
</Popover>
<Popover>
<div @click="handleDownloadFile()" class="download" v-if="is_loadin">
<img src="../../../assets/item/download.png" alt="" style="width: 20px; height: 20px" />下载Word
</div>
<template #content>
<div> 下载Word</div>
</template>
</Popover>
<Popover>
<div @click="DownloadPPT" class="download" v-if="is_loadin">
<img src="../../../assets/item/download.png" alt="" style="width: 20px; height: 20px" />生成PPT
</div>
<template #content>
<div> 生成PPT </div>
</template>
</Popover>
<Modal v-model:visible="visible" title="修改信息" width='1200px'>
<iframe
:src='getUrlParams()'
width="100%"
height="800px"
v-if="visible"
frameborder="0"
seamless
name="iframe"
id="iframe"
></iframe>
<!-- <div class="item-radio">
<div>模版场景 : </div>
<Radio.Group v-model:value="scene">
<Radio.Button :key="val.id" :value="val.id" v-for="(val) in sceneList" @click="getListTemplayBox">{{val.name}}</Radio.Button>
</Radio.Group>
</div>
<div class="item-radio">
<div>模版风格 : </div>
<Radio.Group v-model:value="style">
<Radio.Button :key="val.id" :value="val.id" v-for="(val) in styleList" @click="getListTemplayBox">{{val.name}}</Radio.Button>
</Radio.Group>
</div>
<div class="flex">
<div class="ptt-left flex">
<div class="ptt-item" v-for="(val) in pptList" :key="val.id" @click="handleActive(val)" >
<img :src="val.urlList[0]" alt="" :class="['img',{ 'is_active': activeId === val.id }]">
</div>
</div>
<div class="ptt-right" v-if="urlList.length">
<div class="urlList">
<img :src="urlList[0]" alt="" class="imgs">
</div>
<div class="flex" style="justify-content: space-between;">
<div>
<img :src="urlList[1]" alt="" class="imgsBox">
</div>
<div>
<img :src="urlList[2]" alt="" class="imgsBox">
</div>
<div>
<img :src="urlList[3]" alt="" class="imgsBox">
</div>
</div>
</div>
</div> -->
<template #footer>
<Button @click="cancel">取消</Button>
<!-- <Button type="primary" @click="handleImportJson" :disabled='activeId ? false : true' :loading='loading'>{{ loading ? '生成模板中':'生成模板'}}</Button> -->
</template>
</Modal>
</div>
</template>
<script setup>
import { ref, watch, onMounted, nextTick,defineAsyncComponent } from 'vue';
import DOMPurify from 'dompurify';
import katex from 'katex';
import 'katex/dist/katex.css';
import { copyText } from '@/utils/copyTextToClipboard';
import {
downloadFile,
pptTemplateScenes,
pptTemplateStyes,
pptTemplates,
getListTemplay
} from '../../../api/auth/captcha';
import { Base64 } from 'js-base64';
import {
Modal,
Switch,
Input,
Button,
Popover,
Radio,
Form,
LayoutSider,
Layout,
LayoutContent,
Menu,
MenuItem,
SubMenu,
Tooltip,
Collapse,
CollapsePanel,
Drawer,
Upload,
message,
// Icon,
} from 'ant-design-vue';
// 定义接收的 props
const props = defineProps({
content: {
type: String,
default: '',
},
is_loadin: {
type: Boolean,
default: false,
},
modelMessageId:{
type: String,
default: '',
},
fileUrls: {
type: Array,
default: [],
},
});
const scene = ref(null)
const sceneList = ref([])
const style = ref(null)
const styleList = ref([])
const pptList = ref([])
const urlList = ref([])
const visible = ref(false)
const activeId = ref(null)
const loading = ref(false)
// const url = ref(`http://ppt.bsyxedu.com?userId=${props.modelMessageId}&test=${props.content.replace(htmlTagRegex, '')}`)
const DownloadPPT = () =>{
pptTemplateScenes().then(res=>{
sceneList.value = res.response
scene.value = res.response[0].id
pptTemplateStyes().then(res=>{
styleList.value = res.response
style.value = res.response[0].id
getListTemplayBox()
})
})
visible.value = true
}
const getFileName =(url)=> {
// 按斜杠分割URL,过滤空字符串(处理可能的多个连续斜杠)
const parts = url.split('/').filter(part => part !== '');
// 取最后一个元素(文件名),若数组为空则返回空字符串
return parts.length > 0 ? parts[parts.length - 1] : '';
}
const getUrlParams = () => {
let htmlTagRegex = /<\/?[a-zA-Z][^>]*>/g;
const test = props.content.replace(htmlTagRegex, '')
let test1 = test.replace(/"/g, '')
console.log(test1,'test');
return `http://ppt.bsyxedu.com?userId=${props.modelMessageId}&test=${test1}`
// return `http://localhost:5173/?userId=${props.modelMessageId}&test=${test1}`
}
const handleImportJson =()=>{
let htmlTagRegex = /<\/?[a-zA-Z][^>]*>/g;
const test = props.content.replace(htmlTagRegex, '')
let test1 = test.replace(/"/g, '')
console.log(test1,'test');
window.open(`http://localhost:5173/?userId=${props.modelMessageId}&test=${test1}`)
return
let parem = {
// sceneId:scene.value,
// styleId:style.value
modelMessageId:props.modelMessageId,
pptId: activeId.value
}
loading.value = true
getListTemplay(parem).then(res=>{
loading.value = false
if(res.response && res.response.length){
window.open(res.response)
props.fileUrls.push(res.response)
}else{
message.error('生成出错请重试');
}
}).catch(()=>{
loading.value = false
})
}
const cancel = ()=>{
visible.value = false
activeId.value =null
}
const handleActive =(item)=>{
console.log(item,'item');
urlList.value = item.urlList
activeId.value = item.id
}
const getListTemplayBox =() =>{
activeId.value = null
urlList.value = []
let parem = {
sceneId:scene.value,
styleId:style.value
}
pptTemplates(scene.value,style.value).then(res=>{
pptList.value = res.response
urlList.value = res.response[0].urlList
activeId.value = res.response[0].id
console.log(res,'res');
})
}
const openUrl = (src)=>{
window.open(`http://edu-ftp-web.eduzhida.com/onlinePreview?url=${Base64.encode(src)}`)
}
const handleDownloadFile= () =>{
downloadFile(props.modelMessageId).then(res=>{
// props.fileUrls.push(res.response)
window.open(res.response)
})
}
const handleCopy = () => {
let htmlTagRegex = /<\/?[a-zA-Z][^>]*>/g;
let cleanedText = props.content.replace(htmlTagRegex, '');
cleanedText = cleanedText.replace(/"/g, '"').replace(/&/g, '&'); // 转换其他可能实体
console.log(cleanedText, 'processedContent');
copyText(cleanedText, '复制成功');
};
// 用于存储处理后的内容
const processedContent = ref('');
// 引用 DOM 元素
const container = ref(null);
// Katex 配置
const katexConfig = {
macros: {
'\\text': '#1', // 支持中文
'\\dfrac': '\\frac{#1}{#2}', // 自定义分数显示
},
throwOnError: false,
strict: false,
};
// 转义正则表达式中的特殊字符
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// 存储每个公式的 displayMode 信息
const formulaDisplayModes = [];
// 将所有定界符转换为 $ 符号,并记录 displayMode
const convertToDollar = (str) => {
const delimiters = [
{ left: '$$', right: '$$', display: false },
{ left: '$', right: '$', display: false },
{ left: '( ', right: ' )', display: true },
{ left: '\\[', right: '\\]', display: true },
{ left: '[', right: ']', display: true },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\begin{align*}', right: '\\end{align*}', display: true },
];
delimiters.forEach(({ left, right, display }) => {
const pattern = new RegExp(`${escapeRegExp(left)}([\\s\\S]*?)${escapeRegExp(right)}`, 'g');
str = str.replace(pattern, (match, formula) => {
formulaDisplayModes.push(display);
return `$${formula}$`;
});
});
return str;
};
// 渲染数学公式
const renderMath = async () => {
try {
// 对内容进行安全过滤
let cleanHtml = DOMPurify.sanitize(props.content, {
ALLOWED_TAGS: ['div', 'p', 'span', 'br', 'ol', 'ul', 'li', 'strong', 'em', 'a', 'pre', 'style'],
ALLOWED_ATTR: ['class', 'style', 'href', 'target', 'rel', 'border', 'border-radius', 'background', 'position', 'inset', 'padding', 'margin', 'z-index'],
ADD_ATTR: ['target', 'rel'],
ADD_DATA_URI_TAGS: ['img']
});
// 处理 a 标签,确保安全
cleanHtml = cleanHtml.replace(/<a\s+([^>]*)>/gi, (match, attributes) => {
const hrefAttr = (attributes.match(/href\s*=\s*["']([^"']*)["']/i) || [])[1];
if (hrefAttr) {
return `<a ${attributes} target="_blank" rel="noopener noreferrer">`;
}
return match;
});
cleanHtml = cleanHtml.replace(/```html([\s\S]*?)```/g, (match, codeContent) => {
return `<div class="code-block-wrapper">
<pre class="code-block">${codeContent.trim()}</pre>
</div>`;
});
// 清空之前的 displayMode 记录
formulaDisplayModes.length = 0;
// 将所有定界符转换为 $ 符号,并记录 displayMode
cleanHtml = convertToDollar(cleanHtml);
// 将清理后的 HTML 设置到容器中
if (container.value) {
container.value.innerHTML = cleanHtml;
}
let formulaIndex = 0;
// 只处理 $ 符号定界的公式
const pattern = /\$([\s\S]*?)\$/g;
if (container.value) {
container.value.innerHTML = container.value.innerHTML.replace(pattern, (_, formula) => {
const displayMode = formulaDisplayModes[formulaIndex++] || false;
try {
return katex.renderToString(formula, {
displayMode,
throwOnError: true,
...katexConfig,
});
} catch (e) {
console.error('公式渲染错误:', formula, e);
return `<span class="math-error">$${formula}$</span>`;
}
});
}
// 更新处理后的内容
processedContent.value = container.value ? container.value.innerHTML : '';
} catch (error) {
console.error('公式渲染失败:', error);
}
};
// 组件挂载后渲染公式
onMounted(async () => {
await nextTick();
renderMath();
});
// 监听 content 属性变化,重新渲染公式
watch(() => props.content, renderMath);
</script>
<style scoped lang="scss">
/* 修改为深度选择器 */
:deep(pre) {
border-radius: 8px;
padding: 10px;
background: #F9FAFB;
margin: 10px 0;
white-space: pre-wrap;
overflow-x: auto;
}
.flex-center-warp{
display: flex;
margin-right: 20px;
align-items: center;
cursor: pointer;
margin-bottom:10px;
color: #343a40;
}
.flex-center-warp:hover{
color: #000 !important;
}
.ptt-right{
width: 60%;
margin-top: 20px;
margin-left: 20px;
}
.ptt-left{
width: 40%;
display: flex;
flex-wrap: wrap;
max-height: 530px;
overflow-y: auto;
margin-top: 20px;
}
.ptt-item{
width: 46%;
margin-right: 2%;
cursor: pointer;
border: 2px solid #fff;
margin-bottom: 20px;
height: 150px;
}
.is_active{
border: 2px solid #80318c !important;
}
.img{
width: 100%;
height:150px;
border-radius: 10px;
}
.imgs{
width: 100%;
height: 400px;
border-radius: 4px;
cursor: pointer;
}
.imgsBox{
height: 150px;
width: 200px;
border-radius: 4px;
margin-top: 10px;
cursor: pointer;
}
.item-radio{
display: flex;
align-items: center;
margin-top: 20px;
}
.ant-radio-button-wrapper{
margin-left: 10px;
/* border-radius: 10px !important; */
}
.math-content {
line-height: 1.8;
font-size: 16px;
}
.download {
width: 100px;
height: 30px;
cursor: pointer;
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
color: #343a40;
font-size: 13px;
}
.download:hover {
background: #f1f1f1;
border-radius: 10px;
}
.katex {
font-size: 1.1em;
margin: 0.5em 0;
}
.copys {
width: 30px;
height: 30px;
cursor: pointer;
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
}
.copys:hover {
background: #f1f1f1;
border-radius: 10px;
}
.math-error {
background-color: #fff0f0;
color: #cc0000;
padding: 2px 4px;
border: 1px solid #ffcccc;
border-radius: 3px;
}
a:not([href]) {
cursor: text; /* 无链接时禁用指针 */
}
/* 自动识别可跳转链接 */
a[data-auto-link] {
color: #1677ff;
text-decoration: none;
position: relative;
padding-right: 1.2em;
}
/* 添加外部链接标识 */
a[data-auto-link]::after {
content: '↗';
font-size: 0.8em;
margin-left: 4px;
opacity: 0.6;
position: absolute;
right: 0;
}
/* 悬停效果 */
a[data-auto-link]:hover {
text-decoration: underline;
color: #0958d9;
}
/* 天气提示样式 */
.weather-tip {
color: #d4380d;
border-left: 3px solid #ffa940;
padding-left: 8px;
margin: 12px 0;
}
/* 列表样式增强 */
ol {
counter-reset: list-counter;
padding-left: 1.5em;
}
ol li {
counter-increment: list-counter;
margin: 8px 0;
}
ol li::marker {
color: #666;
font-weight: 500;
}
.markdown-content {
white-space: pre-line;
line-height: 1.6;
}
</style>
怎么给我优化下 代码块识别还有问题
最新发布