From f0e9f27c5fbf28759c5f6520ba49af5649714bb1 Mon Sep 17 00:00:00 2001
From: Dawn <2439646234@qq.com>
Date: Tue, 18 Mar 2025 10:41:00 +0800
Subject: [PATCH] =?UTF-8?q?feat(component):=20:sparkles:=20=E5=A2=9E?=
=?UTF-8?q?=E5=8A=A0=E8=A1=A8=E6=83=85=E5=8C=85=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/codecov.yml | 45 ----------
README.en.md | 4 +-
README.md | 4 +-
package.json | 1 -
pnpm-lock.yaml | 62 --------------
src/components/common/VirtualList.vue | 1 +
.../rightBox/chatBox/ChatFooter.vue | 83 +++++++++++++++----
src/components/rightBox/emoticon/index.vue | 75 +++++++++++++++--
src/hooks/useChatMain.ts | 36 ++++++--
src/services/apis.ts | 2 +-
src/stores/emoji.ts | 4 +-
src/stores/history.ts | 11 ++-
vite.config.ts | 7 --
13 files changed, 181 insertions(+), 154 deletions(-)
delete mode 100644 .github/workflows/codecov.yml
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
deleted file mode 100644
index 058a3fb8..00000000
--- a/.github/workflows/codecov.yml
+++ /dev/null
@@ -1,45 +0,0 @@
-name: Codecov
-
-on:
- pull_request:
- branches: [ master ]
- paths:
- - 'src/**'
- - 'test/**'
-
-jobs:
- codecov:
- # 跳过 Renovate PR
- if: |
- github.actor != 'renovate[bot]' &&
- github.actor != 'renovate-preview[bot]'
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
-
- # 首先安装 pnpm
- - name: Install pnpm
- uses: pnpm/action-setup@v4
- with:
- version: 9
- run_install: false
-
- # 然后设置 Node.js
- - name: Setup Node
- uses: actions/setup-node@v4
- with:
- node-version: '22'
- cache: 'pnpm'
-
- - name: Install dependencies
- run: pnpm install
-
- - name: Run tests with coverage
- run: pnpm coverage
-
- - name: Upload results to Codecov
- uses: codecov/codecov-action@v5
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- slug: HuLaSpark/HuLa
\ No newline at end of file
diff --git a/README.en.md b/README.en.md
index eee0c569..31dae300 100644
--- a/README.en.md
+++ b/README.en.md
@@ -5,9 +5,6 @@
-
-
-

@@ -89,6 +86,7 @@ HuLa is an instant messaging system developed with Tauri, Vite 6, Vue 3, and Typ
| | Message Recall | ✅ Completed |
| | @Mention, Reply Function | ✅ Completed |
| | Message Read Status | ✅ Completed |
+| | Emoji Function | ✅ Completed |
| | Context Menu Based on Message Type | ✅ Completed |
| 👥 **Contact & Group Management** | Friend Add/Remove | ✅ Completed |
| | Friend Search | ✅ Completed |
diff --git a/README.md b/README.md
index eb65543c..52d06972 100644
--- a/README.md
+++ b/README.md
@@ -5,9 +5,6 @@
一款基于Tauri、Vite 6、Vue 3 和 TypeScript 构建的即时通讯系统
-
-
-

