no message

This commit is contained in:
kuaifan 2025-05-25 08:46:25 +08:00
parent 0e1d5e802c
commit f34766ade0
7 changed files with 87 additions and 34 deletions

View File

@ -155,7 +155,7 @@ class UsersController extends AbstractController
// //
if (!Project::withTrashed()->whereUserid($user->userid)->wherePersonal(1)->exists()) { if (!Project::withTrashed()->whereUserid($user->userid)->wherePersonal(1)->exists()) {
Project::createProject([ Project::createProject([
'name' => Doo::translate('个人项目'), 'name' => "📝 " . Doo::translate('个人项目'),
'desc' => Doo::translate('注册时系统自动创建项目,你可以自由删除。'), 'desc' => Doo::translate('注册时系统自动创建项目,你可以自由删除。'),
'personal' => 1, 'personal' => 1,
], $user->userid); ], $user->userid);

View File

@ -22,7 +22,7 @@ class AddProjectsPersonal extends Migration
}); });
if ($isAdd) { if ($isAdd) {
// 更新数据 // 更新数据
\App\Models\Project::whereName('个人项目')->chunkById(100, function ($lists) { \App\Models\Project::where('name','like', '%个人项目%')->chunkById(100, function ($lists) {
/** @var \App\Models\Project $item */ /** @var \App\Models\Project $item */
foreach ($lists as $item) { foreach ($lists as $item) {
if ($item->desc == '注册时系统自动创建项目,你可以自由删除。') { if ($item->desc == '注册时系统自动创建项目,你可以自由删除。') {

View File

@ -1,16 +1,16 @@
/** /**
* Vue指令: v-emoji-class * Vue指令: v-emoji-class
* *
* 用法: * 用法:
* 1. 基本用法: v-emoji-class="className" - 将emoji包装在<span class="className">emoji</span> * 1. 基本用法: v-emoji-class="className" - 将emoji包装在<span class="className">emoji</span>
* 2. 高级用法: v-emoji-class="{className: 'className', tagName: 'div'}" - 自定义标签名 * 2. 高级用法: v-emoji-class="{className: 'className', tagName: 'div'}" - 自定义标签名
* *
* 示例: * 示例:
* <div v-emoji-class="emoji-icon">我爱中国🇨🇳</div> * <div v-emoji-class="emoji-icon">我爱中国🇨🇳</div>
* <p v-emoji-class="{className: 'large-emoji', tagName: 'em'}">Hello 😊</p> * <p v-emoji-class="{className: 'large-emoji', tagName: 'em'}">Hello 😊</p>
*/ */
import { debounce } from "lodash"; import {debounce} from "lodash";
// 正则表达式用于匹配emoji - 使用预编译正则提高性能 // 正则表达式用于匹配emoji - 使用预编译正则提高性能
const emojiRegex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g; const emojiRegex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g;
@ -37,37 +37,37 @@ function containsEmoji(text) {
*/ */
function processTextNode(textNode, className, tagName) { function processTextNode(textNode, className, tagName) {
const text = textNode.textContent; const text = textNode.textContent;
// 快速检查是否包含emoji // 快速检查是否包含emoji
if (!containsEmoji(text)) return false; if (!containsEmoji(text)) return false;
// 重置正则索引并准备替换 // 重置正则索引并准备替换
emojiRegex.lastIndex = 0; emojiRegex.lastIndex = 0;
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
let lastIndex = 0; let lastIndex = 0;
let match; let match;
// 逐个匹配emoji并替换 // 逐个匹配emoji并替换
while ((match = emojiRegex.exec(text)) !== null) { while ((match = emojiRegex.exec(text)) !== null) {
// 添加emoji前的文本 // 添加emoji前的文本
if (match.index > lastIndex) { if (match.index > lastIndex) {
fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index))); fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
} }
// 创建包装emoji的元素 // 创建包装emoji的元素
const emojiWrapper = document.createElement(tagName); const emojiWrapper = document.createElement(tagName);
emojiWrapper.className = className; emojiWrapper.className = className;
emojiWrapper.textContent = match[0]; emojiWrapper.textContent = match[0];
fragment.appendChild(emojiWrapper); fragment.appendChild(emojiWrapper);
lastIndex = emojiRegex.lastIndex; lastIndex = emojiRegex.lastIndex;
} }
// 添加剩余文本 // 添加剩余文本
if (lastIndex < text.length) { if (lastIndex < text.length) {
fragment.appendChild(document.createTextNode(text.substring(lastIndex))); fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
} }
// 替换原始节点 // 替换原始节点
textNode.parentNode.replaceChild(fragment, textNode); textNode.parentNode.replaceChild(fragment, textNode);
return true; return true;
@ -84,12 +84,12 @@ function processNodeEmojis(node, className, tagName) {
// 如果是文本节点,直接处理 // 如果是文本节点,直接处理
if (node.nodeType === Node.TEXT_NODE) { if (node.nodeType === Node.TEXT_NODE) {
return processTextNode(node, className, tagName); return processTextNode(node, className, tagName);
} }
// 如果是元素节点,递归处理其子节点 // 如果是元素节点,递归处理其子节点
if (node.nodeType === Node.ELEMENT_NODE) { if (node.nodeType === Node.ELEMENT_NODE) {
let modified = false; let modified = false;
// 使用childNodes的副本避免在迭代过程中修改集合 // 使用childNodes的副本避免在迭代过程中修改集合
const childNodes = Array.from(node.childNodes); const childNodes = Array.from(node.childNodes);
for (const childNode of childNodes) { for (const childNode of childNodes) {
@ -97,10 +97,10 @@ function processNodeEmojis(node, className, tagName) {
modified = true; modified = true;
} }
} }
return modified; return modified;
} }
return false; return false;
} }
@ -135,32 +135,32 @@ function getContentHash(el) {
*/ */
function processEmoji(el, binding) { function processEmoji(el, binding) {
if (!el) return; if (!el) return;
// 解析绑定值 // 解析绑定值
const { className, tagName } = parseBinding(binding); const {className, tagName} = parseBinding(binding);
if (!className) return; if (!className) return;
// 获取或初始化元素状态 // 获取或初始化元素状态
let state = elementStates.get(el) || {}; let state = elementStates.get(el) || {};
elementStates.set(el, state); elementStates.set(el, state);
// 计算内容哈希值,用于快速比较 // 计算内容哈希值,用于快速比较
const contentHash = getContentHash(el); const contentHash = getContentHash(el);
// 如果内容哈希值与上次相同且已处理过,则跳过 // 如果内容哈希值与上次相同且已处理过,则跳过
if (state.contentHash === contentHash && state.processed) { if (state.contentHash === contentHash && state.processed) {
return; return;
} }
// 创建一个克隆节点进行处理 // 创建一个克隆节点进行处理
const clone = el.cloneNode(true); const clone = el.cloneNode(true);
// 递归处理所有文本节点 // 递归处理所有文本节点
if (processNodeEmojis(clone, className, tagName)) { if (processNodeEmojis(clone, className, tagName)) {
// 使用requestAnimationFrame优化DOM更新 // 使用requestAnimationFrame优化DOM更新
requestAnimationFrame(() => { requestAnimationFrame(() => {
el.innerHTML = clone.innerHTML; el.innerHTML = clone.innerHTML;
// 更新元素状态 // 更新元素状态
state.contentHash = contentHash; state.contentHash = contentHash;
state.processed = true; state.processed = true;
@ -169,6 +169,53 @@ function processEmoji(el, binding) {
} }
} }
/**
* 将文本中的emoji转换成HTML标签
* @param {string} text - 输入文本
* @param {string} className - 添加给emoji的类名
* @param {string} tagName - 包裹emoji的标签名默认为'span'
* @returns {string} - 转换后的HTML字符串
*
* 示例:
* transformEmojiToHtml("我❤️你", "heart", "span")
* // 返回: "我<span class=\"heart\">❤️</span>你"
*/
export function transformEmojiToHtml(text, className, tagName = 'span') {
// 参数验证
if (typeof text !== 'string') return '';
if (!className || typeof className !== 'string') return text;
if (!tagName || typeof tagName !== 'string') tagName = 'span';
// 快速检查是否包含emoji
if (!containsEmoji(text)) return text;
// 重置正则索引并准备替换
emojiRegex.lastIndex = 0;
let result = '';
let lastIndex = 0;
let match;
// 逐个匹配emoji并替换
while ((match = emojiRegex.exec(text)) !== null) {
// 添加emoji前的文本
if (match.index > lastIndex) {
result += text.substring(lastIndex, match.index);
}
// 添加包装后的emoji
result += `<${tagName} class="${className}">${match[0]}</${tagName}>`;
lastIndex = emojiRegex.lastIndex;
}
// 添加剩余文本
if (lastIndex < text.length) {
result += text.substring(lastIndex);
}
return result;
}
// 创建防抖处理函数 - 使用更短的防抖时间提高响应速度 // 创建防抖处理函数 - 使用更短的防抖时间提高响应速度
const debouncedProcessEmoji = debounce(processEmoji, 20); const debouncedProcessEmoji = debounce(processEmoji, 20);
@ -177,24 +224,24 @@ export default {
// 直接处理,不使用防抖 // 直接处理,不使用防抖
processEmoji(el, binding); processEmoji(el, binding);
}, },
update(el, binding) { update(el, binding) {
// 获取元素状态 // 获取元素状态
const state = elementStates.get(el) || {}; const state = elementStates.get(el) || {};
// 只有当绑定值变化时才重新处理 // 只有当绑定值变化时才重新处理
if (binding.oldValue !== binding.value) { if (binding.oldValue !== binding.value) {
debouncedProcessEmoji(el, binding); debouncedProcessEmoji(el, binding);
return; return;
} }
// 内容变化时也需要重新处理 // 内容变化时也需要重新处理
const contentHash = getContentHash(el); const contentHash = getContentHash(el);
if (state.contentHash !== contentHash) { if (state.contentHash !== contentHash) {
debouncedProcessEmoji(el, binding); debouncedProcessEmoji(el, binding);
} }
}, },
unbind(el) { unbind(el) {
// 清理元素状态 // 清理元素状态
elementStates.delete(el); elementStates.delete(el);

View File

@ -138,7 +138,7 @@
@click="toggleRoute('project', {projectId: item.id})"> @click="toggleRoute('project', {projectId: item.id})">
<div class="project-h1"> <div class="project-h1">
<em @click.stop="toggleOpenMenu(item.id)"></em> <em @click.stop="toggleOpenMenu(item.id)"></em>
<div class="title">{{item.name}}</div> <div class="title" v-html="transformEmojiToHtml(item.name, 'no-dark-content')"></div>
<div v-if="item.top_at" class="icon-top"></div> <div v-if="item.top_at" class="icon-top"></div>
<div v-if="item.task_my_num - item.task_my_complete > 0" class="num">{{item.task_my_num - item.task_my_complete}}</div> <div v-if="item.task_my_num - item.task_my_complete > 0" class="num">{{item.task_my_num - item.task_my_complete}}</div>
</div> </div>
@ -388,6 +388,7 @@ import ApproveDetails from "./manage/approve/details.vue";
import notificationKoro from "notification-koro1"; import notificationKoro from "notification-koro1";
import emitter from "../store/events"; import emitter from "../store/events";
import SearchBox from "../components/SearchBox.vue"; import SearchBox from "../components/SearchBox.vue";
import {transformEmojiToHtml} from "../directives/emoji-class";
export default { export default {
components: { components: {
@ -753,6 +754,7 @@ export default {
}, },
methods: { methods: {
transformEmojiToHtml,
chackPass() { chackPass() {
if (this.userInfo.changepass === 1) { if (this.userInfo.changepass === 1) {
this.goForward({name: 'manage-setting-password'}); this.goForward({name: 'manage-setting-password'});

View File

@ -32,7 +32,7 @@
<div class="project-item"> <div class="project-item">
<div class="item-left"> <div class="item-left">
<div class="project-h1"> <div class="project-h1">
<div class="project-name">{{item.name}}</div> <div class="project-name" v-html="transformEmojiToHtml(item.name, 'no-dark-content')"></div>
<div v-if="item.top_at" class="icon-top"></div> <div v-if="item.top_at" class="icon-top"></div>
<div v-if="item.task_my_num - item.task_my_complete > 0" class="num">{{item.task_my_num - item.task_my_complete}}</div> <div v-if="item.task_my_num - item.task_my_complete > 0" class="num">{{item.task_my_num - item.task_my_complete}}</div>
</div> </div>
@ -86,6 +86,7 @@
import {mapState} from "vuex"; import {mapState} from "vuex";
import longpress from "../../../directives/longpress"; import longpress from "../../../directives/longpress";
import TransferDom from "../../../directives/transfer-dom"; import TransferDom from "../../../directives/transfer-dom";
import {transformEmojiToHtml} from "../../../directives/emoji-class";
export default { export default {
name: "ProjectList", name: "ProjectList",
@ -139,6 +140,7 @@ export default {
}, },
methods: { methods: {
transformEmojiToHtml,
searchProject() { searchProject() {
this.projectKeyLoading++; this.projectKeyLoading++;
this.$store.dispatch("getProjects", { this.$store.dispatch("getProjects", {

View File

@ -6,7 +6,7 @@
<div class="project-back" @click="onBack"> <div class="project-back" @click="onBack">
<i class="taskfont">&#xe676;</i> <i class="taskfont">&#xe676;</i>
</div> </div>
<h1 @click="showName" class="user-select-auto">{{projectData.name}}</h1> <h1 @click="showName" class="user-select-auto" v-html="transformEmojiToHtml(projectData.name, 'no-dark-content')"></h1>
<div v-if="loading" class="project-load"><Loading/></div> <div v-if="loading" class="project-load"><Loading/></div>
</div> </div>
<ul class="project-icons"> <ul class="project-icons">
@ -575,6 +575,7 @@ import UserSelect from "../../../components/UserSelect.vue";
import UserAvatarTip from "../../../components/UserAvatar/tip.vue"; import UserAvatarTip from "../../../components/UserAvatar/tip.vue";
import VMPreviewNostyle from "../../../components/VMEditor/nostyle.vue"; import VMPreviewNostyle from "../../../components/VMEditor/nostyle.vue";
import emitter from "../../../store/events"; import emitter from "../../../store/events";
import {transformEmojiToHtml} from "../../../directives/emoji-class";
export default { export default {
name: "ProjectPanel", name: "ProjectPanel",
@ -1074,6 +1075,7 @@ export default {
}, },
methods: { methods: {
transformEmojiToHtml,
showName() { showName() {
if (this.windowLandscape) { if (this.windowLandscape) {
return; return;

View File

@ -6,7 +6,7 @@
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
margin: 32px 20px 16px; margin: 32px 32px 16px;
border-bottom: 1px solid #F4F4F5; border-bottom: 1px solid #F4F4F5;
.calendar-titbox { .calendar-titbox {
@ -71,7 +71,7 @@
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0 48px 6px; padding: 0 32px 6px;
overflow: hidden; overflow: hidden;
} }