fix:完善评论组件
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -33,6 +33,7 @@ declare module 'vue' {
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>智视界</title>
|
||||
<title>智服链</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"axios": "^1.13.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"lodash-es": "^4.17.22",
|
||||
"sass": "^1.97.1",
|
||||
"typescript": "~5.9.3",
|
||||
"unicode-emoji-json": "^0.8.0",
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -45,6 +45,9 @@ importers:
|
||||
dayjs:
|
||||
specifier: ^1.11.19
|
||||
version: 1.11.19
|
||||
lodash-es:
|
||||
specifier: ^4.17.22
|
||||
version: 4.17.22
|
||||
sass:
|
||||
specifier: ^1.97.1
|
||||
version: 1.97.1
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-avatar
|
||||
:size="size"
|
||||
:src="src"
|
||||
:style="{ backgroundColor: !src ? bgColor : '' }"
|
||||
:style="{ backgroundColor: !src ? bgColor : '','--avatar-text-size':fontSize }"
|
||||
class="mj-name-avatar"
|
||||
>
|
||||
<span v-if="!src" class="avatar-text">{{ displayText }}</span>
|
||||
@@ -22,6 +22,10 @@ const displayText = computed(() => {
|
||||
return props.name ? props.name.charAt(0) : '';
|
||||
});
|
||||
|
||||
const fontSize = computed(() => {
|
||||
return `${Math.max(props.size * 0.4, 12)}px`;
|
||||
});
|
||||
|
||||
const bgColor = computed(() => {
|
||||
if (!props.name) return '#409EFF';
|
||||
let hash = 0;
|
||||
@@ -49,7 +53,7 @@ const bgColor = computed(() => {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
.avatar-text {
|
||||
color: var(--el-avatar-text-color);
|
||||
font-size: 16px;
|
||||
font-size: var(--avatar-text-size);
|
||||
letter-spacing: -0.5px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
255
src/modules/Comment/index.scss
Normal file
255
src/modules/Comment/index.scss
Normal file
@@ -0,0 +1,255 @@
|
||||
$color-blue: #409eff;
|
||||
$color-blue-bg: #f5f8ff;
|
||||
$color-text-main: #303133;
|
||||
$color-text-sub: #99a2aa;
|
||||
$color-border: #e4e7ed;
|
||||
$color-white: #fff;
|
||||
|
||||
.comment-app {
|
||||
font-family: -apple-system, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
height: calc(100vh - 170px);
|
||||
|
||||
// 评论的样式
|
||||
.main-publisher{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 200;
|
||||
}
|
||||
.input-wrapper {
|
||||
flex-shrink: 0;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
transition: border-color 0.2s;
|
||||
background-color: $color-white;
|
||||
|
||||
&:focus-within {
|
||||
border-color: $color-blue;
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
border: none;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.input-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
|
||||
.left-icons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
.el-button {
|
||||
color: $color-text-sub;
|
||||
font-size: 18px;
|
||||
padding: 4px;
|
||||
}
|
||||
.el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
margin-left: 8px;
|
||||
color: $color-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 评论列表样式
|
||||
.comment-list {
|
||||
overflow-y: auto;
|
||||
flex: 0 0 calc(100% - 120px);
|
||||
|
||||
.comment-group {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.parent-node {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.user-avatar {
|
||||
background-color: $color-blue;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.node-main {
|
||||
flex: 1;
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.nickname {
|
||||
font-weight: bold;
|
||||
color: $color-text-main;
|
||||
}
|
||||
.createTime {
|
||||
font-size: 13px;
|
||||
color: $color-text-sub;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
margin: 8px 0;
|
||||
font-size: 15px;
|
||||
color: $color-text-main;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
.el-button {
|
||||
font-size: 13px;
|
||||
color: $color-text-sub;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
&:hover {
|
||||
color: $color-blue;
|
||||
}
|
||||
}
|
||||
.delete-btn:hover {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 子评论卡片样式
|
||||
.sub-container {
|
||||
background-color: $color-blue-bg;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #d9e5ff;
|
||||
padding: 16px;
|
||||
margin-top: 10px;
|
||||
|
||||
.sub-node {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sub-main {
|
||||
flex: 1;
|
||||
.sub-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.sub-user-info {
|
||||
font-size: 14px;
|
||||
.nickname {
|
||||
font-weight: bold;
|
||||
}
|
||||
.reply-text {
|
||||
margin: 0 6px;
|
||||
color: $color-text-sub;
|
||||
}
|
||||
.target-name {
|
||||
color: $color-blue;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.createTime {
|
||||
font-size: 12px;
|
||||
color: $color-text-sub;
|
||||
}
|
||||
}
|
||||
.content-body {
|
||||
font-size: 14px;
|
||||
color: $color-text-main;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 展开收起样式
|
||||
.sub-list-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
padding-left: 46px; // 与头像对齐的偏移量
|
||||
|
||||
.expand-line {
|
||||
width: 20px;
|
||||
height: 1px;
|
||||
background-color: #dcdfe6;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
font-size: 13px;
|
||||
color: $color-text-sub;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: $color-blue;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-left: 4px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.mention-highlight) {
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
color: #409eff;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
margin: 0 2px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 评论组件骨架屏
|
||||
.skeleton-comment-parent-node {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
.skeleton-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.skeleton-node-main {
|
||||
flex: 1;
|
||||
|
||||
.skeleton-user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.skeleton-actions {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,6 @@
|
||||
<template>
|
||||
<div class="comment-app">
|
||||
<section class="main-publisher">
|
||||
<div class="input-wrapper">
|
||||
<mentionEditor v-model="mainInput" :users="userList" @select="onUserMentioned">
|
||||
<el-input
|
||||
v-model="mainInput"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:placeholder="t('comment.placeholder')"
|
||||
resize="none"
|
||||
/>
|
||||
</mentionEditor>
|
||||
<div class="input-tools">
|
||||
<div class="left-icons">
|
||||
<!-- 提到-后续迭代 -->
|
||||
<!-- <el-button link title="提到">@</el-button> -->
|
||||
<!-- 图片功能-后续迭代 -->
|
||||
<!-- <el-button link title="图片"><el-icon><Picture /></el-icon></el-button> -->
|
||||
<!-- 附件功能-后续迭代 -->
|
||||
<!-- <el-button link title="附件"><el-icon><Paperclip /></el-icon></el-button> -->
|
||||
|
||||
<!-- 表情功能 -->
|
||||
<emoji-picker @select="(e) => onSelectEmoji(e, 'main')" />
|
||||
</div>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button
|
||||
class="send-btn"
|
||||
type="primary"
|
||||
link
|
||||
@click="submitMainComment"
|
||||
>
|
||||
<el-icon :size="20" :title="t('comment.send')"
|
||||
><Promotion
|
||||
/></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 评论列表 -->
|
||||
<div class="comment-list">
|
||||
<!-- 骨架屏 -->
|
||||
<div v-if="loading">
|
||||
@@ -85,17 +47,27 @@
|
||||
<!-- 详细的内容展示 -->
|
||||
<template v-else>
|
||||
<div v-for="item in commentData" :key="item.id" class="comment-group">
|
||||
<!-- 这个地方需要添加 查看更多-收起 -->
|
||||
<div class="parent-node">
|
||||
<name-avatar :name="item.nickname" :src="item.avatar" :size="36" />
|
||||
<name-avatar
|
||||
:name="item.employee.name"
|
||||
:src="item.employee.avatar"
|
||||
:size="36"
|
||||
/>
|
||||
<div class="node-main">
|
||||
<!-- 当前用户信息展示 -->
|
||||
<div class="user-info">
|
||||
<span class="nickname">{{ item.nickname }}</span>
|
||||
<span class="time">{{ formatTime(item.time) }}</span>
|
||||
<span class="nickname">{{ item.employee.name }}</span>
|
||||
<span class="createTime">{{
|
||||
formatTime(item.createTime)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 回复内容模块 -->
|
||||
<div class="content" v-html="parseMention(item.content)"></div>
|
||||
<div
|
||||
class="content"
|
||||
v-html="parseMention(item.content, item.mentions)"
|
||||
></div>
|
||||
<!-- 回复内容-子集内容操作模块 -->
|
||||
<div class="actions">
|
||||
<el-button link @click="openReply(item, item)">
|
||||
@@ -105,83 +77,128 @@
|
||||
link
|
||||
class="delete-btn"
|
||||
@click="deleteMainComment(item)"
|
||||
v-if="item.canDelete"
|
||||
v-if="currentUser.id === item?.employee.id"
|
||||
>
|
||||
<el-icon><Delete /></el-icon> 删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 回复内容展示(二级-子集评论内容) -->
|
||||
<div v-if="item.children?.length" class="sub-container">
|
||||
<div
|
||||
v-for="reply in item.children"
|
||||
:key="reply.id"
|
||||
v-if="item.children?.length || item.localReplies?.length"
|
||||
class="sub-container"
|
||||
>
|
||||
<!-- 临时数据 -->
|
||||
<div
|
||||
v-for="replies in [
|
||||
...(item.localReplies || []),
|
||||
...(item.showAllReplies
|
||||
? item.children
|
||||
: item.children.slice(0, 1)),
|
||||
]"
|
||||
:key="replies.id"
|
||||
class="sub-node"
|
||||
>
|
||||
<name-avatar
|
||||
:name="reply.nickname"
|
||||
:src="reply.avatar"
|
||||
:name="replies.employee.name"
|
||||
:src="replies.employee.avatar"
|
||||
:size="36"
|
||||
/>
|
||||
<div class="sub-main">
|
||||
<div class="sub-header">
|
||||
<div class="sub-user-info">
|
||||
<span class="nickname">{{ reply.nickname }}</span>
|
||||
<span class="nickname">{{
|
||||
replies.employee.name
|
||||
}}</span>
|
||||
<!-- TODO:这个地方判断 不是c-b这种回复就不展示 -->
|
||||
<template v-if="replies.reply">
|
||||
<span class="reply-text">回复</span>
|
||||
<span class="target-name">@{{ reply.replyTo }}</span>
|
||||
<span class="target-name"
|
||||
>@{{ replies.reply.name }}</span
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
<span class="time">{{ formatTime(reply.time) }}</span>
|
||||
<span class="createTime">{{
|
||||
formatTime(replies.createTime)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="content-body" v-html="parseMention(reply.content)"></div>
|
||||
<div
|
||||
class="content-body"
|
||||
v-html="parseMention(replies.content, replies.mentions)"
|
||||
></div>
|
||||
<!-- 回复 删除功能 -->
|
||||
<div class="actions">
|
||||
<el-button link @click="openReply(item, item)">
|
||||
<el-button link @click="openReply(replies, item)">
|
||||
回复
|
||||
</el-button>
|
||||
<!-- 删除功能-只有自己评论的可以删除 -->
|
||||
<el-button
|
||||
link
|
||||
class="delete-btn"
|
||||
v-if="reply.canDelete"
|
||||
@click="deleteReply(reply, item)"
|
||||
v-if="currentUser.id === replies?.employee.id"
|
||||
@click="deleteReply(replies, item)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展开更多和收起 -->
|
||||
<div v-if="item.children.length > 1" class="sub-list-controls">
|
||||
<div class="expand-line"></div>
|
||||
<el-button link @click="handleExpand(item)">
|
||||
<template v-if="!item.showAllReplies">
|
||||
展开 {{ item.childrenCount }} 条回复
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</template>
|
||||
<template v-else>
|
||||
收起
|
||||
<el-icon><ArrowUp /></el-icon>
|
||||
</template>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 统一回复输入框 -->
|
||||
<div
|
||||
v-if="activeReply.groupId === item.id"
|
||||
class="inline-publisher"
|
||||
>
|
||||
<!-- 评论模块 -->
|
||||
<div class="main-publisher">
|
||||
<div class="input-wrapper">
|
||||
<mentionEditor v-model="replyInput" :users="userList" @select="onUserMentioned">
|
||||
<mentionEditor
|
||||
v-model="mainInput"
|
||||
:users="userList"
|
||||
@select="onUserMentioned"
|
||||
>
|
||||
<el-input
|
||||
v-model="replyInput"
|
||||
:placeholder="`${t('comment.reply')} @${
|
||||
activeReply.targetName
|
||||
}...`"
|
||||
v-model="mainInput"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||
:rows="2"
|
||||
:placeholder="t('comment.placeholder')"
|
||||
resize="none"
|
||||
/>
|
||||
</mentionEditor>
|
||||
<div class="input-tools">
|
||||
<div class="left-icons">
|
||||
<!-- 提到-后续迭代 -->
|
||||
<!-- <el-button link title="提到">@</el-button> -->
|
||||
<!-- 图片功能-后续迭代 -->
|
||||
<!-- <el-button link title="图片"><el-icon><Picture /></el-icon></el-button> -->
|
||||
<!-- 附件功能-后续迭代 -->
|
||||
<!-- <el-button link title="附件"><el-icon><Paperclip /></el-icon></el-button> -->
|
||||
|
||||
<!-- 表情功能 -->
|
||||
<emoji-picker
|
||||
@select="(e) => onSelectEmoji(e, 'reply')"
|
||||
/>
|
||||
<emoji-picker @select="(e) => onSelectEmoji(e, 'main')" />
|
||||
</div>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button
|
||||
class="send-btn"
|
||||
type="primary"
|
||||
link
|
||||
@click="submitReply"
|
||||
@click="handleSendComment('main')"
|
||||
>
|
||||
<el-icon :size="20" :title="t('comment.send')"
|
||||
><Promotion
|
||||
@@ -191,11 +208,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -213,33 +225,69 @@ const { t } = useI18n();
|
||||
// 当前用户信息
|
||||
const currentUser = computed(() => {
|
||||
return {
|
||||
nickname: userStore.userInfo.username,
|
||||
name: userStore.userInfo.username,
|
||||
avatar: userStore.userInfo.avatar,
|
||||
id: 1,
|
||||
};
|
||||
});
|
||||
|
||||
// 评论业务逻辑
|
||||
const activeReply = reactive({ parentId: null, targetName: "" });
|
||||
const mainInput = ref("");
|
||||
const replyInput = ref("");
|
||||
const loading = ref(true); //当前骨架屏显示
|
||||
// 评论数据
|
||||
const commentData = ref([
|
||||
{
|
||||
id: 1,
|
||||
nickname: "李星倩",
|
||||
avatar: "",
|
||||
content: "已完成ROI测算,请审核。",
|
||||
time: "2023-10-27T14:30:00",
|
||||
canDelete: true,
|
||||
createTime: "2023-10-27T14:30:00",
|
||||
mentions: [{ userId: 2, nickname: "冯娜", start: 2, end: 6 }],
|
||||
employee: {
|
||||
id: 1,
|
||||
name: "李星倩",
|
||||
avatar: "",
|
||||
},
|
||||
childrenCount: 3,
|
||||
children: [
|
||||
{
|
||||
id: 101,
|
||||
nickname: "冯娜",
|
||||
replyTo: "李星倩",
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
|
||||
content: "收到,数据已入库,我马上看下。",
|
||||
time: 1767604936684,
|
||||
createTime: 1767604936684,
|
||||
mentions: [{ userId: 2, nickname: "冯娜", start: 11, end: 15 }],
|
||||
employee: {
|
||||
id: 2,
|
||||
name: "冯娜",
|
||||
avatar: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
|
||||
content: "收到,数据已入库,我马上看下。",
|
||||
createTime: 1767604936684,
|
||||
mentions: [{ userId: 2, nickname: "冯娜", start: 11, end: 15 }],
|
||||
replyEmployee: {
|
||||
id: 1,
|
||||
name: "冯娜",
|
||||
},
|
||||
employee: {
|
||||
id: 2,
|
||||
name: "zhanghan",
|
||||
avatar: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
|
||||
content: "收到,数据已入库,我马上看下。",
|
||||
createTime: 1767604936684,
|
||||
mentions: [{ userId: 2, nickname: "冯娜", start: 11, end: 15 }],
|
||||
employee: {
|
||||
id: 3,
|
||||
name: "王五",
|
||||
avatar: "",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -247,9 +295,27 @@ const commentData = ref([
|
||||
|
||||
// 回复
|
||||
const openReply = (target, group) => {
|
||||
activeReply.groupId = group.id;
|
||||
activeReply.targetName = target.nickname;
|
||||
replyInput.value = "";
|
||||
|
||||
// 1. 设置回复的目标关系
|
||||
activeReply.groupId = group.id; // 根评论ID
|
||||
activeReply.parentId = target.id; // 直接父级ID
|
||||
activeReply.targetName = target.employee.name;
|
||||
|
||||
// 2. 沿用 @ 逻辑:自动在输入框插入 @某人
|
||||
const mentionStr = `@${target.employee.name} `;
|
||||
|
||||
// 如果输入框里已经有内容了,在前面追加,否则直接赋值
|
||||
if (!mainInput.value.includes(mentionStr)) {
|
||||
mainInput.value = mentionStr + mainInput.value;
|
||||
}
|
||||
|
||||
// 3. 聚焦到输入框
|
||||
nextTick(() => {
|
||||
const textarea = document.querySelector('.fixed-publisher textarea');
|
||||
textarea?.focus();
|
||||
// 飞书细节:光标移到最后
|
||||
textarea.selectionStart = textarea.value.length;
|
||||
});
|
||||
};
|
||||
|
||||
// 删除回复-删除评论
|
||||
@@ -274,73 +340,162 @@ const onSelectEmoji = (emoji, type) => {
|
||||
console.log("emoji", emoji, type);
|
||||
if (type === "main") {
|
||||
mainInput.value += emoji;
|
||||
} else {
|
||||
replyInput.value += emoji;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消评论
|
||||
const cancelReply = () => {
|
||||
activeReply.parentId = null;
|
||||
activeReply.targetName = "";
|
||||
activeReply.groupId = null;
|
||||
mainInput.value = '';
|
||||
};
|
||||
|
||||
// @圈人的操作
|
||||
const userList = [
|
||||
{ id: 1, nickname: "李星倩" },
|
||||
{ id: 2, nickname: "冯娜" },
|
||||
{ id: 3, nickname: "张三" }
|
||||
{ id: 3, nickname: "张三" },
|
||||
];
|
||||
|
||||
// TODO:@ 获取选中的用户信息
|
||||
const onUserMentioned = (user) => {
|
||||
console.log('Mentioned:', user.nickname);
|
||||
console.log("Mentioned:", user.nickname);
|
||||
};
|
||||
|
||||
const submitMainComment = () => {
|
||||
if (!mainInput.value.trim()) return ElMessage.warning("内容不能为空");
|
||||
/**
|
||||
* 统一发送评论/回复的方法
|
||||
* @param {string} type - 'main' (主评论) 或 'reply' (回复)
|
||||
*/
|
||||
const handleSendComment = async () => {
|
||||
const type = activeReply.parentId ? 'reply' : 'main';
|
||||
const isReply = type === "reply";
|
||||
let rawText = mainInput.value;
|
||||
|
||||
const formattedContent = mainInput.value.replace(
|
||||
/@([^\s@]+)/g,
|
||||
'<span class="mention-highlight">@$1</span>'
|
||||
);
|
||||
if (!rawText.trim()) return ElMessage.warning("内容不能为空");
|
||||
|
||||
commentData.value.unshift({
|
||||
id: Date.now(),
|
||||
...currentUser.value,
|
||||
content: formattedContent,
|
||||
time: new Date().valueOf(),
|
||||
canDelete: true,
|
||||
children: [],
|
||||
});
|
||||
|
||||
mainInput.value = "";
|
||||
};
|
||||
|
||||
// 通用的内容转换函数
|
||||
const parseMention = (text) => {
|
||||
if (!text) return "";
|
||||
return text.replace(
|
||||
/@([^\s@]+)/g,
|
||||
'<span class="mention-highlight">@$1</span>'
|
||||
);
|
||||
};
|
||||
|
||||
const submitReply = () => {
|
||||
const targetGroup = commentData.value.find(i => i.id === activeReply.groupId);
|
||||
if (targetGroup) {
|
||||
const formattedReply = replyInput.value.replace(
|
||||
/@([^\s@]+)/g,
|
||||
'<span class="mention-highlight">@$1</span>'
|
||||
);
|
||||
|
||||
targetGroup.children.push({
|
||||
id: Date.now(),
|
||||
...currentUser.value,
|
||||
replyTo: activeReply.targetName,
|
||||
content: formattedReply, // 使用处理后的 HTML
|
||||
time: new Date().valueOf(),
|
||||
});
|
||||
cancelReply();
|
||||
const prefix = `@${activeReply.targetName} `;
|
||||
// 如果用户没有删掉开头的 @人名,则把它剔除
|
||||
if (rawText.startsWith(prefix)) {
|
||||
rawText = rawText.slice(prefix.length).trimStart();
|
||||
}
|
||||
|
||||
// 构造 Payload
|
||||
const mentionList = [];
|
||||
const regex = /@([^\s@]+)(?=\s|$)/g;
|
||||
let match;
|
||||
while ((match = regex.exec(rawText)) !== null) {
|
||||
const nickname = match[1];
|
||||
const user = userList.find((u) => u.nickname === nickname);
|
||||
if (user) {
|
||||
mentionList.push({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 组装接口请求的参数
|
||||
|
||||
const params = {
|
||||
content: rawText,
|
||||
mentionList: mentionList,
|
||||
// 如果是回复,带上关联 ID
|
||||
reply: {
|
||||
id: isReply ? activeReply.groupId : null,
|
||||
name: isReply ? activeReply.groupId : null,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
// const res = await api.postComment(params);
|
||||
console.log("发送请求, 参数为:", params);
|
||||
|
||||
// 前端 UI 更新
|
||||
updateUIAfterSend(type, params);
|
||||
|
||||
// 清空输入框
|
||||
cancelReply();
|
||||
ElMessage.success(isReply ? "回复成功" : "评论成功");
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
ElMessage.error("发送失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 更新当前的UI层
|
||||
const updateUIAfterSend = (type, params) => {
|
||||
const newComment = {
|
||||
id: Date.now(), // 这个地方需要替换为后端返回的真实id 用作后续的删除
|
||||
employee: {
|
||||
...currentUser.value,
|
||||
},
|
||||
reply: params.reply,
|
||||
content: params.content,
|
||||
mentions: params.mentionList, // 这里直接存入带下标的数组
|
||||
createTime: new Date().valueOf(),
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (type === "main") {
|
||||
// 评论
|
||||
commentData.value.unshift(newComment);
|
||||
} else {
|
||||
//回复某人的数据渲染
|
||||
const targetGroup = commentData.value.find((i) => i.id === params.reply.id);
|
||||
if (targetGroup) {
|
||||
// 记录回复对象的名字以便显示 "@某某"
|
||||
newComment.replyTo = activeReply.targetName;
|
||||
// targetGroup.children.unshift(newComment);
|
||||
if (!targetGroup.localReplies) targetGroup.localReplies = [];
|
||||
targetGroup.localReplies.unshift(newComment);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO:展开更多-收起
|
||||
const handleExpand = async (item) => {
|
||||
try {
|
||||
// 后端获取最终的数据信息
|
||||
// const res = await xxxxxx()
|
||||
const res = [];
|
||||
const combined = [...(item.localReplies || []), ...res];
|
||||
item.children = combined.filter(
|
||||
(v, i, a) => a.findIndex((t) => t.id === v.id) === i
|
||||
);
|
||||
item.localReplies = [];
|
||||
item.showAllReplies = true;
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} text - 原始文本
|
||||
* @param {Array} atUsers - 这条评论真正圈到的人员列表
|
||||
*/
|
||||
const parseMention = (text, atUsers = userList) => {
|
||||
if (!text) return "";
|
||||
if (!atUsers || atUsers.length === 0) return text;
|
||||
// 只循环这条评论里【真正圈到】的人
|
||||
const sortedMentions = [...atUsers].sort((a, b) => b.start - a.start);
|
||||
let result = text;
|
||||
sortedMentions.forEach((m) => {
|
||||
const prefix = result.slice(0, m.start);
|
||||
const suffix = result.slice(m.end);
|
||||
const mentionText = result.slice(m.start, m.end);
|
||||
|
||||
const hasAt = mentionText.trim().startsWith("@");
|
||||
const displayName = hasAt ? mentionText : `@${mentionText}`;
|
||||
|
||||
// 插入高亮标签
|
||||
const highlight = `<span class="mention-highlight" data-user-id="${m.userId}">${displayName} </span>`;
|
||||
|
||||
result = prefix + highlight + suffix;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// 初始化
|
||||
@@ -352,228 +507,5 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$color-blue: #409eff;
|
||||
$color-blue-bg: #f5f8ff;
|
||||
$color-text-main: #303133;
|
||||
$color-text-sub: #99a2aa;
|
||||
$color-border: #e4e7ed;
|
||||
$color-white: #fff;
|
||||
|
||||
.comment-app {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
font-family: -apple-system, sans-serif;
|
||||
|
||||
// 1. 输入框样式
|
||||
.input-wrapper {
|
||||
border: 1px solid $color-border;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
transition: border-color 0.2s;
|
||||
background-color: $color-white;
|
||||
|
||||
&:focus-within {
|
||||
border-color: $color-blue;
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
border: none;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.input-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
|
||||
.left-icons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
.el-button {
|
||||
color: $color-text-sub;
|
||||
font-size: 18px;
|
||||
padding: 4px;
|
||||
}
|
||||
.el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
margin-left: 8px;
|
||||
color: $color-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 评论列表样式
|
||||
.comment-list {
|
||||
margin-top: 30px;
|
||||
|
||||
.comment-group {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.parent-node {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.user-avatar {
|
||||
background-color: $color-blue;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.node-main {
|
||||
flex: 1;
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.nickname {
|
||||
font-weight: bold;
|
||||
color: $color-text-main;
|
||||
}
|
||||
.time {
|
||||
font-size: 13px;
|
||||
color: $color-text-sub;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
margin: 8px 0;
|
||||
font-size: 15px;
|
||||
color: $color-text-main;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
.el-button {
|
||||
font-size: 13px;
|
||||
color: $color-text-sub;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
&:hover {
|
||||
color: $color-blue;
|
||||
}
|
||||
}
|
||||
.delete-btn:hover {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 子评论卡片样式
|
||||
.sub-container {
|
||||
background-color: $color-blue-bg;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #d9e5ff;
|
||||
padding: 16px;
|
||||
margin-top: 10px;
|
||||
|
||||
.sub-node {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sub-main {
|
||||
flex: 1;
|
||||
.sub-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.sub-user-info {
|
||||
font-size: 14px;
|
||||
.nickname {
|
||||
font-weight: bold;
|
||||
}
|
||||
.reply-text {
|
||||
margin: 0 6px;
|
||||
color: $color-text-sub;
|
||||
}
|
||||
.target-name {
|
||||
color: $color-blue;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: $color-text-sub;
|
||||
}
|
||||
}
|
||||
.content-body {
|
||||
font-size: 14px;
|
||||
color: $color-text-main;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline-publisher {
|
||||
margin-top: 15px;
|
||||
.inline-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 在 style 标签内添加 */
|
||||
:deep(.mention-highlight) {
|
||||
background-color: rgba(64, 158, 255, 0.1); /* 浅蓝色背景 */
|
||||
color: #409eff; /* 蓝色文字 */
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
margin: 0 2px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(64, 158, 255, 0.2);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 评论组件骨架屏
|
||||
.skeleton-comment-parent-node {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
.skeleton-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.skeleton-node-main {
|
||||
flex: 1;
|
||||
|
||||
.skeleton-user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.skeleton-actions {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@use "./index.scss" as *;
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
class="mention-popover"
|
||||
:style="{ top: popoverPos.top + 'px', left: popoverPos.left + 'px' }"
|
||||
>
|
||||
<div v-if="loading" class="mention-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="mention-list" v-if="filteredList.length">
|
||||
<div
|
||||
v-for="(user, index) in filteredList"
|
||||
@@ -18,19 +22,23 @@
|
||||
@mousedown.prevent="handleSelect(user)"
|
||||
@mouseenter="selectIndex = index"
|
||||
>
|
||||
<name-avatar :name="user.nickname" :size="20" />
|
||||
<name-avatar :name="user.nickname" :size="30" />
|
||||
<span class="name">{{ user.nickname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="mention-empty">未找到人员</div>
|
||||
<div v-else class="mention-empty">
|
||||
<el-empty :image-size="40" description="未匹配到人员" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick, watch } from 'vue';
|
||||
|
||||
import nameAvatar from '@/components/nameAvatar/index.vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
const props = defineProps({
|
||||
modelValue: String,
|
||||
users: { type: Array, default: () => [] }
|
||||
@@ -46,10 +54,10 @@ const selectIndex = ref(0);
|
||||
const popoverPos = ref({ top: 0, left: 0 });
|
||||
let targetInput = null; // 存储原生 textarea 引用
|
||||
|
||||
const filteredList = computed(() => {
|
||||
return props.users.filter(u => u.nickname.includes(searchKey.value));
|
||||
});
|
||||
|
||||
// 搜索查询模块
|
||||
const filteredList = ref([]);
|
||||
const loading = ref(false);
|
||||
let abortController = null;
|
||||
// 计算像素坐标的核心逻辑
|
||||
const computeCaretPosition = () => {
|
||||
if (!targetInput || !mirrorRef.value) return;
|
||||
@@ -98,15 +106,48 @@ const computeCaretPosition = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 定义圈人的搜索方法
|
||||
const fetchRemoteUsers = debounce(async (keyword) => {
|
||||
if(!popoverPos.value) return;
|
||||
|
||||
// 正在进行的请求,直接取消它
|
||||
if (abortController) abortController.abort();
|
||||
abortController = new AbortController();
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
// const res = await xxxx
|
||||
// filteredList.value = res;
|
||||
|
||||
// 模拟数据返回
|
||||
|
||||
const res = await new Promise((resolve,reject)=>{
|
||||
const timeout = setTimeout(() => {
|
||||
const results = props.users.filter(u =>
|
||||
u.nickname.toLowerCase().includes(keyword.toLowerCase())
|
||||
);
|
||||
resolve(results);
|
||||
}, 300);
|
||||
abortController.signal.addEventListener('abort', () => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error('aborted'));
|
||||
});
|
||||
})
|
||||
filteredList.value = res;
|
||||
selectIndex.value = 0; //每次搜索默认选中第一个
|
||||
} catch (error) {
|
||||
console.log('loading users error',error);
|
||||
} finally{
|
||||
loading.value = false;
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// handleInput 函数 搜索
|
||||
const handleInput = (e) => {
|
||||
const el = e.target;
|
||||
const val = el.value;
|
||||
const pos = el.selectionStart; // 当前光标位置
|
||||
|
||||
// 1. 获取光标之前的文本
|
||||
const textBefore = val.slice(0, pos);
|
||||
|
||||
// 2. 找到光标前最近的一个 @
|
||||
const lastAtPos = textBefore.lastIndexOf("@");
|
||||
|
||||
if (lastAtPos !== -1) {
|
||||
@@ -124,6 +165,8 @@ const handleInput = (e) => {
|
||||
searchKey.value = contentBetween;
|
||||
showPopover.value = true;
|
||||
computeCaretPosition(); // 重新计算位置
|
||||
|
||||
fetchRemoteUsers(contentBetween); //触发当前搜索
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -136,21 +179,20 @@ const handleKeyDown = (e) => {
|
||||
// 兼容删除 删除整个块而不是单独的文字
|
||||
if (e.key === 'Backspace') {
|
||||
const el = e.target;
|
||||
const pos = el.selectionStart; // 获取光标当前位置
|
||||
const pos = el.selectionStart;
|
||||
const val = el.value;
|
||||
|
||||
// 1. 获取光标之前的文本
|
||||
const textBefore = val.slice(0, pos);
|
||||
|
||||
// 2. 正则匹配:光标前是否紧跟 "@姓名 " (注意:这里匹配的是带空格的完整块)
|
||||
// 这里的正则要匹配:以@开头,中间没有空格或@,最后以一个空格结尾
|
||||
// 2. 正则匹配:光标前是否紧跟 "@姓名 "
|
||||
const mentionMatch = textBefore.match(/@([^\s@]+)\s$/);
|
||||
|
||||
if (mentionMatch) {
|
||||
// 匹配到了,说明光标刚好在某个 "@姓名 " 的末尾
|
||||
const wholeMatch = mentionMatch[0]; // 例如 "@李星倩 "
|
||||
const wholeMatch = mentionMatch[0];
|
||||
|
||||
e.preventDefault(); // 阻止浏览器默认的删除一个字符的行为
|
||||
e.preventDefault();
|
||||
|
||||
// 3. 计算新值:删掉整个匹配到的块
|
||||
const startPos = pos - wholeMatch.length;
|
||||
@@ -188,13 +230,76 @@ const handleSelect = (user) => {
|
||||
const textBefore = val.slice(0, pos);
|
||||
const lastAtPos = textBefore.lastIndexOf("@");
|
||||
|
||||
const newValue = val.slice(0, lastAtPos) + `@${user.nickname} ` + val.slice(pos);
|
||||
const insertName = `@${user.nickname} `; // 注意结尾有空格
|
||||
const newValue = val.slice(0, lastAtPos) + insertName + val.slice(pos);
|
||||
|
||||
emit('update:modelValue', newValue);
|
||||
emit('select', user);
|
||||
|
||||
// 传给父组件一个包含位置的对象
|
||||
emit('select', {
|
||||
...user,
|
||||
start: lastAtPos,
|
||||
end: lastAtPos + insertName.length
|
||||
});
|
||||
|
||||
showPopover.value = false;
|
||||
nextTick(() => targetInput.focus());
|
||||
};
|
||||
|
||||
// 禁止用户移入到当前选中的@块中
|
||||
const checkSelection = (el) => {
|
||||
// 如果用户当前选中了一段范围(selectionStart !== selectionEnd),说明是在做选择操作
|
||||
// 这种情况下不进行纠偏,否则用户无法选中并删除块
|
||||
if (el.selectionStart !== el.selectionEnd) return;
|
||||
|
||||
const pos = el.selectionStart;
|
||||
const val = el.value;
|
||||
|
||||
const regex = /@([^\s@]+)\s/g;
|
||||
|
||||
// 回复的时候锁死第一个@用户
|
||||
const firstMentionMatch = val.match(/^@([^\s@]+)\s/);
|
||||
if (firstMentionMatch) {
|
||||
const mentionEndPos = firstMentionMatch[0].length;
|
||||
if (pos < mentionEndPos) {
|
||||
el.setSelectionRange(mentionEndPos, mentionEndPos);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配所有格式为 "@姓名 " 的位置
|
||||
let match;
|
||||
while ((match = regex.exec(val)) !== null) {
|
||||
const start = match.index;
|
||||
const end = match.index + match[0].length;
|
||||
|
||||
// 如果当前光标落在 [start + 1, end - 1] 之间(即 @符号之后,空格之前)
|
||||
if (pos > start && pos < end) {
|
||||
// 计算中点位置,判断光标离哪头近就弹到哪头
|
||||
const mid = (start + end) / 2;
|
||||
const targetPos = pos < mid ? start : end;
|
||||
|
||||
// 强制设置光标位置
|
||||
el.setSelectionRange(targetPos, targetPos);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const syncMirrorStyle = () => {
|
||||
if (!targetInput || !mirrorRef.value) return;
|
||||
const style = window.getComputedStyle(targetInput);
|
||||
const mirror = mirrorRef.value;
|
||||
|
||||
const propList = ['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'paddingTop', 'paddingLeft', 'paddingRight', 'paddingBottom', 'borderWidth', 'boxSizing', 'letterSpacing', 'wordBreak', 'whiteSpace', 'wordWrap'];
|
||||
propList.forEach(prop => mirror.style[prop] = style[prop]);
|
||||
|
||||
mirror.style.width = targetInput.clientWidth + 'px'; // 使用 clientWidth 排除滚动条影响
|
||||
mirror.style.height = targetInput.clientHeight + 'px';
|
||||
};
|
||||
|
||||
let resizeObserver = null;
|
||||
|
||||
onMounted(() => {
|
||||
targetInput = containerRef.value.querySelector('textarea');
|
||||
if (targetInput) {
|
||||
@@ -203,6 +308,27 @@ onMounted(() => {
|
||||
targetInput.addEventListener('scroll', () => {
|
||||
if (showPopover.value) computeCaretPosition();
|
||||
});
|
||||
|
||||
targetInput.addEventListener('mouseup', () => checkSelection(targetInput));
|
||||
targetInput.addEventListener('keyup', (e) => {
|
||||
// 只有按左右方向键时才需要校验,输入字符时 handleInput 会处理
|
||||
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
|
||||
checkSelection(targetInput);
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
syncMirrorStyle(); // 抽取出来的样式同步函数
|
||||
if (showPopover.value) computeCaretPosition();
|
||||
});
|
||||
resizeObserver.observe(targetInput);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -233,6 +359,17 @@ onMounted(() => {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e4e7ed;
|
||||
min-width: 160px;
|
||||
transition: top 0.1s ease-out, left 0.1s ease-out;
|
||||
overflow: hidden;
|
||||
transform: translateY(-100%);
|
||||
margin-top: -26px;
|
||||
&.el-zoom-in-top-enter-active {
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
.mention-loading{
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mention-list {
|
||||
@@ -246,11 +383,11 @@ onMounted(() => {
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&.active {
|
||||
background-color: #f0f7ff;
|
||||
color: #409eff;
|
||||
background-color: var(--el-color-primary-light-9, #ecf5ff);
|
||||
color: var(--el-color-primary, #409eff);
|
||||
}
|
||||
|
||||
.name {
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
<div class="info-text">
|
||||
<div class="title-row">
|
||||
<span class="main-title">集团1</span>
|
||||
<el-tag size="small" effect="plain" class="title-tag"
|
||||
>集团</el-tag
|
||||
>
|
||||
<!-- TODO:这块不要展示tag -->
|
||||
<!-- <el-tag size="small" effect="plain" class="title-tag">集团</el-tag> -->
|
||||
</div>
|
||||
<div class="sub-id">ID: 3</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<div class="personnel">
|
||||
<el-drawer
|
||||
v-model="drawer"
|
||||
size="80%"
|
||||
title="测试评论"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
destroy-on-close
|
||||
class="standard-ui-back-drawer"
|
||||
>
|
||||
<div style="padding: 20px">
|
||||
<Comment />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="drawer = false">取 消</el-button>
|
||||
<el-button type="primary" @click="drawer = false">确 定</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Comment from "@/modules/Comment/index.vue";
|
||||
import { reactive, ref, onMounted } from "vue";
|
||||
|
||||
defineOptions({ name: "Personnel" });
|
||||
const drawer = ref(true);
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.personnel {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user