fix: 修复markdown渲染脚本问题

This commit is contained in:
ccnetcore
2026-02-12 18:09:45 +08:00
parent d4d89b989c
commit 4ab4d7b6db
3 changed files with 71 additions and 9 deletions

View File

@@ -10,7 +10,9 @@
"Bash(npm install marked --save)", "Bash(npm install marked --save)",
"Bash(pnpm add marked)", "Bash(pnpm add marked)",
"Bash(pnpm lint:*)", "Bash(pnpm lint:*)",
"Bash(pnpm list:*)" "Bash(pnpm list:*)",
"Bash(pnpm vue-tsc:*)",
"Bash(pnpm build:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -10,11 +10,13 @@ import { useDesignStore } from '@/stores';
interface Props { interface Props {
content: string; content: string;
theme?: 'light' | 'dark' | 'auto'; theme?: 'light' | 'dark' | 'auto';
sanitize?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
content: '', content: '',
theme: 'auto', theme: 'auto',
sanitize: true,
}); });
const designStore = useDesignStore(); const designStore = useDesignStore();
@@ -94,7 +96,12 @@ const renderer = {
// 行内代码 // 行内代码
codespan(token: { text: string }) { codespan(token: { text: string }) {
return `<code class="inline-code">${token.text}</code>`; // 转义 HTML 标签,防止 <script> 等标签被浏览器解析
const escapedText = token.text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return `<code class="inline-code">${escapedText}</code>`;
}, },
// 链接 // 链接
@@ -148,11 +155,23 @@ async function renderContent(content: string) {
// 包装表格,添加 table-wrapper 以支持横向滚动 // 包装表格,添加 table-wrapper 以支持横向滚动
rawHtml = rawHtml.replace(/<table>/g, '<div class="table-wrapper"><table>'); rawHtml = rawHtml.replace(/<table>/g, '<div class="table-wrapper"><table>');
rawHtml = rawHtml.replace(/<\/table>/g, '</table></div>'); rawHtml = rawHtml.replace(/<\/table>/g, '</table></div>');
// 使用 DOMPurify 清理 HTML防止 XSS // 转义 script 标签,防止浏览器将其当作真实脚本解析
renderedHtml.value = DOMPurify.sanitize(rawHtml, { // 使用字符串拼接避免在源码中出现 script 标签字面量
ADD_TAGS: ['iframe'], const scriptStart = '<' + 'script';
ADD_ATTR: ['target', 'data-code', 'data-html'], const scriptEnd = '<' + '/script' + '>';
}); rawHtml = rawHtml.replace(new RegExp(scriptStart + '(.*?)>', 'gi'), '&lt;script$1&gt;');
rawHtml = rawHtml.replace(new RegExp(scriptEnd.replace('/', '\\/'), 'gi'), '&lt;/script&gt;');
// 使用 DOMPurify 清理 HTML防止 XSS可通过 sanitize 属性禁用)
if (props.sanitize) {
renderedHtml.value = DOMPurify.sanitize(rawHtml, {
ADD_TAGS: ['iframe'],
ADD_ATTR: ['target', 'data-code', 'data-html'],
});
}
else {
renderedHtml.value = rawHtml;
}
// 渲染后绑定按钮事件 // 渲染后绑定按钮事件
nextTick(() => { nextTick(() => {

View File

@@ -8,6 +8,7 @@ interface TokenFormData {
expireTime: string; expireTime: string;
premiumQuotaLimit: number | null; premiumQuotaLimit: number | null;
quotaUnit: string; quotaUnit: string;
isEnableLog?: boolean;
} }
interface Props { interface Props {
@@ -42,6 +43,7 @@ const localFormData = ref<TokenFormData>({
const submitting = ref(false); const submitting = ref(false);
const neverExpire = ref(false); // 永不过期开关 const neverExpire = ref(false); // 永不过期开关
const unlimitedQuota = ref(false); // 无限制额度开关 const unlimitedQuota = ref(false); // 无限制额度开关
const isEnableLog = ref(false); // 是否启用请求日志(只读)
// 移动端检测 // 移动端检测
const isMobile = ref(false); const isMobile = ref(false);
@@ -107,6 +109,9 @@ watch(() => props.visible, (newVal) => {
// 判断是否永不过期 // 判断是否永不过期
neverExpire.value = !props.formData.expireTime; neverExpire.value = !props.formData.expireTime;
// 读取是否启用请求日志(只读字段)
isEnableLog.value = props.formData.isEnableLog || false;
localFormData.value = { localFormData.value = {
...props.formData, ...props.formData,
premiumQuotaLimit: displayValue, premiumQuotaLimit: displayValue,
@@ -196,13 +201,13 @@ const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥'
<el-dialog <el-dialog
:model-value="visible" :model-value="visible"
:title="dialogTitle" :title="dialogTitle"
:width="isMobile ? '95%' : '540px'" :width="isMobile ? '95%' : '640px'"
:fullscreen="isMobile" :fullscreen="isMobile"
:close-on-click-modal="false" :close-on-click-modal="false"
:show-close="!submitting" :show-close="!submitting"
@close="handleClose" @close="handleClose"
> >
<el-form :model="localFormData" :label-width="isMobile ? '100%' : '110px'" :label-position="isMobile ? 'top' : 'right'"> <el-form :model="localFormData" :label-width="isMobile ? '100%' : '150px'" :label-position="isMobile ? 'top' : 'right'">
<el-form-item label="API密钥名称" required> <el-form-item label="API密钥名称" required>
<el-input <el-input
v-model="localFormData.name" v-model="localFormData.name"
@@ -288,6 +293,21 @@ const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥'
超出配额后API密钥将无法继续使用 超出配额后API密钥将无法继续使用
</div> </div>
</el-form-item> </el-form-item>
<!-- 仅编辑模式显示:请求日志开关(只读) -->
<el-form-item v-if="mode === 'edit'" label="请求日志存储">
<div class="form-item-inline">
<el-switch
v-model="isEnableLog"
disabled
/>
<span class="switch-status-text">{{ isEnableLog ? '已开启' : '已关闭' }}</span>
</div>
<div class="form-hint warning-hint">
<el-icon><i-ep-warning-filled /></el-icon>
此临时存储功能仅面向企业套餐用户,仅用于企业内部审计
</div>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -356,6 +376,15 @@ const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥'
color: #409eff; color: #409eff;
flex-shrink: 0; flex-shrink: 0;
} }
&.warning-hint {
background: #fdf6ec;
border-left-color: #e6a23c;
.el-icon {
color: #e6a23c;
}
}
} }
.dialog-footer { .dialog-footer {
@@ -364,6 +393,18 @@ const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥'
gap: 12px; gap: 12px;
} }
.switch-status-text {
font-size: 14px;
color: #606266;
margin-left: 8px;
}
.form-item-inline {
display: flex;
align-items: center;
gap: 8px;
}
:deep(.el-form-item__label) { :deep(.el-form-item__label) {
font-weight: 600; font-weight: 600;
color: #303133; color: #303133;