chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
<template>
<view class="markdown-custom-process-group">
<view class="group-header" @tap="toggleExpanded">
<view class="header-left">
<text class="group-title">{{ getI18nText('Mobile.ThirdParty.MpHtml.toolCall') }}</text>
</view>
<view class="header-right">
<text class="process-count">{{ processCount + ' ' + getI18nText('Mobile.ThirdParty.MpHtml.itemCount') }}</text>
<text class="iconfont icon-a-Chevrondown expand-icon" :class="{ 'is-expanded': isExpanded }"></text>
</view>
</view>
<view v-if="isExpanded" class="group-content">
<slot />
</view>
</view>
</template>
<script>
import { t } from '@/utils/i18n'
export default {
name: 'MarkdownContainerGroup',
props: {
// 仅用于计算数量,不再直接渲染
childs: {
type: Array,
default: () => []
}
},
data() {
return {
isExpanded: false
}
},
computed: {
processCount() {
// 过滤掉可能的空白节点,支持原生 container 和 PC 端 HTML 标签名,同时排除隐藏的 Event 类型
return (this.childs || []).filter(n =>
(n.name === 'container' || n.name === 'markdown-custom-process') &&
n.attrs?.type !== 'Event'
).length
}
},
methods: {
getI18nText(key, params) {
return t(key, params)
},
toggleExpanded() {
this.isExpanded = !this.isExpanded
}
}
}
</script>
<style lang="scss" scoped>
.markdown-custom-process-group {
margin: 16rpx 0;
border-radius: 12rpx;
border: 1rpx solid rgba(0, 0, 0, 0.08);
background-color: #ffffff;
overflow: hidden;
.group-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 20rpx 24rpx;
background-color: rgba(0, 0, 0, 0.02);
.header-left {
display: flex;
flex-direction: row;
align-items: center;
.group-title {
font-size: 26rpx;
color: #333333;
}
}
.header-right {
display: flex;
flex-direction: row;
align-items: center;
gap: 12rpx;
.process-count {
font-size: 24rpx;
color: #8c8c8c;
}
.expand-icon {
font-size: 24rpx;
color: #8c8c8c;
transition: transform 0.3s ease;
&.is-expanded {
transform: rotate(180deg);
}
}
}
}
.group-content {
padding: 12rpx 24rpx;
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
max-height: 500rpx;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
}
</style>

View File

@@ -0,0 +1,648 @@
<template>
<view v-if="toolCall?.type !== 'Event'" class="tool-call-status">
<!-- Plan 类型直接显示任务列表 -->
<template v-if="isPlanType">
<!-- Plan 头部信息 -->
<view class="plan-header">
<view class="plan-info">
<text class="plan-name">{{
toolCall.name || getI18nText("Mobile.ThirdParty.MpHtml.executionPlan")
}}</text>
<!-- <view class="plan-status-display">
<view class="status-icon" :class="getStatusIconClass(toolCall.status)">
<text :class="getStatusIconType(toolCall.status)" :style="{color: getStatusIconColor(toolCall.status),fontSize: '32rpx'}"></text>
</view>
<text class="plan-status-text">{{ getStatusText(toolCall.status) }}</text>
</view> -->
</view>
<view class="plan-expand-icon" @tap="togglePlanExpanded">
<text class="iconfont" :class="planExpanded ? 'icon-MinusOutlined' : 'icon-Plus'"></text>
</view>
</view>
<!-- Plan 任务列表直接展示 -->
<view v-if="planExpanded && planTaskList.length > 0" class="plan-task-list">
<view v-for="(task, index) in planTaskList" :key="index" class="plan-task-item">
<!-- 字体图标实现的状态 -->
<view class="task-status-icon" :class="getTaskStatusClass(task.status)">
<text class="task-icon iconfont" :class="getTaskStatusIcon(task.status)"></text>
</view>
<text class="task-content">{{ task.content }}</text>
</view>
</view>
</template>
<!-- Plan 类型显示工具调用状态 -->
<template v-else>
<!-- 工具调用头部信息 -->
<view class="tool-header" @tap="toggleExpanded">
<view class="tool-info">
<text class="tool-name">{{ toolNameDisplay }}</text>
<view class="tool-status-display">
<view class="status-icon" :class="getStatusIconClass(toolCall.status)">
<text :class="getStatusIconType(toolCall.status)" :style="{color: getStatusIconColor(toolCall.status),fontSize: '32rpx'}"></text>
</view>
<text class="tool-status-text">{{ getStatusText(toolCall.status) }}</text>
</view>
</view>
<view class="tool-actions">
<view class="action-icon" @tap.stop.prevent="handleShowDetails(detailData)">
<text class="iconfont icon-ProfileOutlined"></text>
</view>
<!-- <view class="action-icon" @tap.stop.prevent="handleCopyToClipboard">
<text class="iconfont icon-Copy"></text>
</view> -->
<view v-if="isPageType(toolCall)" class="action-icon" @tap.stop.prevent="openPreviewPage(toolCall)">
<text class="iconfont icon-eye-open"></text>
</view>
</view>
</view>
<!-- 展开后的详细信息 调用参数/调用结果-->
<!-- <view v-if="expanded" class="tool-details-expanded">
<view v-if="detailData.params && Object.keys(detailData.params).length > 0" class="detail-section">
<view class="section-header">
<text class="section-title">{{
getI18nText("Mobile.ThirdParty.MpHtml.callArguments")
}}</text>
</view>
<view class="arguments-content">
<text class="arguments-text">{{ formatArguments(detailData.params) }}</text>
</view>
</view>
<view v-if="detailData.response" class="detail-section">
<view class="section-header">
<text class="section-title">{{
getI18nText("Mobile.ThirdParty.MpHtml.callResult")
}}</text>
</view>
<view class="output-content">
<text class="output-text">{{ formatResult(detailData.response) }}</text>
</view>
</view>
</view> -->
</template>
</view>
</template>
<script>
import uniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue'
import { AgentComponentTypeEnum } from '@/types/enums/agent.uts';
import { getCurrentPageParams } from '@/utils/common';
import { getFileProxyUrlByConversationIdAndFilePath, jumpToFilePreviewPage } from '@/utils/system.uts';
import { t } from '@/utils/i18n';
export default {
name: 'Container',
components: {
uniIcons,
// toolDetailsModal
},
props: {
data: {
type: Object,
required: true
}
},
data() {
return {
// expanded: false,
planExpanded: true, // Plan 类型默认展开
toolCall: this.data
}
},
computed: {
// 判断是否为 Plan 类型
isPlanType() {
return this.toolCall.type === AgentComponentTypeEnum.Plan || this.toolCall.type === 'Plan'
},
// 格式化显示的工具名称,处理多行代码情况
toolNameDisplay() {
let name = this.toolCall.name || this.toolCall.type || '';
// 还原引号实体
name = name.replace(/&quot;/g, '"');
// 替换换行符为空格,防止换行符干扰单行省略号显示
return name.replace(/\n/g, ' ').trim();
},
// Plan 任务列表
planTaskList() {
const result = this.toolCall.result || {}
const data = result.data
if (Array.isArray(data)) {
return data
}
return []
},
detailData() {
const _result = this.toolCall.result || {}
return {
// 从结果中提取输入参数,如果没有则提供空对象
params: _result.input || {},
// 使用结果的 data 作为响应数据,如果没有则提供空对象
response: _result.data || null,
}
}
},
watch: {
data: {
handler(newData) {
this.toolCall = newData;
},
immediate: true
}
},
methods: {
getI18nText(key) {
return t(key)
},
// 打开页面首页
openPreviewPage(data) {
uni.$emit('page_preview_executing', data);
},
// 切换展开状态
async toggleExpanded() {
const result = this.data?.result;
// 打开预览页面
if (result?.kind === 'edit') {
const params = getCurrentPageParams();
const conversationId = params.conversationId;
const file_path = result?.input?.file_path;
const fileProxyUrl = await getFileProxyUrlByConversationIdAndFilePath(conversationId, file_path);
jumpToFilePreviewPage(conversationId, fileProxyUrl);
return;
}
// this.expanded = !this.expanded
},
// 切换 Plan 展开状态
togglePlanExpanded() {
this.planExpanded = !this.planExpanded
},
// 显示详情
showDetails() {
if (!this.toolCall.result) {
return uni.showToast({
icon: 'none',
title: t('Mobile.ThirdParty.MpHtml.emptyResult'),
})
}
// this.expanded = !this.expanded
},
// 处理显示详情点击(阻止冒泡)
handleShowDetails() {
uni.$emit('page_preview_detail', this.toolCall, this.detailData);
},
// 处理复制到剪贴板点击(阻止冒泡)
handleCopyToClipboard(event) {
event.stopPropagation()
this.copyToClipboard()
},
// 复制结果
copyResult() {
// 实现复制功能
console.log('复制结果')
},
// 复制到剪贴板
copyToClipboard() {
// 实现复制到剪贴板功能
uni.setClipboardData({
data: this.formatArguments(this.detailData),
success: () => {
uni.showToast({
title: t('Mobile.Common.copySuccess'),
})
},
fail: () => {
uni.showToast({
title: t('Mobile.Common.copyFailed'),
})
}
})
},
// 获取状态图标类型
getStatusIconType(status) {
const iconMap = {
'EXECUTING': 'icon-Loader', // 执行中显示加载图标
'FINISHED': 'icon-Check', // 已完成显示对勾
'FAILED': 'icon-waring' // 执行失败显示叉号
}
return ` iconfont ${iconMap[status] || 'icon-Check'}`
},
// 获取状态图标样式类
getStatusIconClass(status) {
const statusClassMap = {
'EXECUTING': 'status-icon-executing',
'FINISHED': 'status-icon-finished',
'FAILED': 'status-icon-failed'
}
return statusClassMap[status] || 'status-icon-default'
},
// 获取状态图标颜色
getStatusIconColor(status) {
const statusColorMap = {
'EXECUTING': '#1890ff',
'FINISHED': '#52c41a',
// 'FAILED': '#ff4d4f'
'FAILED': '#ffcb00'
}
return statusColorMap[status] || '#999'
},
// 获取状态文本
getStatusText(status) {
return '';
// const statusTextMap = {
// 'EXECUTING': '执行中',
// 'FINISHED': '已完成',
// 'FAILED': '执行失败'
// }
// return statusTextMap[status] || '未知状态'
},
// 获取任务状态样式类
getTaskStatusClass(status) {
const classMap = {
'completed': 'task-status-completed',
'pending': 'task-status-pending',
'in_progress': 'task-status-in-progress',
'failed': 'task-status-failed'
}
return classMap[status] || 'task-status-pending'
},
// 获取任务状态图标 (使用字体图标unicode)
getTaskStatusIcon(status) {
const iconMap = {
'pending': 'icon-BorderOutlined', // 待处理
'in_progress': 'icon-HourglassOutlined', // 进行中
'completed': 'icon-CheckSquareOutlined', // 已完成
'failed': 'icon-CloseSquareOutlined' // 失败
}
return iconMap[status] || 'icon-BorderOutlined'
},
// 格式化参数显示
formatArguments(args) {
try {
return JSON.stringify(args, null, 2)
} catch (error) {
return String(args)
}
},
// 格式化结果显示
formatResult(result) {
if (result && typeof result === 'object') {
try {
return JSON.stringify(result, null, 2)
} catch (error) {
return String(result)
}
}
return String(result)
},
// 格式化执行时间
formatExecutionTime(time) {
if (time < 1000) {
return `${time}ms`
} else if (time < 60000) {
return `${(time / 1000).toFixed(2)}s`
} else {
return `${(time / 60000).toFixed(2)}min`
}
},
isPageType(toolCall) {
return toolCall.type === AgentComponentTypeEnum.Page;
}
}
}
</script>
<style lang="scss" scoped>
.tool-call-status {
width: 100%;
margin: 16rpx 0;
border-radius: 16rpx;
background-color: rgba(12, 20, 102, 0.04);
overflow: hidden;
.iconfont {
font-size: 32rpx;
color: #333;
}
// Plan 类型样式
.plan-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 24rpx;
.plan-info {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
gap: 16rpx;
.plan-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
line-height: 40rpx;
}
.plan-status-display {
display: flex;
align-items: center;
flex-direction: row;
gap: 8rpx;
.status-icon {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&.status-icon-executing {
animation: spin 1s linear infinite;
}
}
.plan-status-text {
font-size: 24rpx;
color: #666;
line-height: 32rpx;
}
}
}
// .plan-expand-icon {
// .iconfont {
// font-size: 32rpx;
// color: #333;
// }
// }
}
// Plan 任务列表
.plan-task-list {
padding: 0 24rpx 24rpx;
border-top: 2rpx solid rgba(0, 0, 0, 0.06);
margin-top: 0;
.plan-task-item {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 8rpx;
padding: 12rpx 0;
.task-status-icon {
flex-shrink: 0;
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-top: 4rpx;
.task-icon {
font-family: 'iconfont';
font-size: 32rpx;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #8c8c8c;
}
}
.task-content {
flex: 1;
font-size: 26rpx;
color: #262626;
line-height: 40rpx;
}
}
}
// 非 Plan 类型样式(原有样式)
.tool-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 24rpx;
.tool-info {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
gap: 16rpx;
.tool-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
line-height: 40rpx;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tool-status-display {
display: flex;
align-items: center;
flex-direction: row;
gap: 8rpx;
.status-icon {
width: 32rpx;
height: 32rpx;
// border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&.status-icon-executing {
animation: spin 1s linear infinite;
.iconfont {
color: #1890ff;
}
}
&.status-icon-finished {
color: #52c41a;
}
&.status-icon-failed {
color: #ff4d4f;
}
&.status-icon-default {
color: #999;
}
}
.tool-status-text {
font-size: 24rpx;
color: #666;
line-height: 32rpx;
margin-right: 6px;
}
}
}
.tool-actions {
display: flex;
align-items: center;
flex-direction: row;
gap: 16rpx;
.action-icon {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6rpx;
transition: background-color 0.3s ease;
cursor: pointer;
.iconfont {
// font-size: 32rpx;
font-weight: 400;
// color: #333;
line-height: 36rpx;
}
&:hover {
background-color: #f0f0f0;
}
}
}
}
.tool-details-expanded {
padding: 24rpx;
background-color: #f8f9fa;
border-top: 2rpx solid #f0f0f0;
.detail-section {
margin-bottom: 24rpx;
&:last-child {
margin-bottom: 0;
}
.section-header {
margin-bottom: 16rpx;
.section-title {
font-size: 26rpx;
font-weight: 600;
color: #333;
line-height: 36rpx;
}
}
.execute-id-content,
.arguments-content,
.output-content,
.error-content,
.execution-time {
padding: 16rpx;
background-color: #ffffff;
border-radius: 8rpx;
border: 2rpx solid #e9ecef;
.execute-id-text,
.arguments-text,
.output-text,
.error-text,
.time-text {
font-size: 24rpx;
line-height: 36rpx;
color: #495057;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
white-space: pre-wrap;
word-break: break-word;
}
}
&.error-section {
.error-content {
background-color: #fff5f5;
border-color: #ffccc7;
.error-text {
color: #cf1322;
}
}
}
}
}
// 加载动画
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
// 响应式设计
@media (max-width: 750rpx) {
.tool-header {
padding: 20rpx;
.tool-info {
.tool-name {
font-size: 26rpx;
}
.tool-status-display {
.tool-status-text {
font-size: 22rpx;
}
}
}
}
.tool-details-expanded {
padding: 20rpx;
}
}
}
</style>