@@ -89,6 +86,7 @@ HuLa 是一款基于 Tauri、Vite 6、Vue 3 和 TypeScript 构建的即时通讯
| | 消息撤回 | ✅ 已完成 |
| | @提醒、回复功能 | ✅ 已完成 |
| | 消息已读状态 | ✅ 已完成 |
+| | 表情包功能 | ✅ 已完成 |
| | 根据消息类型右键菜单 | ✅ 已完成 |
| 👥 **联系人、群聊管理** | 好友添加与删除 | ✅ 已完成 |
| | 好友搜索 | ✅ 已完成 |
diff --git a/package.json b/package.json
index b620f58c..4ebc0915 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,6 @@
},
"dependencies": {
"@actions/github": "^6.0.0",
- "@codecov/vite-plugin": "^1.6.0",
"@fingerprintjs/fingerprintjs": "^4.6.0",
"@tauri-apps/api": "2.3.0",
"@tauri-apps/plugin-autostart": "2.2.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e5ba8fe6..1a167c44 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,9 +11,6 @@ importers:
'@actions/github':
specifier: ^6.0.0
version: 6.0.0
- '@codecov/vite-plugin':
- specifier: ^1.6.0
- version: 1.7.0(vite@6.2.2(@types/node@22.10.7)(jiti@2.4.2)(sass@1.83.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))
'@fingerprintjs/fingerprintjs':
specifier: ^4.6.0
version: 4.6.0
@@ -240,21 +237,12 @@ importers:
packages:
- '@actions/core@1.11.1':
- resolution: {integrity: sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==}
-
- '@actions/exec@1.1.1':
- resolution: {integrity: sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==}
-
'@actions/github@6.0.0':
resolution: {integrity: sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==}
'@actions/http-client@2.2.3':
resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==}
- '@actions/io@1.1.3':
- resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
-
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
@@ -443,16 +431,6 @@ packages:
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
- '@codecov/bundler-plugin-core@1.7.0':
- resolution: {integrity: sha512-QsLwtwfy9KEe0CjqNE2Z/SPiCMn4CHAJ9cqTosZCX9YMKPi/WyFivv0pYE7CXA8ntG0l4Xc9kr36DUCuNRW0LQ==}
- engines: {node: '>=18.0.0'}
-
- '@codecov/vite-plugin@1.7.0':
- resolution: {integrity: sha512-Cy/NxuEiGCf9X62dKewxnFf6Uv6CxfCCJAd8AfbOYbg96vwjwuq6BMoCIAk9gUFmwurw9EOKGPwoW0lIrUMIzA==}
- engines: {node: '>=18.0.0'}
- peerDependencies:
- vite: 4.x || 5.x || 6.x
-
'@commitlint/cli@19.6.1':
resolution: {integrity: sha512-8hcyA6ZoHwWXC76BoC8qVOSr8xHy00LZhZpauiD0iO0VYbVhMnED0da85lTfIULxl7Lj4c6vZgF0Wu/ed1+jlQ==}
engines: {node: '>=v18'}
@@ -4810,10 +4788,6 @@ packages:
'@nuxt/kit':
optional: true
- unplugin@1.16.0:
- resolution: {integrity: sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==}
- engines: {node: '>=14.0.0'}
-
unplugin@1.16.1:
resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==}
engines: {node: '>=14.0.0'}
@@ -5127,20 +5101,8 @@ packages:
resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==}
engines: {node: '>=18'}
- zod@3.24.1:
- resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
-
snapshots:
- '@actions/core@1.11.1':
- dependencies:
- '@actions/exec': 1.1.1
- '@actions/http-client': 2.2.3
-
- '@actions/exec@1.1.1':
- dependencies:
- '@actions/io': 1.1.3
-
'@actions/github@6.0.0':
dependencies:
'@actions/http-client': 2.2.3
@@ -5153,8 +5115,6 @@ snapshots:
tunnel: 0.0.6
undici: 5.28.5
- '@actions/io@1.1.3': {}
-
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.8
@@ -5433,21 +5393,6 @@ snapshots:
'@bcoe/v8-coverage@1.0.2': {}
- '@codecov/bundler-plugin-core@1.7.0':
- dependencies:
- '@actions/core': 1.11.1
- '@actions/github': 6.0.0
- chalk: 4.1.2
- semver: 7.7.1
- unplugin: 1.16.0
- zod: 3.24.1
-
- '@codecov/vite-plugin@1.7.0(vite@6.2.2(@types/node@22.10.7)(jiti@2.4.2)(sass@1.83.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))':
- dependencies:
- '@codecov/bundler-plugin-core': 1.7.0
- unplugin: 1.16.0
- vite: 6.2.2(@types/node@22.10.7)(jiti@2.4.2)(sass@1.83.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
-
'@commitlint/cli@19.6.1(@types/node@22.10.7)(typescript@5.7.2)':
dependencies:
'@commitlint/format': 19.5.0
@@ -10061,11 +10006,6 @@ snapshots:
- rollup
- supports-color
- unplugin@1.16.0:
- dependencies:
- acorn: 8.14.0
- webpack-virtual-modules: 0.6.2
-
unplugin@1.16.1:
dependencies:
acorn: 8.14.0
@@ -10416,5 +10356,3 @@ snapshots:
yocto-queue@1.1.1: {}
yoctocolors-cjs@2.1.2: {}
-
- zod@3.24.1: {}
diff --git a/src/components/common/VirtualList.vue b/src/components/common/VirtualList.vue
index f6cb93a2..8d87fc2f 100644
--- a/src/components/common/VirtualList.vue
+++ b/src/components/common/VirtualList.vue
@@ -537,6 +537,7 @@ defineExpose
({
padding-right: 6px;
/* 兼容性写法,确保在各种浏览器中都有一致的滚动条空间 */
overflow-y: scroll;
+ overscroll-behavior: none; /* 禁止mac的“触底反弹”效果 */
/* 滚动条样式 */
&::-webkit-scrollbar {
diff --git a/src/components/rightBox/chatBox/ChatFooter.vue b/src/components/rightBox/chatBox/ChatFooter.vue
index 1ca14145..d4992c11 100644
--- a/src/components/rightBox/chatBox/ChatFooter.vue
+++ b/src/components/rightBox/chatBox/ChatFooter.vue
@@ -28,13 +28,33 @@
-webkit-backdrop-filter: blur(10px);
box-shadow: 2px 2px 12px 2px var(--box-shadow-color);
border: 1px solid var(--box-shadow-color);
+ width: auto;
">
-
+
- 表情
+
+
最近使用
+
+
+
![]()
+
{{ emoji }}
+
+
+
@@ -109,17 +129,36 @@ import { emitTo } from '@tauri-apps/api/event'
import { useGlobalStore } from '@/stores/global.ts'
import type { ContactItem, SessionItem } from '@/services/types'
import { useContactStore } from '@/stores/contacts'
+import { useHistoryStore } from '@/stores/history'
const { id } = defineProps<{
id: SessionItem['id']
}>()
const globalStore = useGlobalStore()
const contactStore = useContactStore()
+const historyStore = useHistoryStore()
const { open, onChange, reset } = useFileDialog()
const MsgInputRef = ref()
const msgInputDom = ref(null)
-const emojiShow = ref()
+const emojiShow = ref(false)
+const recentlyTip = ref(false)
+const recentEmojis = computed(() => {
+ return historyStore.emoji.slice(0, 15)
+})
const { insertNodeAtRange, triggerInputEvent, imgPaste, FileOrVideoPaste } = useCommon()
+
+/**
+ * 检查字符串是否为URL
+ */
+const checkIsUrl = (str: string) => {
+ try {
+ new URL(str)
+ return true
+ } catch {
+ return false
+ }
+}
+
// 判断是否为单聊
const isSingleChat = computed(() => {
return globalStore.currentSession?.type === RoomTypeEnum.SINGLE
@@ -131,6 +170,13 @@ const isFriend = computed(() => {
return contactStore.contactsList.some((contact: ContactItem) => contact.uid === id)
})
+// 监听emojiShow的变化,当emojiShow为true时关闭recentlyTip
+watch(emojiShow, (newValue) => {
+ if (newValue === true) {
+ recentlyTip.value = false
+ }
+})
+
/**
* 选择表情,并把表情插入输入框
* @param item 选择的表情
@@ -145,16 +191,6 @@ const emojiHandle = (item: string, type: 'emoji' | 'emoji-url' = 'emoji') => {
// 确保输入框有焦点
inp.focus()
- // 检查是否为 URL
- const isUrl = (str: string) => {
- try {
- new URL(str)
- return true
- } catch {
- return false
- }
- }
-
// 尝试获取最后的编辑范围
let lastEditRange: SelectionRange | null = MsgInputRef.value?.getLastEditRange()
@@ -186,7 +222,7 @@ const emojiHandle = (item: string, type: 'emoji' | 'emoji-url' = 'emoji') => {
}
// 根据内容类型插入不同的节点
- if (isUrl(item)) {
+ if (checkIsUrl(item)) {
// 如果是URL,创建图片元素并插入
const imgElement = document.createElement('img')
imgElement.src = item
@@ -215,6 +251,23 @@ const emojiHandle = (item: string, type: 'emoji' | 'emoji-url' = 'emoji') => {
// 保持焦点在输入框
inp.focus()
+
+ // 添加到最近使用表情列表
+ updateRecentEmojis(item)
+}
+
+/**
+ * 更新最近使用的表情列表
+ */
+const updateRecentEmojis = (emoji: string) => {
+ const currentEmojis = [...historyStore.emoji]
+ const index = currentEmojis.indexOf(emoji)
+ if (index !== -1) {
+ currentEmojis.splice(index, 1)
+ }
+ currentEmojis.unshift(emoji)
+ const updatedEmojis = currentEmojis.slice(0, 15)
+ historyStore.setEmoji(updatedEmojis)
}
const handleCap = async () => {
@@ -250,7 +303,7 @@ onChange((files) => {
reset()
})
-onMounted(() => {
+onMounted(async () => {
if (MsgInputRef.value) {
msgInputDom.value = MsgInputRef.value.messageInputDom
}
diff --git a/src/components/rightBox/emoticon/index.vue b/src/components/rightBox/emoticon/index.vue
index e18a2595..1f018ef5 100644
--- a/src/components/rightBox/emoticon/index.vue
+++ b/src/components/rightBox/emoticon/index.vue
@@ -11,7 +11,7 @@
align="center"
justify="center"
class="emoji-item"
- v-for="(item, index) in [...new Set(emojiRef.historyList)]"
+ v-for="(item, index) in [...new Set(emojiRef.historyList)].filter((emoji) => !checkIsUrl(emoji))"
:key="index"
@click.stop="chooseEmoji(item)">
{{ item }}
@@ -64,7 +64,36 @@
-
暂无实现
+
+ 我的表情包
+
+
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
暂无表情包
@@ -101,6 +130,7 @@
diff --git a/src/hooks/useChatMain.ts b/src/hooks/useChatMain.ts
index 92d0b6f1..759fd7db 100644
--- a/src/hooks/useChatMain.ts
+++ b/src/hooks/useChatMain.ts
@@ -15,6 +15,7 @@ import { save } from '@tauri-apps/plugin-dialog'
import { useDownload } from '@/hooks/useDownload'
import { useGroupStore } from '@/stores/group'
import { useWindow } from './useWindow'
+import { useEmojiStore } from '@/stores/emoji'
export const useChatMain = () => {
const { removeTag, openMsgSession, userUid } = useCommon()
@@ -24,6 +25,7 @@ export const useChatMain = () => {
const globalStore = useGlobalStore()
const groupStore = useGroupStore()
const chatStore = useChatStore()
+ const emojiStore = useEmojiStore()
const userStore = useUserStore()?.userInfo
const { downloadFile } = useDownload()
// const userInfo = useUserStore()?.userInfo
@@ -51,9 +53,17 @@ export const useChatMain = () => {
{
label: '转发',
icon: 'share',
- click: () => {}
+ click: () => {
+ window.$message.warning('暂未实现')
+ }
+ },
+ {
+ label: '收藏',
+ icon: 'collection-files',
+ click: () => {
+ window.$message.warning('暂未实现')
+ }
},
- { label: '收藏', icon: 'collection-files' },
{
label: '回复',
icon: 'reply',
@@ -110,6 +120,9 @@ export const useChatMain = () => {
} else {
delete item.message.body.translatedText
}
+ },
+ visible: (item: MessageType) => {
+ return item.message.type === MsgEnum.TEXT
}
},
...commonMenuList.value
@@ -146,13 +159,6 @@ export const useChatMain = () => {
])
/** 图片类型右键菜单 */
const imageMenuList = ref
([
- {
- label: '添加到表情',
- icon: 'add-expression',
- click: (item: any) => {
- console.log(item)
- }
- },
{
label: '复制',
icon: 'copy',
@@ -188,6 +194,18 @@ export const useChatMain = () => {
window.$message.error('保存图片失败')
}
}
+ },
+ {
+ label: '添加到表情',
+ icon: 'add-expression',
+ click: async (item: MessageType) => {
+ const imageUrl = item.message.body.url
+ if (!imageUrl) {
+ window.$message.error('获取图片地址失败')
+ return
+ }
+ await emojiStore.addEmoji(imageUrl)
+ }
}
// {
// label: '在文件夹中显示',
diff --git a/src/services/apis.ts b/src/services/apis.ts
index 145c706c..b0ec47aa 100644
--- a/src/services/apis.ts
+++ b/src/services/apis.ts
@@ -70,7 +70,7 @@ export default {
/** 上传头像 */
uploadAvatar: (data: { avatar: string }) => POST(urls.uploadAvatar, data),
/** 新增表情包 */
- addEmoji: (data: { uid: string; expressionUrl: string }) => POST(urls.addEmoji, data),
+ addEmoji: (data: { expressionUrl: string }) => POST(urls.addEmoji, data),
/** 获取表情 **/
getEmoji: (params: { uid: string }) => GET(urls.getEmoji, { params }),
/** 删除Emoji */
diff --git a/src/stores/emoji.ts b/src/stores/emoji.ts
index aa12e034..ecadefbc 100644
--- a/src/stores/emoji.ts
+++ b/src/stores/emoji.ts
@@ -27,9 +27,9 @@ export const useEmojiStore = defineStore(StoresEnum.EMOJI, () => {
const addEmoji = async (emojiUrl: string) => {
const { uid } = userStore.userInfo
if (!uid || !emojiUrl) return
- apis.addEmoji({ uid, expressionUrl: emojiUrl }).then((res) => {
+ apis.addEmoji({ expressionUrl: emojiUrl }).then((res) => {
if (res) {
- window.$message.success('添加成功')
+ window.$message.success('添加表情成功')
}
})
await getEmojiList()
diff --git a/src/stores/history.ts b/src/stores/history.ts
index 00a1d360..5cd0fc79 100644
--- a/src/stores/history.ts
+++ b/src/stores/history.ts
@@ -5,14 +5,23 @@ export const useHistoryStore = defineStore(
StoresEnum.HISTORY,
() => {
const emoji = ref([])
+ // 记录上次选择的表情选项卡索引
+ const lastEmojiTabIndex = ref(0)
const setEmoji = (item: string[]) => {
emoji.value = item
}
+ // 设置上次选择的表情选项卡索引
+ const setLastEmojiTabIndex = (index: number) => {
+ lastEmojiTabIndex.value = index
+ }
+
return {
emoji,
- setEmoji
+ setEmoji,
+ lastEmojiTabIndex,
+ setLastEmojiTabIndex
}
},
{
diff --git a/vite.config.ts b/vite.config.ts
index f837ca48..f2318c7c 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -7,7 +7,6 @@ import { getRootPath, getSrcPath } from './build/config/getPath'
import vueJsx from '@vitejs/plugin-vue-jsx'
import UnoCSS from '@unocss/vite'
import terser from '@rollup/plugin-terser'
-import { codecovVitePlugin } from '@codecov/vite-plugin'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
// https://vitejs.dev/config/
@@ -57,12 +56,6 @@ export default defineConfig(({ mode }: ConfigEnv) => {
resolvers: [NaiveUiResolver()],
dts: 'src/typings/components.d.ts'
}),
- /** 代码覆盖率插件 */
- codecovVitePlugin({
- enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined,
- bundleName: 'hula',
- uploadToken: process.env.CODECOV_TOKEN
- }),
/** 压缩代码 */
terser({
format: {