perf: 优化超长文本信息

This commit is contained in:
kuaifan 2024-12-13 15:45:20 +08:00
parent 9e92c61fbf
commit 49aa1434aa
12 changed files with 227 additions and 18 deletions

View File

@ -14,6 +14,7 @@ use App\Module\Base;
use App\Module\Timer;
use App\Module\Extranet;
use App\Module\TimeRange;
use App\Module\MsgTool;
use App\Module\Table\OnlineData;
use App\Models\FileContent;
use App\Models\AbstractModel;
@ -1096,21 +1097,29 @@ class DialogController extends AbstractController
return Base::retError('消息发送保存失败');
}
$ext = $markdown ? 'md' : 'htm';
$fileData = [
'name' => "LongText-{$strlen}.{$ext}",
'size' => $size,
'file' => $file,
'path' => $path,
'url' => Base::fillUrl($path),
'thumb' => '',
'width' => -1,
'height' => -1,
'ext' => $ext,
$text = MsgTool::truncateText($text, 500, $ext);
$desc = strip_tags($markdown ? Base::markdown2html($text) : $text);
$desc = mb_substr(WebSocketDialogMsg::filterEscape($desc), 0, 200);
$msgData = [
'desc' => $desc, // 描述内容
'text' => $text, // 简要内容
'type' => $ext, // 内容类型
'file' => [
'name' => "LongText-{$strlen}.{$ext}",
'size' => $size,
'file' => $file,
'path' => $path,
'url' => Base::fillUrl($path),
'thumb' => '',
'width' => -1,
'height' => -1,
'ext' => $ext,
],
];
if (empty($key)) {
$key = mb_substr(strip_tags($text), 0, 200);
$key = $desc;
}
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid, false, false, $silence, $key);
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'longtext', $msgData, $user->userid, false, false, $silence, $key);
} else {
$msgData = ['text' => $text];
if ($markdown) {
@ -1638,6 +1647,18 @@ class DialogController extends AbstractController
$msg = File::formatFileData($msg);
$data['content'] = $msg['content'];
$data['file_mode'] = $msg['file_mode'];
} elseif ($data['type'] == 'longtext') {
$data['content'] = [
'type' => 'htm',
'content' => Doo::translate("内容不存在")
];
if (isset($data['msg']['file']['path'])) {
$filePath = public_path($data['msg']['file']['path']);
if (file_exists($filePath)) {
$data['content']['type'] = $data['msg']['type'];
$data['content']['content'] = file_get_contents($filePath);
}
}
}
//
return Base::retSuccess('success', $data);

View File

@ -592,6 +592,9 @@ class WebSocketDialogMsg extends AbstractModel
case 'text':
return self::previewTextMsg($data['msg'], $preserveHtml);
case 'longtext':
return $data['msg']['desc'] ? Base::cutStr($data['msg']['desc'], 50) : ("[" . Doo::translate("长文本") . "]");
case 'vote':
$action = Doo::translate("投票");
return "[{$action}] " . self::previewTextMsg($data['msg'], $preserveHtml);
@ -774,14 +777,24 @@ class WebSocketDialogMsg extends AbstractModel
break;
}
}
$key = str_replace([""", "&", "<", ">"], "", $key);
$key = str_replace(["\r", "\n", "\t", " "], " ", $key);
$key = preg_replace("/^\/[A-Za-z]+/", " ", $key);
$key = preg_replace("/\s+/", " ", $key);
$this->key = trim($key);
$this->key = self::filterEscape($key);
$this->save();
}
/**
* 过滤转义
* @param $content
* @return string
*/
public static function filterEscape($content)
{
$content = str_replace([""", "&", "<", ">"], "", $content);
$content = str_replace(["\r", "\n", "\t", " "], " ", $content);
$content = preg_replace("/^\/[A-Za-z]+/", " ", $content);
$content = preg_replace("/\s+/", " ", $content);
return trim($content);
}
/**
* 返回引用消息(如果是文本只取预览)
* @return array|mixed

108
app/Module/MsgTool.php Normal file
View File

@ -0,0 +1,108 @@
<?php
namespace App\Module;
use DOMDocument;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Exception\CommonMarkException;
use League\HTMLToMarkdown\HtmlConverter;
class MsgTool
{
/**
* 截取文本并保持标签完整性
*
* @param string $text 要截取的文本
* @param int $length 截取长度
* @param string $type 文本类型 (htm md)
* @return string 处理后的文本
*/
public static function truncateText($text, $length, $type = 'htm')
{
if (empty($text) || mb_strlen($text) <= $length) {
return $text;
}
$isMd = strtolower($type) === 'md';
// 如果是Markdown转换为HTML
if ($isMd) {
$converter = new CommonMarkConverter();
try {
$text = $converter->convert($text);
} catch (CommonMarkException) {
return "";
}
}
// 创建DOM文档
$dom = new DOMDocument('1.0', 'UTF-8');
libxml_use_internal_errors(true);
$dom->loadHTML(mb_convert_encoding($text, 'HTML-ENTITIES', 'UTF-8'));
libxml_clear_errors();
// 获取body元素
$body = $dom->getElementsByTagName('body')->item(0);
$truncatedHtml = '';
$currentLength = 0;
// 递归函数来遍历节点并截取内容
self::traverseNodes($body, $currentLength, $length, $truncatedHtml);
// 如果是Markdown转换回Markdown
if ($isMd) {
$converter = new HtmlConverter();
$truncatedHtml = $converter->convert($truncatedHtml);
}
return $truncatedHtml;
}
/**
* 递归遍历节点
* @param $node
* @param $currentLength
* @param $length
* @param $truncatedHtml
* @return void
*/
private static function traverseNodes($node, &$currentLength, $length, &$truncatedHtml)
{
foreach ($node->childNodes as $child) {
if ($currentLength >= $length) {
break;
}
if ($child->nodeType === XML_TEXT_NODE) {
$textContent = $child->textContent;
$remainingLength = $length - $currentLength;
if (mb_strlen($textContent) > $remainingLength) {
$truncatedHtml .= htmlspecialchars(mb_substr($textContent, 0, $remainingLength) . '...');
$currentLength += $remainingLength;
} else {
$truncatedHtml .= htmlspecialchars($textContent);
$currentLength += mb_strlen($textContent);
}
} elseif ($child->nodeType === XML_ELEMENT_NODE) {
$truncatedHtml .= '<' . $child->nodeName;
// 添加属性
if ($child->hasAttributes()) {
foreach ($child->attributes as $attr) {
$truncatedHtml .= ' ' . $attr->nodeName . '="' . htmlspecialchars($attr->nodeValue) . '"';
}
}
$truncatedHtml .= '>';
self::traverseNodes($child, $currentLength, $length, $truncatedHtml);
if ($currentLength < $length || $child->firstChild) {
$truncatedHtml .= '</' . $child->nodeName . '>';
}
}
}
}
}