View File

@@ -0,0 +1,6 @@
/**
* @fileoverview Container 插件
*/
function Container(vm) {}
export default Container;

View File

@@ -0,0 +1,38 @@
/**
* 按优先级从 processingList 中获取数据
* 优先级顺序FINISHED -> FAILED -> EXECUTING
* @param executeId 执行ID
* @param processingList 处理列表
* @returns 按优先级排序后的数据
*/
export const getProcessingDataByPriority = (executeId, processingList) => {
if (!processingList || !Array.isArray(processingList)) {
return {};
}
// 过滤出匹配 executeId 的项目
const matchedItems = processingList.filter(
(item) => item.executeId === executeId
);
if (matchedItems.length === 0) {
return {};
}
// 定义状态优先级(数字越小优先级越高)
const statusPriority = {
FINISHED: 1,
FAILED: 2,
EXECUTING: 3,
};
// 按优先级排序,优先级高的在前
const sortedItems = matchedItems.sort((a, b) => {
const priorityA = statusPriority[a.status] || 999;
const priorityB = statusPriority[b.status] || 999;
return priorityA - priorityB;
});
// 返回优先级最高的项目
return sortedItems[0] || {};
};

View File

@@ -0,0 +1,5 @@
export default {
copyByLongPress: true, // 是否需要长按代码块时显示复制代码内容菜单
showLanguageName: true, // 是否在代码块右上角显示语言的名称
showLineNumber: true, // 是否显示行号
};

View File

@@ -0,0 +1,157 @@
/**
* @fileoverview highlight 插件
* Include prismjs (https://prismjs.com)
*/
import prism from "./prism.min";
import installLanguages from "./prism-languages";
import config from "./config";
installLanguages(prism);
const getCopyButtonText = () => {
try {
const lang = String(uni.getStorageSync("I18N_LANG") || "zh-CN")
.toLowerCase()
.trim();
return lang.startsWith("en")
? "Copy code"
: "复制代码";
} catch (error) {
return "复制代码";
}
};
import Parser from "../parser";
function Highlight(vm) {
this.vm = vm;
}
Highlight.prototype.onParse = function (node, vm) {
if (node.name === "pre") {
if (vm.options.editable) {
node.attrs.class = (node.attrs.class || "") + " hl-pre";
return;
}
let i;
for (i = node.children.length; i--; ) {
if (node.children[i].name === "code") break;
}
if (i === -1) return;
const code = node.children[i];
let className = code.attrs.class + " " + node.attrs.class;
i = className.indexOf("language-");
if (i === -1) {
i = className.indexOf("lang-");
if (i === -1) {
className = "language-text";
i = 9;
} else {
i += 5;
}
} else {
i += 9;
}
let j;
for (j = i; j < className.length; j++) {
if (className[j] === " ") break;
}
const lang = className.substring(i, j);
if (code.children.length) {
const text = this.vm.getText(code.children).replace(/&amp;/g, "&")?.trim();
if (!text) return;
// #ifndef H5
// 非 H5 平台:将 pre 节点标记为 c=1并调用 vm.expose() 向上传播,
// 强制整个祖先链使用递归渲染,确保 pre 运在任何嵌套层级中都能被独立渲染
node.c = 1;
vm.expose();
// #endif
// #ifdef H5
if (node.c) {
node.c = undefined;
}
// #endif
if (prism.languages[lang]) {
code.children = new Parser(this.vm).parse(
// 加一层 pre 保留空白符
"<pre>" +
prism
.highlight(text, prism.languages[lang], lang)
.replace(/token /g, "hl-") +
"</pre>"
)[0].children;
}
node.attrs.class = "hl-pre";
code.attrs.class = "hl-code";
// Create hl-body wrapper
const hlBody = {
name: "div",
attrs: { class: "hl-body" },
children: []
};
// Create inner hl-container for stretching
const hlContainer = {
name: "div",
attrs: { class: "hl-container" },
children: []
};
if (config.showLineNumber) {
const line = text.split("\n").length;
const children = [];
for (let k = 1; k <= line; k++) {
children.push({
name: "div",
attrs: { class: "span" },
children: [{ type: "text", text: String(k) }],
});
}
hlContainer.children.push({
name: "div",
attrs: { class: "line-numbers-rows" },
children,
});
}
code.name = "div";
hlContainer.children.push(code);
hlBody.children.push(hlContainer);
node.children = [];
if (config.showLanguageName && vm.options.showHeader) {
const langChildren = [{ type: "text", text: lang }];
// #ifdef H5
// H5: 复制按钮放入 rich-text 内部,通过 srcElement.dataset 实现点击复制
langChildren.push({
name: "div",
attrs: {
class: "hl-copy-btn",
"data-content": text,
"data-action": "copy",
},
children: [{ type: "text", text: getCopyButtonText() }],
});
// #endif
// #ifndef H5
// 非 H5 平台:将代码文本存储到节点上,由 node.vue 渲染真实的可交互复制按钮
node._copyText = text;
// #endif
node.children.push({
name: "div",
attrs: {
class: "hl-language",
},
children: langChildren,
});
}
if (config.copyByLongPress) {
node.attrs.style += (node.attrs.style || "") + ";user-select:none";
// node.attrs["data-content"] = text;
// vm.expose();
}
node.children.push(hlBody);
}
}
};
export default Highlight;

View File

