+
{{ targetUser.nickname }}
-
发送添加朋友申请
+
+ 发送添加朋友申请
+
-
备注
+
+ 备注
+
-
-
+
+
diff --git a/apps/web-antd/src/views/im/home/components/friend/friend-item.vue b/apps/web-antd/src/views/im/home/components/friend/friend-item.vue
index 9feebabbc..03d9e2b45 100644
--- a/apps/web-antd/src/views/im/home/components/friend/friend-item.vue
+++ b/apps/web-antd/src/views/im/home/components/friend/friend-item.vue
@@ -1,47 +1,47 @@
@@ -54,7 +54,9 @@ function handleContextMenu(event: MouseEvent) {
-->
@@ -70,7 +72,9 @@ function handleContextMenu(event: MouseEvent) {
/>
-
+
{{ friend.displayName || friend.nickname }}
diff --git a/apps/web-antd/src/views/im/home/components/group/group-admin-set-dialog.vue b/apps/web-antd/src/views/im/home/components/group/group-admin-set-dialog.vue
index 060355ae6..59dabfb0a 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-admin-set-dialog.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-admin-set-dialog.vue
@@ -1,82 +1,84 @@
@@ -107,7 +109,9 @@ async function handleOk() {
-
+
diff --git a/apps/web-antd/src/views/im/home/components/group/group-avatar.vue b/apps/web-antd/src/views/im/home/components/group/group-avatar.vue
index 021714f65..c5971c7b8 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-avatar.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-avatar.vue
@@ -1,30 +1,30 @@
diff --git a/apps/web-antd/src/views/im/home/components/group/group-create-dialog.vue b/apps/web-antd/src/views/im/home/components/group/group-create-dialog.vue
index 18b87a585..f53abf166 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-create-dialog.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-create-dialog.vue
@@ -1,89 +1,95 @@
@@ -127,7 +133,12 @@ async function handleOk() {
-
diff --git a/apps/web-antd/src/views/im/home/components/group/group-info-card.vue b/apps/web-antd/src/views/im/home/components/group/group-info-card.vue
index 82918a7cb..cb292cad3 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-info-card.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-info-card.vue
@@ -1,97 +1,97 @@
@@ -103,13 +103,22 @@ async function handleApply(group: GroupLite) {
- GroupInfo 内部按 groupStore 缓存推导 member / stranger,浮层只负责接 chat / apply 事件做业务
-->
-
+
diff --git a/apps/web-antd/src/views/im/home/components/group/group-info.vue b/apps/web-antd/src/views/im/home/components/group/group-info.vue
index 5ee86e4d4..acc52cf41 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-info.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-info.vue
@@ -1,38 +1,38 @@
@@ -125,7 +134,10 @@ function convertGroupMemberLite(member: GroupMember, friend: Friend | undefined)
>
{{ group.showGroupName || group.name }}
-
+
{{ memberCountText }}
diff --git a/apps/web-antd/src/views/im/home/components/group/group-item.vue b/apps/web-antd/src/views/im/home/components/group/group-item.vue
index 9fa60e27e..6a42f83fb 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-item.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-item.vue
@@ -1,18 +1,18 @@
@@ -23,7 +23,9 @@ defineEmits<{
-->
-
+
{{ group.showGroupName || group.name }}
diff --git a/apps/web-antd/src/views/im/home/components/group/group-member-add-dialog.vue b/apps/web-antd/src/views/im/home/components/group/group-member-add-dialog.vue
index 9c1145a3d..616cb1dac 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-member-add-dialog.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-member-add-dialog.vue
@@ -1,127 +1,133 @@
@@ -150,7 +156,12 @@ async function handleOk() {
取消
-
+
添加
diff --git a/apps/web-antd/src/views/im/home/components/group/group-member-grid.vue b/apps/web-antd/src/views/im/home/components/group/group-member-grid.vue
index e7cc7d6bc..c955cb4e0 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-member-grid.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-member-grid.vue
@@ -1,24 +1,24 @@
diff --git a/apps/web-antd/src/views/im/home/components/group/group-member-item.vue b/apps/web-antd/src/views/im/home/components/group/group-member-item.vue
index 1ed9fa0ad..861ea1343 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-member-item.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-member-item.vue
@@ -1,49 +1,52 @@
@@ -54,8 +57,10 @@ const roleLabelClass = computed(() => {
-->
{
/>
{{ member.showName }}
diff --git a/apps/web-antd/src/views/im/home/components/group/group-member-remove-dialog.vue b/apps/web-antd/src/views/im/home/components/group/group-member-remove-dialog.vue
index b81b00374..e14e55eb5 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-member-remove-dialog.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-member-remove-dialog.vue
@@ -1,59 +1,59 @@
diff --git a/apps/web-antd/src/views/im/home/components/group/group-member.vue b/apps/web-antd/src/views/im/home/components/group/group-member.vue
index 2ae97a22e..6c8467fb7 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-member.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-member.vue
@@ -1,38 +1,38 @@
@@ -43,7 +43,7 @@ const avatarSize = computed(() => Math.ceil(props.height * 0.75))
Math.ceil(props.height * 0.75))
/>
{{ member.showName }}
diff --git a/apps/web-antd/src/views/im/home/components/group/group-mute-member-dialog.vue b/apps/web-antd/src/views/im/home/components/group/group-mute-member-dialog.vue
index 5f5315c95..f2540ffc1 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-mute-member-dialog.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-mute-member-dialog.vue
@@ -1,22 +1,22 @@
-
+
- 禁言成员
-
+
+ 禁言成员
+
+
{{ memberName }}
-
禁言时长
+
+ 禁言时长
+
取消
- 确定
+
+ 确定
+
diff --git a/apps/web-antd/src/views/im/home/components/group/group-owner-transfer-dialog.vue b/apps/web-antd/src/views/im/home/components/group/group-owner-transfer-dialog.vue
index 50bda5e96..624132a51 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-owner-transfer-dialog.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-owner-transfer-dialog.vue
@@ -1,79 +1,79 @@
diff --git a/apps/web-antd/src/views/im/home/components/group/group-request-list-dialog.vue b/apps/web-antd/src/views/im/home/components/group/group-request-list-dialog.vue
index 8a67420d4..79a8b927a 100644
--- a/apps/web-antd/src/views/im/home/components/group/group-request-list-dialog.vue
+++ b/apps/web-antd/src/views/im/home/components/group/group-request-list-dialog.vue
@@ -1,59 +1,59 @@
@@ -173,85 +176,92 @@ function updateLocalResult(id: number, handleResult: number) {
-
+
-
+
-
-
-
-
- {{ latest.userNickname || `用户 ${latest.userId}` }}
+
+
+
+
+ {{ latest.userNickname || `用户 ${latest.userId}` }}
+
+
+
+ 通过
+
+ {{
+ latest.inviterNickname || `用户 ${latest.inviterUserId}`
+ }}
+
+ 的邀请进群
+
+ 申请加入
+
-
-
- 通过
-
- {{ latest.inviterNickname || `用户 ${latest.inviterUserId}` }}
-
- 的邀请进群
-
-
申请加入
+ 已同意
+
+
+ 已拒绝
+
+
+
+ 确认
+
+
+ 拒绝
+
-
+
- 已同意
-
-
- 已拒绝
-
-
-
- 确认
-
-
- 拒绝
-
+
+ {{
+ latest.inviterUserId
+ ? latest.inviterNickname || `用户 ${latest.inviterUserId}`
+ : latest.userNickname || `用户 ${latest.userId}`
+ }}:
+
+ {{ latest.applyContent }}
-
-
-
- {{
- latest.inviterUserId
- ? latest.inviterNickname || `用户 ${latest.inviterUserId}`
- : latest.userNickname || `用户 ${latest.userId}`
- }}:
-
- {{ latest.applyContent }}
-
-
-
+
-
-
-
-
- {{ item.userNickname || `用户 ${item.userId}` }}
+
+
+
+
+ {{ item.userNickname || `用户 ${item.userId}` }}
+
+
+
+ 通过
+
+ {{ item.inviterNickname || `用户 ${item.inviterUserId}` }}
+
+ 的邀请进群
+
+ 申请加入
+
-
-
- 通过
-
- {{ item.inviterNickname || `用户 ${item.inviterUserId}` }}
-
- 的邀请进群
-
-
申请加入
+ 已同意
+
+
+ 已拒绝
+
+
+
+ 确认
+
+
+ 拒绝
+
-
- 已同意
-
-
- 已拒绝
-
-
-
- 确认
-
-
- 拒绝
-
-
-
@@ -334,17 +346,18 @@ function updateLocalResult(id: number, handleResult: number) {
height: 28px;
padding: 0 12px;
font-size: 13px;
- border-radius: 4px;
cursor: pointer;
border: 1px solid transparent;
+ border-radius: 4px;
transition:
background-color 0.15s,
border-color 0.15s,
color 0.15s;
}
+
.im-group-request-list__btn:disabled {
- opacity: 0.6;
cursor: not-allowed;
+ opacity: 0.6;
}
.im-group-request-list__btn--primary {
@@ -352,6 +365,7 @@ function updateLocalResult(id: number, handleResult: number) {
background-color: var(--ant-color-primary);
border-color: var(--ant-color-primary);
}
+
.im-group-request-list__btn--primary:hover:not(:disabled) {
background-color: var(--ant-color-primary-hover);
border-color: var(--ant-color-primary-hover);
@@ -362,6 +376,7 @@ function updateLocalResult(id: number, handleResult: number) {
background-color: var(--ant-color-bg-container);
border-color: var(--ant-color-border);
}
+
.im-group-request-list__btn--ghost:hover:not(:disabled) {
color: var(--ant-color-primary);
border-color: var(--ant-color-primary);
diff --git a/apps/web-antd/src/views/im/home/components/paged-scroller.vue b/apps/web-antd/src/views/im/home/components/paged-scroller.vue
index ed1842a2f..4b2a08086 100644
--- a/apps/web-antd/src/views/im/home/components/paged-scroller.vue
+++ b/apps/web-antd/src/views/im/home/components/paged-scroller.vue
@@ -1,89 +1,100 @@
diff --git a/apps/web-antd/src/views/im/home/components/picker/conversation-picker-panel.vue b/apps/web-antd/src/views/im/home/components/picker/conversation-picker-panel.vue
index 19ef61c26..960a40fb7 100644
--- a/apps/web-antd/src/views/im/home/components/picker/conversation-picker-panel.vue
+++ b/apps/web-antd/src/views/im/home/components/picker/conversation-picker-panel.vue
@@ -1,88 +1,94 @@
@@ -158,7 +167,9 @@ function handleToggle(conversation: Conversation) {
- 最近转发
+
+ 最近转发
+
@@ -241,7 +254,11 @@ function handleToggle(conversation: Conversation) {
- 最近聊天
+
+ 最近聊天
+
-import type { FriendLite } from '../../types'
+import type { FriendLite } from '../../types';
-import { computed, ref } from 'vue'
+import { computed, ref } from 'vue';
-import { IconifyIcon as Icon } from '@vben/icons'
+import { IconifyIcon as Icon } from '@vben/icons';
-import { Input, message } from 'ant-design-vue'
+import { Input, message } from 'ant-design-vue';
-import { PagedScroller } from '..'
-import { useFriendBuckets } from '../../composables/useFriendBuckets'
-import { useSelectedItems } from '../../composables/useSelectedItems'
-import { UserAvatar } from '../user'
+import { PagedScroller } from '..';
+import { useFriendBuckets } from '../../composables/useFriendBuckets';
+import { useSelectedItems } from '../../composables/useSelectedItems';
+import { UserAvatar } from '../user';
-defineOptions({ name: 'ImFriendPickerPanel' })
+defineOptions({ name: 'ImFriendPickerPanel' });
const props = withDefaults(
defineProps<{
/** 禁用 id:列表里展示置灰、不可勾选、不计入已选数(典型:邀请入群时已在群成员) */
- disabledIds?: number[]
+ disabledIds?: number[];
/** 全量好友列表 */
- friends: FriendLite[]
+ friends: FriendLite[];
/** 隐藏 id:不展示(hide > locked > disabled) */
- hideIds?: number[]
+ hideIds?: number[];
/** 锁定 id:默认勾选、不可取消、计入已选数(典型:私聊侧 +建群锁定对方) */
- lockedIds?: number[]
+ lockedIds?: number[];
/** 已选数上限;不传或 <=0 时不限 */
- maxSize?: number
+ maxSize?: number;
/** 已选好友 id(v-model);按数组顺序即为点击顺序 */
- selectedIds: number[]
+ selectedIds: number[];
}>(),
{
lockedIds: () => [],
disabledIds: () => [],
hideIds: () => [],
- maxSize: 0
- }
-)
+ maxSize: 0,
+ },
+);
const emit = defineEmits<{
- 'update:selectedIds': [value: number[]]
-}>()
+ 'update:selectedIds': [value: number[]];
+}>();
-const keyword = ref('')
+const keyword = ref('');
/** id → friend 映射,已选反查 / 三态判定共用,避免每次 O(N) 扫 */
const byId = computed(() => {
- const map = new Map()
+ const map = new Map();
for (const friend of props.friends) {
- map.set(friend.id, friend)
+ map.set(friend.id, friend);
}
- return map
-})
+ return map;
+});
/** 三态 id 集合:每次过滤复用 */
-const hideSet = computed(() => new Set(props.hideIds))
-const lockedSet = computed(() => new Set(props.lockedIds))
-const disabledSet = computed(() => new Set(props.disabledIds))
-const selectedSet = computed(() => new Set(props.selectedIds))
+const hideSet = computed(() => new Set(props.hideIds));
+const lockedSet = computed(() => new Set(props.lockedIds));
+const disabledSet = computed(() => new Set(props.disabledIds));
+const selectedSet = computed(() => new Set(props.selectedIds));
/** 候选好友:剔除 hideIds(hide 优先级最高) */
const candidates = computed(() =>
- props.friends.filter((friend) => !hideSet.value.has(friend.id))
-)
+ props.friends.filter((friend) => !hideSet.value.has(friend.id)),
+);
/** 委托 useFriendBuckets:搜索规则复用,左侧列表按滚动分页渲染 */
-const { filtered } = useFriendBuckets(candidates, keyword)
+const { filtered } = useFriendBuckets(candidates, keyword);
/** 已选数 + 已选好友列表:三态优先级 + 顺序拼接由 useSelectedItems 统一承担 */
-const { selectedCount, selectedItems: selectedFriends } = useSelectedItems(
- () => props.selectedIds,
- () => props.lockedIds,
- () => props.disabledIds,
- () => props.hideIds,
- byId
-)
+const { selectedCount, selectedItems: selectedFriends } =
+ useSelectedItems(
+ () => props.selectedIds,
+ () => props.lockedIds,
+ () => props.disabledIds,
+ () => props.hideIds,
+ byId,
+ );
/** 是否被锁定 */
function isLocked(friend: FriendLite): boolean {
- return lockedSet.value.has(friend.id)
+ return lockedSet.value.has(friend.id);
}
/** 是否被禁用:locked / hide 已被前置过滤,剩下的才算 disabled */
function isDisabled(friend: FriendLite): boolean {
- return !lockedSet.value.has(friend.id) && disabledSet.value.has(friend.id)
+ return !lockedSet.value.has(friend.id) && disabledSet.value.has(friend.id);
}
/** 是否选中:locked 视为永远选中 */
function isSelected(friend: FriendLite): boolean {
- return selectedSet.value.has(friend.id)
+ return selectedSet.value.has(friend.id);
}
/** 圆形勾选指示器的 class:选中 / 锁定走绿底,禁用灰底,未选空心圆 */
function getCheckClass(friend: FriendLite): string {
if (isLocked(friend) || isSelected(friend)) {
- return 'bg-[#07c160] border border-solid border-[#07c160]'
+ return 'bg-[#07c160] border border-solid border-[#07c160]';
}
if (isDisabled(friend)) {
- return 'bg-[var(--ant-color-fill)] border border-solid border-[var(--ant-color-border)]'
+ return 'bg-[var(--ant-color-fill)] border border-solid border-[var(--ant-color-border)]';
}
- return 'border border-solid border-[var(--ant-color-border)] bg-[var(--ant-color-bg-container)]'
+ return 'border border-solid border-[var(--ant-color-border)] bg-[var(--ant-color-bg-container)]';
}
/** 切换选中态:locked / disabled 不响应;右栏 × 移除 / 行 click 都走这里 */
function handleToggle(friend: FriendLite) {
if (isLocked(friend) || isDisabled(friend)) {
- return
+ return;
}
- const next = [...props.selectedIds]
- const index = next.indexOf(friend.id)
+ const next = [...props.selectedIds];
+ const index = next.indexOf(friend.id);
if (index === -1) {
if (props.maxSize > 0 && selectedCount.value >= props.maxSize) {
- message.error(`最多选择 ${props.maxSize} 位好友`)
- return
+ message.error(`最多选择 ${props.maxSize} 位好友`);
+ return;
}
- next.push(friend.id)
+ next.push(friend.id);
} else {
- next.splice(index, 1)
+ next.splice(index, 1);
}
- emit('update:selectedIds', next)
+ emit('update:selectedIds', next);
}
@@ -144,13 +145,19 @@ function handleToggle(friend: FriendLite) {
-
+
@@ -160,7 +167,10 @@ function handleToggle(friend: FriendLite) {
:class="getCheckClass(item as FriendLite)"
>
- {{ (item as FriendLite).displayName || (item as FriendLite).nickname }}
+ {{
+ (item as FriendLite).displayName ||
+ (item as FriendLite).nickname
+ }}
-
+
{{ keyword ? '没有匹配的好友' : '暂无好友' }}
diff --git a/apps/web-antd/src/views/im/home/components/picker/group-member-picker-panel.vue b/apps/web-antd/src/views/im/home/components/picker/group-member-picker-panel.vue
index a36650d5c..4558e72cf 100644
--- a/apps/web-antd/src/views/im/home/components/picker/group-member-picker-panel.vue
+++ b/apps/web-antd/src/views/im/home/components/picker/group-member-picker-panel.vue
@@ -1,129 +1,132 @@
@@ -162,7 +165,10 @@ function handleToggle(member: GroupMemberLite) {
:class="getCheckClass(item as GroupMemberLite)"
>
内 @use + @include 即可,避免全局污染
+/*
+* IM 选择类弹窗的公共样式 mixin
+* 每个业务壳在自己的
diff --git a/apps/web-antd/src/views/im/home/pages/conversation/components/input/message-input.vue b/apps/web-antd/src/views/im/home/pages/conversation/components/input/message-input.vue
index 2f5be45d1..47e000a89 100644
--- a/apps/web-antd/src/views/im/home/pages/conversation/components/input/message-input.vue
+++ b/apps/web-antd/src/views/im/home/pages/conversation/components/input/message-input.vue
@@ -1167,8 +1167,9 @@ async function onVideoPicked(e: Event) {
.message-input__tool:deep(svg) {
font-size: 18px !important;
color: var(--ant-color-text) !important;
- fill: currentColor !important;
+ fill: currentcolor !important;
}
+
.message-input__tool:hover,
.message-input__tool:hover:deep(svg) {
color: var(--ant-color-primary) !important;
@@ -1203,10 +1204,10 @@ async function onVideoPicked(e: Event) {
/* 用 data-empty 而非 :empty:浏览器在删空后会留下
,:empty 不命中;data-empty 由 syncEditorState 维护 */
.message-input__editor[data-empty]::before {
- content: attr(data-placeholder);
+ position: absolute;
color: var(--ant-color-text-placeholder);
pointer-events: none;
- position: absolute;
+ content: attr(data-placeholder);
}
/* @ token 走主色高亮;contenteditable=false 让 backspace 整段删而不是逐字符 */
diff --git a/apps/web-antd/src/views/im/home/pages/conversation/components/input/voice-recorder.vue b/apps/web-antd/src/views/im/home/pages/conversation/components/input/voice-recorder.vue
index 0598afa4c..fd6147c04 100644
--- a/apps/web-antd/src/views/im/home/pages/conversation/components/input/voice-recorder.vue
+++ b/apps/web-antd/src/views/im/home/pages/conversation/components/input/voice-recorder.vue
@@ -311,14 +311,14 @@ onUnmounted(() => {
diff --git a/apps/web-antd/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue b/apps/web-antd/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue
index 9c3016740..f9533dd42 100644
--- a/apps/web-antd/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue
+++ b/apps/web-antd/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue
@@ -193,15 +193,15 @@ async function handleRemove(pinnedMessage: Message) {
diff --git a/apps/web-antd/src/views/im/home/pages/conversation/components/message/group-request-pending.vue b/apps/web-antd/src/views/im/home/pages/conversation/components/message/group-request-pending.vue
index 4c401d7ee..02d29dd35 100644
--- a/apps/web-antd/src/views/im/home/pages/conversation/components/message/group-request-pending.vue
+++ b/apps/web-antd/src/views/im/home/pages/conversation/components/message/group-request-pending.vue
@@ -83,6 +83,6 @@ const pendingCount = computed(() => groupRequestStore.getUnhandledGroupRequestCo
diff --git a/apps/web-antd/src/views/im/home/pages/conversation/components/message/material-bubble.vue b/apps/web-antd/src/views/im/home/pages/conversation/components/message/material-bubble.vue
index a7972d28b..fc4209bcd 100644
--- a/apps/web-antd/src/views/im/home/pages/conversation/components/message/material-bubble.vue
+++ b/apps/web-antd/src/views/im/home/pages/conversation/components/message/material-bubble.vue
@@ -145,7 +145,7 @@ const onClick = async () => {
transition: box-shadow 0.15s ease;
&:hover {
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+ box-shadow: 0 2px 12px rgb(0 0 0 / 8%);
}
}
diff --git a/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-bubble.vue b/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-bubble.vue
index ef9a11ba6..b7092ef5e 100644
--- a/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-bubble.vue
+++ b/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-bubble.vue
@@ -359,29 +359,33 @@ onBeforeUnmount(() => {
颜色与气泡背景对应,留 1px 视觉吃进去,省一张图片 */
.message-bubble--other::before,
.message-bubble--self::before {
- content: '';
position: absolute;
top: 12px;
width: 0;
height: 0;
+ content: '';
border-style: solid;
}
+
.message-bubble--other {
--im-message-bubble-other-bg: #f5f5f5;
}
+
.message-bubble--other.message-bubble--text,
.message-bubble--other.message-bubble--voice {
background-color: var(--im-message-bubble-other-bg);
}
+
.message-bubble--other::before {
left: -5px;
- border-width: 5px 6px 5px 0;
border-color: transparent var(--im-message-bubble-other-bg) transparent transparent;
+ border-width: 5px 6px 5px 0;
}
+
.message-bubble--self::before {
right: -5px;
- border-width: 5px 0 5px 6px;
border-color: transparent transparent transparent #95ec69;
+ border-width: 5px 0 5px 6px;
}
/* 整体放进 :global(),避免 Vue scoped 把 `:global(.dark) .xxx` 塌缩成裸 `.dark` 而把变量刷到 */
@@ -393,6 +397,7 @@ onBeforeUnmount(() => {
.message-bubble__voice-icon :deep(svg) {
fill: #606266 !important;
}
+
.message-bubble__voice-icon.im-voice-playing :deep(svg) {
fill: #409eff !important;
}
@@ -407,6 +412,7 @@ onBeforeUnmount(() => {
100% {
transform: scale(1);
}
+
50% {
transform: scale(1.15);
}
diff --git a/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-history.vue b/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-history.vue
index 06c00e1ba..acfb8c3dd 100644
--- a/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-history.vue
+++ b/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-history.vue
@@ -754,16 +754,18 @@ function locateMessage(messageId: number) {
position: absolute;
top: 100%;
right: 0;
- margin-top: 4px;
display: none;
- white-space: nowrap;
+ margin-top: 4px;
color: #1989fa;
+ white-space: nowrap;
cursor: pointer;
transition: color 0.15s;
}
+
.im-message-history__row:hover .im-message-history__locate {
display: block;
}
+
.im-message-history__locate:hover {
color: #146fc7;
}
@@ -775,16 +777,18 @@ function locateMessage(messageId: number) {
color: #1989fa;
transition: color 0.15s;
}
+
.im-message-history__tab:hover {
color: #2f81d4;
}
+
.im-message-history__tab--active::after {
- content: '';
position: absolute;
- left: 0;
right: 0;
bottom: 0;
+ left: 0;
height: 2px;
+ content: '';
background: #1989fa;
border-radius: 1px;
}
@@ -793,15 +797,18 @@ function locateMessage(messageId: number) {
.im-message-history__calendar :deep(.ant-picker-calendar-header) {
padding: 4px 8px;
}
+
.im-message-history__calendar :deep(.ant-picker-content) {
font-size: 12px;
}
+
.im-message-history__calendar :deep(.ant-picker-cell) {
padding: 1px 0;
}
+
.im-message-history__calendar :deep(.ant-picker-cell .ant-picker-calendar-date) {
height: 28px;
- margin: 0 2px;
padding: 2px 4px 0;
+ margin: 0 2px;
}
diff --git a/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-item.vue b/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-item.vue
index 1dda6c735..ea2b73855 100644
--- a/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-item.vue
+++ b/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-item.vue
@@ -1047,6 +1047,7 @@ function handleDelete() {
.im-loading-spin {
animation: im-loading-spin 1s linear infinite;
}
+
@keyframes im-loading-spin {
to {
transform: rotate(360deg);
diff --git a/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-panel.vue b/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-panel.vue
index dc6154e0b..baf6b83ce 100644
--- a/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-panel.vue
+++ b/apps/web-antd/src/views/im/home/pages/conversation/components/message/message-panel.vue
@@ -752,7 +752,7 @@ watch(
width: 20px;
height: 20px;
color: var(--ant-color-text) !important;
- fill: currentColor !important;
+ fill: currentcolor !important;
transition: color 0.15s;
}
@@ -760,6 +760,7 @@ watch(
box-sizing: content-box;
padding: 4px;
}
+
.message-panel__header-icon:hover,
.message-panel__header-icon:hover :deep(svg) {
color: var(--ant-color-primary) !important;
@@ -777,6 +778,7 @@ watch(
.message-panel__message-anchor {
transition: background-color 0.6s ease;
}
+
.message-panel__message-anchor--highlight {
background-color: var(--ant-color-warning-bg);
}
diff --git a/apps/web-antdv-next/src/views/im/home/components/group/group-request-list-dialog.vue b/apps/web-antdv-next/src/views/im/home/components/group/group-request-list-dialog.vue
index b2816b547..040ebb6c3 100644
--- a/apps/web-antdv-next/src/views/im/home/components/group/group-request-list-dialog.vue
+++ b/apps/web-antdv-next/src/views/im/home/components/group/group-request-list-dialog.vue
@@ -334,17 +334,18 @@ function updateLocalResult(id: number, handleResult: number) {
height: 28px;
padding: 0 12px;
font-size: 13px;
- border-radius: 4px;
cursor: pointer;
border: 1px solid transparent;
+ border-radius: 4px;
transition:
background-color 0.15s,
border-color 0.15s,
color 0.15s;
}
+
.im-group-request-list__btn:disabled {
- opacity: 0.6;
cursor: not-allowed;
+ opacity: 0.6;
}
.im-group-request-list__btn--primary {
@@ -352,6 +353,7 @@ function updateLocalResult(id: number, handleResult: number) {
background-color: var(--ant-color-primary);
border-color: var(--ant-color-primary);
}
+
.im-group-request-list__btn--primary:hover:not(:disabled) {
background-color: var(--ant-color-primary-hover);
border-color: var(--ant-color-primary-hover);
@@ -362,6 +364,7 @@ function updateLocalResult(id: number, handleResult: number) {
background-color: var(--ant-color-bg-container);
border-color: var(--ant-color-border);
}
+
.im-group-request-list__btn--ghost:hover:not(:disabled) {
color: var(--ant-color-primary);
border-color: var(--ant-color-primary);
diff --git a/apps/web-antdv-next/src/views/im/home/components/picker/conversation-picker-panel.vue b/apps/web-antdv-next/src/views/im/home/components/picker/conversation-picker-panel.vue
index a528c4c43..cf6e9879a 100644
--- a/apps/web-antdv-next/src/views/im/home/components/picker/conversation-picker-panel.vue
+++ b/apps/web-antdv-next/src/views/im/home/components/picker/conversation-picker-panel.vue
@@ -363,6 +363,7 @@ function handleToggle(conversation: Conversation) {
.im-conversation-picker__recent::-webkit-scrollbar {
height: 4px;
}
+
.im-conversation-picker__recent::-webkit-scrollbar-thumb {
background-color: var(--ant-color-border);
border-radius: 2px;
diff --git a/apps/web-antdv-next/src/views/im/home/components/rtc/rtc-call-participant-tile.vue b/apps/web-antdv-next/src/views/im/home/components/rtc/rtc-call-participant-tile.vue
index 6a12111c2..63530767d 100644
--- a/apps/web-antdv-next/src/views/im/home/components/rtc/rtc-call-participant-tile.vue
+++ b/apps/web-antdv-next/src/views/im/home/components/rtc/rtc-call-participant-tile.vue
@@ -97,12 +97,14 @@ const setAudioRef = useMediaStreamElement(() => props.particip
.tile-dot {
animation: tile-dot 1.4s infinite ease-in-out both;
}
+
@keyframes tile-dot {
0%,
80%,
100% {
opacity: 0.3;
}
+
40% {
opacity: 1;
}
diff --git a/apps/web-antdv-next/src/views/im/home/components/rtc/rtc-call-running.vue b/apps/web-antdv-next/src/views/im/home/components/rtc/rtc-call-running.vue
index 9b8d774c8..ce945ab9b 100644
--- a/apps/web-antdv-next/src/views/im/home/components/rtc/rtc-call-running.vue
+++ b/apps/web-antdv-next/src/views/im/home/components/rtc/rtc-call-running.vue
@@ -279,11 +279,13 @@ const formattedDuration = computed(() =>
.reconnect-dot {
animation: reconnect-pulse 1s ease-in-out infinite;
}
+
@keyframes reconnect-pulse {
0%,
100% {
opacity: 0.3;
}
+
50% {
opacity: 1;
}
diff --git a/apps/web-antdv-next/src/views/im/home/index.vue b/apps/web-antdv-next/src/views/im/home/index.vue
index 30b436390..1110192ea 100644
--- a/apps/web-antdv-next/src/views/im/home/index.vue
+++ b/apps/web-antdv-next/src/views/im/home/index.vue
@@ -255,6 +255,7 @@ watch(
--ant-color-text-secondary: hsl(var(--foreground) / 65%);
--ant-color-text-placeholder: hsl(var(--foreground) / 45%);
--ant-color-text-disabled: hsl(var(--foreground) / 30%);
+
/*
* fill 系列:浅色下用 Element Plus 风格的「冷调浅灰实色」而非半透明深色,
* 否则面板(会话列表 / 消息面板等)叠在灰底上会显脏发暗,和 Vue3+EP 的干净白差距明显。
diff --git a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/conversation/conversation-group-side.vue b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/conversation/conversation-group-side.vue
index 0b35584ed..49a860010 100644
--- a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/conversation/conversation-group-side.vue
+++ b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/conversation/conversation-group-side.vue
@@ -808,13 +808,13 @@ function handleOpenTransferOwner() {
/* 「添加 / 移出」瓦片:hover 时联动内部 icon-tile 走主色,跨子元素的 hover 联动无法用单元素工具类表达 */
.im-conversation-group-side__tile-wrap:hover .im-conversation-group-side__icon-tile {
color: var(--ant-color-primary);
- border-color: var(--ant-color-primary);
background-color: var(--ant-color-primary-bg);
+ border-color: var(--ant-color-primary);
}
/* :deep 穿透 Icon 内部 svg; el-icon 全局 color 在暗色模式下被主题盖过,锁 fill 到当前色 */
.im-conversation-group-side__icon-tile :deep(svg) {
- fill: currentColor !important;
+ fill: currentcolor !important;
}
/* 相邻信息行加分隔线; 相邻兄弟选择器无法用工具类表达 */
diff --git a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/conversation/conversation-private-side.vue b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/conversation/conversation-private-side.vue
index 761651ba0..e46621ef2 100644
--- a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/conversation/conversation-private-side.vue
+++ b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/conversation/conversation-private-side.vue
@@ -261,13 +261,13 @@ function handleGroupCreated(groupId: number) {
/* 「+」 tile: hover 时联动内部 icon-tile 走主色; 跨子元素的 hover 联动无法用单元素工具类表达 */
.im-conversation-private-side__tile-wrap-clickable:hover .im-conversation-private-side__icon-tile {
color: var(--ant-color-primary);
- border-color: var(--ant-color-primary);
background-color: var(--ant-color-primary-bg);
+ border-color: var(--ant-color-primary);
}
/* :deep 穿透 Icon 内部 svg; el-icon 全局 color 在暗色模式下被主题盖过,锁 fill 到当前色 */
.im-conversation-private-side__icon-tile :deep(svg) {
- fill: currentColor !important;
+ fill: currentcolor !important;
}
/* 相邻信息行加分隔线; 相邻兄弟选择器无法用工具类表达 */
diff --git a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/input/face-picker.vue b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/input/face-picker.vue
index 0a32854ad..fd3cc2fb1 100644
--- a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/input/face-picker.vue
+++ b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/input/face-picker.vue
@@ -340,14 +340,14 @@ onUnmounted(() => {
diff --git a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/input/message-input.vue b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/input/message-input.vue
index 4186a076c..67eb37335 100644
--- a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/input/message-input.vue
+++ b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/input/message-input.vue
@@ -1168,8 +1168,9 @@ async function onVideoPicked(e: Event) {
.message-input__tool:deep(svg) {
font-size: 18px !important;
color: var(--ant-color-text) !important;
- fill: currentColor !important;
+ fill: currentcolor !important;
}
+
.message-input__tool:hover,
.message-input__tool:hover:deep(svg) {
color: var(--ant-color-primary) !important;
@@ -1204,10 +1205,10 @@ async function onVideoPicked(e: Event) {
/* 用 data-empty 而非 :empty:浏览器在删空后会留下
,:empty 不命中;data-empty 由 syncEditorState 维护 */
.message-input__editor[data-empty]::before {
- content: attr(data-placeholder);
+ position: absolute;
color: var(--ant-color-text-placeholder);
pointer-events: none;
- position: absolute;
+ content: attr(data-placeholder);
}
/* @ token 走主色高亮;contenteditable=false 让 backspace 整段删而不是逐字符 */
diff --git a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/input/voice-recorder.vue b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/input/voice-recorder.vue
index a74bcc9f8..b63d7ae76 100644
--- a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/input/voice-recorder.vue
+++ b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/input/voice-recorder.vue
@@ -311,14 +311,14 @@ onUnmounted(() => {
diff --git a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue
index d2901978d..0258d9ead 100644
--- a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue
+++ b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue
@@ -193,15 +193,15 @@ async function handleRemove(pinnedMessage: Message) {
diff --git a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/group-request-pending.vue b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/group-request-pending.vue
index 4c401d7ee..02d29dd35 100644
--- a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/group-request-pending.vue
+++ b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/group-request-pending.vue
@@ -83,6 +83,6 @@ const pendingCount = computed(() => groupRequestStore.getUnhandledGroupRequestCo
diff --git a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/material-bubble.vue b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/material-bubble.vue
index afe25de0d..37f4fab80 100644
--- a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/material-bubble.vue
+++ b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/material-bubble.vue
@@ -145,7 +145,7 @@ const onClick = async () => {
transition: box-shadow 0.15s ease;
&:hover {
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+ box-shadow: 0 2px 12px rgb(0 0 0 / 8%);
}
}
diff --git a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/message-bubble.vue b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/message-bubble.vue
index d24d1f681..05c1c8147 100644
--- a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/message-bubble.vue
+++ b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/message-bubble.vue
@@ -359,29 +359,33 @@ onBeforeUnmount(() => {
颜色与气泡背景对应,留 1px 视觉吃进去,省一张图片 */
.message-bubble--other::before,
.message-bubble--self::before {
- content: '';
position: absolute;
top: 12px;
width: 0;
height: 0;
+ content: '';
border-style: solid;
}
+
.message-bubble--other {
--im-message-bubble-other-bg: #f5f5f5;
}
+
.message-bubble--other.message-bubble--text,
.message-bubble--other.message-bubble--voice {
background-color: var(--im-message-bubble-other-bg);
}
+
.message-bubble--other::before {
left: -5px;
- border-width: 5px 6px 5px 0;
border-color: transparent var(--im-message-bubble-other-bg) transparent transparent;
+ border-width: 5px 6px 5px 0;
}
+
.message-bubble--self::before {
right: -5px;
- border-width: 5px 0 5px 6px;
border-color: transparent transparent transparent #95ec69;
+ border-width: 5px 0 5px 6px;
}
/* 整体放进 :global(),避免 Vue scoped 把 `:global(.dark) .xxx` 塌缩成裸 `.dark` 而把变量刷到 */
@@ -393,6 +397,7 @@ onBeforeUnmount(() => {
.message-bubble__voice-icon :deep(svg) {
fill: #606266 !important;
}
+
.message-bubble__voice-icon.im-voice-playing :deep(svg) {
fill: #409eff !important;
}
@@ -407,6 +412,7 @@ onBeforeUnmount(() => {
100% {
transform: scale(1);
}
+
50% {
transform: scale(1.15);
}
diff --git a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/message-history.vue b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/message-history.vue
index 5ef46f33f..fbd900ab1 100644
--- a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/message-history.vue
+++ b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/message-history.vue
@@ -754,16 +754,18 @@ function locateMessage(messageId: number) {
position: absolute;
top: 100%;
right: 0;
- margin-top: 4px;
display: none;
- white-space: nowrap;
+ margin-top: 4px;
color: #1989fa;
+ white-space: nowrap;
cursor: pointer;
transition: color 0.15s;
}
+
.im-message-history__row:hover .im-message-history__locate {
display: block;
}
+
.im-message-history__locate:hover {
color: #146fc7;
}
@@ -775,16 +777,18 @@ function locateMessage(messageId: number) {
color: #1989fa;
transition: color 0.15s;
}
+
.im-message-history__tab:hover {
color: #2f81d4;
}
+
.im-message-history__tab--active::after {
- content: '';
position: absolute;
- left: 0;
right: 0;
bottom: 0;
+ left: 0;
height: 2px;
+ content: '';
background: #1989fa;
border-radius: 1px;
}
@@ -793,15 +797,18 @@ function locateMessage(messageId: number) {
.im-message-history__calendar :deep(.ant-picker-calendar-header) {
padding: 4px 8px;
}
+
.im-message-history__calendar :deep(.ant-picker-content) {
font-size: 12px;
}
+
.im-message-history__calendar :deep(.ant-picker-cell) {
padding: 1px 0;
}
+
.im-message-history__calendar :deep(.ant-picker-cell .ant-picker-calendar-date) {
height: 28px;
- margin: 0 2px;
padding: 2px 4px 0;
+ margin: 0 2px;
}
diff --git a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/message-item.vue b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/message-item.vue
index 1b3d0a87a..d73b56add 100644
--- a/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/message-item.vue
+++ b/apps/web-antdv-next/src/views/im/home/pages/conversation/components/message/message-item.vue
@@ -1047,6 +1047,7 @@ function handleDelete() {
.im-loading-spin {
animation: im-loading-spin 1s linear infinite;
}
+
@keyframes im-loading-spin {
to {
transform: rotate(360deg);
diff --git a/apps/web-ele/src/views/im/home/components/picker/conversation-picker-panel.vue b/apps/web-ele/src/views/im/home/components/picker/conversation-picker-panel.vue
index 7e50e3fc7..a1bc06e7f 100644
--- a/apps/web-ele/src/views/im/home/components/picker/conversation-picker-panel.vue
+++ b/apps/web-ele/src/views/im/home/components/picker/conversation-picker-panel.vue
@@ -363,6 +363,7 @@ function handleToggle(conversation: Conversation) {
.im-conversation-picker__recent::-webkit-scrollbar {
height: 4px;
}
+
.im-conversation-picker__recent::-webkit-scrollbar-thumb {
background-color: var(--ant-color-border);
border-radius: 2px;
diff --git a/apps/web-ele/src/views/im/home/components/rtc/rtc-call-participant-tile.vue b/apps/web-ele/src/views/im/home/components/rtc/rtc-call-participant-tile.vue
index 6a12111c2..63530767d 100644
--- a/apps/web-ele/src/views/im/home/components/rtc/rtc-call-participant-tile.vue
+++ b/apps/web-ele/src/views/im/home/components/rtc/rtc-call-participant-tile.vue
@@ -97,12 +97,14 @@ const setAudioRef = useMediaStreamElement(() => props.particip
.tile-dot {
animation: tile-dot 1.4s infinite ease-in-out both;
}
+
@keyframes tile-dot {
0%,
80%,
100% {
opacity: 0.3;
}
+
40% {
opacity: 1;
}
diff --git a/apps/web-ele/src/views/im/home/components/rtc/rtc-call-running.vue b/apps/web-ele/src/views/im/home/components/rtc/rtc-call-running.vue
index 9b8d774c8..ce945ab9b 100644
--- a/apps/web-ele/src/views/im/home/components/rtc/rtc-call-running.vue
+++ b/apps/web-ele/src/views/im/home/components/rtc/rtc-call-running.vue
@@ -279,11 +279,13 @@ const formattedDuration = computed(() =>
.reconnect-dot {
animation: reconnect-pulse 1s ease-in-out infinite;
}
+
@keyframes reconnect-pulse {
0%,
100% {
opacity: 0.3;
}
+
50% {
opacity: 1;
}
diff --git a/apps/web-ele/src/views/im/home/index.vue b/apps/web-ele/src/views/im/home/index.vue
index 639c1e74d..edc89b12f 100644
--- a/apps/web-ele/src/views/im/home/index.vue
+++ b/apps/web-ele/src/views/im/home/index.vue
@@ -254,6 +254,7 @@ watch(
--ant-color-text-secondary: hsl(var(--foreground) / 65%);
--ant-color-text-placeholder: hsl(var(--foreground) / 45%);
--ant-color-text-disabled: hsl(var(--foreground) / 30%);
+
/*
* fill 系列:浅色下用 Element Plus 风格的「冷调浅灰实色」而非半透明深色,
* 否则面板(会话列表 / 消息面板等)叠在灰底上会显脏发暗,和 Vue3+EP 的干净白差距明显。
diff --git a/apps/web-ele/src/views/im/home/pages/conversation/components/conversation/conversation-group-side.vue b/apps/web-ele/src/views/im/home/pages/conversation/components/conversation/conversation-group-side.vue
index ca75807dc..a26c288f3 100644
--- a/apps/web-ele/src/views/im/home/pages/conversation/components/conversation/conversation-group-side.vue
+++ b/apps/web-ele/src/views/im/home/pages/conversation/components/conversation/conversation-group-side.vue
@@ -812,13 +812,13 @@ function handleOpenTransferOwner() {
/* 「添加 / 移出」瓦片:hover 时联动内部 icon-tile 走主色,跨子元素的 hover 联动无法用单元素工具类表达 */
.im-conversation-group-side__tile-wrap:hover .im-conversation-group-side__icon-tile {
color: var(--ant-color-primary);
- border-color: var(--ant-color-primary);
background-color: var(--ant-color-primary-bg);
+ border-color: var(--ant-color-primary);
}
/* :deep 穿透 Icon 内部 svg; el-icon 全局 color 在暗色模式下被主题盖过,锁 fill 到当前色 */
.im-conversation-group-side__icon-tile :deep(svg) {
- fill: currentColor !important;
+ fill: currentcolor !important;
}
/* 相邻信息行加分隔线; 相邻兄弟选择器无法用工具类表达 */
diff --git a/apps/web-ele/src/views/im/home/pages/conversation/components/input/face-picker.vue b/apps/web-ele/src/views/im/home/pages/conversation/components/input/face-picker.vue
index 6187ca39e..d7d960952 100644
--- a/apps/web-ele/src/views/im/home/pages/conversation/components/input/face-picker.vue
+++ b/apps/web-ele/src/views/im/home/pages/conversation/components/input/face-picker.vue
@@ -340,14 +340,14 @@ onUnmounted(() => {
diff --git a/apps/web-ele/src/views/im/home/pages/conversation/components/input/message-input.vue b/apps/web-ele/src/views/im/home/pages/conversation/components/input/message-input.vue
index 746555b20..9d275b1a5 100644
--- a/apps/web-ele/src/views/im/home/pages/conversation/components/input/message-input.vue
+++ b/apps/web-ele/src/views/im/home/pages/conversation/components/input/message-input.vue
@@ -1168,8 +1168,9 @@ async function onVideoPicked(e: Event) {
.message-input__tool:deep(svg) {
font-size: 18px !important;
color: var(--ant-color-text) !important;
- fill: currentColor !important;
+ fill: currentcolor !important;
}
+
.message-input__tool:hover,
.message-input__tool:hover:deep(svg) {
color: var(--ant-color-primary) !important;
@@ -1204,10 +1205,10 @@ async function onVideoPicked(e: Event) {
/* 用 data-empty 而非 :empty:浏览器在删空后会留下
,:empty 不命中;data-empty 由 syncEditorState 维护 */
.message-input__editor[data-empty]::before {
- content: attr(data-placeholder);
+ position: absolute;
color: var(--ant-color-text-placeholder);
pointer-events: none;
- position: absolute;
+ content: attr(data-placeholder);
}
/* @ token 走主色高亮;contenteditable=false 让 backspace 整段删而不是逐字符 */
diff --git a/apps/web-ele/src/views/im/home/pages/conversation/components/input/voice-recorder.vue b/apps/web-ele/src/views/im/home/pages/conversation/components/input/voice-recorder.vue
index f2709cdeb..dfd3651cc 100644
--- a/apps/web-ele/src/views/im/home/pages/conversation/components/input/voice-recorder.vue
+++ b/apps/web-ele/src/views/im/home/pages/conversation/components/input/voice-recorder.vue
@@ -6,257 +6,276 @@ import {
onUnmounted,
ref,
useTemplateRef,
- watch
-} from 'vue'
+ watch,
+} from 'vue';
-import { ElButton, ElMessage } from 'element-plus'
+import { ElButton, ElMessage } from 'element-plus';
-import { formatSeconds } from '#/views/im/utils/time'
+import { formatSeconds } from '#/views/im/utils/time';
-defineOptions({ name: 'ImVoiceRecorder' })
+defineOptions({ name: 'ImVoiceRecorder' });
const props = withDefaults(
defineProps<{
- maxDuration?: number // 最长录制秒数
- modelValue: boolean // 是否显示
+ maxDuration?: number; // 最长录制秒数
+ modelValue: boolean; // 是否显示
}>(),
{
- maxDuration: 60
- }
-)
+ maxDuration: 60,
+ },
+);
const emit = defineEmits<{
- send: [payload: { blob: Blob; duration: number; extension: string; mimeType: string }] // 录制完成数据
- 'update:modelValue': [value: boolean]
-}>()
+ send: [
+ payload: {
+ blob: Blob;
+ duration: number;
+ extension: string;
+ mimeType: string;
+ },
+ ]; // 录制完成数据
+ 'update:modelValue': [value: boolean];
+}>();
const VOICE_MIME_TYPE_OPTIONS = [
{ extension: 'webm', mimeType: 'audio/webm;codecs=opus' },
{ extension: 'webm', mimeType: 'audio/webm' },
{ extension: 'm4a', mimeType: 'audio/mp4' },
{ extension: 'ogg', mimeType: 'audio/ogg;codecs=opus' },
- { extension: 'ogg', mimeType: 'audio/ogg' }
-]
+ { extension: 'ogg', mimeType: 'audio/ogg' },
+];
const visible = computed({
get: () => props.modelValue,
- set: (v) => emit('update:modelValue', v)
-})
+ set: (v) => emit('update:modelValue', v),
+});
-const rootRef = useTemplateRef('rootRef')
+const rootRef = useTemplateRef('rootRef');
/** 录制状态 */
-type Status = 'idle' | 'preview' | 'recording'
-const status = ref('idle')
-const duration = ref(0)
-const previewUrl = ref('')
+type Status = 'idle' | 'preview' | 'recording';
+const status = ref('idle');
+const duration = ref(0);
+const previewUrl = ref('');
-let mediaRecorder: MediaRecorder | null = null
-let audioChunks: Blob[] = []
-let mediaStream: MediaStream | null = null
-let timer: null | ReturnType = null
-let recordedBlob: Blob | null = null
-let discarding = false
-let recordingMimeType = ''
-let recordingExtension = ''
-let recordedMimeType = ''
-let recordedExtension = ''
+let mediaRecorder: MediaRecorder | null = null;
+let audioChunks: Blob[] = [];
+let mediaStream: MediaStream | null = null;
+let timer: null | ReturnType = null;
+let recordedBlob: Blob | null = null;
+let discarding = false;
+let recordingMimeType = '';
+let recordingExtension = '';
+let recordedMimeType = '';
+let recordedExtension = '';
/** 计时器文案 */
-const timerText = computed(() => formatSeconds(duration.value))
+const timerText = computed(() => formatSeconds(duration.value));
/** 监听面板显示状态 */
watch(visible, (v) => {
if (v) {
- document.addEventListener('click', handleDocumentClick)
+ document.addEventListener('click', handleDocumentClick);
} else {
- document.removeEventListener('click', handleDocumentClick)
- resetAll()
+ document.removeEventListener('click', handleDocumentClick);
+ resetAll();
}
-})
+});
/** 处理外部点击 */
function handleDocumentClick(e: MouseEvent) {
if (!props.modelValue || !rootRef.value) {
- return
+ return;
}
if (rootRef.value.contains(e.target as Node)) {
- return
+ return;
}
if (status.value !== 'idle') {
- return
+ return;
}
- visible.value = false
+ visible.value = false;
}
/** 获取支持的录音格式 */
function getSupportedVoiceMimeType() {
if (typeof MediaRecorder === 'undefined' || !MediaRecorder.isTypeSupported) {
- return undefined
+ return undefined;
}
- return VOICE_MIME_TYPE_OPTIONS.find((item) => MediaRecorder.isTypeSupported(item.mimeType))
+ return VOICE_MIME_TYPE_OPTIONS.find((item) =>
+ MediaRecorder.isTypeSupported(item.mimeType),
+ );
}
/** 获取录音文件后缀 */
function getVoiceExtension(mimeType: string) {
- const normalizedMimeType = mimeType.split(';')[0]
+ const normalizedMimeType = mimeType.split(';')[0];
if (normalizedMimeType === 'audio/mp4') {
- return 'm4a'
+ return 'm4a';
}
if (normalizedMimeType === 'audio/ogg') {
- return 'ogg'
+ return 'ogg';
}
- return 'webm'
+ return 'webm';
}
/** 创建录音器 */
function createVoiceRecorder(stream: MediaStream) {
- const supportedMimeType = getSupportedVoiceMimeType()
+ const supportedMimeType = getSupportedVoiceMimeType();
const recorder = supportedMimeType
? new MediaRecorder(stream, { mimeType: supportedMimeType.mimeType })
- : new MediaRecorder(stream)
- recordingMimeType = recorder.mimeType || supportedMimeType?.mimeType || ''
- recordingExtension = supportedMimeType?.extension || (recordingMimeType ? getVoiceExtension(recordingMimeType) : '')
- return recorder
+ : new MediaRecorder(stream);
+ recordingMimeType = recorder.mimeType || supportedMimeType?.mimeType || '';
+ recordingExtension =
+ supportedMimeType?.extension ||
+ (recordingMimeType ? getVoiceExtension(recordingMimeType) : '');
+ return recorder;
}
/** 开始录制 */
async function startRecord() {
- if (typeof MediaRecorder === 'undefined' || !navigator.mediaDevices?.getUserMedia) {
- ElMessage.error('当前浏览器不支持录音(需要 HTTPS 或 localhost)')
- return
+ if (
+ typeof MediaRecorder === 'undefined' ||
+ !navigator.mediaDevices?.getUserMedia
+ ) {
+ ElMessage.error('当前浏览器不支持录音(需要 HTTPS 或 localhost)');
+ return;
}
try {
- mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })
+ mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
} catch {
- ElMessage.error('无法获取麦克风权限')
- return
+ ElMessage.error('无法获取麦克风权限');
+ return;
}
- audioChunks = []
- discarding = false
+ audioChunks = [];
+ discarding = false;
try {
- mediaRecorder = createVoiceRecorder(mediaStream)
+ mediaRecorder = createVoiceRecorder(mediaStream);
} catch {
- cleanupStream()
- ElMessage.error('当前浏览器不支持录音格式')
- return
+ cleanupStream();
+ ElMessage.error('当前浏览器不支持录音格式');
+ return;
}
mediaRecorder.addEventListener('dataavailable', (event: BlobEvent) => {
if (event.data.size > 0) {
- audioChunks.push(event.data)
+ audioChunks.push(event.data);
}
- })
+ });
mediaRecorder.addEventListener('stop', () => {
if (discarding) {
- return
+ return;
}
- recordedMimeType = recordingMimeType || mediaRecorder?.mimeType || audioChunks[0]?.type || 'audio/webm'
- recordedExtension = recordingExtension || getVoiceExtension(recordedMimeType)
- recordedBlob = new Blob(audioChunks, { type: recordedMimeType })
- previewUrl.value = URL.createObjectURL(recordedBlob)
- status.value = 'preview'
- })
- mediaRecorder.start()
- status.value = 'recording'
- duration.value = 0
+ recordedMimeType =
+ recordingMimeType ||
+ mediaRecorder?.mimeType ||
+ audioChunks[0]?.type ||
+ 'audio/webm';
+ recordedExtension =
+ recordingExtension || getVoiceExtension(recordedMimeType);
+ recordedBlob = new Blob(audioChunks, { type: recordedMimeType });
+ previewUrl.value = URL.createObjectURL(recordedBlob);
+ status.value = 'preview';
+ });
+ mediaRecorder.start();
+ status.value = 'recording';
+ duration.value = 0;
timer = setInterval(() => {
- duration.value++
+ duration.value++;
if (duration.value >= props.maxDuration) {
- stopRecord()
+ stopRecord();
}
- }, 1000)
+ }, 1000);
}
/** 停止录制 */
function stopRecord() {
if (duration.value < 1) {
- ElMessage.warning('录音时间太短')
- resetAll()
- return
+ ElMessage.warning('录音时间太短');
+ resetAll();
+ return;
}
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
- mediaRecorder.stop()
+ mediaRecorder.stop();
}
- cleanupStream()
+ cleanupStream();
if (timer) {
- clearInterval(timer)
- timer = null
+ clearInterval(timer);
+ timer = null;
}
}
/** 重新录制 */
function restart() {
- clearPreview()
- duration.value = 0
- status.value = 'idle'
+ clearPreview();
+ duration.value = 0;
+ status.value = 'idle';
}
/** 发送录音 */
function handleSend() {
if (!recordedBlob) {
- return
+ return;
}
emit('send', {
blob: recordedBlob,
duration: duration.value,
extension: recordedExtension,
- mimeType: recordedMimeType || recordedBlob.type
- })
- visible.value = false
+ mimeType: recordedMimeType || recordedBlob.type,
+ });
+ visible.value = false;
}
/** 取消录制 */
function handleCancel() {
- visible.value = false
+ visible.value = false;
}
/** 重置录制资源 */
function resetAll() {
- discarding = true
+ discarding = true;
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
- mediaRecorder.stop()
+ mediaRecorder.stop();
}
- cleanupStream()
+ cleanupStream();
if (timer) {
- clearInterval(timer)
- timer = null
+ clearInterval(timer);
+ timer = null;
}
- audioChunks = []
- duration.value = 0
- status.value = 'idle'
- recordingMimeType = ''
- recordingExtension = ''
- recordedMimeType = ''
- recordedExtension = ''
- clearPreview()
+ audioChunks = [];
+ duration.value = 0;
+ status.value = 'idle';
+ recordingMimeType = '';
+ recordingExtension = '';
+ recordedMimeType = '';
+ recordedExtension = '';
+ clearPreview();
}
/** 释放预览音频 */
function clearPreview() {
- recordedBlob = null
+ recordedBlob = null;
if (previewUrl.value) {
- URL.revokeObjectURL(previewUrl.value)
- previewUrl.value = ''
+ URL.revokeObjectURL(previewUrl.value);
+ previewUrl.value = '';
}
}
/** 关闭麦克风采集 */
function cleanupStream() {
- mediaStream?.getTracks().forEach((t) => t.stop())
- mediaStream = null
+ mediaStream?.getTracks().forEach((t) => t.stop());
+ mediaStream = null;
}
onMounted(() => {
if (props.modelValue) {
- document.addEventListener('click', handleDocumentClick)
+ document.addEventListener('click', handleDocumentClick);
}
-})
+});
-onBeforeUnmount(resetAll)
+onBeforeUnmount(resetAll);
onUnmounted(() => {
- document.removeEventListener('click', handleDocumentClick)
-})
+ document.removeEventListener('click', handleDocumentClick);
+});
@@ -269,14 +288,18 @@ onUnmounted(() => {
>
-
+
{{ timerText }}
点击下方按钮开始录制
- 录制中,最长 {{ maxDuration }} 秒
+ 录制中,最长 {{ maxDuration }} 秒
录制完成,可试听后发送
@@ -284,7 +307,9 @@ onUnmounted(() => {
@@ -293,16 +318,22 @@ onUnmounted(() => {
取消
- 开始录制
+
+ 开始录制
+
取消
- 停止录制
+
+ 停止录制
+
取消
重新录制
- 发送
+
+ 发送
+
@@ -311,14 +342,15 @@ onUnmounted(() => {
diff --git a/apps/web-ele/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue b/apps/web-ele/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue
index 6534146fd..5fe93cd35 100644
--- a/apps/web-ele/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue
+++ b/apps/web-ele/src/views/im/home/pages/conversation/components/message/group-pinned-message.vue
@@ -193,15 +193,15 @@ async function handleRemove(pinnedMessage: Message) {
diff --git a/apps/web-ele/src/views/im/home/pages/conversation/components/message/group-request-pending.vue b/apps/web-ele/src/views/im/home/pages/conversation/components/message/group-request-pending.vue
index 4c401d7ee..02d29dd35 100644
--- a/apps/web-ele/src/views/im/home/pages/conversation/components/message/group-request-pending.vue
+++ b/apps/web-ele/src/views/im/home/pages/conversation/components/message/group-request-pending.vue
@@ -83,6 +83,6 @@ const pendingCount = computed(() => groupRequestStore.getUnhandledGroupRequestCo
diff --git a/apps/web-ele/src/views/im/home/pages/conversation/components/message/material-bubble.vue b/apps/web-ele/src/views/im/home/pages/conversation/components/message/material-bubble.vue
index 7a950415d..b2da6dd44 100644
--- a/apps/web-ele/src/views/im/home/pages/conversation/components/message/material-bubble.vue
+++ b/apps/web-ele/src/views/im/home/pages/conversation/components/message/material-bubble.vue
@@ -145,7 +145,7 @@ const onClick = async () => {
transition: box-shadow 0.15s ease;
&:hover {
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+ box-shadow: 0 2px 12px rgb(0 0 0 / 8%);
}
}
diff --git a/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-bubble.vue b/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-bubble.vue
index dc5893b09..beab66998 100644
--- a/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-bubble.vue
+++ b/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-bubble.vue
@@ -360,29 +360,33 @@ onBeforeUnmount(() => {
颜色与气泡背景对应,留 1px 视觉吃进去,省一张图片 */
.message-bubble--other::before,
.message-bubble--self::before {
- content: '';
position: absolute;
top: 12px;
width: 0;
height: 0;
+ content: '';
border-style: solid;
}
+
.message-bubble--other {
--im-message-bubble-other-bg: #f5f5f5;
}
+
.message-bubble--other.message-bubble--text,
.message-bubble--other.message-bubble--voice {
background-color: var(--im-message-bubble-other-bg);
}
+
.message-bubble--other::before {
left: -5px;
- border-width: 5px 6px 5px 0;
border-color: transparent var(--im-message-bubble-other-bg) transparent transparent;
+ border-width: 5px 6px 5px 0;
}
+
.message-bubble--self::before {
right: -5px;
- border-width: 5px 0 5px 6px;
border-color: transparent transparent transparent #95ec69;
+ border-width: 5px 0 5px 6px;
}
/* 整体放进 :global(),避免 Vue scoped 把 `:global(.dark) .xxx` 塌缩成裸 `.dark` 而把变量刷到 */
@@ -394,6 +398,7 @@ onBeforeUnmount(() => {
.message-bubble__voice-icon :deep(svg) {
fill: #606266 !important;
}
+
.message-bubble__voice-icon.im-voice-playing :deep(svg) {
fill: #409eff !important;
}
@@ -408,6 +413,7 @@ onBeforeUnmount(() => {
100% {
transform: scale(1);
}
+
50% {
transform: scale(1.15);
}
diff --git a/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-history.vue b/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-history.vue
index 8ad6ecd1a..b0cd058c2 100644
--- a/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-history.vue
+++ b/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-history.vue
@@ -753,16 +753,18 @@ function locateMessage(messageId: number) {
position: absolute;
top: 100%;
right: 0;
- margin-top: 4px;
display: none;
- white-space: nowrap;
+ margin-top: 4px;
color: #1989fa;
+ white-space: nowrap;
cursor: pointer;
transition: color 0.15s;
}
+
.im-message-history__row:hover .im-message-history__locate {
display: block;
}
+
.im-message-history__locate:hover {
color: #146fc7;
}
@@ -774,16 +776,18 @@ function locateMessage(messageId: number) {
color: #1989fa;
transition: color 0.15s;
}
+
.im-message-history__tab:hover {
color: #2f81d4;
}
+
.im-message-history__tab--active::after {
- content: '';
position: absolute;
- left: 0;
right: 0;
bottom: 0;
+ left: 0;
height: 2px;
+ content: '';
background: #1989fa;
border-radius: 1px;
}
@@ -792,15 +796,18 @@ function locateMessage(messageId: number) {
.im-message-history__calendar :deep(.el-calendar__header) {
padding: 4px 8px;
}
+
.im-message-history__calendar :deep(.el-calendar-table) {
font-size: 12px;
}
+
.im-message-history__calendar :deep(.el-calendar-table td) {
padding: 1px 0;
}
+
.im-message-history__calendar :deep(.el-calendar-day) {
height: 28px;
- margin: 0 2px;
padding: 2px 4px 0;
+ margin: 0 2px;
}
diff --git a/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-item.vue b/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-item.vue
index 3c2c90723..bc201d224 100644
--- a/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-item.vue
+++ b/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-item.vue
@@ -1047,6 +1047,7 @@ function handleDelete() {
.im-loading-spin {
animation: im-loading-spin 1s linear infinite;
}
+
@keyframes im-loading-spin {
to {
transform: rotate(360deg);
diff --git a/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-panel.vue b/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-panel.vue
index 80e91675d..72766b31c 100644
--- a/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-panel.vue
+++ b/apps/web-ele/src/views/im/home/pages/conversation/components/message/message-panel.vue
@@ -757,17 +757,18 @@ watch(
width: 20px;
height: 20px;
color: var(--ant-color-text) !important;
- fill: currentColor !important;
+ fill: currentcolor !important;
transition: color 0.15s;
}
.message-panel__header-icon {
box-sizing: content-box;
- border: 0;
- background: transparent;
- cursor: pointer;
padding: 4px;
+ cursor: pointer;
+ background: transparent;
+ border: 0;
}
+
.message-panel__header-icon:hover,
.message-panel__header-icon:hover :deep(svg) {
color: var(--ant-color-primary) !important;
@@ -785,6 +786,7 @@ watch(
.message-panel__message-anchor {
transition: background-color 0.6s ease;
}
+
.message-panel__message-anchor--highlight {
background-color: var(--ant-color-warning-bg);
}