perf: 优化搜索表情

This commit is contained in:
kuaifan 2022-11-18 11:58:29 +08:00
parent c2d852eb3a
commit 963474f32e
9 changed files with 269 additions and 215 deletions

View File

@ -338,30 +338,6 @@ class IndexController extends InvokeController
return abort(404);
}
/**
* 搜索表情
* @return array
*/
public function emo__search()
{
$key = Request::input('key');
if (empty($key)) {
return Base::retError("key empty");
}
return Cache::remember("emo__search:" . md5($key), now()->addDay(), function () use ($key) {
$res = Ihttp::ihttp_get("http://www.adoutu.com/search?keyword=" . urlencode($key));
if (Base::isError($res)) {
return $res;
}
$content = Base::getMiddle($res['data'], '<!--图片列表-->', '<!--分页器-->');
preg_match_all("/<img\s+src=\"(.*?)\"/s", $content, $matchs);
if ($matchs && $matchs[1]) {
return Base::retSuccess('success', array_slice($matchs[1], 0, 20));
}
return Base::retError("result empty");
});
}
/**
* 设置语言和皮肤
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View

View File

@ -537,7 +537,7 @@ class WebSocketDialogMsg extends AbstractModel
{
if (!$text) return '';
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?alt=\"(\S+)\"[^>]*?>/", "[$1]", $text);
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?>/", "[表情]", $text);
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?>/", "[动画表情]", $text);
$text = preg_replace("/<img\s+class=\"browse\"[^>]*?>/", "[图片]", $text);
if (!$preserveHtml) {
$text = strip_tags($text);
@ -558,15 +558,15 @@ class WebSocketDialogMsg extends AbstractModel
// 图片 [:IMAGE:className:width:height:src:alt:]
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|gif);base64,(.*?)\"(.*?)>(<\/img>)*/s", $text, $matchs);
foreach ($matchs[2] as $key => $base64) {
$tmpPath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5s($base64) . "." . $matchs[1][$key];
if (file_put_contents(public_path($tmpPath), base64_decode($base64))) {
$imagesize = getimagesize(public_path($tmpPath));
if (Base::imgThumb(public_path($tmpPath), public_path($tmpPath) . "_thumb.jpg", 320, 0)) {
$tmpPath .= "_thumb.jpg";
$imagePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($imagePath));
$imagePath .= md5s($base64) . "." . $matchs[1][$key];
if (file_put_contents(public_path($imagePath), base64_decode($base64))) {
$imageSize = getimagesize(public_path($imagePath));
if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) {
$imagePath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imagesize[0]}:{$imagesize[1]}:{$tmpPath}::]", $text);
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
}
}
// 表情图片
@ -574,38 +574,73 @@ class WebSocketDialogMsg extends AbstractModel
foreach ($matchs[1] as $key => $str) {
preg_match("/data-asset=\"(.*?)\"/", $str, $matchAsset);
preg_match("/data-name=\"(.*?)\"/", $str, $matchName);
if (file_exists(public_path($matchAsset[1]))) {
$imagesize = getimagesize(public_path($matchAsset[1]));
$text = str_replace($matchs[0][$key], "[:IMAGE:emoticon:{$imagesize[0]}:{$imagesize[1]}:{$matchAsset[1]}:{$matchName[1]}:]", $text);
$imageSize = null;
$imagePath = "";
$imageName = "";
if ($matchAsset[1] === "emosearch") {
preg_match("/src=\"(.*?)\"/", $str, $matchSrc);
if ($matchSrc) {
$srcMd5 = md5($matchSrc[1]);
$imagePath = "uploads/emosearch/" . substr($srcMd5, 0, 2) . "/" . substr($srcMd5, 32 - 2) . "/";
Base::makeDir(public_path($imagePath));
$imagePath .= md5s($matchSrc[1]);
if (file_exists(public_path($imagePath))) {
$imageSize = getimagesize(public_path($imagePath));
} else {
$image = file_get_contents($matchSrc[1]);
if ($image && file_put_contents(public_path($imagePath), $image)) {
$imageSize = getimagesize(public_path($imagePath));
// 添加后缀
if ($imageSize && !str_contains($imagePath, '.')) {
preg_match("/^image\/(png|jpg|jpeg|gif)$/", $imageSize['mime'], $matchMine);
if ($matchMine) {
$imageNewPath = $imagePath . "." . $matchMine[1];
if (rename(public_path($imagePath), public_path($imageNewPath))) {
$imagePath = $imageNewPath;
}
}
}
}
}
}
} elseif (file_exists(public_path($matchAsset[1]))) {
$imagePath = $matchAsset[1];
$imageName = $matchName[1];
$imageSize = getimagesize(public_path($matchAsset[1]));
}
if ($imageSize) {
$text = str_replace($matchs[0][$key], "[:IMAGE:emoticon:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}:{$imageName}:]", $text);
} else {
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text);
}
}
// 其他网络图片
preg_match_all("/<img[^>]*?src=([\"'])(.*?\.(png|jpg|jpeg|gif))\\1[^>]*?>/is", $text, $matchs);
foreach ($matchs[2] as $key => $str) {
if (str_starts_with($str, "{{RemoteURL}}")) {
$tmpPath = Base::leftDelete($str, "{{RemoteURL}}");
$tmpPath = Base::rightDelete($tmpPath, "_thumb.jpg");
$imagePath = Base::leftDelete($str, "{{RemoteURL}}");
$imagePath = Base::rightDelete($imagePath, "_thumb.jpg");
} else {
$tmpPath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5s($str) . "." . $matchs[3][$key];
$imagePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($imagePath));
$imagePath .= md5s($str) . "." . $matchs[3][$key];
}
if (file_exists(public_path($tmpPath))) {
$imagesize = getimagesize(public_path($tmpPath));
if (Base::imgThumb(public_path($tmpPath), public_path($tmpPath) . "_thumb.jpg", 320, 0)) {
$tmpPath .= "_thumb.jpg";
if (file_exists(public_path($imagePath))) {
$imageSize = getimagesize(public_path($imagePath));
if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) {
$imagePath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imagesize[0]}:{$imagesize[1]}:{$tmpPath}::]", $text);
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
} else {
$image = file_get_contents($str);
if (empty($image)) {
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text);
} else if (file_put_contents(public_path($tmpPath), $image)) {
$imagesize = getimagesize(public_path($tmpPath));
if (Base::imgThumb(public_path($tmpPath), public_path($tmpPath) . "_thumb.jpg", 320, 0)) {
$tmpPath .= "_thumb.jpg";
} else if (file_put_contents(public_path($imagePath), $image)) {
$imageSize = getimagesize(public_path($imagePath));
if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) {
$imagePath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imagesize[0]}:{$imagesize[1]}:{$tmpPath}::]", $text);
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
}
}
}