@@ -0,0 +1,96 @@
/**
* @fileoverview Additional Prism grammars for Java, Python, and TypeScript
*/
export default function (Prism) {
// JSON
if (!Prism.languages.json) {
Prism.languages.json = {
'property': {
pattern: /"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,
greedy: true
},
'string': {
pattern: /"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,
greedy: true
},
'comment': /\/\/.*|\/\*[\s\S]*?\*\//,
'number': /-?\d+\.?\d*(?:[eE][+-]?\d+)?/,
'punctuation': /[{}[\],]/,
'operator': /:/,
'boolean': /\b(?:true|false)\b/,
'null': /\bnull\b/
};
}
// Java
if (!Prism.languages.java) {
Prism.languages.java = Prism.languages.extend('clike', {
'keyword': /\b(?:abstract|continue|for|new|switch|assert|default|goto|package|synchronized|boolean|do|if|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while)\b/,
'number': /\b0b[01]+\b|\b0x[\da-f]*\.?[\da-f]+(?:p[+-]?\d+)?\b|(?:\b\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?[df]?\b/i,
'operator': {
pattern: /(^|[^.])(?:<<=?|>>>=?|>>=?|==?|!=|&&?|\|\|?|[+*\/%^-]=?|[?:~]|[-+]{1,2})/m,
lookbehind: true
}
});
Prism.languages.insertBefore('java', 'function', {
'annotation': {
alias: 'punctuation',
pattern: /(^|[^.])@\w+/,
lookbehind: true
}
});
}
// Python
if (!Prism.languages.python) {
Prism.languages.python = {
'comment': {
pattern: /(^|[^\\])#.*/,
lookbehind: true
},
'string-interpolation': {
pattern: /(?:f|rf|fr)(?:"""[\s\S]*?"""|'''[\s\S]*?'''|"(?:\\.|[^\\"\r\n])*"|'(?:\\.|[^\\'\r\n])*')/i,
greedy: true,
inside: {
'interpolation': {
pattern: /((?:^|[^{])(?:{{)*){[\s\S]*?}/,
lookbehind: true,
inside: {
'rest': null
}
},
'string': /[\s\S]+/
}
},
'string': {
pattern: /(?:[bru]|rb|br)?(?:"""[\s\S]*?"""|'''[\s\S]*?'''|"(?:\\.|[^\\"\r\n])*"|'(?:\\.|[^\\'\r\n])*')/i,
greedy: true
},
'function': {
pattern: /((?:^|\s)def\s+)\w+(?=\s*\()/g,
lookbehind: true
},
'class-name': {
pattern: /(\bclass\s+)\w+/i,
lookbehind: true
},
'keyword': /\b(?:as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,
'builtin': /\b(?:bool|bytearray|bytes|complex|dict|float|frozenset|int|list|set|str|tuple|type|Any|Callable|Dict|List|Optional|Set|Tuple|Union)\b/,
'boolean': /\b(?:True|False|None)\b/,
'number': /\b(?:\d+\.?\d*(?:[eE][+-]?\d+)?|0x[\da-f]+|0b[01]+|0o[0-7]+)\b/i,
'operator': /[-+*/%=<>!&|^~]+/,
'punctuation': /[()\[\]{},.:;]/
};
Prism.languages.python['string-interpolation'].inside['interpolation'].inside.rest = Prism.languages.python;
}
// TypeScript
if (!Prism.languages.typescript) {
Prism.languages.typescript = Prism.languages.extend('javascript', {
'keyword': /\b(?:abstract|as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|is|keyof|let|module|namespace|new|null|of|package|private|protected|public|readonly|return|set|static|super|switch|this|throw|try|type|typeof|var|void|while|with|yield|any|boolean|constructor|declare|never|number|object|string|symbol|unknown)\b/,
'operator': /\.[.]{2}|=>|\+\+|--|\*\*|[-+*/%]=?|[!=]=?=?|&&?|\|\|?|[?^%]|~(?!=)/
});
Prism.languages.ts = Prism.languages.typescript;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,78 @@
/**
* @fileoverview latex 插件
* katex.min.js来源 https://github.com/rojer95/katex-mini
*/
import parse from "./katex.min";
function Latex() {}
Latex.prototype.onParse = function (node, vm) {
// $...$包裹的内容为latex公式
if (!vm.options.editable && node.type === "text" && node.text.includes("$")) {
const part = node.text.split(/(\${1,2})/);
const children = [];
let status = 0;
for (let i = 0; i < part.length; i++) {
if (i % 2 === 0) {
// 文本内容
if (part[i]) {
if (status === 0) {
children.push({
type: "text",
text: part[i],
});
} else {
if (status === 1) {
// 行内公式
const nodes = parse.default(part[i]);
children.push({
name: "span",
attrs: {},
l: "T",
f: "display:inline-block",
children: nodes,
});
} else {
// 块公式
const nodes = parse.default(part[i], {
displayMode: true,
});
children.push({
name: "div",
attrs: {
style: "text-align:center",
},
children: nodes,
});
}
}
}
} else {
// 分隔符
if (part[i] === "$" && part[i + 2] === "$") {
// 行内公式
status = 1;
part[i + 2] = "";
} else if (part[i] === "$$" && part[i + 2] === "$$") {
// 块公式
status = 2;
part[i + 2] = "";
} else {
if (part[i] && part[i] !== "$$") {
// 普通$符号
part[i + 1] = part[i] + part[i + 1];
}
// 重置状态
status = 0;
}
}
}
delete node.type;
delete node.text;
node.name = "span";
node.attrs = {};
node.children = children;
}
};
export default Latex;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,129 @@
/**
* @fileoverview markdown 插件
* Include markdown-it (https://github.com/markdown-it/markdown-it)
* Include github-markdown-css (https://github.com/sindresorhus/github-markdown-css)
*/
import MarkdownIt from "markdown-it";
import markdownItContainer from "markdown-it-container";
let index = 0;
// 初始化 markdown-it 实例
const md = new MarkdownIt({
html: true, // 允许 HTML 标签
linkify: true, // 自动将 URL 转换为链接
typographer: true, // 启用智能排版
breaks: false, // 将换行符转换为 <br> 标签
langPrefix: "language-",
});
md.use(markdownItContainer, "container", {
validate: function (params) {
return params.trim().match(/^\s*container\s+(.*)$/);
},
render: function (tokens, idx) {
var m = tokens[idx].info.trim().match(/^container\s+(.*)$/);
const data =
m?.[1]?.split(" ").reduce((acc, item) => {
const [key, value] = item.split("=");
// 防止 value 为 undefined 导致 replace 调用失败
if (key && value !== undefined) {
acc[key] = value.replace(/^"|"$/g, "");
}
return acc;
}, {}) || {};
if (tokens[idx].nesting === 1) {
// opening tag
return `<container data=${JSON.stringify(data)}>`;
} else {
// closing tag
return `</container>`;
}
},
});
md.use(markdownItContainer, "container-group", {
validate: function (params) {
return params.trim().match(/^\s*container-group\s*(.*)$/);
},
render: function (tokens, idx) {
if (tokens[idx].nesting === 1) {
// opening tag
return `<container-group>`;
} else {
// closing tag
return `</container-group>`;
}
},
});
md.use(markdownItContainer, "task-result", {
validate: function (params) {
return params.trim().match(/^task-result\b/);
},
render: function (tokens, idx) {
if (tokens[idx].nesting === 1) {
return "<task-result>";
} else {
return "</task-result>";
}
},
});
function Markdown(vm) {
this.vm = vm;
vm._ids = {};
}
Markdown.prototype.onUpdate = function (content) {
if (this.vm.markdown) {
// 归一化换行符,防止不同平台下识别失败
content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
// 解决中文标点符号后粗体失效的问题,增加零宽空格
content = content.replace(
/\*\*([^*]+)\*\*([,。!?;:])/g,
"**$1**&#8203;$2"
);
const result = md.render(content);
return result;
}
};
Markdown.prototype.onParse = function (node, vm) {
if (vm.options.markdown) {
// 中文 id 需要转换,否则无法跳转
if (
vm.options.useAnchor &&
node.attrs &&
/[\u4e00-\u9fa5]/.test(node.attrs.id)
) {
const id = "t" + index++;
this.vm._ids[node.attrs.id] = id;
node.attrs.id = id;
}
if (
[
"p",
"table",
"tr",
"th",
"td",
"blockquote",
"pre",
"hr",
"code",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
].includes(node.name)
) {
if (node.name === "table") node.f = "overflow-x: auto;display:block";
node.attrs.class = `md-${node.name} ${node.attrs.class || ""}`;
}
}
};
export default Markdown;

View File

@@ -0,0 +1,53 @@
/**
* @fileoverview markdown 插件
* Include marked (https://github.com/markedjs/marked)
* Include github-markdown-css (https://github.com/sindresorhus/github-markdown-css)
*/
import marked from "./marked.min";
let index = 0;
function Markdown(vm) {
this.vm = vm;
vm._ids = {};
}
Markdown.prototype.onUpdate = function (content) {
if (this.vm.markdown) {
// 解决中文标点符号后粗体失效的问题,增加零宽空格
content = content.replace(
/\*\*([^*]+)\*\*([,。!?;:])/g,
"**$1**&#8203;$2"
);
const result = marked(content);
return result;
}
};
Markdown.prototype.onParse = function (node, vm) {
if (vm.options.markdown) {
// 中文 id 需要转换,否则无法跳转
if (
vm.options.useAnchor &&
node.attrs &&
/[\u4e00-\u9fa5]/.test(node.attrs.id)
) {
const id = "t" + index++;
this.vm._ids[node.attrs.id] = id;
node.attrs.id = id;
}
if (
node.name === "p" ||
node.name === "table" ||
node.name === "tr" ||
node.name === "th" ||
node.name === "td" ||
node.name === "blockquote" ||
node.name === "pre" ||
node.name === "code"
) {
node.attrs.class = `md-${node.name} ${node.attrs.class || ""}`;
}
}
};
export default Markdown;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,523 @@
<template>
<view id="_root" :class="(selectable ? '_select ' : '') + '_root'" :style="containerStyle">
<slot v-if="!nodes[0]" />
<!-- #ifndef APP-PLUS-NVUE -->
<node v-else :childs="nodes" :processing-list="processingList"
:opts="[lazyLoad, loadingImg, errorImg, showImgMenu, selectable]" name="span" />
<!-- #endif -->
<!-- #ifdef APP-PLUS-NVUE -->
<web-view ref="web" src="/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'"
@onPostMessage="_onMessage" />
<!-- #endif -->
</view>
</template>
<script>
/**
* mp-html v2.5.1
* @description 富文本组件
* @tutorial https://github.com/jin-yufeng/mp-html
* @property {String} container-style 容器的样式
* @property {String} content 用于渲染的 html 字符串
* @property {Boolean} copy-link 是否允许外部链接被点击时自动复制
* @property {String} domain 主域名,用于拼接链接
* @property {String} error-img 图片出错时的占位图链接
* @property {Boolean} lazy-load 是否开启图片懒加载
* @property {string} loading-img 图片加载过程中的占位图链接
* @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频
* @property {Boolean} preview-img 是否允许图片被点击时自动预览
* @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动
* @property {Boolean | String} selectable 是否开启长按复制
* @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题
* @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单
* @property {Object} tag-style 标签的默认样式
* @property {Boolean | Number} use-anchor 是否使用锚点链接
* @event {Function} load dom 结构加载完毕时触发
* @event {Function} ready 所有图片加载完毕时触发
* @event {Function} imgtap 图片被点击时触发
* @event {Function} linktap 链接被点击时触发
* @event {Function} play 音视频播放时触发
* @event {Function} error 媒体加载出错时触发
*/
// #ifndef APP-PLUS-NVUE
import node from './node/node.vue'
// #endif
import Parser from './parser.js'
import markdownIt from './markdown-it/index.js'
import highlight from './highlight/index.js'
import latex from './latex/index.js'
import container from './container/index.js'
// import markdown from './markdown/index.js'
// const plugins = [markdown, highlight, latex,]
const plugins = [markdownIt, highlight, latex, container]
// #ifdef APP-PLUS-NVUE
const dom = weex.requireModule('dom')
// #endif
export default {
name: 'mp-html',
data() {
return {
nodes: [],
// #ifdef APP-PLUS-NVUE
height: 3
// #endif
}
},
props: {
markdown: Boolean,
containerStyle: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
processingList: {
type: Array,
default: () => []
},
conversationId: {
type: [String, Number],
default: ''
},
copyLink: {
type: [Boolean, String],
default: true
},
domain: String,
errorImg: {
type: String,
default: ''
},
lazyLoad: {
type: [Boolean, String],
default: false
},
loadingImg: {
type: String,
default: ''
},
pauseVideo: {
type: [Boolean, String],
default: true
},
previewImg: {
type: [Boolean, String],
default: true
},
scrollTable: [Boolean, String],
selectable: [Boolean, String],
setTitle: {
type: [Boolean, String],
default: true
},
showImgMenu: {
type: [Boolean, String],
default: true
},
tagStyle: Object,
useAnchor: [Boolean, Number],
showHeader: {
type: Boolean,
default: true
}
},
// #ifdef VUE3
emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'],
// #endif
// #ifndef APP-PLUS-NVUE
components: {
node
},
// #endif
watch: {
content(content) {
this.setContent(content)
}
},
created() {
this.plugins = []
for (let i = plugins.length; i--;) {
this.plugins.push(new plugins[i](this))
}
},
mounted() {
if (this.content && !this.nodes.length) {
this.setContent(this.content)
}
},
beforeDestroy() {
this._hook('onDetached')
},
methods: {
/**
* @description 将锚点跳转的范围限定在一个 scroll-view 内
* @param {Object} page scroll-view 所在页面的示例
* @param {String} selector scroll-view 的选择器
* @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
*/
in(page, selector, scrollTop) {
// #ifndef APP-PLUS-NVUE
if (page && selector && scrollTop) {
this._in = {
page,
selector,
scrollTop
}
}
// #endif
},
/**
* @description 锚点跳转
* @param {String} id 要跳转的锚点 id
* @param {Number} offset 跳转位置的偏移量
* @returns {Promise}
*/
navigateTo(id, offset) {
id = this._ids[decodeURI(id)] || id
return new Promise((resolve, reject) => {
if (!this.useAnchor) {
reject(Error('Anchor is disabled'))
return
}
offset = offset || parseInt(this.useAnchor) || 0
// #ifdef APP-PLUS-NVUE
if (!id) {
dom.scrollToElement(this.$refs.web, {
offset
})
resolve()
} else {
this._navigateTo = {
resolve,
reject,
offset
}
this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
}
// #endif
// #ifndef APP-PLUS-NVUE
let deep = ' '
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
deep = '>>>'
// #endif
const selector = uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this._in ? this._in.page : this)
// #endif
.select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
if (this._in) {
selector.select(this._in.selector).scrollOffset()
.select(this._in.selector).boundingClientRect()
} else {
// 获取 scroll-view 的位置和滚动距离
selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
}
selector.exec(res => {
if (!res[0]) {
reject(Error('Label not found'))
return
}
const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
if (this._in) {
// scroll-view 跳转
this._in.page[this._in.scrollTop] = scrollTop
} else {
// 页面跳转
uni.pageScrollTo({
scrollTop,
duration: 300
})
}
resolve()
})
// #endif
})
},
/**
* @description 获取文本内容
* @return {String}
*/
getText(nodes) {
let text = '';
(function traversal(nodes) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === 'text') {
text += node.text.replace(/&amp;/g, '&')
} else if (node.name === 'br') {
text += '\n'
} else {
// 块级标签前后加换行
const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')
if (isBlock && text && text[text.length - 1] !== '\n') {
text += '\n'
}
// 递归获取子节点的文本
if (node.children) {
traversal(node.children)
}
if (isBlock && text[text.length - 1] !== '\n') {
text += '\n'
} else if (node.name === 'td' || node.name === 'th') {
text += '\t'
}
}
}
})(nodes || this.nodes)
return text
},
/**
* @description 获取内容大小和位置
* @return {Promise}
*/
getRect() {
return new Promise((resolve, reject) => {
uni.createSelectorQuery()
// #ifndef MP-ALIPAY
.in(this)
// #endif
.select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
})
},
/**
* @description 暂停播放媒体
*/
pauseMedia() {
for (let i = (this._videos || []).length; i--;) {
this._videos[i].pause()
}
// #ifdef APP-PLUS
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].pause()'
// #ifndef APP-PLUS-NVUE
let page = this.$parent
while (!page.$scope) page = page.$parent
page.$scope.$getAppWebview().evalJS(command)
// #endif
// #ifdef APP-PLUS-NVUE
this.$refs.web.evalJs(command)
// #endif
// #endif
},
/**
* @description 设置媒体播放速率
* @param {Number} rate 播放速率
*/
setPlaybackRate(rate) {
this.playbackRate = rate
for (let i = (this._videos || []).length; i--;) {
this._videos[i].playbackRate(rate)
}
// #ifdef APP-PLUS
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].playbackRate=' + rate
// #ifndef APP-PLUS-NVUE
let page = this.$parent
while (!page.$scope) page = page.$parent
page.$scope.$getAppWebview().evalJS(command)
// #endif
// #ifdef APP-PLUS-NVUE
this.$refs.web.evalJs(command)
// #endif
// #endif
},
/**
* @description 设置内容
* @param {String} content html 内容
* @param {Boolean} append 是否在尾部追加
*/
setContent(content, append) {
if (!append || !this.imgList) {
this.imgList = []
}
const nodes = new Parser(this).parse(content)
// #ifdef APP-PLUS-NVUE
if (this._ready) {
this._set(nodes, append)
}
// #endif
this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
// #ifndef APP-PLUS-NVUE
this._videos = []
this.$nextTick(() => {
this._hook('onLoad')
this.$emit('load')
})
if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {
// 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
let height = 0
const callback = rect => {
if (!rect || !rect.height) rect = {}
// 350ms 总高度无变化就触发 ready 事件
if (rect.height === height) {
this.$emit('ready', rect)
} else {
height = rect.height
setTimeout(() => {
this.getRect().then(callback).catch(callback)
}, 350)
}
}
this.getRect().then(callback).catch(callback)
} else {
// 未设置懒加载,等待所有图片加载完毕
if (!this.imgList._unloadimgs) {
this.getRect().then(rect => {
this.$emit('ready', rect)
}).catch(() => {
this.$emit('ready', {})
})
}
}
// #endif
},
/**
* @description 调用插件钩子函数
*/
_hook(name) {
for (let i = plugins.length; i--;) {
if (this.plugins[i][name]) {
this.plugins[i][name]()
}
}
},
// #ifdef APP-PLUS-NVUE
/**
* @description 设置内容
*/
_set(nodes, append) {
this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes).replace(/%22/g, '') + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
},
/**
* @description 接收到 web-view 消息
*/
_onMessage(e) {
const message = e.detail.data[0]
switch (message.action) {
// web-view 初始化完毕
case 'onJSBridgeReady':
this._ready = true
if (this.nodes) {
this._set(this.nodes)
}
break
// 内容 dom 加载完毕
case 'onLoad':
this.height = message.height
this._hook('onLoad')
this.$emit('load')
break
// 所有图片加载完毕
case 'onReady':
this.getRect().then(res => {
this.$emit('ready', res)
}).catch(() => {
this.$emit('ready', {})
})
break
// 总高度发生变化
case 'onHeightChange':
this.height = message.height
break
// 图片点击
case 'onImgTap':
this.$emit('imgtap', message.attrs)
if (this.previewImg) {
uni.previewImage({
current: parseInt(message.attrs.i),
urls: this.imgList
})
}
break
// 链接点击
case 'onLinkTap': {
const href = message.attrs.href
this.$emit('linktap', message.attrs)
if (href) {
// 锚点跳转
if (href[0] === '#') {
if (this.useAnchor) {
dom.scrollToElement(this.$refs.web, {
offset: message.offset
})
}
} else if (href.includes('://')) {
// 打开外链
if (this.copyLink) {
plus.runtime.openWeb(href)
}
} else {
uni.navigateTo({
url: href,
fail() {
uni.switchTab({
url: href
})
}
})
}
}
break
}
case 'onPlay':
this.$emit('play')
break
// 获取到锚点的偏移量
case 'getOffset':
if (typeof message.offset === 'number') {
dom.scrollToElement(this.$refs.web, {
offset: message.offset + this._navigateTo.offset
})
this._navigateTo.resolve()
} else {
this._navigateTo.reject(Error('Label not found'))
}
break
// 点击
case 'onClick':
this.$emit('tap')
this.$emit('click')
break
// 出错
case 'onError':
this.$emit('error', {
source: message.source,
attrs: message.attrs
})
}
}
// #endif
}
}
</script>
<style>
/* #ifndef APP-PLUS-NVUE */
/* 根节点样式 */
._root {
padding: 1px 0;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
color: rgb(64, 64, 64);
white-space: pre-wrap;
}
/* 长按复制 */
._select {
user-select: text;
}
/* #endif */
</style>

