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 @@

An Instant Messaging System Built with Tauri, Vite 6, Vue 3, and TypeScript

- - - CI @@ -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 构建的即时通讯系统

- - - CI @@ -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; "> @@ -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: {