View File

@ -10,6 +10,7 @@
"require": {
"php": "^8.0",
"ext-curl": "*",
"ext-dom": "*",
"ext-gd": "*",
"ext-imagick": "*",
"ext-json": "*",

View File

@ -802,3 +802,6 @@ webhook地址最长仅支持255个字符。
更新子任务标签
AI机器人不存在
内容不存在
长文本

View File

@ -1903,3 +1903,5 @@ WiFi签到延迟时长为±1分钟。
请选择示例标签
全部保存成功
消息详情
长文本

View File

@ -401,6 +401,8 @@ import {convertLocalResourcePath} from "../components/Replace/utils";
switch (data.type) {
case 'text':
return $A.getMsgTextPreview(data.msg, imgClassName)
case 'longtext':
return data.msg.desc ? $A.cutString(data.msg.desc, 50) : ("[" + $A.L('长文本') + "]")
case 'vote':
return `[${$A.L('投票')}]` + $A.getMsgTextPreview(data.msg, imgClassName)
case 'word-chain':

View File

@ -27,6 +27,8 @@
<div ref="content" class="dialog-content" :class="contentClass">
<!--文本-->
<TextMsg v-if="msgData.type === 'text'" :msgId="msgData.id" :msg="msgData.msg" @viewText="viewText"/>
<!--长文本-->
<LongTextMsg v-else-if="msgData.type === 'longtext'" :msgId="msgData.id" :msg="msgData.msg" @viewText="viewText" @downFile="downFile"/>
<!--文件-->
<FileMsg v-else-if="msgData.type === 'file'" :msg="msgData.msg" @viewFile="viewFile" @downFile="downFile"/>
<!--录音-->
@ -177,6 +179,7 @@ import {mapGetters, mapState} from "vuex";
import longpress from "../../../../directives/longpress";
import TextMsg from "./text.vue";
import LongTextMsg from "./longtext.vue";
import FileMsg from "./file.vue";
import RecordMsg from "./record.vue";
import LocationMsg from "./location.vue";
@ -199,6 +202,7 @@ export default {
MeetingMsg,
LocationMsg,
RecordMsg,
LongTextMsg,
TextMsg,
FileMsg,
WCircle

View File

@ -0,0 +1,30 @@
<template>
<div class="content-text no-dark-content">
<DialogMarkdown v-if="msg.type === 'md'" @click="viewText" :text="msg.text"/>
<pre v-else @click="viewText" v-html="$A.formatTextMsg(msg.text, userId)"></pre>
<div class="content-longtext-footer">
<span @click="downFile">{{$L('查看详情')}}</span>
</div>
</div>
</template>
<script>
import DialogMarkdown from "../DialogMarkdown.vue";
export default {
components: {DialogMarkdown},
props: {
msgId: Number,
msg: Object,
},
methods: {
viewText(e) {
this.$emit('viewText', e);
},
downFile() {
this.$emit('downFile');
},
},
}
</script>

View File

@ -3644,6 +3644,10 @@ export default {
if (!$A.isJson(data)) {
data = this.operateItem
}
if (data.type === 'longtext') {
this.onViewFile(data)
return;
}
$A.modalConfirm({
language: false,
title: this.$L('下载文件'),

View File

@ -7,6 +7,10 @@
<TEditor v-else-if="isType('text')" :value="msgDetail.content.content" height="100%" readOnly/>
<Drawio v-else-if="isType('drawio')" v-model="msgDetail.content" :title="msgDetail.msg.name" readOnly/>
<Minder v-else-if="isType('mind')" :value="msgDetail.content" readOnly/>
<template v-else-if="msgDetail.type === 'longtext'">
<VMPreview v-if="msgDetail.content.type === 'md'" :value="msgDetail.content.content"/>
<div v-else class="view-code" v-html="$A.formatTextMsg(msgDetail.content.content, userId)"></div>
</template>
<template v-else-if="isType('code')">
<div v-if="isLongText(msgDetail.msg.name)" class="view-code" v-html="$A.formatTextMsg(msgDetail.content.content, userId)"></div>
<AceEditor v-else v-model="msgDetail.content.content" :ext="msgDetail.msg.ext" class="view-editor" readOnly/>
@ -105,7 +109,10 @@ export default {
},
title() {
const {msg} = this.msgDetail;
const {type, msg} = this.msgDetail;
if (type === 'longtext') {
return this.$L('消息详情');
}
if (msg && msg.name) {
return msg.name;
}

View File

@ -659,6 +659,7 @@
position: relative;
&.text,
&.longtext,
&.record,
&.word-chain {
max-width: 70%;
@ -1460,6 +1461,19 @@
}
}
.content-longtext-footer {
display: flex;
align-items: center;
justify-content: center;
margin-top: 12px;
border-top: 1px solid rgba(227, 227, 227, 0.42);
padding-top: 12px;
padding-bottom: 2px;
> span {
cursor: pointer;
}
}
.mention {
color: $flow-status-end-color;
background-color: transparent;