View File

@ -51,6 +51,7 @@
"vue-clipboard2": "^0.3.3",
"vue-kityminder-ggg": "^1.3.10",
"vue-loader": "^15.9.8",
"vue-jsonp": "^2.0.0",
"vue-resize-observer": "^2.0.16",
"vue-router": "^3.5.3",
"vue-template-compiler": "^2.6.14",

View File

@ -373,7 +373,7 @@
getMsgTextPreview(text) {
if (!text) return '';
text = text.replace(/<img\s+class="emoticon"[^>]*?alt="(\S+)"[^>]*?>/g, "[$1]")
text = text.replace(/<img\s+class="emoticon"[^>]*?>/g, `[${$A.L('表情')}]`)
text = text.replace(/<img\s+class="emoticon"[^>]*?>/g, `[${$A.L('动画表情')}]`)
text = text.replace(/<img\s+class="browse"[^>]*?>/g, `[${$A.L('图片')}]`)
text = text.replace(/&nbsp;/g," ")
return text.replace(/<[^>]+>/g,"")

View File

@ -1,15 +1,25 @@
<template>
<div class="chat-emoji-wrapper">
<ul class="chat-emoji-box scrollbar-overlay" :class="[type, 'no-dark-content']">
<div class="chat-emoji-box">
<div v-if="type === 'emosearch'" class="chat-emoji-emosearch">
<Input clearable v-model="emosearchKey" :placeholder="$L('搜索表情')">
<Icon :type="emosearchLoad ? 'ios-loading' : 'ios-search'" :class="{'icon-loading': emosearchLoad}" slot="prefix" />
</Input>
</div>
<ul class="scrollbar-overlay" :class="[type, 'no-dark-content']">
<li v-for="item in list" @click="onSelect(item)">
<img v-if="item.type === 'emoticon'" :src="item.src" :title="item.name" :alt="item.name"/>
<span v-else v-html="item.html" :title="item.name"></span>
</li>
</ul>
</div>
<ul class="chat-emoji-menu">
<li :class="{active: type === 'emoji'}" @click="type='emoji'">
<span class="no-dark-content">&#128512;</span>
</li>
<li :class="{active: type === 'emosearch'}" @click="type='emosearch'">
<i class="taskfont">&#xe6f8;</i>
</li>
<li v-for="item in emoticonList" :class="{active: type === 'emoticon' && emoticonPath == item.path}" @click="onEmoticon(item.path)">
<img :title="item.name" :alt="item.name" :src="item.src"/>
</li>
@ -18,20 +28,40 @@
</template>
<script>
import { jsonp } from 'vue-jsonp'
export default {
name: 'ChatEmoji',
props: {
searchKey: {
type: String,
default: ''
}
},
data() {
return {
type: 'emoji',
emoticonPath: '',
emosearchKey: '',
emosearchCache: null,
emosearchLoad: false,
emosearchTimer: null,
emosearchList: [],
};
},
mounted() {
if (this.searchKey) {
this.emosearchKey = this.searchKey;
}
},
watch: {
type() {
this.onEmosearch()
},
emosearchKey() {
this.onEmosearch()
}
},
computed: {
list() {
@ -48,6 +78,8 @@ export default {
html: item.code_decimal,
}
})
} else if (this.type === 'emosearch') {
return this.emosearchList;
} else if (this.type === 'emoticon') {
const data = this.emoticonList.find(({path}) => path === this.emoticonPath)
if (data) {
@ -56,6 +88,7 @@ export default {
}
return [];
},
emoticonList() {
if ($A.isArray(window.emoticonData)) {
let baseUrl = $A.apiUrl("../images/emoticon")
@ -76,6 +109,46 @@ export default {
}
},
methods: {
onEmosearch() {
if (this.type !== 'emosearch' || this.emosearchCache === this.emosearchKey) {
return
}
this.emosearchCache = this.emosearchKey;
//
this.emosearchLoad = true;
this.emosearchTimer && clearTimeout(this.emosearchTimer)
this.emosearchTimer = setTimeout(_ => {
jsonp('https://pic.sogou.com/napi/wap/pic', {
query: this.emosearchKey + ' 表情'
}).then(data => {
this.emosearchList = []
if (data.status === 0) {
const items = data.data.items
if (items.length > 0) {
this.emosearchList = items.map(item => {
return {
type: 'emoticon',
asset: 'emosearch',
name: item.title,
src: item.thumbUrl,
height: item.thumbHeight,
width: item.thumbWidth,
}
})
}
}
if (this.emosearchList.length === 0) {
$A.noticeWarning("没有搜索到任何表情")
}
}).catch(_ => {
this.emosearchList = []
$A.noticeWarning("搜索结果为空")
}).finally(_ => {
this.emosearchLoad = false;
})
}, 300)
},
onEmoticon(path) {
this.type = 'emoticon';
this.emoticonPath = path;

View File

@ -47,7 +47,7 @@
<ETooltip slot="reference" ref="emojiTip" :disabled="windowSmall || showEmoji" placement="top" :content="$L('表情')">
<i class="taskfont">&#xe7ad;</i>
</ETooltip>
<ChatEmoji @on-select="onSelectEmoji"/>
<ChatEmoji v-if="showEmoji" @on-select="onSelectEmoji" :searchKey="emojiQuickKey"/>
</EPopover>
<ETooltip v-else ref="emojiTip" :disabled="windowSmall || showEmoji" placement="top" :content="$L('表情')">
<i class="taskfont" @click="showEmoji=!showEmoji">&#xe7ad;</i>
@ -125,7 +125,7 @@
</div>
<!-- 移动端表情底部 -->
<ChatEmoji v-if="emojiBottom && showEmoji" @on-select="onSelectEmoji"/>
<ChatEmoji v-if="emojiBottom && showEmoji" @on-select="onSelectEmoji" :searchKey="emojiQuickKey"/>
<!-- 录音浮窗 -->
<transition name="fade">
@ -230,6 +230,7 @@ export default {
showEmoji: false,
emojiQuickTimer: null,
emojiQuickShow: false,
emojiQuickKey: '',
emojiQuickItems: [],
observer: null,
@ -710,20 +711,19 @@ export default {
&& text.length >= 1
&& text.length <= 4
&& $A.isArray(window.emoticonData)) {
// 线
this.searchEmoji(text);
//
this.emojiQuickKey = text;
this.emojiQuickItems = [];
let baseUrl = $A.apiUrl("../images/emoticon")
window.emoticonData.some(data => {
let item = data.list.find(({name}) => $A.strExists(name, text))
let item = data.list.find(d => $A.strExists(d.name + (d.key ? ` ${d.key}` : ''), text))
if (item) {
this.emojiQuickItems.push(Object.assign(item, {
type: `emoticon`,
asset: `images/emoticon/${data.path}/${item.path}`,
src: `${baseUrl}/${data.path}/${item.path}`
}))
if (this.emojiQuickItems.length >= 2) {
if (this.emojiQuickItems.length >= 3) {
return true
}
}
@ -740,65 +740,6 @@ export default {
}, 100)
},
searchEmoji(text) {
this.emojiSearchKey = text;
this.emojiSearchTimer && clearTimeout(this.emojiSearchTimer);
this.emojiSearchTimer = setTimeout(_ => {
if (this.emojiSearchKey !== text) {
return;
}
this.$store.dispatch("call", {
url: '../emo/search',
data: {
key: text,
},
checkNetwork: false,
}).then(({data}) => {
if (this.emojiSearchKey !== text) {
return;
}
const array = this.getRandomArrayElements(data.map(item => {
return {
type: "online",
name: this.$L("动画表情"),
src: this.asciiConvertNative(item)
}
}), 3 - this.emojiQuickItems.length)
if (array.length > 0) {
this.emojiQuickItems.push(...array)
this.$nextTick(_ => {
this.emojiQuickShow = true
this.$refs.emojiQuickRef.updatePopper()
})
}
});
}, 800)
},
asciiConvertNative(val) {
let asciicode = val.split("\\u");
let nativeValue = asciicode[0];
for (let i = 1; i < asciicode.length; i++) {
let code = asciicode[i];
nativeValue += String.fromCharCode(parseInt("0x" + code.substring(0, 4)));
if (code.length > 4) {
nativeValue += code.substring(4, code.length);
}
}
return nativeValue
},
getRandomArrayElements(arr, count) {
let shuffled = arr.slice(0), i = arr.length, min = i - count, temp, index;
while (i-- > min) {
index = Math.floor((i + 1) * Math.random());
temp = shuffled[index];
shuffled[index] = shuffled[i];
shuffled[i] = temp;
}
return shuffled.slice(min);
},
setText(value) {
if (this.quill) {
this.quill.setText(value)

View File

@ -2159,13 +2159,12 @@ export default {
} else {
state.dialogIns.push(data);
}
// 会话消息总数量大于200时只保留最近打开的5个会话
const msg_max = 200
// 会话消息总数量大于100时只保留最近打开的5个会话
const msg_max = 100
const retain_num = 5
if (state.dialogMsgs.length > msg_max) {
state.dialogHistory = state.dialogHistory.filter(id => id != data.dialog_id)
state.dialogHistory.push(data.dialog_id)
if (state.dialogHistory.length > retain_num) {
if (state.dialogMsgs.length > msg_max && state.dialogHistory.length > retain_num) {
const historys = state.dialogHistory.slice().reverse()
const newIds = []
const delIds = []
@ -2181,7 +2180,6 @@ export default {
}
state.dialogHistory = newIds
}
}
},
/**

View File

@ -291,6 +291,7 @@
.chat-emoji-wrapper {
.chat-emoji-box {
> ul {
width: auto;
padding: 8px 2px;
&::after {
@ -298,12 +299,17 @@
flex: auto;
}
> li {
> img {
transition: none;
}
&:hover {
> img {
transform: none;
}
}
}
}
}
.chat-emoji-menu {
width: 100%;
padding: 3px 0;
@ -325,7 +331,8 @@
z-index: 1;
}
> span,
> img {
> img,
> i {
position: static;
z-index: 2;
}
@ -344,9 +351,19 @@
display: flex;
flex-direction: column;
.chat-emoji-emosearch {
flex-shrink: 0;
padding: 8px 8px 0;
}
.chat-emoji-box {
width: 360px;
display: flex;
flex-direction: column;
height: 280px;
> ul {
flex: 1;
width: 360px;
height: 0;
display: grid;
justify-content: space-between;
grid-template-columns: repeat(auto-fill, 40px);
@ -366,18 +383,21 @@
display: inline-block;
cursor: pointer;
user-select: none;
transition: transform 0.3s;
> img {
max-width: 100%;
max-height: 100%;
pointer-events: none;
-webkit-touch-callout: none;
transition: transform 0.3s;
}
&:hover {
> img {
transform: scale(1.4);
}
}
}
&.emosearch,
&.emoticon {
grid-template-columns: repeat(auto-fill, 72px);
> li {
@ -387,6 +407,7 @@
}
}
}
}
.chat-emoji-menu {
width: 376px;
height: 40px;
@ -416,6 +437,12 @@
font-size: 22px;
}
> i {
width: 48px;
font-size: 18px;
text-align: center;
}
> img {
width: 24px;
height: 24px;
@ -687,6 +714,7 @@
background-color: #ffffff;
.chat-emoji-box {
height: 246px;
> ul {
grid-template-columns: repeat(auto-fill, 50px);
> li {
width: 50px;
@ -694,6 +722,7 @@
line-height: 50px;
font-size: 28px;
}
&.emosearch,
&.emoticon {
grid-template-columns: repeat(auto-fill, 80px);
> li {
@ -703,6 +732,7 @@
}
}
}
}
.chat-emoji-menu {
border-radius: 0;
background-color: #f8f8f8;

View File

@ -9,16 +9,16 @@
{name: '撸串去', path: '02.gif'},
{name: '来喝酒', path: '03.gif'},
{name: '走啦', path: '04.gif'},
{name: '蟹蟹', path: '05.gif'},
{name: '蟹蟹', key: '谢谢', path: '05.gif'},
{name: '加个好友呗', path: '06.gif'},
{name: '醉了', path: '07.gif'},
{name: '哈喽', path: '08.gif'},
{name: '哈喽', key: '你好', path: '08.gif'},
{name: '呼叫呼叫', path: '09.gif'},
{name: '收到over', path: '10.gif'},
{name: '呕', path: '11.gif'},
{name: '来嘛', path: '12.gif'},
{name: '借酒消愁', path: '13.gif'},
{name: '你还太年轻', path: '14.gif'},
{name: '借酒消愁', key: '烦', path: '13.gif'},
{name: '你还太年轻', key: '小样', path: '14.gif'},
{name: '来 再战', path: '15.gif'},
{name: '醒醒', path: '16.gif'},
{name: '请你吃饭', path: '17.gif'},
@ -27,7 +27,7 @@
{name: '好的', path: '20.gif'},
{name: '你又迟到了', path: '21.gif'},
{name: '自罚一杯', path: '22.gif'},
{name: '哈哈', path: '23.gif'},
{name: '哈哈', path: '23.gif'},
{name: '少喝点酒', path: '24.gif'},
]
},
@ -36,20 +36,20 @@
path: '02',
icon: 'icon.png',
list: [
{name: '爱老虎油', path: '01.gif'},
{name: '爱老虎油', key: '爱你', path: '01.gif'},
{name: '比心', path: '02.gif'},
{name: '呐,送你花花', path: '03.gif'},
{name: '赞', path: '04.gif'},
{name: '赞', key: '真棒 牛逼', path: '04.gif'},
{name: '买买买', path: '05.gif'},
{name: '饥饿', path: '06.gif'},
{name: '问号', path: '07.gif'},
{name: '围观', path: '08.gif'},
{name: '哈哈哈哈哈', path: '09.gif'},
{name: '哈哈', path: '09.gif'},
{name: '加一', path: '10.gif'},
{name: '嗯嗯', path: '11.gif'},
{name: '暗中观察', path: '12.gif'},
{name: '生日快乐', path: '13.gif'},
{name: '蟹蟹', path: '14.gif'},
{name: '蟹蟹', key: '谢谢', path: '14.gif'},
{name: '加油', path: '15.gif'},
{name: '突然吃瓜', path: '16.gif'},
{name: '抱抱', path: '17.gif'},
@ -79,10 +79,10 @@
{name: '开始摆烂', path: '11.gif'},
{name: '震惊', path: '12.gif'},
{name: '收到', path: '13.gif'},
{name: '赞', path: '14.gif'},
{name: '赞', key: '真棒 牛逼', path: '14.gif'},
{name: '心花怒放', path: '15.gif'},
{name: '无聊', path: '16.gif'},
{name: 'NONONO', path: '17.gif'},
{name: 'NONONO', key: '不要 不好', path: '17.gif'},
{name: '扎心了', path: '18.gif'},
{name: '哼', path: '19.gif'},
{name: '晚安', path: '20.gif'},
@ -113,7 +113,7 @@
{name: '打卡', path: '18.gif'},
{name: '摇摆', path: '19.gif'},
{name: '脑瓜嗡嗡', path: '20.gif'},
{name: 'yes sir', path: '21.gif'},
{name: 'yes sir', key: '是的', path: '21.gif'},
{name: '不开心', path: '22.gif'},
{name: '对', path: '23.gif'},
{name: '比猪', path: '24.gif'},