View File

@@ -0,0 +1,893 @@
<template>
<view
:id="attrs.id"
:class="'_block _' + name + ' ' + attrs.class"
:style="attrs.style"
>
<block v-for="(n, i) in childs" v-bind:key="i">
<!-- 图片 -->
<!-- 占位图 -->
<image
v-if="
n.name === 'img' && !n.t && ((opts[1] && !ctrl[i]) || ctrl[i] < 0)
"
class="_img"
:style="n.attrs.style"
:src="ctrl[i] < 0 ? opts[2] : opts[1]"
mode="widthFix"
/>
<!-- 显示图片 -->
<!-- #ifdef H5 || (APP-PLUS && VUE2) -->
<img
v-if="n.name === 'img'"
:id="n.attrs.id"
:class="'_img ' + n.attrs.class"
:style="(ctrl[i] === -1 ? 'display:none;' : '') + n.attrs.style"
:src="n.attrs.src || (ctrl.load ? n.attrs['data-src'] : '')"
:data-i="i"
@load="imgLoad"
@error="mediaError"
@tap.stop="imgTap"
@longpress="imgLongTap"
/>
<!-- #endif -->
<!-- #ifndef H5 || (APP-PLUS && VUE2) -->
<!-- 表格中的图片,使用 rich-text 防止大小不正确 -->
<rich-text
v-if="n.name === 'img' && n.t"
:style="'display:' + n.t"
:nodes="[
{
attrs: { style: n.attrs.style || '', src: n.attrs.src },
name: 'img',
},
]"
:data-i="i"
@tap.stop="imgTap"
/>
<!-- #endif -->
<!-- #ifdef APP-HARMONY -->
<image
v-else-if="n.name === 'img'"
:id="n.attrs.id"
:class="'_img ' + n.attrs.class"
:style="
(ctrl[i] === -1 ? 'display:none;' : '') +
'width:' +
ctrl[i] +
'px;' +
n.attrs.style
"
:src="n.attrs.src || (ctrl.load ? n.attrs['data-src'] : '')"
:mode="!n.h ? 'widthFix' : !n.w ? 'heightFix' : n.m || 'scaleToFill'"
:data-i="i"
@load="imgLoad"
@error="mediaError"
@tap.stop="imgTap"
@longpress="imgLongTap"
/>
<!-- #endif -->
<!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU -->
<image
v-else-if="n.name === 'img'"
:id="n.attrs.id"
:class="'_img ' + n.attrs.class"
:style="
(ctrl[i] === -1 ? 'display:none;' : '') +
'width:' +
(ctrl[i] || 1) +
'px;height:1px;' +
n.attrs.style
"
:src="n.attrs.src"
:mode="!n.h ? 'widthFix' : !n.w ? 'heightFix' : n.m || 'scaleToFill'"
:lazy-load="opts[0]"
:webp="n.webp"
:show-menu-by-longpress="opts[3] && !n.attrs.ignore"
:image-menu-prevent="!opts[3] || n.attrs.ignore"
:data-i="i"
@load="imgLoad"
@error="mediaError"
@tap.stop="imgTap"
@longpress="imgLongTap"
/>
<!-- #endif -->
<!-- #ifdef MP-KUAISHOU -->
<image
v-else-if="n.name === 'img'"
:id="n.attrs.id"
:class="'_img ' + n.attrs.class"
:style="(ctrl[i] === -1 ? 'display:none;' : '') + n.attrs.style"
:src="n.attrs.src"
:lazy-load="opts[0]"
:data-i="i"
@load="imgLoad"
@error="mediaError"
@tap.stop="imgTap"
></image>
<!-- #endif -->
<!-- #ifdef APP-PLUS && VUE3 -->
<image
v-else-if="n.name === 'img'"
:id="n.attrs.id"
:class="'_img ' + n.attrs.class"
:style="
(ctrl[i] === -1 ? 'display:none;' : '') +
'width:' +
(ctrl[i] || 1) +
'px;' +
n.attrs.style
"
:src="n.attrs.src || (ctrl.load ? n.attrs['data-src'] : '')"
:mode="!n.h ? 'widthFix' : !n.w ? 'heightFix' : n.m || ''"
:data-i="i"
@load="imgLoad"
@error="mediaError"
@tap.stop="imgTap"
@longpress="imgLongTap"
/>
<!-- #endif -->
<!-- 文本 -->
<!-- #ifdef MP-WEIXIN -->
<text
v-else-if="n.text"
:user-select="opts[4] == 'force' && isiOS"
decode
>{{ n.text }}</text
>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->
<text v-else-if="n.text" decode>{{ n.text }}</text>
<!-- #endif -->
<text v-else-if="n.name === 'br'">{{ "\n" }}</text>
<!-- 链接 -->
<view
v-else-if="n.name === 'a'"
:id="n.attrs.id"
:class="(n.attrs.href ? '_a ' : '') + n.attrs.class"
hover-class="_hover"
:style="'display:inline;' + n.attrs.style"
:data-i="i"
@tap.stop="linkTap"
>
<node
name="span"
:childs="n.children"
:opts="opts"
style="display: inherit"
/>
</view>
<!-- 视频 -->
<!-- #ifdef APP-PLUS -->
<view
v-else-if="n.html"
:id="n.attrs.id"
:class="'_video ' + n.attrs.class"
:style="n.attrs.style"
v-html="n.html"
:data-i="i"
@vplay.stop="play"
/>
<!-- #endif -->
<!-- #ifndef APP-PLUS -->
<video
v-else-if="n.name === 'video'"
:id="n.attrs.id"
:class="n.attrs.class"
:style="n.attrs.style"
:autoplay="n.attrs.autoplay"
:controls="n.attrs.controls"
:loop="n.attrs.loop"
:muted="n.attrs.muted"
:object-fit="n.attrs['object-fit']"
:poster="n.attrs.poster"
:src="n.src[ctrl[i] || 0]"
:data-i="i"
@play="play"
@error="mediaError"
/>
<!-- #endif -->
<!-- #ifdef H5 || APP-PLUS -->
<iframe
v-else-if="n.name === 'iframe'"
:style="n.attrs.style"
:allowfullscreen="n.attrs.allowfullscreen"
:frameborder="n.attrs.frameborder"
:src="n.attrs.src"
/>
<embed
v-else-if="n.name === 'embed'"
:style="n.attrs.style"
:src="n.attrs.src"
/>
<!-- #endif -->
<!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->
<!-- 音频 -->
<audio
v-else-if="n.name === 'audio'"
:id="n.attrs.id"
:class="n.attrs.class"
:style="n.attrs.style"
:author="n.attrs.author"
:controls="n.attrs.controls"
:loop="n.attrs.loop"
:name="n.attrs.name"
:poster="n.attrs.poster"
:src="n.src[ctrl[i] || 0]"
:data-i="i"
@play="play"
@error="mediaError"
/>
<!-- #endif -->
<view
v-else-if="(n.name === 'table' && n.c) || n.name === 'li'"
:id="n.attrs.id"
:class="'_' + n.name + ' ' + n.attrs.class"
:style="n.attrs.style"
>
<node v-if="n.name === 'li'" :childs="n.children" :opts="opts" />
<view
v-else
v-for="(tbody, x) in n.children"
v-bind:key="x"
:class="'_' + tbody.name + ' ' + tbody.attrs.class"
:style="tbody.attrs.style"
>
<node
v-if="tbody.name === 'td' || tbody.name === 'th'"
:childs="tbody.children"
:opts="opts"
/>
<block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
<view
v-if="tr.name === 'td' || tr.name === 'th'"
:class="'_' + tr.name + ' ' + tr.attrs.class"
:style="tr.attrs.style"
>
<node :childs="tr.children" :opts="opts" />
</view>
<view
v-else
:class="'_' + tr.name + ' ' + tr.attrs.class"
:style="tr.attrs.style"
>
<view
v-for="(td, z) in tr.children"
v-bind:key="z"
:class="'_' + td.name + ' ' + td.attrs.class"
:style="td.attrs.style"
>
<node :childs="td.children" :opts="opts" />
</view>
</view>
</block>
</view>
</view>
<markdown-container
v-else-if="n.name == 'container'"
:data="getRenderData(n.attrs.data)"
data-source="markdown-container"
/>
<!-- markdown-custom-process 工具调用组件 -->
<markdown-container
v-else-if="n.name == 'markdown-custom-process'"
:data="getRenderData(n.attrs)"
data-source="markdown-container"
/>
<markdown-container-group
v-else-if="n.name == 'container-group' || n.name == 'markdown-custom-process-group'"
:childs="n.children"
>
<node :childs="n.children" :opts="opts" :processing-list="processingList" />
</markdown-container-group>
<!-- task-result 任务结果组件 -->
<task-result
v-else-if="n.name === 'task-result'"
:data="n"
:conversation-id="getConversationId()"
/>
<!-- 富文本 -->
<!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
<rich-text
v-else-if="!n.c && (n.l || !handler.isInline(n.name, n.attrs.style))"
:id="n.attrs.id"
:style="n.f"
:user-select="opts[4]"
:nodes="[n]"
@tap="copyCode"
/>
<!-- #endif -->
<!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->
<!-- hl-language 头部节点:将 rich-text 和原生复制按钮并排在同一行,避免给对布局的依赖 -->
<view
v-else-if="
!n.c && n.attrs && n.attrs.class === 'hl-language' && copyText
"
class="hl-language-native"
>
<text class="hl-language-name">{{
(n.children && n.children[0] && n.children[0].text) || ""
}}</text>
<view class="hl-copy-btn-native" @tap.stop="doCopyText(copyText)">
<text class="hl-copy-btn-text">{{
getI18nText("Mobile.ThirdParty.MpHtml.copyCode")
}}</text>
</view>
</view>
<rich-text
v-else-if="!n.c"
:id="n.attrs.id"
:style="'display:inline;' + n.f"
:class="
n.name === 'div' && n.attrs?.class === 'event'
? 'div-event-style'
: ''
"
:preview="false"
:selectable="opts[4]"
:user-select="opts[4]"
:nodes="[n]"
@tap="handleCurrentTap(n)"
/>
<!-- #endif -->
<!-- 继续递归 -->
<view
v-else-if="n.c === 2"
:id="n.attrs.id"
:class="'_block _' + n.name + ' ' + n.attrs.class"
:style="n.f + ';' + n.attrs.style"
>
<node
v-for="(n2, j) in n.children"
v-bind:key="j"
:style="n2.f"
:name="n2.name"
:attrs="n2.attrs"
:childs="n2.children"
:opts="opts"
:copy-text="n2._copyText"
/>
</view>
<node
v-else
:style="n.f"
:name="n.name"
:attrs="n.attrs"
:childs="n.children"
:opts="opts"
:copy-text="n._copyText"
/>
</block>
</view>
</template>
<script module="handler" lang="wxs">
// 行内标签列表
var inlineTags = {
abbr: true,
b: true,
big: true,
code: true,
del: true,
em: true,
i: true,
ins: true,
label: true,
q: true,
small: true,
span: true,
strong: true,
sub: true,
sup: true,
};
/**
* @description 判断是否为行内标签
*/
module.exports = {
isInline: function (tagName, style) {
return (
inlineTags[tagName] || (style || "").indexOf("display:inline") !== -1
);
},
};
</script>
<script>
import markdownContainer from '../container/container.vue'
import markdownContainerGroup from '../container/container-group.vue'
import taskResult from '../task-result/task-result.vue'
import { getProcessingDataByPriority } from '../container/utils'
import node from './node'
import { t } from '@/utils/i18n'
export default {
name: 'node',
options: {
// #ifdef MP-WEIXIN
virtualHost: true,
// #endif
// #ifdef MP-TOUTIAO
addGlobalClass: false
// #endif
},
data () {
return {
ctrl: {},
root: null, // Initialize root to prevent "not defined" warning during render
// #ifdef MP-WEIXIN
isiOS: uni.getDeviceInfo().system.includes('iOS')
// #endif
}
},
props: {
name: String,
processingList: Array,
attrs: {
type: Object,
default () {
return {}
}
},
childs: Array,
opts: Array,
copyText: String
},
components: {
markdownContainer,
markdownContainerGroup,
taskResult,
// #ifndef ((H5 || APP-PLUS) && VUE3) || APP-HARMONY
node
// #endif
},
mounted () {
this.$nextTick(() => {
for (this.root = this.$parent; this.root.$options.name !== 'mp-html'; this.root = this.root.$parent);
})
// #ifdef H5 || APP-PLUS
if (this.opts[0]) {
let i
for (i = this.childs.length; i--;) {
if (this.childs[i].name === 'img') break
}
if (i !== -1) {
this.observer = uni.createIntersectionObserver(this).relativeToViewport({
top: 500,
bottom: 500
})
this.observer.observe('._img', res => {
if (res.intersectionRatio) {
this.$set(this.ctrl, 'load', 1)
this.observer.disconnect()
}
})
}
}
// #endif
},
beforeDestroy () {
// #ifdef H5 || APP-PLUS
if (this.observer) {
this.observer.disconnect()
}
// #endif
},
methods:{
getI18nText (key) {
return t(key)
},
showTextToast (title) {
uni.showToast({
title,
icon: 'none'
})
},
/**
* 获取会话ID用于task-result等组件
*/
getConversationId() {
// 尝试从根组件获取会话ID
if (this.root && this.root.conversationId) {
return this.root.conversationId
}
// 尝试从mp-html组件获取
for (let parent = this.$parent; parent; parent = parent.$parent) {
if (parent.conversationId) {
return parent.conversationId
}
}
return ''
},
getRenderData (data: any) {
if(!data) {
return {}
}
if(typeof data === 'string') {
try {
data = JSON.parse(data)
} catch (e) {
console.warn('[mp-html/node] getRenderData JSON.parse error:', e, 'data:', data)
return {} // Return empty object on parse failure
}
}
// Ensure data is an object before spreading
if (typeof data !== 'object' || data === null) {
return {}
}
const result = {
...data,
// 在 HTML 解析过程中mp-html 的解析器会将属性名自动转为全小写,也就是 executeid
...getProcessingDataByPriority(data.executeId || data.executeid, this.processingList)
}
return result
},
// #ifdef MP-WEIXIN
toJSON () { return this },
// #endif
/**
* @description 播放视频事件
* @param {Event} e
*/
play (e) {
const i = e.currentTarget.dataset.i
const node = this.childs[i]
this.root.$emit('play', {
source: node.name,
attrs: {
...node.attrs,
src: node.src[this.ctrl[i] || 0]
}
})
// #ifndef APP-PLUS
if (this.root.pauseVideo) {
let flag = false
const id = e.target.id
for (let i = this.root._videos.length; i--;) {
if (this.root._videos[i].id === id) {
flag = true
} else {
this.root._videos[i].pause() // 自动暂停其他视频
}
}
// 将自己加入列表
if (!flag) {
const ctx = uni.createVideoContext(id
// #ifndef MP-BAIDU
, this
// #endif
)
ctx.id = id
if (this.root.playbackRate) {
ctx.playbackRate(this.root.playbackRate)
}
this.root._videos.push(ctx)
}
}
// #endif
},
/**
* @description 图片点击事件
* @param {Event} e
*/
imgTap (e) {
const node = this.childs[e.currentTarget.dataset.i]
if (node.a) {
this.linkTap(node.a)
return
}
if (node.attrs.ignore) return
// #ifdef H5 || APP-PLUS
node.attrs.src = node.attrs.src || node.attrs['data-src']
// #endif
// #ifndef APP-HARMONY
this.root.$emit('imgtap', node.attrs)
// #endif
// #ifdef APP-HARMONY
this.root.$emit('imgtap', {
...node.attrs
})
// #endif
// 自动预览图片
if (this.root.previewImg) {
uni.previewImage({
// #ifdef MP-WEIXIN
showmenu: this.root.showImgMenu,
// #endif
// #ifdef MP-ALIPAY
enablesavephoto: this.root.showImgMenu,
enableShowPhotoDownload: this.root.showImgMenu,
// #endif
current: parseInt(node.attrs.i),
urls: this.root.imgList
})
}
},
/**
* @description 图片长按
*/
imgLongTap (e) {
// #ifdef APP-PLUS
const attrs = this.childs[e.currentTarget.dataset.i].attrs
if (this.opts[3] && !attrs.ignore) {
uni.showActionSheet({
itemList: ['保存图片'],
success: () => {
const save = path => {
uni.saveImageToPhotosAlbum({
filePath: path,
success: () => {
this.showTextToast(t('Mobile.ThirdParty.MpHtml.saveSuccess'))
}
})
}
if (this.root.imgList[attrs.i].startsWith('http')) {
uni.downloadFile({
url: this.root.imgList[attrs.i],
success: res => save(res.tempFilePath)
})
} else {
save(this.root.imgList[attrs.i])
}
}
})
}
// #endif
},
/**
* @description 图片加载完成事件
* @param {Event} e
*/
imgLoad (e) {
const i = e.currentTarget.dataset.i
/* #ifndef H5 || (APP-PLUS && VUE2) */
if (!this.childs[i].w) {
// 设置原宽度
this.$set(this.ctrl, i, e.detail.width)
} else /* #endif */ if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {
// 加载完毕,取消加载中占位图
this.$set(this.ctrl, i, 1)
}
this.checkReady()
},
/**
* @description 检查是否所有图片加载完毕
*/
checkReady () {
if (this.root && !this.root.lazyLoad) {
this.root._unloadimgs -= 1
if (!this.root._unloadimgs) {
setTimeout(() => {
this.root.getRect().then(rect => {
this.root.$emit('ready', rect)
}).catch(() => {
this.root.$emit('ready', {})
})
}, 350)
}
}
},
/**
* @description 链接点击事件
* @param {Event} e
*/
linkTap (e) {
const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}
const attrs = node.attrs || e
const href = attrs.href
this.root.$emit('linktap', Object.assign({
innerText: this.root.getText(node.children || []) // 链接内的文本内容
}, attrs))
if (href) {
if (href[0] === '#') {
// 跳转锚点
this.root.navigateTo(href.substring(1)).catch(() => { })
} else if (href.split('?')[0].includes('://')) {
// 复制外部链接
if (this.root.copyLink) {
// #ifdef H5
window.open(href)
// #endif
// #ifdef MP
uni.setClipboardData({
data: href,
success: () => this.showTextToast(t('Mobile.Link.copied'))
})
// #endif
// #ifdef APP-PLUS
plus.runtime.openWeb(href)
// #endif
}
} else {
// 跳转页面
uni.navigateTo({
url: href,
fail () {
uni.switchTab({
url: href,
fail () { }
})
}
})
}
}
},
/**
* @description 复制代码事件
* @param {Event} e
*/
copyCode (e) {
// srcElement \u5728\u5fae\u4fe1\u5c0f\u7a0b\u5e8f\u4e2d\u4e0d\u5b58\u5728\uff0c\u52a0\u5b89\u5168\u8bbf\u95ee
const data = (e.srcElement && e.srcElement.dataset) || (e.currentTarget && e.currentTarget.dataset)
if(!data || data.action !== 'copy') {
return
}
const content = data.content
if (content) {
// 实现复制到剪贴板功能
uni.setClipboardData({
data: content,
success: () => {
this.showTextToast(t('Mobile.Common.copySuccess'))
},
fail: () => {
this.showTextToast(t('Mobile.Common.copyFailed'))
}
})
}
},
/**
* @description 当前节点点击事件
* @param {Event} e
*/
handleCurrentTap(e){
// #ifdef MP-WEIXIN
if (e.name==='div' && e.attrs?.class==='event') {
uni.$emit('message-event-delegate', e)
return
}
// #endif
// Handle copy code button in highlight blocks
if (e.attrs?.['data-action'] === 'copy' && e.attrs?.['data-content']) {
uni.setClipboardData({
data: e.attrs['data-content'],
success: () => {
this.showTextToast(t('Mobile.Common.copySuccess'))
},
fail: () => {
this.showTextToast(t('Mobile.Common.copyFailed'))
}
})
}
},
/**
* @description 复制代码文本(非 H5 平台使用)
* @param {String} text
*/
doCopyText (text) {
if (!text) return
uni.setClipboardData({
data: text,
success: () => {
this.showTextToast(t('Mobile.Common.copySuccess'))
},
fail: () => {
this.showTextToast(t('Mobile.Common.copyFailed'))
}
})
},
/**
* @description 错误事件
* @param {Event} e
*/
mediaError (e) {
const i = e.currentTarget.dataset.i
const node = this.childs[i]
// 加载其他源
if (node.name === 'video' || node.name === 'audio') {
let index = (this.ctrl[i] || 0) + 1
if (index > node.src.length) {
index = 0
}
if (index < node.src.length) {
this.$set(this.ctrl, i, index)
return
}
} else if (node.name === 'img') {
// #ifdef H5 && VUE3
if (this.opts[0] && !this.ctrl.load) return
// #endif
// 显示错误占位图
if (this.opts[2]) {
this.$set(this.ctrl, i, -1)
}
this.checkReady()
}
if (this.root) {
this.root.$emit('error', {
source: node.name,
attrs: node.attrs,
// #ifndef H5 && VUE3
errMsg: e.detail.errMsg
// #endif
})
}
}
}
}
</script>
<style lang="scss">
@import "../styles/index.scss";
// 会话输出内容点击事件
// 事件绑定配置样式
.div-event-style {
display: inline-block !important;
font-size: 12px;
margin: 0 4px;
padding: 4px 8px;
cursor: pointer;
max-width: 100px;
min-width: 18px;
border-radius: 24px;
color: rgba(0, 0, 0, 60%) !important;
background-color: rgba(0, 0, 0, 5%);
line-height: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
background-color: rgba(235, 235, 235);
}
}
/* H5 平台hl-language 头部行的原生包裹层匹配 .hl-language 的布局 */
.hl-language-native {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 2px 10px;
background-color: #252526;
border-radius: 6px 6px 0 0;
font-size: 12px;
width: 100%;
box-sizing: border-box;
flex-shrink: 0;
}
/* H5 平台的原生可交互复制按钮 */
.hl-copy-btn-native {
display: flex;
align-items: center;
padding: 2px 4px;
border-radius: 4px;
cursor: pointer;
.hl-copy-btn-text {
font-size: 10px;
color: #999;
line-height: normal;
white-space: normal;
}
}
/* 语言名称文本 */
.hl-language-name {
font-size: 12px;
color: #999;
flex: 1;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
/* HTML 基础元素样式 */
/* a 标签默认效果 */
._a {
padding: 1.5px 0 1.5px 0;
color: rgb(59, 130, 246);
word-break: break-all;
}
/* a 标签点击态效果 */
._hover {
text-decoration: underline;
opacity: 0.7;
}
/* 图片默认效果 */
._img {
max-width: 100%;
-webkit-touch-callout: none;
}
/* 内部样式 */
._block {
display: block;
}
._b,
._strong {
font-weight: bold;
}
._code {
font-family: monospace;
}
._del {
text-decoration: line-through;
}
._em,
._i {
font-style: italic;
}
._h1 {
font-size: 2em;
}
._h2 {
font-size: 1.5em;
}
._h3 {
font-size: 1.17em;
}
._h5 {
font-size: 0.83em;
}
._h6 {
font-size: 0.67em;
}
._h1,
._h2,
._h3,
._h4,
._h5,
._h6 {
display: block;
font-weight: bold;
}
._image {
height: 1px;
}
._ins {
text-decoration: underline;
}
._li {
display: list-item;
overflow: unset;
line-height: 1.5;
}
._ol {
list-style-type: decimal;
}
._ol,
._ul {
display: block;
padding-left: 40px;
margin: 1em 0;
}
._q::before {
content: '"';
}
._q::after {
content: '"';
}
._sub {
font-size: smaller;
vertical-align: sub;
}
._sup {
font-size: smaller;
vertical-align: super;
}
._thead,
._tbody,
._tfoot {
display: table-row-group;
}
._tr {
display: table-row;
}
._td,
._th {
display: table-cell;
vertical-align: middle;
}
._th {
font-weight: bold;
text-align: center;
}
._ul {
list-style-type: disc;
}
._ul ._ul {
margin: 0;
list-style-type: circle;
}
._ul ._ul ._ul {
list-style-type: square;
}
._abbr,
._b,
._code,
._del,
._em,
._i,
._ins,
._label,
._q,
._span,
._strong,
._sub,
._sup {
display: inline;
}
/* #ifdef APP-PLUS */
._video {
width: 300px;
height: 225px;
}
/* #endif */

View File

@@ -0,0 +1,238 @@
/* 代码高亮样式 - 基于 uni-ai-msg-code 深色主题 */
/* FiraCode 字体已被移除以减小包体积 */
/* @font-face {
font-family: CodeFontFamily;
src: url('@uni_modules/mp-html/static/font/FiraCode-Regular.ttf') format('truetype');
} */
:deep(.hl-code) {
color: #d4d4d4;
background: transparent;
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
font-size: 14px;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
flex: 1;
min-width: 0;
overflow-x: auto;
overflow-y: hidden; /* 防止代码区自己产生内部垂直滚动条 */
padding: 10px 12px; /* 代码内容的内边距 */
box-sizing: border-box;
min-height: 100%;
align-self: stretch;
}
:deep(.hl-pre) {
color: #d4d4d4;
background: #1e1e1e;
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
font-size: 14px;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
border-radius: 6px;
position: relative;
display: flex;
flex-direction: column;
padding: 0; /* Remove old padding */
overflow: hidden; /* Stop whole wrapper from sliding */
}
/* 代码头部样式 */
:deep(.hl-language) {
margin: 0; /* 确保没有外边距 */
line-height: normal; /* 重置行高 */
white-space: normal; /* 重置空白处理 */
user-select: none;
position: relative;
top: 0;
left: 0;
width: 100%;
box-sizing: border-box;
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 2px 10px;
background-color: #252526;
border-radius: 6px 6px 0 0;
font-size: 12px;
color: #999;
}
:deep(.hl-body) {
/* wrapper handle scrolling ONLY */
max-height: 60vh;
overflow-y: auto; /* vertically scroll the code and line numbers */
overflow-x: hidden;
width: 100%;
position: relative;
background-color: #1e1e1e;
}
:deep(.hl-container) {
display: flex;
flex-direction: row;
align-items: stretch; /* flex cross-axis stretch without scroll boundaries */
width: 100%;
box-sizing: border-box;
}
/* 代码高亮颜色 - 基于 uni-ai-msg-code 深色主题 */
:deep(.hl-block-comment),
:deep(.hl-cdata),
:deep(.hl-comment),
:deep(.hl-doctype),
:deep(.hl-prolog) {
color: #999; /* 注释使用灰色 */
}
:deep(.hl-punctuation) {
color: #d4d4d4; /* 标点符号使用默认文本颜色 */
}
:deep(.hl-attr-name),
:deep(.hl-deleted),
:deep(.hl-namespace),
:deep(.hl-tag) {
color: #d16969; /* 元信息使用红色 */
}
:deep(.hl-function-name) {
color: #dcdcaa; /* 函数使用黄色 */
}
:deep(.hl-boolean),
:deep(.hl-function),
:deep(.hl-number) {
color: #b5cea8; /* 常量使用浅绿色 */
}
:deep(.hl-class-name),
:deep(.hl-constant),
:deep(.hl-property),
:deep(.hl-symbol) {
color: #4ec9b0; /* 实体使用青色 */
}
:deep(.hl-atrule),
:deep(.hl-builtin),
:deep(.hl-important),
:deep(.hl-keyword),
:deep(.hl-selector) {
color: #569cd6; /* 关键字使用蓝色 */
}
:deep(.hl-attr-value),
:deep(.hl-char),
:deep(.hl-regex),
:deep(.hl-string),
:deep(.hl-variable) {
color: #ce9178; /* 字符串使用橙色 */
}
:deep(.hl-entity),
:deep(.hl-operator),
:deep(.hl-url) {
color: #4ec9b0; /* 支持类使用青色 */
}
:deep(.hl-bold),
:deep(.hl-important) {
font-weight: 700;
}
:deep(.hl-italic) {
font-style: italic;
}
:deep(.hl-entity) {
cursor: help;
}
:deep(.hl-inserted) {
color: #4ec9b0; /* 插入内容使用青色 */
}
/* 行号样式 */
:deep(.line-numbers-rows) {
position: static;
flex-shrink: 0;
min-width: 3.5em;
text-align: center;
color: #666;
font-size: 14px;
line-height: 1.5;
user-select: none;
padding-top: 10px; /* align with code padding */
padding-bottom: 10px;
background-color: transparent;
border-right: 1px solid #333;
display: flex;
flex-direction: column;
box-sizing: border-box;
min-height: 100%;
align-self: stretch;
}
:deep(.line-numbers-rows .span) {
display: flex;
height: 1.5em; /* Match code line height */
align-items: center;
justify-content: center;
}
/* 复制按钮样式 */
:deep(.hl-copy-btn) {
display: flex;
align-items: center;
padding: 2px 4px;
border-radius: 4px;
cursor: pointer;
font-size: 10px;
color: #999;
}
:deep(.hl-copy-btn:hover) {
background-color: #37373d;
}
/* 滚动条样式 */
:deep(.hl-pre::-webkit-scrollbar) {
height: 8px;
}
:deep(.hl-pre::-webkit-scrollbar-track) {
background: #252526;
border-radius: 4px;
}
:deep(.hl-pre::-webkit-scrollbar-thumb) {
background: #666;
border-radius: 4px;
}
:deep(.hl-pre::-webkit-scrollbar-thumb:hover) {
background: #888;
}

View File

@@ -0,0 +1,19 @@
/* mp-html 样式集合 - 统一导入文件 */
// 代码高亮样式
@import './highlight.scss';
// KaTeX 字体定义
@import './katex-fonts.scss';
// KaTeX 数学公式核心样式
@import './katex-styles.scss';
// KaTeX 字体大小样式
@import './katex-sizing.scss';
// Markdown 通用样式
@import './markdown.scss';
// HTML 基础元素样式
@import './basic-elements.scss';

View File

@@ -0,0 +1,110 @@
/* KaTeX 字体定义 - 仅保留 TTF 格式以减小包体积
* 已移除不常用的特殊字体Caligraphic花体、Fraktur哥特体、Script手写体
*/
@font-face {
font-family: KaTeX_AMS;
src: url(/subpackages/static/fonts/KaTeX_AMS-Regular.ttf) format("truetype");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: KaTeX_Main;
src: url(/subpackages/static/fonts/KaTeX_Main-Bold.ttf) format("truetype");
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: KaTeX_Main;
src: url(/subpackages/static/fonts/KaTeX_Main-BoldItalic.ttf) format("truetype");
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: KaTeX_Main;
src: url(/subpackages/static/fonts/KaTeX_Main-Italic.ttf) format("truetype");
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: KaTeX_Main;
src: url(/subpackages/static/fonts/KaTeX_Main-Regular.ttf) format("truetype");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: KaTeX_Math;
src: url(/subpackages/static/fonts/KaTeX_Math-BoldItalic.ttf) format("truetype");
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: KaTeX_Math;
src: url(/subpackages/static/fonts/KaTeX_Math-Italic.ttf) format("truetype");
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: KaTeX_SansSerif;
src: url(/subpackages/static/fonts/KaTeX_SansSerif-Bold.ttf) format("truetype");
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: KaTeX_SansSerif;
src: url(/subpackages/static/fonts/KaTeX_SansSerif-Italic.ttf) format("truetype");
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: KaTeX_SansSerif;
src: url(/subpackages/static/fonts/KaTeX_SansSerif-Regular.ttf) format("truetype");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: KaTeX_Size1;
src: url(/subpackages/static/fonts/KaTeX_Size1-Regular.ttf) format("truetype");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: KaTeX_Size2;
src: url(/subpackages/static/fonts/KaTeX_Size2-Regular.ttf) format("truetype");
font-weight: 400;
font-style: normal;
}
/* 以下字体已被移除以减小包体积 */
/*
@font-face {
font-family: KaTeX_Size3;
src: url(/subpackages/static/fonts/KaTeX_Size3-Regular.ttf) format("truetype");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: KaTeX_Size4;
src: url(/subpackages/static/fonts/KaTeX_Size4-Regular.ttf) format("truetype");
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: KaTeX_Typewriter;
src: url(/subpackages/static/fonts/KaTeX_Typewriter-Regular.ttf) format("truetype");
font-weight: 400;
font-style: normal;
}
*/

View File

@@ -0,0 +1,860 @@
/* KaTeX 字体大小样式 */
:deep(.katex .fontsize-ensurer.reset-size1.size1),
:deep(.katex .sizing.reset-size1.size1) {
font-size: 1em
}
:deep(.katex .fontsize-ensurer.reset-size1.size2),
:deep(.katex .sizing.reset-size1.size2) {
font-size: 1.2em
}
:deep(.katex .fontsize-ensurer.reset-size1.size3),
:deep(.katex .sizing.reset-size1.size3) {
font-size: 1.4em
}
:deep(.katex .fontsize-ensurer.reset-size1.size4),
:deep(.katex .sizing.reset-size1.size4) {
font-size: 1.6em
}
:deep(.katex .fontsize-ensurer.reset-size1.size5),
:deep(.katex .sizing.reset-size1.size5) {
font-size: 1.8em
}
:deep(.katex .fontsize-ensurer.reset-size1.size6),
:deep(.katex .sizing.reset-size1.size6) {
font-size: 2em
}
:deep(.katex .fontsize-ensurer.reset-size1.size7),
:deep(.katex .sizing.reset-size1.size7) {
font-size: 2.4em
}
:deep(.katex .fontsize-ensurer.reset-size1.size8),
:deep(.katex .sizing.reset-size1.size8) {
font-size: 2.88em
}
:deep(.katex .fontsize-ensurer.reset-size1.size9),
:deep(.katex .sizing.reset-size1.size9) {
font-size: 3.456em
}
:deep(.katex .fontsize-ensurer.reset-size1.size10),
:deep(.katex .sizing.reset-size1.size10) {
font-size: 4.148em
}
:deep(.katex .fontsize-ensurer.reset-size1.size11),
:deep(.katex .sizing.reset-size1.size11) {
font-size: 4.976em
}
:deep(.katex .fontsize-ensurer.reset-size2.size1),
:deep(.katex .sizing.reset-size2.size1) {
font-size: .83333333em
}
:deep(.katex .fontsize-ensurer.reset-size2.size2),
:deep(.katex .sizing.reset-size2.size2) {
font-size: 1em
}
:deep(.katex .fontsize-ensurer.reset-size2.size3),
:deep(.katex .sizing.reset-size2.size3) {
font-size: 1.16666667em
}
:deep(.katex .fontsize-ensurer.reset-size2.size4),
:deep(.katex .sizing.reset-size2.size4) {
font-size: 1.33333333em
}
:deep(.katex .fontsize-ensurer.reset-size2.size5),
:deep(.katex .sizing.reset-size2.size5) {
font-size: 1.5em
}
:deep(.katex .fontsize-ensurer.reset-size2.size6),
:deep(.katex .sizing.reset-size2.size6) {
font-size: 1.66666667em
}
:deep(.katex .fontsize-ensurer.reset-size2.size7),
:deep(.katex .sizing.reset-size2.size7) {
font-size: 2em
}
:deep(.katex .fontsize-ensurer.reset-size2.size8),
:deep(.katex .sizing.reset-size2.size8) {
font-size: 2.4em
}
:deep(.katex .fontsize-ensurer.reset-size2.size9),
:deep(.katex .sizing.reset-size2.size9) {
font-size: 2.88em
}
:deep(.katex .fontsize-ensurer.reset-size2.size10),
:deep(.katex .sizing.reset-size2.size10) {
font-size: 3.45666667em
}
:deep(.katex .fontsize-ensurer.reset-size2.size11),
:deep(.katex .sizing.reset-size2.size11) {
font-size: 4.14666667em
}
:deep(.katex .fontsize-ensurer.reset-size3.size1),
:deep(.katex .sizing.reset-size3.size1) {
font-size: .71428571em
}
:deep(.katex .fontsize-ensurer.reset-size3.size2),
:deep(.katex .sizing.reset-size3.size2) {
font-size: .85714286em
}
:deep(.katex .fontsize-ensurer.reset-size3.size3),
:deep(.katex .sizing.reset-size3.size3) {
font-size: 1em
}
:deep(.katex .fontsize-ensurer.reset-size3.size4),
:deep(.katex .sizing.reset-size3.size4) {
font-size: 1.14285714em
}
:deep(.katex .fontsize-ensurer.reset-size3.size5),
:deep(.katex .sizing.reset-size3.size5) {
font-size: 1.28571429em
}
:deep(.katex .fontsize-ensurer.reset-size3.size6),
:deep(.katex .sizing.reset-size3.size6) {
font-size: 1.42857143em
}
:deep(.katex .fontsize-ensurer.reset-size3.size7),
:deep(.katex .sizing.reset-size3.size7) {
font-size: 1.71428571em
}
:deep(.katex .fontsize-ensurer.reset-size3.size8),
:deep(.katex .sizing.reset-size3.size8) {
font-size: 2.05714286em
}
:deep(.katex .fontsize-ensurer.reset-size3.size9),
:deep(.katex .sizing.reset-size3.size9) {
font-size: 2.46857143em
}
:deep(.katex .fontsize-ensurer.reset-size3.size10),
:deep(.katex .sizing.reset-size3.size10) {
font-size: 2.96285714em
}
:deep(.katex .fontsize-ensurer.reset-size3.size11),
:deep(.katex .sizing.reset-size3.size11) {
font-size: 3.55428571em
}
:deep(.katex .fontsize-ensurer.reset-size4.size1),
:deep(.katex .sizing.reset-size4.size1) {
font-size: .625em
}
:deep(.katex .fontsize-ensurer.reset-size4.size2),
:deep(.katex .sizing.reset-size4.size2) {
font-size: .75em
}
:deep(.katex .fontsize-ensurer.reset-size4.size3),
:deep(.katex .sizing.reset-size4.size3) {
font-size: .875em
}
:deep(.katex .fontsize-ensurer.reset-size4.size4),
:deep(.katex .sizing.reset-size4.size4) {
font-size: 1em
}
:deep(.katex .fontsize-ensurer.reset-size4.size5),
:deep(.katex .sizing.reset-size4.size5) {
font-size: 1.125em
}
:deep(.katex .fontsize-ensurer.reset-size4.size6),
:deep(.katex .sizing.reset-size4.size6) {
font-size: 1.25em
}
:deep(.katex .fontsize-ensurer.reset-size4.size7),
:deep(.katex .sizing.reset-size4.size7) {
font-size: 1.5em
}
:deep(.katex .fontsize-ensurer.reset-size4.size8),
:deep(.katex .sizing.reset-size4.size8) {
font-size: 1.8em
}
:deep(.katex .fontsize-ensurer.reset-size4.size9),
:deep(.katex .sizing.reset-size4.size9) {
font-size: 2.16em
}
:deep(.katex .fontsize-ensurer.reset-size4.size10),
:deep(.katex .sizing.reset-size4.size10) {
font-size: 2.5925em
}
:deep(.katex .fontsize-ensurer.reset-size4.size11),
:deep(.katex .sizing.reset-size4.size11) {
font-size: 3.11em
}
:deep(.katex .fontsize-ensurer.reset-size5.size1),
:deep(.katex .sizing.reset-size5.size1) {
font-size: .55555556em
}
:deep(.katex .fontsize-ensurer.reset-size5.size2),
:deep(.katex .sizing.reset-size5.size2) {
font-size: .66666667em
}
:deep(.katex .fontsize-ensurer.reset-size5.size3),
:deep(.katex .sizing.reset-size5.size3) {
font-size: .77777778em
}
:deep(.katex .fontsize-ensurer.reset-size5.size4),
:deep(.katex .sizing.reset-size5.size4) {
font-size: .88888889em
}
:deep(.katex .fontsize-ensurer.reset-size5.size5),
:deep(.katex .sizing.reset-size5.size5) {
font-size: 1em
}
:deep(.katex .fontsize-ensurer.reset-size5.size6),
:deep(.katex .sizing.reset-size5.size6) {
font-size: 1.11111111em
}
:deep(.katex .fontsize-ensurer.reset-size5.size7),
:deep(.katex .sizing.reset-size5.size7) {
font-size: 1.33333333em
}
:deep(.katex .fontsize-ensurer.reset-size5.size8),
:deep(.katex .sizing.reset-size5.size8) {
font-size: 1.6em
}
:deep(.katex .fontsize-ensurer.reset-size5.size9),
:deep(.katex .sizing.reset-size5.size9) {
font-size: 1.92em
}
:deep(.katex .fontsize-ensurer.reset-size5.size10),
:deep(.katex .sizing.reset-size5.size10) {
font-size: 2.30444444em
}
:deep(.katex .fontsize-ensurer.reset-size5.size11),
:deep(.katex .sizing.reset-size5.size11) {
font-size: 2.76444444em
}
:deep(.katex .fontsize-ensurer.reset-size6.size1),
:deep(.katex .sizing.reset-size6.size1) {
font-size: .5em
}
:deep(.katex .fontsize-ensurer.reset-size6.size2),
:deep(.katex .sizing.reset-size6.size2) {
font-size: .6em
}
:deep(.katex .fontsize-ensurer.reset-size6.size3),
:deep(.katex .sizing.reset-size6.size3) {
font-size: .7em
}
:deep(.katex .fontsize-ensurer.reset-size6.size4),
:deep(.katex .sizing.reset-size6.size4) {
font-size: .8em
}
:deep(.katex .fontsize-ensurer.reset-size6.size5),
:deep(.katex .sizing.reset-size6.size5) {
font-size: .9em
}
:deep(.katex .fontsize-ensurer.reset-size6.size6),
:deep(.katex .sizing.reset-size6.size6) {
font-size: 1em
}
:deep(.katex .fontsize-ensurer.reset-size6.size7),
:deep(.katex .sizing.reset-size6.size7) {
font-size: 1.2em
}
:deep(.katex .fontsize-ensurer.reset-size6.size8),
:deep(.katex .sizing.reset-size6.size8) {
font-size: 1.44em
}
:deep(.katex .fontsize-ensurer.reset-size6.size9),
:deep(.katex .sizing.reset-size6.size9) {
font-size: 1.728em
}
:deep(.katex .fontsize-ensurer.reset-size6.size10),
:deep(.katex .sizing.reset-size6.size10) {
font-size: 2.074em
}
:deep(.katex .fontsize-ensurer.reset-size6.size11),
:deep(.katex .sizing.reset-size6.size11) {
font-size: 2.488em
}
:deep(.katex .fontsize-ensurer.reset-size7.size1),
:deep(.katex .sizing.reset-size7.size1) {
font-size: .41666667em
}
:deep(.katex .fontsize-ensurer.reset-size7.size2),
:deep(.katex .sizing.reset-size7.size2) {
font-size: .5em
}
:deep(.katex .fontsize-ensurer.reset-size7.size3),
:deep(.katex .sizing.reset-size7.size3) {
font-size: .58333333em
}
:deep(.katex .fontsize-ensurer.reset-size7.size4),
:deep(.katex .sizing.reset-size7.size4) {
font-size: .66666667em
}
:deep(.katex .fontsize-ensurer.reset-size7.size5),
:deep(.katex .sizing.reset-size7.size5) {
font-size: .75em
}
:deep(.katex .fontsize-ensurer.reset-size7.size6),
:deep(.katex .sizing.reset-size7.size6) {
font-size: .83333333em
}
:deep(.katex .fontsize-ensurer.reset-size7.size7),
:deep(.katex .sizing.reset-size7.size7) {
font-size: 1em
}
:deep(.katex .fontsize-ensurer.reset-size7.size8),
:deep(.katex .sizing.reset-size7.size8) {
font-size: 1.2em
}
:deep(.katex .fontsize-ensurer.reset-size7.size9),
:deep(.katex .sizing.reset-size7.size9) {
font-size: 1.44em
}
:deep(.katex .fontsize-ensurer.reset-size7.size10),
:deep(.katex .sizing.reset-size7.size10) {
font-size: 1.72833333em
}
:deep(.katex .fontsize-ensurer.reset-size7.size11),
:deep(.katex .sizing.reset-size7.size11) {
font-size: 2.07333333em
}
:deep(.katex .fontsize-ensurer.reset-size8.size1),
:deep(.katex .sizing.reset-size8.size1) {
font-size: .34722222em
}
:deep(.katex .fontsize-ensurer.reset-size8.size2),
:deep(.katex .sizing.reset-size8.size2) {
font-size: .41666667em
}
:deep(.katex .fontsize-ensurer.reset-size8.size3),
:deep(.katex .sizing.reset-size8.size3) {
font-size: .48611111em
}
:deep(.katex .fontsize-ensurer.reset-size8.size4),
:deep(.katex .sizing.reset-size8.size4) {
font-size: .55555556em
}
:deep(.katex .fontsize-ensurer.reset-size8.size5),
:deep(.katex .sizing.reset-size8.size5) {
font-size: .625em
}
:deep(.katex .fontsize-ensurer.reset-size8.size6),
:deep(.katex .sizing.reset-size8.size6) {
font-size: .69444444em
}
:deep(.katex .fontsize-ensurer.reset-size8.size7),
:deep(.katex .sizing.reset-size8.size7) {
font-size: .83333333em
}
:deep(.katex .fontsize-ensurer.reset-size8.size8),
:deep(.katex .sizing.reset-size8.size8) {
font-size: 1em
}
:deep(.katex .fontsize-ensurer.reset-size8.size9),
:deep(.katex .sizing.reset-size8.size9) {
font-size: 1.2em
}
:deep(.katex .fontsize-ensurer.reset-size8.size10),
:deep(.katex .sizing.reset-size8.size10) {
font-size: 1.44027778em
}
:deep(.katex .fontsize-ensurer.reset-size8.size11),
:deep(.katex .sizing.reset-size8.size11) {
font-size: 1.72777778em
}
:deep(.katex .fontsize-ensurer.reset-size9.size1),
:deep(.katex .sizing.reset-size9.size1) {
font-size: .28935185em
}
:deep(.katex .fontsize-ensurer.reset-size9.size2),
:deep(.katex .sizing.reset-size9.size2) {
font-size: .34722222em
}
:deep(.katex .fontsize-ensurer.reset-size9.size3),
:deep(.katex .sizing.reset-size9.size3) {
font-size: .40509259em
}
:deep(.katex .fontsize-ensurer.reset-size9.size4),
:deep(.katex .sizing.reset-size9.size4) {
font-size: .46296296em
}
:deep(.katex .fontsize-ensurer.reset-size9.size5),
:deep(.katex .sizing.reset-size9.size5) {
font-size: .52083333em
}
:deep(.katex .fontsize-ensurer.reset-size9.size6),
:deep(.katex .sizing.reset-size9.size6) {
font-size: .5787037em
}
:deep(.katex .fontsize-ensurer.reset-size9.size7),
:deep(.katex .sizing.reset-size9.size7) {
font-size: .69444444em
}
:deep(.katex .fontsize-ensurer.reset-size9.size8),
:deep(.katex .sizing.reset-size9.size8) {
font-size: .83333333em
}
:deep(.katex .fontsize-ensurer.reset-size9.size9),
:deep(.katex .sizing.reset-size9.size9) {
font-size: 1em
}
:deep(.katex .fontsize-ensurer.reset-size9.size10),
:deep(.katex .sizing.reset-size9.size10) {
font-size: 1.20023148em
}
:deep(.katex .fontsize-ensurer.reset-size9.size11),
:deep(.katex .sizing.reset-size9.size11) {
font-size: 1.43981481em
}
:deep(.katex .fontsize-ensurer.reset-size10.size1),
:deep(.katex .sizing.reset-size10.size1) {
font-size: .24108004em
}
:deep(.katex .fontsize-ensurer.reset-size10.size2),
:deep(.katex .sizing.reset-size10.size2) {
font-size: .28929605em
}
:deep(.katex .fontsize-ensurer.reset-size10.size3),
:deep(.katex .sizing.reset-size10.size3) {
font-size: .33751205em
}
:deep(.katex .fontsize-ensurer.reset-size10.size4),
:deep(.katex .sizing.reset-size10.size4) {
font-size: .38572806em
}
:deep(.katex .fontsize-ensurer.reset-size10.size5),
:deep(.katex .sizing.reset-size10.size5) {
font-size: .43394407em
}
:deep(.katex .fontsize-ensurer.reset-size10.size6),
:deep(.katex .sizing.reset-size10.size6) {
font-size: .48216008em
}
:deep(.katex .fontsize-ensurer.reset-size10.size7),
:deep(.katex .sizing.reset-size10.size7) {
font-size: .57859209em
}
:deep(.katex .fontsize-ensurer.reset-size10.size8),
:deep(.katex .sizing.reset-size10.size8) {
font-size: .69431051em
}
:deep(.katex .fontsize-ensurer.reset-size10.size9),
:deep(.katex .sizing.reset-size10.size9) {
font-size: .83317261em
}
:deep(.katex .fontsize-ensurer.reset-size10.size10),
:deep(.katex .sizing.reset-size10.size10) {
font-size: 1em
}
:deep(.katex .fontsize-ensurer.reset-size10.size11),
:deep(.katex .sizing.reset-size10.size11) {
font-size: 1.19961427em
}
:deep(.katex .fontsize-ensurer.reset-size11.size1),
:deep(.katex .sizing.reset-size11.size1) {
font-size: .20096463em
}
:deep(.katex .fontsize-ensurer.reset-size11.size2),
:deep(.katex .sizing.reset-size11.size2) {
font-size: .24115756em
}
:deep(.katex .fontsize-ensurer.reset-size11.size3),
:deep(.katex .sizing.reset-size11.size3) {
font-size: .28135048em
}
:deep(.katex .fontsize-ensurer.reset-size11.size4),
:deep(.katex .sizing.reset-size11.size4) {
font-size: .32154341em
}
:deep(.katex .fontsize-ensurer.reset-size11.size5),
:deep(.katex .sizing.reset-size11.size5) {
font-size: .36173633em
}
:deep(.katex .fontsize-ensurer.reset-size11.size6),
:deep(.katex .sizing.reset-size11.size6) {
font-size: .40192926em
}
:deep(.katex .fontsize-ensurer.reset-size11.size7),
:deep(.katex .sizing.reset-size11.size7) {
font-size: .48231511em
}
:deep(.katex .fontsize-ensurer.reset-size11.size8),
:deep(.katex .sizing.reset-size11.size8) {
font-size: .57877814em
}
:deep(.katex .fontsize-ensurer.reset-size11.size9),
:deep(.katex .sizing.reset-size11.size9) {
font-size: .69453376em
}
:deep(.katex .fontsize-ensurer.reset-size11.size10),
:deep(.katex .sizing.reset-size11.size10) {
font-size: .83360129em
}
:deep(.katex .fontsize-ensurer.reset-size11.size11),
:deep(.katex .sizing.reset-size11.size11) {
font-size: 1em
}
:deep(.katex .delimsizing.size1) {
font-family: KaTeX_Size1
}
:deep(.katex .delimsizing.size2) {
font-family: KaTeX_Size2
}
:deep(.katex .delimsizing.size3) {
font-family: KaTeX_Size3
}
:deep(.katex .delimsizing.size4) {
font-family: KaTeX_Size4
}
:deep(.katex .delimsizing.mult .delim-size1>.katex-span) {
font-family: KaTeX_Size1
}
:deep(.katex .delimsizing.mult .delim-size4>.katex-span) {
font-family: KaTeX_Size4
}
:deep(.katex .nulldelimiter) {
display: inline-block;
width: .12em
}
:deep(.katex .delimcenter) {
position: relative
}
:deep(.katex .op-symbol) {
position: relative
}
:deep(.katex .op-symbol.small-op) {
font-family: KaTeX_Size1
}
:deep(.katex .op-symbol.large-op) {
font-family: KaTeX_Size2
}
:deep(.katex .op-limits>.vlist-t) {
text-align: center
}
:deep(.katex .accent>.vlist-t) {
text-align: center
}
:deep(.katex .accent .accent-body) {
position: relative
}
.katex .accent .accent-body:not(.accent-full) {
width: 0
}
:deep(.katex .overlay) {
display: block
}
:deep(.katex .mtable .vertical-separator) {
display: inline-block;
min-width: 1px
}
:deep(.katex .mtable .arraycolsep) {
display: inline-block
}
:deep(.katex .mtable .col-align-c>.vlist-t) {
text-align: center
}
:deep(.katex .mtable .col-align-l>.vlist-t) {
text-align: left
}
:deep(.katex .mtable .col-align-r>.vlist-t) {
text-align: right
}
:deep(.katex .svg-align) {
text-align: left
}
:deep(.katex .katex-svg) {
display: block;
position: absolute;
width: 100%;
height: inherit;
fill: currentColor;
stroke: currentColor;
fill-rule: nonzero;
fill-opacity: 1;
stroke-width: 1;
stroke-linecap: butt;
stroke-linejoin: miter;
stroke-miterlimit: 4;
stroke-dasharray: none;
stroke-dashoffset: 0;
stroke-opacity: 1
}
:deep(.katex .katex-svg path) {
stroke: none
}
:deep(.katex img) {
border-style: none;
min-width: 0;
min-height: 0;
max-width: none;
max-height: none
}
:deep(.katex .stretchy) {
width: 100%;
display: block;
position: relative;
overflow: hidden
}
:deep(.katex .stretchy::after),
:deep(.katex .stretchy::before) {
content: ""
}
:deep(.katex .hide-tail) {
width: 100%;
position: relative;
overflow: hidden
}
:deep(.katex .halfarrow-left) {
position: absolute;
left: 0;
width: 50.2%;
overflow: hidden
}
:deep(.katex .halfarrow-right) {
position: absolute;
right: 0;
width: 50.2%;
overflow: hidden
}
:deep(.katex .brace-left) {
position: absolute;
left: 0;
width: 25.1%;
overflow: hidden
}
:deep(.katex .brace-center) {
position: absolute;
left: 25%;
width: 50%;
overflow: hidden
}
:deep(.katex .brace-right) {
position: absolute;
right: 0;
width: 25.1%;
overflow: hidden
}
:deep(.katex .x-arrow-pad) {
padding: 0 .5em
}
:deep(.katex .cd-arrow-pad) {
padding: 0 .55556em 0 .27778em
}
:deep(.katex .mover),
:deep(.katex .munder),
:deep(.katex .x-arrow) {
text-align: center
}
:deep(.katex .boxpad) {
padding: 0 .3em
}
:deep(.katex .fbox),
:deep(.katex .fcolorbox) {
box-sizing: border-box;
border: .04em solid
}
:deep(.katex .cancel-pad) {
padding: 0 .2em
}
:deep(.katex .cancel-lap) {
margin-left: -.2em;
margin-right: -.2em
}
:deep(.katex .sout) {
border-bottom-style: solid;
border-bottom-width: .08em
}
:deep(.katex .angl) {
box-sizing: border-box;
border-top: .049em solid;
border-right: .049em solid;
margin-right: .03889em
}
:deep(.katex .anglpad) {
padding: 0 .03889em
}
:deep(.katex .eqn-num::before) {
counter-increment: katexEqnNo;
content: "(" counter(katexEqnNo) ")"
}
:deep(.katex .mml-eqn-num::before) {
counter-increment: mmlEqnNo;
content: "(" counter(mmlEqnNo) ")"
}
:deep(.katex .mtr-glue) {
width: 50%
}
:deep(.katex .cd-vert-arrow) {
display: inline-block;
position: relative
}
:deep(.katex .cd-label-left) {
display: inline-block;
position: absolute;
right: calc(50% + .3em);
text-align: left
}
:deep(.katex .cd-label-right) {
display: inline-block;
position: absolute;
left: calc(50% + .3em);
text-align: right
}

View File

@@ -0,0 +1,314 @@
/* KaTeX 数学公式样式 */
:deep(.katex) {
counter-reset: katexEqnNo mmlEqnNo;
font: normal 1.21em KaTeX_Main, Times New Roman, serif;
line-height: 1.2;
text-indent: 0;
text-rendering: auto
}
:deep(.katex text),
:deep(.katex view) {
-ms-high-contrast-adjust: none !important;
border-color: currentColor
}
:deep(.katex .katex-mathml) {
position: absolute;
clip: rect(1px, 1px, 1px, 1px);
padding: 0;
border: 0;
height: 1px;
width: 1px;
overflow: hidden
}
:deep(.katex .katex-html>.newline) {
display: block
}
:deep(.katex .base) {
position: relative;
display: inline-block;
white-space: nowrap;
width: min-content
}
:deep(.katex .strut) {
display: inline-block
}
:deep(.katex .textbf) {
font-weight: 700
}
:deep(.katex .textit) {
font-style: italic
}
:deep(.katex .textrm) {
font-family: KaTeX_Main
}
:deep(.katex .textsf) {
font-family: KaTeX_SansSerif
}
:deep(.katex .texttt) {
font-family: KaTeX_Typewriter
}
:deep(.katex .mathnormal) {
font-family: KaTeX_Math;
font-style: italic
}
:deep(.katex .mathit) {
font-family: KaTeX_Main;
font-style: italic
}
:deep(.katex .mathrm) {
font-style: normal
}
:deep(.katex .mathbf) {
font-family: KaTeX_Main;
font-weight: 700
}
:deep(.katex .boldsymbol) {
font-family: KaTeX_Math;
font-weight: 700;
font-style: italic
}
:deep(.katex .amsrm) {
font-family: KaTeX_AMS
}
:deep(.katex .mathbb),
:deep(.katex .textbb) {
font-family: KaTeX_AMS
}
:deep(.katex .mathcal) {
font-family: KaTeX_Caligraphic
}
:deep(.katex .mathfrak),
:deep(.katex .textfrak) {
font-family: KaTeX_Fraktur
}
:deep(.katex .mathtt) {
font-family: KaTeX_Typewriter
}
:deep(.katex .mathscr),
:deep(.katex .textscr) {
font-family: KaTeX_Script
}
:deep(.katex .mathsf),
:deep(.katex .textsf) {
font-family: KaTeX_SansSerif
}
:deep(.katex .mathboldsf),
:deep(.katex .textboldsf) {
font-family: KaTeX_SansSerif;
font-weight: 700
}
:deep(.katex .mathitsf),
:deep(.katex .textitsf) {
font-family: KaTeX_SansSerif;
font-style: italic
}
:deep(.katex .mainrm) {
font-family: KaTeX_Main;
font-style: normal
}
:deep(.katex .vlist-t) {
display: inline-table;
table-layout: fixed;
border-collapse: collapse
}
:deep(.katex .vlist-r) {
display: table-row
}
:deep(.katex .vlist) {
display: table-cell;
vertical-align: bottom;
position: relative
}
:deep(.katex .vlist>.katex-span) {
display: block;
height: 0;
position: relative
}
:deep(.katex .vlist>.katex-span>.katex-span) {
display: inline-block
}
:deep(.katex .vlist>.katex-span>.pstrut) {
overflow: hidden;
width: 0
}
:deep(.katex .vlist-t2) {
margin-right: -2px
}
:deep(.katex .vlist-s) {
display: table-cell;
vertical-align: bottom;
font-size: 1px;
width: 2px;
min-width: 2px
}
:deep(.katex .vbox) {
display: inline-flex;
flex-direction: column;
align-items: baseline
}
:deep(.katex .hbox) {
display: inline-flex;
flex-direction: row;
width: 100%
}
:deep(.katex .thinbox) {
display: inline-flex;
flex-direction: row;
width: 0;
max-width: 0
}
:deep(.katex .msupsub) {
text-align: left
}
:deep(.katex .mfrac>.katex-span>.katex-span) {
text-align: center
}
:deep(.katex .mfrac .frac-line) {
display: inline-block;
width: 100%;
border-bottom-style: solid
}
:deep(.katex .hdashline),
:deep(.katex .hline),
:deep(.katex .mfrac .frac-line),
:deep(.katex .overline .overline-line),
:deep(.katex .rule),
:deep(.katex .underline .underline-line) {
min-height: 1px
}
:deep(.katex .mspace) {
display: inline-block
}
:deep(.katex .clap),
:deep(.katex .llap),
:deep(.katex .rlap) {
width: 0;
position: relative
}
:deep(.katex .clap>.inner),
:deep(.katex .llap>.inner),
:deep(.katex .rlap>.inner) {
position: absolute
}
:deep(.katex .clap>.fix),
:deep(.katex .llap>.fix),
:deep(.katex .rlap>.fix) {
display: inline-block
}
:deep(.katex .llap>.inner) {
right: 0
}
:deep(.katex .clap>.inner),
:deep(.katex .rlap>.inner) {
left: 0
}
:deep(.katex .clap>.inner>.katex-span) {
margin-left: -50%;
margin-right: 50%
}
:deep(.katex .rule) {
display: inline-block;
border: solid 0;
position: relative
}
:deep(.katex .hline),
:deep(.katex .overline .overline-line),
:deep(.katex .underline .underline-line) {
display: inline-block;
width: 100%;
border-bottom-style: solid
}
:deep(.katex .hdashline) {
display: inline-block;
width: 100%;
border-bottom-style: dashed
}
:deep(.katex .sqrt>.root) {
margin-left: .27777778em;
margin-right: -.55555556em
}
:deep(.katex-display) {
display: block;
margin: 1em 0;
text-align: center;
overflow-x: auto;
overflow-y: hidden;
}
:deep(.katex-display>.katex) {
display: block;
text-align: center;
white-space: nowrap
}
:deep(.katex-display>.katex>.katex-html) {
display: block;
position: relative
}
:deep(.katex-display>.katex>.katex-html>.tag) {
position: absolute;
right: 0
}
:deep(.katex-display.leqno>.katex>.katex-html>.tag) {
left: 0;
right: auto
}
:deep(.katex-display.fleqn>.katex) {
text-align: left;
padding-left: 2em
}

View File

@@ -0,0 +1,114 @@
$mdZoom: 1.143;
$lineHeight: calc($mdZoom * 25px);
$fontWeightStrong: 600;
/* Markdown 通用样式 */
:deep(.md-p) {
margin-block-start: 1em;
margin-block-end: 1em;
line-height: 1.5;
strong {
font-weight: $fontWeightStrong;
}
}
:deep(.md-table),
:deep(.md-blockquote) {
margin-bottom: 16px;
}
:deep(.md-table) {
box-sizing: border-box;
display: table;
width: max-content;
min-width: 100%;
overflow: auto;
border-spacing: 0;
border-collapse: collapse;
}
:deep(.md-tr) {
background-color: #fff;
border-top: 1px solid #c6cbd1;
}
.md-table .md-tr:nth-child(2n) {
background-color: #f6f8fa;
}
:deep(.md-th),
:deep(.md-td) {
padding: 6px 13px !important;
border: 1px solid #dfe2e5;
}
:deep(.md-th) {
font-weight: 600;
}
:deep(.md-blockquote) {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
}
:deep(.md-code),
:deep(.md-code code) {
display: inline-block !important;
padding: 0.2em 0.4em;
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
font-size: 85%;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
word-break: break-all;
overflow-wrap: anywhere;
}
:deep(.md-pre .md-code) {
padding: 0;
font-size: 100%;
background: transparent;
border: 0;
}
:deep(.md-hr) {
height: 0.5px;
border: none;
margin: 30rpx 0;
background-color: #d9d9d9;
}
/**
H1 20px H2 18px H3 16px H4 16px H5 16px
**/
:deep(.md-h1) {
font-size: 20px;
border-bottom: 2rpx solid #d9d9d9;
padding-bottom: .3em;
line-height: 1.5;
font-weight: $fontWeightStrong;
margin: calc($mdZoom * 16px) 0 calc($mdZoom * 12px) 0;
}
:deep(.md-h2) {
font-size: 18px;
font-weight: $fontWeightStrong;
line-height: 1.5;
margin: calc($mdZoom * 16px) 0 calc($mdZoom * 12px) 0;
}
:deep(.md-h3),
:deep(.md-h4),
:deep(.md-h5) {
font-size: 16px;
font-weight: $fontWeightStrong;
line-height: $lineHeight;
margin: calc($mdZoom * 16px) 0 calc($mdZoom * 12px) 0;
}
:deep(.md-h6) {
font-size: 14px;
font-weight: $fontWeightStrong;
line-height: $lineHeight;
margin: calc($mdZoom * 16px) 0 calc($mdZoom * 12px) 0;
}

View File

@@ -0,0 +1,201 @@
<template>
<view class="task-result" @tap="handleClick">
<!-- 左侧图标 -->
<view class="task-result-icon">
<text class="iconfont">&#xe64e;</text>
</view>
<!-- 文件描述/名称 -->
<view class="task-result-action">
<text class="action-text">{{ displayText }}</text>
</view>
<!-- 右侧图标 -->
<view class="task-result-arrow">
<text class="iconfont">&#xe63f;</text>
</view>
</view>
</template>
<script>
import {
getFileProxyUrlByConversationIdAndFilePath,
jumpToFilePreviewPage,
} from "@/utils/system.uts";
export default {
name: "TaskResult",
props: {
// task-result 节点数据
data: {
type: Object,
default: () => ({}),
},
// 会话ID
conversationId: {
type: [String, Number],
default: "",
},
},
computed: {
// 提取描述文本
description() {
return this.extractChildText("description");
},
// 提取文件名
fileName() {
return this.extractChildText("file");
},
// 显示文本:优先显示描述,其次显示文件名
displayText() {
return this.description || this.fileName || "查看结果";
},
},
methods: {
/**
* 从子节点中提取指定类型的文本内容
* @param tagName 标签名
* @returns 文本内容
*/
extractChildText(tagName) {
const children = this.data?.children || [];
for (const child of children) {
if (child.name === tagName) {
// 递归提取文本
return this.getTextContent(child);
}
}
return "";
},
/**
* 递归获取节点的文本内容
* @param node 节点
* @returns 文本内容
*/
getTextContent(node) {
if (!node) return "";
// 如果是文本节点
if (node.type === "text") {
return node.text || "";
}
// 如果有子节点,递归处理
if (node.children && Array.isArray(node.children)) {
return node.children
.map((child) => this.getTextContent(child))
.join("");
}
return "";
},
/**
* 点击处理:打开预览视图
*/
async handleClick() {
let conversationId = String(this.conversationId);
// 移除 chat- 前缀因为页面参数使用的是纯数字ID
if (conversationId && conversationId.startsWith("chat-")) {
conversationId = conversationId.replace("chat-", "");
}
if (conversationId) {
try {
const fileId = this.fileName.split(`${conversationId}/`).pop();
const fileProxyUrl =
await getFileProxyUrlByConversationIdAndFilePath(
conversationId,
fileId,
);
jumpToFilePreviewPage(conversationId, fileProxyUrl);
} catch (error) {
console.error("[TaskResult] 获取文件列表失败:", error);
}
} else {
// 如果没有会话ID发送事件让父组件处理
uni.$emit("task_result_click", {
data: this.data,
fileName: this.fileName,
description: this.description,
});
}
},
},
};
</script>
<style lang="scss">
.task-result {
display: flex;
flex-direction: row;
align-items: center;
gap: 8rpx;
padding: 20rpx 20rpx;
margin: 16rpx 0;
border-radius: 16rpx;
background-color: rgba(12, 20, 102, 0.04);
width: 100%;
box-sizing: border-box;
&:active {
background-color: rgba(12, 20, 102, 0.08);
}
.task-result-icon {
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 32rpx;
color: rgba(0, 0, 0, 0.45);
}
}
.task-result-action {
flex: 1;
min-width: 0;
overflow: hidden;
.action-text {
font-size: 28rpx;
font-weight: 600;
color: #333;
line-height: 40rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.task-result-arrow {
flex-shrink: 0;
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 24rpx;
color: rgba(0, 0, 0, 0.25);
}
}
// 响应式设计
@media (max-width: 750rpx) {
.task-result-action {
.action-text {
font-size: 26rpx;
}
}
}
}
</style>