mirror of
https://gitee.com/ccnetcore/Yi
synced 2026-03-02 15:50:54 +08:00
fix: 修复markdown渲染脚本问题
This commit is contained in:
@@ -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": []
|
||||||
|
|||||||
@@ -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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
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'), '<script$1>');
|
||||||
|
rawHtml = rawHtml.replace(new RegExp(scriptEnd.replace('/', '\\/'), 'gi'), '</script>');
|
||||||
|
|
||||||
|
// 使用 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(() => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user