├── .vscode └── extensions.json ├── public ├── ad.png ├── down.png ├── qq-bg.png ├── bili-bg.png ├── favicon.ico ├── group-bg.png ├── badge │ ├── crown.png │ ├── clover.png │ ├── diamond.png │ ├── ssh-online.png │ └── web-online.png ├── github-bg.png ├── poster-dark.png ├── title-dark.png ├── title-light.png └── poster-light.png ├── .github └── logo.png ├── .env ├── src ├── assets │ ├── iconfont.ttf │ ├── iconfont.woff │ ├── iconfont.woff2 │ ├── themes │ │ ├── dark.css │ │ └── light.css │ ├── base.css │ ├── main.css │ └── iconfont.css ├── constant │ ├── userType.js │ ├── messageSource.js │ ├── textContentType.js │ └── messageType.js ├── utils │ ├── eventBus.js │ ├── common.js │ ├── theme.js │ ├── date.js │ ├── ws.js │ └── axios.js ├── api │ ├── notify.js │ ├── login.js │ ├── message.js │ ├── user.js │ ├── chatList.js │ ├── file.js │ └── video.js ├── emoji │ ├── emoji.js │ ├── emojiText.json │ └── miyoushe.json ├── stores │ ├── useGroupStore.js │ ├── useThemeStore.js │ ├── useChatMsgStore.js │ ├── useGlobalStore.js │ └── useUserInfoStore.js ├── main.js ├── components │ ├── LinyuDotHint.vue │ ├── Msg │ │ ├── MsgContent │ │ │ ├── CallMsg.vue │ │ │ ├── TimeMsg.vue │ │ │ ├── RecallMsg.vue │ │ │ ├── EmojiMsg.vue │ │ │ ├── TextMsg.vue │ │ │ └── MarkDownTextMsg.vue │ │ ├── LinyuReferenceContent.vue │ │ ├── LinyuChatListContent.vue │ │ ├── LinyuMsg.vue │ │ └── LinyuMsgContent.vue │ ├── GradientText.vue │ ├── LinyuModal.vue │ ├── LinyuLabel.vue │ ├── LinyuTextButton.vue │ ├── LinyuIconButton.vue │ ├── LinyuButton.vue │ ├── LoadingDots.vue │ ├── ToastProvider.vue │ ├── LinyuLoading.vue │ ├── ChatSkeleton.vue │ ├── BorderGradientButton.vue │ ├── LinyuToast.vue │ ├── LinyuDialog.vue │ ├── LinyuImg.vue │ ├── LinyuCircleProgress.vue │ ├── LinyuAvatar.vue │ ├── LinyuPopup.vue │ ├── LinyuEmojiBox.vue │ ├── ModifyUserInfo.vue │ ├── LinyuInput.vue │ ├── LinyuCardCarousel.vue │ ├── Notify.vue │ ├── LinyuTooltip.vue │ ├── LinyuDraggableWindow.vue │ ├── LinyuMsgInput.vue │ ├── FileTransfer.vue │ └── VideoChat.vue ├── router │ └── index.js ├── App.vue └── views │ └── LoginPage.vue ├── postcss.config.js ├── jsconfig.json ├── .prettierrc.json ├── .editorconfig ├── tailwind.config.js ├── deploy ├── set-server-url.sh └── nginx.conf ├── Dockerfile ├── index.html ├── .gitignore ├── vite.config.js ├── eslint.config.js ├── package.json ├── README.md └── LICENSE /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /public/ad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/ad.png -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/.github/logo.png -------------------------------------------------------------------------------- /public/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/down.png -------------------------------------------------------------------------------- /public/qq-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/qq-bg.png -------------------------------------------------------------------------------- /public/bili-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/bili-bg.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/group-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/group-bg.png -------------------------------------------------------------------------------- /public/badge/crown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/badge/crown.png -------------------------------------------------------------------------------- /public/github-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/github-bg.png -------------------------------------------------------------------------------- /public/poster-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/poster-dark.png -------------------------------------------------------------------------------- /public/title-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/title-dark.png -------------------------------------------------------------------------------- /public/title-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/title-light.png -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_HTTP_URL=http://127.0.0.1:9200 2 | VITE_WS_URL=ws://127.0.0.1:9100 3 | VITE_LINYU_VERSION=1.1.3 -------------------------------------------------------------------------------- /public/badge/clover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/badge/clover.png -------------------------------------------------------------------------------- /public/badge/diamond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/badge/diamond.png -------------------------------------------------------------------------------- /public/poster-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/poster-light.png -------------------------------------------------------------------------------- /src/assets/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/src/assets/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/src/assets/iconfont.woff -------------------------------------------------------------------------------- /public/badge/ssh-online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/badge/ssh-online.png -------------------------------------------------------------------------------- /public/badge/web-online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/public/badge/web-online.png -------------------------------------------------------------------------------- /src/assets/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linyu-im/linyu-mini-web/HEAD/src/assets/iconfont.woff2 -------------------------------------------------------------------------------- /src/constant/userType.js: -------------------------------------------------------------------------------- 1 | export const UserType = { 2 | User: 'user', // 普通用户 3 | Bot: 'bot', // 机器人 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/eventBus.js: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | 3 | const eventBus = mitt() 4 | export default eventBus 5 | -------------------------------------------------------------------------------- /src/constant/messageSource.js: -------------------------------------------------------------------------------- 1 | export const MessageSource = { 2 | Group: 'group', // 群聊 3 | User: 'user', // 私聊 4 | } 5 | -------------------------------------------------------------------------------- /src/constant/textContentType.js: -------------------------------------------------------------------------------- 1 | export const TextContentType = { 2 | Text: 'text', // 文本 3 | At: 'at', // at用户 4 | } 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | 8 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | }, 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "$schema": "https://json.schemastore.org/prettierrc", 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /src/api/notify.js: -------------------------------------------------------------------------------- 1 | import Http from '@/utils/axios' 2 | 3 | export default { 4 | getLatestNotify() { 5 | return Http.get('/api/v1/notify/get') 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /src/constant/messageType.js: -------------------------------------------------------------------------------- 1 | export const MessageType = { 2 | Text: 'text', // 文本 3 | Recall: 'recall', // 撤回 4 | Emoji: 'emoji', // 表情 5 | Call: 'call', //音视频 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}] 2 | charset = utf-8 3 | indent_size = 2 4 | indent_style = space 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | -------------------------------------------------------------------------------- /src/emoji/emoji.js: -------------------------------------------------------------------------------- 1 | import emojiText from '@/emoji/emojiText.json' 2 | import miyoushe from '@/emoji/miyoushe.json' 3 | 4 | const emojis = [emojiText, miyoushe] 5 | 6 | export default emojis 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["index.html", "./src/**/*.{html,js,ts,jsx,tsx,vue}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | } 9 | 10 | -------------------------------------------------------------------------------- /deploy/set-server-url.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 替换Vue打包后的API URL地址 3 | echo "Replacing API URL in dist" 4 | sed -i "s|http://127.0.0.1:9200|$SERVER_HTTP_URL|g" /usr/share/nginx/html/assets/*.js 5 | sed -i "s|ws://127.0.0.1:9100|$SERVER_WS_URL|g" /usr/share/nginx/html/assets/*.js 6 | exec "$@" 7 | -------------------------------------------------------------------------------- /src/stores/useGroupStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useGroupStore = defineStore('group', { 4 | state: () => ({ 5 | name: 'linyu在线聊天群', 6 | }), 7 | actions: { 8 | setName(name) { 9 | this.theme = name 10 | }, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /src/utils/common.js: -------------------------------------------------------------------------------- 1 | export function formatSize(size) { 2 | if (size < 1024) { 3 | return size + ' B' 4 | } 5 | let units = ['KB', 'MB', 'GB', 'TB'] 6 | let i = -1 7 | while (size >= 1024 && i < units.length - 1) { 8 | size /= 1024 9 | i++ 10 | } 11 | return size.toFixed(1) + ' ' + units[i] 12 | } 13 | -------------------------------------------------------------------------------- /src/api/login.js: -------------------------------------------------------------------------------- 1 | import Http from '@/utils/axios' 2 | 3 | export default { 4 | verify(param) { 5 | return Http.post('/api/v1/login/verify', param) 6 | }, 7 | publicKey() { 8 | return Http.get('/api/v1/login/public-key') 9 | }, 10 | login(param) { 11 | return Http.post('/api/v1/login', param) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/api/message.js: -------------------------------------------------------------------------------- 1 | import Http from '@/utils/axios' 2 | 3 | export default { 4 | send(param) { 5 | return Http.post('/api/v1/message/send', param) 6 | }, 7 | record(param) { 8 | return Http.post('/api/v1/message/record', param) 9 | }, 10 | recall(param) { 11 | return Http.post('/api/v1/message/recall', param) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | COPY ./dist /usr/share/nginx/html 4 | 5 | COPY ./deploy/nginx.conf /etc/nginx/nginx.conf 6 | 7 | COPY ./deploy/set-server-url.sh /usr/local/bin/set-server-url.sh 8 | 9 | RUN chmod +x /usr/local/bin/set-server-url.sh 10 | 11 | EXPOSE 80 443 12 | 13 | CMD ["sh", "-c", "/usr/local/bin/set-server-url.sh && nginx -g 'daemon off;'"] -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Linyu-mini 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/stores/useThemeStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useThemeStore = defineStore('theme', { 4 | state: () => ({ 5 | theme: 'light', 6 | }), 7 | actions: { 8 | async setTheme(newTheme) { 9 | this.theme = newTheme 10 | document.documentElement.setAttribute('data-theme', newTheme) 11 | }, 12 | }, 13 | persist: true, 14 | }) 15 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import Http from '@/utils/axios' 2 | 3 | export default { 4 | list() { 5 | return Http.get('/api/v1/user/list') 6 | }, 7 | listMap() { 8 | return Http.get('/api/v1/user/list/map') 9 | }, 10 | onlineWeb() { 11 | return Http.get('/api/v1/user/online/web') 12 | }, 13 | update(param) { 14 | return Http.post('/api/v1/user/update', param) 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /src/stores/useChatMsgStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useChatMsgStore = defineStore('chat-msg', { 4 | state: () => ({ 5 | referenceMsg: null, //要引用的消息 6 | userListMap: new Map(), //全部用户 7 | }), 8 | actions: { 9 | setReferenceMsg(msg) { 10 | this.referenceMsg = msg 11 | }, 12 | setUserListMap(map) { 13 | this.userListMap = map 14 | }, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | 3 | import { createApp } from 'vue' 4 | import { createPinia } from 'pinia' 5 | import App from './App.vue' 6 | import router from './router' 7 | import piniaPluginPersistedState from 'pinia-plugin-persistedstate' 8 | 9 | const app = createApp(App) 10 | const pinia = createPinia() 11 | pinia.use(piniaPluginPersistedState) 12 | app.use(router) 13 | app.use(pinia) 14 | 15 | app.mount('#app') 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | -------------------------------------------------------------------------------- /src/assets/themes/dark.css: -------------------------------------------------------------------------------- 1 | [data-theme='dark'] { 2 | --primary-color: 76, 155, 255; 3 | --minor-color: 33, 33, 33; 4 | --background-color: 33, 33, 33; 5 | --text-color: 255, 255, 255; 6 | --screen-bg-color: linear-gradient(120deg, rgba(30, 31, 34, 0.8), rgba(30, 31, 34, 0.9)); 7 | --scrren-grid-bg-color: linear-gradient(to right, rgba(33, 33, 33, 0.08) 1px, transparent 100px), 8 | linear-gradient(to bottom, rgba(33, 33, 33, 0.08) 1px, transparent 100px); 9 | --group-bg-color: 209, 151, 0; 10 | } 11 | -------------------------------------------------------------------------------- /src/api/chatList.js: -------------------------------------------------------------------------------- 1 | import Http from '@/utils/axios' 2 | 3 | export default { 4 | group() { 5 | return Http.get('/api/v1/chat-list/group') 6 | }, 7 | privateList() { 8 | return Http.get('/api/v1/chat-list/list/private') 9 | }, 10 | create(param) { 11 | return Http.post('/api/v1/chat-list/create', param) 12 | }, 13 | read(param) { 14 | return Http.post('/api/v1/chat-list/read', param) 15 | }, 16 | delete(param) { 17 | return Http.post('/api/v1/chat-list/delete', param) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/stores/useGlobalStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useGlobalStore = defineStore('global', { 4 | state: () => ({ 5 | isOpenGlobalDialog: false, 6 | dialogTitle: '', 7 | dialogContent: '', 8 | }), 9 | actions: { 10 | setGlobalDialog(isOpen, title, content) { 11 | this.isOpenGlobalDialog = isOpen 12 | this.dialogTitle = title 13 | this.dialogContent = content 14 | }, 15 | closeGlobalDialog() { 16 | this.isOpenGlobalDialog = false 17 | }, 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import {fileURLToPath, URL} from 'node:url' 2 | 3 | import {defineConfig} from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | // import vueDevTools from 'vite-plugin-vue-devtools' 6 | 7 | // https://vite.dev/config/ 8 | export default defineConfig({ 9 | server: { 10 | host: '0.0.0.0', 11 | }, 12 | plugins: [ 13 | vue(), 14 | // vueDevTools(), 15 | ], 16 | resolve: { 17 | alias: { 18 | '@': fileURLToPath(new URL('./src', import.meta.url)) 19 | }, 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/components/LinyuDotHint.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | 27 | -------------------------------------------------------------------------------- /src/api/file.js: -------------------------------------------------------------------------------- 1 | import Http from '@/utils/axios' 2 | 3 | export default { 4 | offer(param) { 5 | return Http.post(`/api/v1/file/offer`, param) 6 | }, 7 | answer(param) { 8 | return Http.post(`/api/v1/file/answer`, param) 9 | }, 10 | candidate(param) { 11 | return Http.post(`/api/v1/file/candidate`, param) 12 | }, 13 | cancel(param) { 14 | return Http.post(`/api/v1/file/cancel`, param) 15 | }, 16 | invite(param) { 17 | return Http.post(`/api/v1/file/invite`, param) 18 | }, 19 | accept(param) { 20 | return Http.post(`/api/v1/file/accept`, param) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /src/api/video.js: -------------------------------------------------------------------------------- 1 | import Http from '@/utils/axios' 2 | 3 | export default { 4 | offer(param) { 5 | return Http.post(`/api/v1/video/offer`, param) 6 | }, 7 | answer(param) { 8 | return Http.post(`/api/v1/video/answer`, param) 9 | }, 10 | candidate(param) { 11 | return Http.post(`/api/v1/video/candidate`, param) 12 | }, 13 | hangup(param) { 14 | return Http.post(`/api/v1/video/hangup`, param) 15 | }, 16 | invite(param) { 17 | return Http.post(`/api/v1/video/invite`, param) 18 | }, 19 | accept(param) { 20 | return Http.post(`/api/v1/video/accept`, param) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/themes/light.css: -------------------------------------------------------------------------------- 1 | [data-theme='light'] { 2 | --primary-color: 76, 155, 255; 3 | --minor-color: 160, 217, 246; 4 | --background-color: 255, 255, 255; 5 | --text-color: 33, 33, 33; 6 | --screen-bg-color: linear-gradient( 7 | 120deg, 8 | rgba(var(--minor-color), 0.2), 9 | #edf2f9, 10 | rgba(var(--primary-color), 0.4) 11 | ); 12 | --scrren-grid-bg-color: linear-gradient( 13 | to right, 14 | rgba(76, 155, 255, 0.08) 1px, 15 | transparent 100px 16 | ), 17 | linear-gradient(to bottom, rgba(76, 155, 255, 0.08) 1px, transparent 100px); 18 | --group-bg-color: 33, 33, 33; 19 | } 20 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import pluginVue from 'eslint-plugin-vue' 3 | import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' 4 | 5 | export default [ 6 | { 7 | name: 'app/files-to-lint', 8 | files: ['**/*.{js,mjs,jsx,vue}'], 9 | }, 10 | 11 | { 12 | name: 'app/files-to-ignore', 13 | ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'], 14 | }, 15 | 16 | js.configs.recommended, 17 | ...pluginVue.configs['flat/essential'], 18 | skipFormatting, 19 | 20 | { 21 | name: 'app/custom-rules', 22 | rules: { 23 | 'vue/multi-word-component-names': 'off', 24 | }, 25 | }, 26 | ] 27 | -------------------------------------------------------------------------------- /src/components/Msg/MsgContent/CallMsg.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | 20 | 28 | -------------------------------------------------------------------------------- /src/assets/base.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | margin: 0; 6 | font-weight: normal; 7 | } 8 | 9 | body { 10 | min-height: 100vh; 11 | color: #1f1f1f; 12 | transition: 13 | color 0.5s, 14 | background-color 0.5s; 15 | line-height: 1.6; 16 | font-family: 17 | Inter, 18 | -apple-system, 19 | BlinkMacSystemFont, 20 | 'Segoe UI', 21 | Roboto, 22 | Oxygen, 23 | Ubuntu, 24 | Cantarell, 25 | 'Fira Sans', 26 | 'Droid Sans', 27 | 'Helvetica Neue', 28 | sans-serif; 29 | font-size: 15px; 30 | text-rendering: optimizeLegibility; 31 | -webkit-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | } 34 | -------------------------------------------------------------------------------- /src/stores/useUserInfoStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useUserInfoStore = defineStore('user-info', { 4 | state: () => ({ 5 | userId: '', 6 | userName: '', 7 | email: '', 8 | avatar: '', 9 | }), 10 | actions: { 11 | async setUserInfo(userInfo) { 12 | this.userId = userInfo.userId 13 | this.userName = userInfo.userName 14 | this.email = userInfo.email 15 | this.avatar = userInfo.avatar 16 | }, 17 | async clearUserInfo() { 18 | this.userId = '' 19 | this.userName = '' 20 | this.email = '' 21 | this.avatar = '' 22 | }, 23 | async setUserAvatar(avatar) { 24 | this.avatar = avatar 25 | }, 26 | }, 27 | persist: true, 28 | }) 29 | -------------------------------------------------------------------------------- /src/components/GradientText.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 35 | -------------------------------------------------------------------------------- /src/components/LinyuModal.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 37 | -------------------------------------------------------------------------------- /src/components/Msg/LinyuReferenceContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/Msg/MsgContent/TimeMsg.vue: -------------------------------------------------------------------------------- 1 | 8 | 15 | 33 | -------------------------------------------------------------------------------- /src/components/LinyuLabel.vue: -------------------------------------------------------------------------------- 1 | 6 | 18 | 19 | 33 | -------------------------------------------------------------------------------- /src/components/LinyuTextButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 37 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | @import './themes/light.css'; 3 | @import './themes/dark.css'; 4 | @import './iconfont.css'; 5 | 6 | @tailwind base; 7 | @tailwind components; 8 | @tailwind utilities; 9 | 10 | ::-webkit-scrollbar { 11 | width: 5px; 12 | height: 5px; 13 | } 14 | 15 | ::-webkit-scrollbar-track { 16 | background: transparent; 17 | } 18 | 19 | ::-webkit-scrollbar-thumb { 20 | border-radius: 3px; 21 | background: rgba(140, 140, 140, 0.3); 22 | } 23 | 24 | ::view-transition-old(root), 25 | ::view-transition-new(root) { 26 | animation: none; 27 | mix-blend-mode: normal; 28 | } 29 | 30 | ::view-transition-old(root) { 31 | z-index: 1; 32 | } 33 | 34 | ::view-transition-new(root) { 35 | z-index: 9999; 36 | } 37 | 38 | .dark::view-transition-old(root) { 39 | z-index: 9999; 40 | } 41 | 42 | .dark::view-transition-new(root) { 43 | z-index: 1; 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Msg/MsgContent/RecallMsg.vue: -------------------------------------------------------------------------------- 1 | 9 | 14 | 38 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import Chat from '@/views/ChatPage.vue' 3 | import Login from '@/views/LoginPage.vue' 4 | import ws from '@/utils/ws.js' 5 | 6 | const router = createRouter({ 7 | history: createWebHistory(import.meta.env.BASE_URL), 8 | routes: [ 9 | { 10 | path: '/login', 11 | name: 'login', 12 | component: Login, 13 | }, 14 | { 15 | path: '/', 16 | name: 'chat', 17 | component: Chat, 18 | }, 19 | ], 20 | }) 21 | 22 | router.beforeEach((to, from, next) => { 23 | let token = window.localStorage.getItem('x-token') 24 | if (token) ws.connect(token) 25 | if (!token && to.path !== '/login') { 26 | next({ path: '/login' }) 27 | return 28 | } 29 | if ((token && to.path === '/login') || !to.matched.length) { 30 | next({ path: '/' }) 31 | return 32 | } 33 | next() 34 | }) 35 | 36 | export default router 37 | -------------------------------------------------------------------------------- /src/components/LinyuIconButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | 25 | 41 | -------------------------------------------------------------------------------- /src/components/Msg/MsgContent/EmojiMsg.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 30 | 31 | 47 | -------------------------------------------------------------------------------- /src/utils/theme.js: -------------------------------------------------------------------------------- 1 | import { useThemeStore } from '@/stores/useThemeStore.js' 2 | import { nextTick } from 'vue' 3 | 4 | export function toggleDark(event, theme) { 5 | const themeStore = useThemeStore() 6 | const x = event.clientX 7 | const y = event.clientY 8 | const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y)) 9 | const isDark = theme === 'dark' 10 | const transition = document.startViewTransition(async () => { 11 | await themeStore.setTheme(theme) 12 | document.documentElement.className = theme 13 | await nextTick() 14 | }) 15 | transition.ready.then(() => { 16 | const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`] 17 | document.documentElement.animate( 18 | { 19 | clipPath: isDark ? [...clipPath].reverse() : clipPath, 20 | }, 21 | { 22 | duration: 400, 23 | easing: 'ease-out', 24 | pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)', 25 | }, 26 | ) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/LinyuButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 13 | 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linyu-mini-web", 3 | "version": "1.1.3", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint . --fix", 11 | "format": "prettier --write src/" 12 | }, 13 | "dependencies": { 14 | "axios": "^1.7.7", 15 | "highlight.js": "^11.11.1", 16 | "jsencrypt": "^3.3.2", 17 | "markdown-it": "^14.1.0", 18 | "mitt": "^3.0.1", 19 | "pinia": "^2.3.0", 20 | "pinia-plugin-persistedstate": "^4.2.0", 21 | "vue": "^3.5.13", 22 | "vue-router": "^4.4.5" 23 | }, 24 | "devDependencies": { 25 | "@eslint/js": "^9.14.0", 26 | "@vitejs/plugin-vue": "^5.2.1", 27 | "@vue/eslint-config-prettier": "^10.1.0", 28 | "autoprefixer": "^10.4.20", 29 | "eslint": "^9.14.0", 30 | "eslint-plugin-vue": "^9.30.0", 31 | "less": "^4.2.0", 32 | "less-loader": "^12.2.0", 33 | "postcss": "^8.4.40", 34 | "prettier": "^3.3.3", 35 | "tailwindcss": "^3.4.7", 36 | "vite": "^6.0.1", 37 | "vite-plugin-vue-devtools": "^7.6.5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/LoadingDots.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | 37 | 55 | -------------------------------------------------------------------------------- /src/components/Msg/LinyuChatListContent.vue: -------------------------------------------------------------------------------- 1 | 14 | 27 | 34 | -------------------------------------------------------------------------------- /src/components/ToastProvider.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/LinyuLoading.vue: -------------------------------------------------------------------------------- 1 | 11 | 16 | 17 | 62 | -------------------------------------------------------------------------------- /src/components/ChatSkeleton.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 28 | 29 | 63 | -------------------------------------------------------------------------------- /src/components/BorderGradientButton.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | 29 | 68 | -------------------------------------------------------------------------------- /src/components/LinyuToast.vue: -------------------------------------------------------------------------------- 1 | 8 | 26 | 27 | 76 | -------------------------------------------------------------------------------- /src/components/Msg/MsgContent/TextMsg.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 45 | 46 | 62 | -------------------------------------------------------------------------------- /src/components/LinyuDialog.vue: -------------------------------------------------------------------------------- 1 | 19 | 43 | 44 | 68 | -------------------------------------------------------------------------------- /deploy/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log notice; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 16 | '$status $body_bytes_sent "$http_referer" ' 17 | '"$http_user_agent" "$http_x_forwarded_for"'; 18 | 19 | access_log /var/log/nginx/access.log main; 20 | 21 | sendfile on; 22 | keepalive_timeout 65; 23 | 24 | server { 25 | listen 80; 26 | # 对应服务 27 | server_name $SERVER_NAME; 28 | 29 | # 前端服务 30 | location / { 31 | root /usr/share/nginx/html; 32 | index index.html; 33 | try_files $uri $uri/ /index.html; 34 | } 35 | 36 | # 后端api 37 | location /api/ { 38 | proxy_pass http://linyu-mini-server:9200; 39 | proxy_set_header Host $host; 40 | proxy_set_header X-Real-IP $remote_addr; 41 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 42 | } 43 | 44 | # 后端websocket 45 | location /ws { 46 | proxy_pass http://linyu-mini-server:9100; 47 | proxy_http_version 1.1; 48 | proxy_set_header Upgrade $http_upgrade; 49 | proxy_set_header Connection 'upgrade'; 50 | proxy_set_header Host $host; 51 | proxy_set_header X-Real-IP $remote_addr; 52 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 53 | } 54 | 55 | error_page 404 /index.html; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/LinyuImg.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 44 | 45 | 76 | -------------------------------------------------------------------------------- /src/components/LinyuCircleProgress.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 53 | 54 | 84 | -------------------------------------------------------------------------------- /src/components/LinyuAvatar.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 60 | 61 | 78 | -------------------------------------------------------------------------------- /src/components/LinyuPopup.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 83 | 84 | 100 | -------------------------------------------------------------------------------- /src/components/LinyuEmojiBox.vue: -------------------------------------------------------------------------------- 1 | 30 | 52 | 84 | -------------------------------------------------------------------------------- /src/assets/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'iconfont'; /* Project id 4783727 */ 3 | src: 4 | url('iconfont.woff2?t=1736424106139') format('woff2'), 5 | url('iconfont.woff?t=1736424106139') format('woff'), 6 | url('iconfont.ttf?t=1736424106139') format('truetype'); 7 | } 8 | 9 | .iconfont { 10 | font-family: 'iconfont' !important; 11 | font-size: 16px; 12 | font-style: normal; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | .icon-xiazai:before { 18 | content: '\e676'; 19 | } 20 | 21 | .icon-wenjian:before { 22 | content: '\eac4'; 23 | } 24 | 25 | .icon-quxiao:before { 26 | content: '\e669'; 27 | } 28 | 29 | .icon-yunxu:before { 30 | content: '\e679'; 31 | } 32 | 33 | .icon-shexiangtou_guanbi:before { 34 | content: '\eca5'; 35 | } 36 | 37 | .icon-shexiangtou:before { 38 | content: '\eca6'; 39 | } 40 | 41 | .icon-maikefengguan:before { 42 | content: '\e653'; 43 | } 44 | 45 | .icon-maikefengkai:before { 46 | content: '\e654'; 47 | } 48 | 49 | .icon-yuyintonghua:before { 50 | content: '\e969'; 51 | } 52 | 53 | .icon-shipingtonghua:before { 54 | content: '\e9f5'; 55 | } 56 | 57 | .icon-shousuo:before { 58 | content: '\e74a'; 59 | } 60 | 61 | .icon-guaduan:before { 62 | content: '\e640'; 63 | } 64 | 65 | .icon-jieting:before { 66 | content: '\e641'; 67 | } 68 | 69 | .icon-biaoqing:before { 70 | content: '\e656'; 71 | } 72 | 73 | .icon-zhiding:before { 74 | content: '\e6b6'; 75 | } 76 | 77 | .icon-shanchu:before { 78 | content: '\e61a'; 79 | } 80 | 81 | .icon-lianjie:before { 82 | content: '\e745'; 83 | } 84 | 85 | .icon-yinyong:before { 86 | content: '\e8ce'; 87 | } 88 | 89 | .icon-chehui:before { 90 | content: '\e649'; 91 | } 92 | 93 | .icon-fuzhi:before { 94 | content: '\e744'; 95 | } 96 | 97 | .icon-minglinghang:before { 98 | content: '\e601'; 99 | } 100 | 101 | .icon-github:before { 102 | content: '\e6f6'; 103 | } 104 | 105 | .icon-bilibili:before { 106 | content: '\e600'; 107 | } 108 | 109 | .icon-fasong2:before { 110 | content: '\e722'; 111 | } 112 | 113 | .icon-fasong:before { 114 | content: '\e630'; 115 | } 116 | 117 | .icon-tuichu:before { 118 | content: '\e610'; 119 | } 120 | 121 | .icon-shezhi:before { 122 | content: '\e60c'; 123 | } 124 | 125 | .icon-liebiao:before { 126 | content: '\e662'; 127 | } 128 | 129 | .icon-xiaoxi:before { 130 | content: '\e781'; 131 | } 132 | 133 | .icon-yueliang:before { 134 | content: '\e62e'; 135 | } 136 | 137 | .icon-taiyang:before { 138 | content: '\e8c7'; 139 | } 140 | -------------------------------------------------------------------------------- /src/components/Msg/MsgContent/MarkDownTextMsg.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 64 | 65 | 102 | -------------------------------------------------------------------------------- /src/components/ModifyUserInfo.vue: -------------------------------------------------------------------------------- 1 | 35 | 71 | 92 | -------------------------------------------------------------------------------- /src/utils/date.js: -------------------------------------------------------------------------------- 1 | export function daysDifference(dateString) { 2 | const today = new Date() 3 | today.setHours(0, 0, 0, 0) 4 | const inputDate = new Date(dateString) 5 | inputDate.setHours(0, 0, 0, 0) 6 | const timeDifference = today - inputDate 7 | const daysDifference = timeDifference / (1000 * 60 * 60 * 24) 8 | return daysDifference 9 | } 10 | 11 | export function calculateAge(birthDateString) { 12 | const birthDate = new Date(birthDateString) 13 | const today = new Date() 14 | let age = today.getFullYear() - birthDate.getFullYear() 15 | const monthDifference = today.getMonth() - birthDate.getMonth() 16 | if (monthDifference < 0 || (monthDifference === 0 && today.getDate() < birthDate.getDate())) { 17 | age-- 18 | } 19 | return age 20 | } 21 | 22 | export function getDateParts(dateString) { 23 | const date = new Date(dateString) 24 | const year = date.getFullYear() 25 | const month = String(date.getMonth() + 1).padStart(2, '0') 26 | const day = String(date.getDate()).padStart(2, '0') 27 | return `${year}-${month}-${day}` 28 | } 29 | 30 | export function getMonthAndDayParts(dateString) { 31 | const date = new Date(dateString) 32 | const month = String(date.getMonth() + 1).padStart(2, '0') 33 | const day = String(date.getDate()).padStart(2, '0') 34 | return `${month}-${day}` 35 | } 36 | 37 | export function formatTime(dateStr) { 38 | const date = new Date(dateStr) 39 | const now = new Date() 40 | const diffMs = now - date 41 | const oneDay = 24 * 60 * 60 * 1000 42 | const oneWeek = 7 * oneDay 43 | 44 | // 检查是否是昨天 45 | const isYesterday = 46 | now.getDate() - date.getDate() === 1 && 47 | now.getMonth() === date.getMonth() && 48 | now.getFullYear() === date.getFullYear() 49 | 50 | if (diffMs < oneDay && !isYesterday) { 51 | // 当天消息 52 | const hours = date.getHours() 53 | const minutes = date.getMinutes() 54 | const day = hours < 12 ? '上午' : '下午' 55 | return `${day} ${hours}:${minutes < 10 ? '0' + minutes : minutes}` 56 | } else if (isYesterday) { 57 | // 昨天的消息 58 | const hours = date.getHours() 59 | const minutes = date.getMinutes() 60 | return `昨天 ${hours}:${minutes < 10 ? '0' + minutes : minutes}` 61 | } else if (diffMs < oneWeek) { 62 | // 超过1天,小于1周 63 | const daysOfWeek = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'] 64 | const day = daysOfWeek[date.getDay()] 65 | const hours = date.getHours() 66 | const minutes = date.getMinutes() 67 | return `${day} ${hours}:${minutes < 10 ? '0' + minutes : minutes}` 68 | } else { 69 | // 大于1周 70 | const year = date.getFullYear() 71 | const month = date.getMonth() + 1 // 月份从0开始 72 | const day = date.getDate() 73 | const hours = date.getHours() 74 | const minutes = date.getMinutes() 75 | return `${year}-${month < 10 ? '0' + month : month}-${day < 10 ? '0' + day : day} ${hours}:${minutes < 10 ? '0' + minutes : minutes}` 76 | } 77 | } 78 | 79 | export function formatTimingTime(time) { 80 | const hours = Math.floor(time / 3600) 81 | const minutes = Math.floor((time % 3600) / 60) 82 | const seconds = time % 60 83 | return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}` 84 | } 85 | -------------------------------------------------------------------------------- /src/utils/ws.js: -------------------------------------------------------------------------------- 1 | import EventBus from '@/utils/eventBus.js' 2 | 3 | let ws = null 4 | let heartTimer = null 5 | let timer = null 6 | let lockReconnect = false 7 | let token = null 8 | const reconnectCountMax = 200 9 | let reconnectCount = 0 10 | let isConnect = false 11 | 12 | function response(event) { 13 | if (event.type !== 'message') { 14 | onCloseHandler() 15 | return 16 | } 17 | let wsContent 18 | try { 19 | wsContent = JSON.parse(event.data) 20 | } catch { 21 | onCloseHandler() 22 | return 23 | } 24 | if (wsContent.type) { 25 | if (wsContent.data && wsContent.data.code === -1) { 26 | onCloseHandler() 27 | } else { 28 | switch (wsContent.type) { 29 | case 'msg': { 30 | EventBus.emit('on-receive-msg', wsContent.content) 31 | break 32 | } 33 | case 'notify': { 34 | wsContent.content.content = JSON.parse(wsContent.content?.content) 35 | EventBus.emit('on-receive-notify', wsContent.content) 36 | break 37 | } 38 | case 'video': { 39 | EventBus.emit('on-receive-video', wsContent.content) 40 | break 41 | } 42 | case 'file': { 43 | EventBus.emit('on-receive-file', wsContent.content) 44 | break 45 | } 46 | } 47 | } 48 | } else { 49 | onCloseHandler() 50 | } 51 | } 52 | 53 | function connect(tokenStr) { 54 | if (isConnect || ws) return 55 | isConnect = true 56 | token = tokenStr 57 | try { 58 | const wsIp = import.meta.env.VITE_WS_URL 59 | ws = new WebSocket(wsIp + '/ws?x-token=' + token) 60 | 61 | ws.onopen = () => { 62 | console.log('Connected to server') 63 | clearTimer() 64 | sendHeartPack() 65 | } 66 | 67 | ws.onmessage = response 68 | ws.onclose = onCloseHandler 69 | ws.onerror = onCloseHandler 70 | } catch { 71 | onCloseHandler() 72 | } 73 | } 74 | 75 | function send(msg) { 76 | if (ws && ws.readyState === WebSocket.OPEN) ws.send(msg) 77 | } 78 | 79 | const sendHeartPack = () => { 80 | heartTimer = setInterval(() => { 81 | send('heart') 82 | }, 9900) 83 | } 84 | 85 | const onCloseHandler = () => { 86 | clearHeartPackTimer() 87 | if (ws) { 88 | ws.close() 89 | ws = null 90 | } 91 | isConnect = false 92 | if (lockReconnect) return 93 | lockReconnect = true 94 | if (timer) { 95 | clearTimeout(timer) 96 | timer = null 97 | } 98 | if (reconnectCount >= reconnectCountMax) { 99 | reconnectCount = 0 100 | return 101 | } 102 | if (token) { 103 | timer = setTimeout(() => { 104 | connect(token) 105 | reconnectCount++ 106 | lockReconnect = false 107 | }, 5000) 108 | } 109 | } 110 | 111 | const clearHeartPackTimer = () => { 112 | console.log('Closing connection') 113 | if (heartTimer) { 114 | clearInterval(heartTimer) 115 | heartTimer = null 116 | } 117 | } 118 | 119 | const clearTimer = () => { 120 | if (timer) { 121 | clearInterval(timer) 122 | timer = null 123 | } 124 | } 125 | 126 | const disConnect = () => { 127 | clearHeartPackTimer() 128 | token = null 129 | if (ws) { 130 | ws.close() 131 | ws = null 132 | } 133 | isConnect = false 134 | } 135 | 136 | export default { connect, disConnect } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |
5 |

Linyu-Mini

6 |

一个轻量级的在线聊天室系统,支持实时消息交流,适合多场景使用。系统采用轻量级架构,具备快速响应能力,同时提供多种实用功能,如用户登录、消息记录、群组聊天等,确保良好的用户体验和高效的沟通效果

7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | ## 介绍 15 | 16 | 林语Mini(Linyu-mini)是一款基于 Vite 5 和 Vue 3 构建的高性能即时通讯在线聊天系统。系统以轻量化设计为核心,具备快速部署和便捷扩展的特点,适用于企业内部协作、团队沟通以及小型社交平台等多种场景。 17 | 18 | ## 相关环境 19 | 20 | - node版本:v20.12.2 21 | - npm版本:10.5.0 22 | 23 | ## 技术栈 24 | 25 | - Vite 5:一款现代化的前端构建工具,具有超快的热更新速度和极致的构建性能,提供了极佳的开发体验和优化后的生产构建,使前端开发更加高效便捷。 26 | 27 | - Vue 3:一种渐进式JavaScript框架,采用响应式数据绑定和组件化开发模式,提供简洁的API和强大的功能,帮助开发者构建高性能、可维护的用户界面。 28 | 29 | - WebSocket:一种全双工通信协议,专为实时通信应用设计,能够在客户端和服务器之间保持长连接,支持即时消息的实时推送和低延迟传输,确保系统能够快速响应用户操作。 30 | 31 | ## 项目效果 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
浅色深色
50 | 51 | ## 安装与运行 52 | 53 | ```bash 54 | # 克隆linyu-mini项目 55 | git clone https://github.com/linyu-im/linyu-mini-web.git 56 | 57 | # 进入项目目录 58 | cd linyu-mini-web 59 | 60 | # 安装依赖 61 | npm install 62 | 63 | # 服务运行 64 | npm run dev 65 | 66 | # 服务打包 67 | npm run build 68 | ``` 69 | 70 | ## 免责声明 71 | 72 | ### 1. 基本声明 73 | 74 | 本软件作为开源项目提供,在法律允许的最大范围内,开发者不对软件的功能性、安全性或适用性作出任何形式的保证,无论是明示的还是暗示的。 75 | 76 | ### 2. 使用风险声明 77 | 78 | 2.1 本软件按"现状"提供,使用者需自行承担使用本软件的全部风险。 79 | 2.2 开发者不对软件的运行可靠性、适用性或与特定需求的兼容性提供任何保证。 80 | 2.3 使用者应在充分评估风险的基础上决定是否使用本软件。 81 | 82 | ### 3. 责任限制与豁免 83 | 84 | 在任何情况下,开发者及其关联方均不对因使用或无法使用本软件而导致的任何损失或损害承担责任,包括但不限于: 85 | 86 | - 数据丢失或泄露 87 | - 利润损失 88 | - 系统中断 89 | - 商业机会损失 90 | - 其他直接、间接或衍生性损失 91 | 92 | ### 4. 用户义务与责任 93 | 94 | 4.1 使用者应确保其对本软件的使用符合所有适用的法律法规要求。 95 | 4.2 对本软件进行修改、分发或二次开发的使用者,需自行承担由此产生的全部责任,包括但不限于: 96 | 97 | - 法律风险 98 | - 知识产权风险 99 | - 安全风险 100 | - 数据保护责任 101 | 102 | ### 5. 开发者权利 103 | 104 | 5.1 开发者保留对本软件进行更新、修改、调整或停止维护的权利。 105 | 5.2 开发者可能在不事先通知的情况下修改本软件或相关服务。 106 | 5.3 开发者保留对本免责声明进行修改的权利。 107 | 108 | ### 6. 开源贡献 109 | 110 | 6.1 本软件欢迎社区贡献,但贡献者需遵守相关开源协议。 111 | 6.2 开发者不对第三方贡献的代码质量和安全性负责。 112 | 113 | ### 7. 其他条款 114 | 115 | 7.1 本免责声明的任何部分被认定为无效或不可执行时,其余部分仍然有效。 116 | 7.2 本免责声明的最终解释权归开发者所有。 -------------------------------------------------------------------------------- /src/components/LinyuInput.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 92 | 93 | 139 | -------------------------------------------------------------------------------- /src/utils/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { useGlobalStore } from '@/stores/useGlobalStore.js' 3 | 4 | const SERVICE_URL = import.meta.env.VITE_HTTP_URL 5 | export { SERVICE_URL } 6 | 7 | // request 请求之前 8 | axios.interceptors.request.use((config) => { 9 | config.headers['x-token'] = localStorage.getItem('x-token') 10 | return config 11 | }) 12 | 13 | // http response 拦截器 14 | axios.interceptors.response.use( 15 | (response) => { 16 | const globalStore = useGlobalStore() 17 | if (response.data.code === -1) { 18 | globalStore.setGlobalDialog(true, '认证失效', '您的登录过期,请重新登录') 19 | } 20 | if (response.data.code === -3) { 21 | globalStore.setGlobalDialog(true, '请求失败', '您的账号已在其它地方登录,请重新登录') 22 | } 23 | return Promise.resolve(response) 24 | }, 25 | (error) => { 26 | if (error.response && error.response.data) { 27 | return Promise.reject(error.response.data) 28 | } else { 29 | return Promise.reject(error.message) 30 | } 31 | }, 32 | ) 33 | export default class Http { 34 | static send(config, loading, isBlob) { 35 | // const currentUrl = encodeURIComponent(window.location.href); 36 | const configs = Object.assign( 37 | { 38 | timeout: 30000, 39 | }, 40 | config, 41 | ) 42 | return axios(configs) 43 | .then((res) => { 44 | if (isBlob) { 45 | return res 46 | } 47 | return res.data 48 | }) 49 | .catch((error) => { 50 | throw error 51 | }) 52 | } 53 | 54 | static post(url, params = {}, loading) { 55 | const config = { 56 | method: 'post', 57 | url: SERVICE_URL + url, 58 | data: params, 59 | } 60 | return Http.send(config, loading) 61 | } 62 | 63 | static formData(url, params = {}, loading) { 64 | const config = { 65 | method: 'post', 66 | url: SERVICE_URL + url, 67 | data: params, 68 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 69 | } 70 | return Http.send(config, loading) 71 | } 72 | 73 | static delete(url, params = {}, loading) { 74 | const config = { 75 | method: 'delete', 76 | url: SERVICE_URL + url, 77 | data: params, 78 | } 79 | return Http.send(config, loading) 80 | } 81 | 82 | static put(url, params = {}, loading) { 83 | const config = { 84 | method: 'put', 85 | url: SERVICE_URL + url, 86 | data: params, 87 | } 88 | return Http.send(config, loading) 89 | } 90 | 91 | static download(url, params = {}, loading) { 92 | const config = { 93 | responseType: 'blob', 94 | method: 'post', 95 | url: SERVICE_URL + url, 96 | data: params, 97 | } 98 | let isBlob = true 99 | return Http.send(config, loading, isBlob) 100 | } 101 | 102 | static get(url, params = {}, loading) { 103 | let urlParams = [] 104 | Object.keys(params).forEach((key) => { 105 | urlParams.push(`${key}=${encodeURIComponent(params[key])}`) 106 | }) 107 | if (urlParams.length) { 108 | urlParams = `${SERVICE_URL + url}?${urlParams.join('&')}` 109 | } else { 110 | urlParams = SERVICE_URL + url 111 | } 112 | const config = { 113 | url: urlParams, 114 | params: { 115 | randomTime: new Date().getTime(), 116 | }, 117 | } 118 | return Http.send(config, loading) 119 | } 120 | 121 | static get2(url, params = {}, loading) { 122 | const config = { 123 | method: 'post', 124 | url: SERVICE_URL + url, 125 | data: params, 126 | params: { 127 | randomTime: new Date().getTime(), 128 | }, 129 | } 130 | return Http.send(config, loading) 131 | } 132 | 133 | static post2(url, params = {}, loading) { 134 | const config = { 135 | method: 'post', 136 | url: SERVICE_URL + url, 137 | headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' }, 138 | data: params, 139 | } 140 | return Http.send(config, loading) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/components/LinyuCardCarousel.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 109 | 110 | 178 | -------------------------------------------------------------------------------- /src/components/Notify.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 68 | 69 | 149 | -------------------------------------------------------------------------------- /src/components/LinyuTooltip.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 143 | 144 | 161 | -------------------------------------------------------------------------------- /src/components/Msg/LinyuMsg.vue: -------------------------------------------------------------------------------- 1 | 48 | 74 | 152 | -------------------------------------------------------------------------------- /src/components/Msg/LinyuMsgContent.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 118 | 119 | 173 | -------------------------------------------------------------------------------- /src/components/LinyuDraggableWindow.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 207 | 208 | 236 | -------------------------------------------------------------------------------- /src/emoji/emojiText.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emoji表情", 3 | "icon": "😀", 4 | "list": [ 5 | { 6 | "name": "笑脸", 7 | "icon": "😀" 8 | }, 9 | { 10 | "name": "大笑", 11 | "icon": "😃" 12 | }, 13 | { 14 | "name": "微笑", 15 | "icon": "😄" 16 | }, 17 | { 18 | "name": "得意", 19 | "icon": "😁" 20 | }, 21 | { 22 | "name": "捧腹大笑", 23 | "icon": "😆" 24 | }, 25 | { 26 | "name": "流汗笑", 27 | "icon": "😅" 28 | }, 29 | { 30 | "name": "笑哭", 31 | "icon": "😂" 32 | }, 33 | { 34 | "name": "捧腹大笑", 35 | "icon": "🤣" 36 | }, 37 | { 38 | "name": "微笑脸", 39 | "icon": "😊" 40 | }, 41 | { 42 | "name": "天使", 43 | "icon": "😇" 44 | }, 45 | { 46 | "name": "愉快", 47 | "icon": "🙂" 48 | }, 49 | { 50 | "name": "倒立", 51 | "icon": "🙃" 52 | }, 53 | { 54 | "name": "眨眼", 55 | "icon": "😉" 56 | }, 57 | { 58 | "name": "轻松", 59 | "icon": "😌" 60 | }, 61 | { 62 | "name": "爱心", 63 | "icon": "😍" 64 | }, 65 | { 66 | "name": "亲吻", 67 | "icon": "😘" 68 | }, 69 | { 70 | "name": "亲吻脸", 71 | "icon": "😗" 72 | }, 73 | { 74 | "name": "送飞吻", 75 | "icon": "😙" 76 | }, 77 | { 78 | "name": "亲吻眼睛", 79 | "icon": "😚" 80 | }, 81 | { 82 | "name": "美味", 83 | "icon": "😋" 84 | }, 85 | { 86 | "name": "调皮", 87 | "icon": "😛" 88 | }, 89 | { 90 | "name": "调皮脸", 91 | "icon": "😝" 92 | }, 93 | { 94 | "name": "顽皮", 95 | "icon": "😜" 96 | }, 97 | { 98 | "name": "疯狂", 99 | "icon": "🤪" 100 | }, 101 | { 102 | "name": "挑衅", 103 | "icon": "🤨" 104 | }, 105 | { 106 | "name": "思考", 107 | "icon": "🧐" 108 | }, 109 | { 110 | "name": "书呆子", 111 | "icon": "🤓" 112 | }, 113 | { 114 | "name": "酷", 115 | "icon": "😎" 116 | }, 117 | { 118 | "name": "星星眼", 119 | "icon": "🤩" 120 | }, 121 | { 122 | "name": "得意", 123 | "icon": "😏" 124 | }, 125 | { 126 | "name": "不爽", 127 | "icon": "😒" 128 | }, 129 | { 130 | "name": "失望", 131 | "icon": "😞" 132 | }, 133 | { 134 | "name": "伤心", 135 | "icon": "😔" 136 | }, 137 | { 138 | "name": "担心", 139 | "icon": "😟" 140 | }, 141 | { 142 | "name": "困惑", 143 | "icon": "😕" 144 | }, 145 | { 146 | "name": "忧郁", 147 | "icon": "🙁" 148 | }, 149 | { 150 | "name": "沮丧", 151 | "icon": "😣" 152 | }, 153 | { 154 | "name": "痛苦", 155 | "icon": "😖" 156 | }, 157 | { 158 | "name": "疲惫", 159 | "icon": "😫" 160 | }, 161 | { 162 | "name": "难过", 163 | "icon": "😩" 164 | }, 165 | { 166 | "name": "哭泣", 167 | "icon": "😢" 168 | }, 169 | { 170 | "name": "大哭", 171 | "icon": "😭" 172 | }, 173 | { 174 | "name": "惊讶", 175 | "icon": "😮" 176 | }, 177 | { 178 | "name": "风吹", 179 | "icon": "💨" 180 | }, 181 | { 182 | "name": "愤怒", 183 | "icon": "😤" 184 | }, 185 | { 186 | "name": "生气", 187 | "icon": "😠" 188 | }, 189 | { 190 | "name": "愤怒爆炸", 191 | "icon": "😡" 192 | }, 193 | { 194 | "name": "骂人", 195 | "icon": "🤬" 196 | }, 197 | { 198 | "name": "爆炸", 199 | "icon": "🤯" 200 | }, 201 | { 202 | "name": "惊恐", 203 | "icon": "😳" 204 | }, 205 | { 206 | "name": "尖叫", 207 | "icon": "😱" 208 | }, 209 | { 210 | "name": "害怕", 211 | "icon": "😨" 212 | }, 213 | { 214 | "name": "恐惧", 215 | "icon": "😰" 216 | }, 217 | { 218 | "name": "伤心", 219 | "icon": "😥" 220 | }, 221 | { 222 | "name": "汗水", 223 | "icon": "😓" 224 | }, 225 | { 226 | "name": "拥抱", 227 | "icon": "🤗" 228 | }, 229 | { 230 | "name": "思考", 231 | "icon": "🤔" 232 | }, 233 | { 234 | "name": "惊讶捂嘴", 235 | "icon": "🤭" 236 | }, 237 | { 238 | "name": "安静", 239 | "icon": "🤫" 240 | }, 241 | { 242 | "name": "撒谎", 243 | "icon": "🤥" 244 | }, 245 | { 246 | "name": "无语", 247 | "icon": "😶" 248 | }, 249 | { 250 | "name": "冷静", 251 | "icon": "😐" 252 | }, 253 | { 254 | "name": "无表情", 255 | "icon": "😑" 256 | }, 257 | { 258 | "name": "尴尬", 259 | "icon": "😬" 260 | }, 261 | { 262 | "name": "翻白眼", 263 | "icon": "🙄" 264 | }, 265 | { 266 | "name": "惊讶", 267 | "icon": "😯" 268 | }, 269 | { 270 | "name": "担忧", 271 | "icon": "😦" 272 | }, 273 | { 274 | "name": "难过", 275 | "icon": "😧" 276 | }, 277 | { 278 | "name": "惊讶", 279 | "icon": "😮" 280 | }, 281 | { 282 | "name": "震惊", 283 | "icon": "😲" 284 | }, 285 | { 286 | "name": "困倦", 287 | "icon": "😴" 288 | }, 289 | { 290 | "name": "流口水", 291 | "icon": "🤤" 292 | }, 293 | { 294 | "name": "困倦", 295 | "icon": "😪" 296 | }, 297 | { 298 | "name": "头晕", 299 | "icon": "😵" 300 | }, 301 | { 302 | "name": "闪光", 303 | "icon": "💫" 304 | }, 305 | { 306 | "name": "闭嘴", 307 | "icon": "🤐" 308 | }, 309 | { 310 | "name": "呕吐", 311 | "icon": "🤢" 312 | }, 313 | { 314 | "name": "恶心", 315 | "icon": "🤮" 316 | }, 317 | { 318 | "name": "打喷嚏", 319 | "icon": "🤧" 320 | }, 321 | { 322 | "name": "生病", 323 | "icon": "😷" 324 | }, 325 | { 326 | "name": "发烧", 327 | "icon": "🤒" 328 | }, 329 | { 330 | "name": "头疼", 331 | "icon": "🤕" 332 | }, 333 | { 334 | "name": "发财", 335 | "icon": "🤑" 336 | }, 337 | { 338 | "name": "牛仔", 339 | "icon": "🤠" 340 | }, 341 | { 342 | "name": "恶魔", 343 | "icon": "😈" 344 | }, 345 | { 346 | "name": "鬼脸", 347 | "icon": "👿" 348 | }, 349 | { 350 | "name": "妖怪", 351 | "icon": "👹" 352 | }, 353 | { 354 | "name": "面具", 355 | "icon": "👺" 356 | }, 357 | { 358 | "name": "小丑", 359 | "icon": "🤡" 360 | }, 361 | { 362 | "name": "便便", 363 | "icon": "💩" 364 | }, 365 | { 366 | "name": "鬼魂", 367 | "icon": "👻" 368 | }, 369 | { 370 | "name": "骷髅", 371 | "icon": "💀" 372 | }, 373 | { 374 | "name": "外星人", 375 | "icon": "👽" 376 | }, 377 | { 378 | "name": "外星飞船", 379 | "icon": "👾" 380 | }, 381 | { 382 | "name": "机器人", 383 | "icon": "🤖" 384 | }, 385 | { 386 | "name": "南瓜", 387 | "icon": "🎃" 388 | }, 389 | { 390 | "name": "猫咪", 391 | "icon": "😺" 392 | }, 393 | { 394 | "name": "猫咪微笑", 395 | "icon": "😸" 396 | }, 397 | { 398 | "name": "猫咪笑", 399 | "icon": "😹" 400 | }, 401 | { 402 | "name": "猫咪爱心", 403 | "icon": "😻" 404 | }, 405 | { 406 | "name": "猫咪眯眼", 407 | "icon": "😼" 408 | }, 409 | { 410 | "name": "猫咪亲吻", 411 | "icon": "😽" 412 | }, 413 | { 414 | "name": "猫咪叫", 415 | "icon": "🙀" 416 | }, 417 | { 418 | "name": "猫咪哭泣", 419 | "icon": "😿" 420 | }, 421 | { 422 | "name": "猫咪生气", 423 | "icon": "😾" 424 | }, 425 | { 426 | "name": "小狗", 427 | "icon": "🐶" 428 | }, 429 | { 430 | "name": "一百分", 431 | "icon": "💯" 432 | } 433 | ] 434 | } 435 | -------------------------------------------------------------------------------- /src/views/LoginPage.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 164 | 165 | 286 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/components/LinyuMsgInput.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 364 | 365 | 429 | -------------------------------------------------------------------------------- /src/components/FileTransfer.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 320 | 321 | 398 | -------------------------------------------------------------------------------- /src/components/VideoChat.vue: -------------------------------------------------------------------------------- 1 | 144 | 363 | 488 | -------------------------------------------------------------------------------- /src/emoji/miyoushe.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "米游兔", 3 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/62158b26331a045bbaabf48d1fc5c8eb_980401343708652819.png", 4 | "list": [ 5 | { 6 | "name": "阿姬-倒地了", 7 | "icon": "https://bbs-static.miyoushe.com/static/2023/05/22/a2fa805a0a782cdb8ab44ddaf8d5343d_2757305461654236072.png" 8 | }, 9 | { 10 | "name": "阿姬-倒地", 11 | "icon": "https://upload-bbs.miyoushe.com/upload/2022/12/08/a2fa805a0a782cdb8ab44ddaf8d5343d_7408497524870727211.png" 12 | }, 13 | { 14 | "name": "阿姬-得意", 15 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/b171cc5b6c8d8b6257a3bf8163fb2680_567796524796359289.png" 16 | }, 17 | { 18 | "name": "阿姬-低落", 19 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/9e8bcae8c207bef5c02c176cdb6916ee_7651949051600440018.png" 20 | }, 21 | { 22 | "name": "阿姬-调查", 23 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/43338db72e2bb898d13a84c2fc302cb8_7770278814004091890.png" 24 | }, 25 | { 26 | "name": "阿姬-惊讶", 27 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/829f0a1d533765b80769dd1a38dc338c_9182108590963360703.png" 28 | }, 29 | { 30 | "name": "阿姬-开心", 31 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/4b4fb49d4bd402c530600e758ebb06bd_5477506439621383475.png" 32 | }, 33 | { 34 | "name": "阿姬-灵感", 35 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/62158b26331a045bbaabf48d1fc5c8eb_980401343708652819.png" 36 | }, 37 | { 38 | "name": "阿姬-期待", 39 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/955210989c73f207db2268b43b95f7d5_2639242042836509216.png" 40 | }, 41 | { 42 | "name": "阿姬-疑问", 43 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/d3f9322fcc499df3fca778e8c366f34d_4195564687951765835.png" 44 | }, 45 | { 46 | "name": "吃雪糕", 47 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/e380fdc2668f3dd0e8c8b9420a3cf124_3493794542940321040.png" 48 | }, 49 | { 50 | "name": "米游兔-加油", 51 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/01/18/838dba51bd47c248d1f02b2e24a28b18_6640243918864203211.png" 52 | }, 53 | { 54 | "name": "米游兔—飚汗", 55 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/74442ea449af598301cd16ed2841d3b0_5330135795233750081.png" 56 | }, 57 | { 58 | "name": "阿君-得意", 59 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/2b2c2b9c68222997c21867d0d2bf8e09_3346073008037045533.png" 60 | }, 61 | { 62 | "name": "米游君-喝可乐", 63 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/01/18/06f3f4c54fc51d82ba3e575194504d80_6167770388617778929.png" 64 | }, 65 | { 66 | "name": "米游君-认真工作", 67 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/dec64f96a41d05d0f0c73432e7636e7b_3857353910783327632.png" 68 | }, 69 | { 70 | "name": "米游君-杂耍", 71 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/01/18/c932711f2b7c36254e7afde91596f358_7200038206758881023.png" 72 | }, 73 | { 74 | "name": "绝区姬-嗨", 75 | "icon": "https://img-static.mihoyo.com/communityweb/upload/2645aa282eee6e34ff5fbd546476df2b.png" 76 | }, 77 | { 78 | "name": "绝区姬-哇吼", 79 | "icon": "https://img-static.mihoyo.com/communityweb/upload/6e1e0ce67ffad5de3b21cb50d37a1eac.png" 80 | }, 81 | { 82 | "name": "米游君-认真工作", 83 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/dec64f96a41d05d0f0c73432e7636e7b_5424104392001050566.png" 84 | }, 85 | { 86 | "name": "绝区姬-自拍", 87 | "icon": "https://img-static.mihoyo.com/communityweb/upload/d6279aaf178f56d095a1abae4a5ba3c6.png" 88 | }, 89 | { 90 | "name": "绝区姬-自信", 91 | "icon": "https://img-static.mihoyo.com/communityweb/upload/45f524b90875b85a461bafa9fe9b483d.png" 92 | }, 93 | { 94 | "name": "米游姬-抱抱", 95 | "icon": "https://img-static.mihoyo.com/communityweb/upload/7a514b631b93abb424cc01ade18020d5.png" 96 | }, 97 | { 98 | "name": "米游姬-好耶", 99 | "icon": "https://img-static.mihoyo.com/communityweb/upload/6abecce847497d0cca150543bbf14709.png" 100 | }, 101 | { 102 | "name": "米游姬-抱米游兔", 103 | "icon": "https://img-static.mihoyo.com/communityweb/upload/402fcfc2bd296f8189e6c85df3227f73.png" 104 | }, 105 | { 106 | "name": "米游姬-期待哦", 107 | "icon": "https://img-static.mihoyo.com/communityweb/upload/5f401916a1ef1c133b827949ff765692.png" 108 | }, 109 | { 110 | "name": "米游姬-抛心心", 111 | "icon": "https://img-static.mihoyo.com/communityweb/upload/a2016a74dc5c31f7ee259e646415407c.png" 112 | }, 113 | { 114 | "name": "米游姬-献花", 115 | "icon": "https://img-static.mihoyo.com/communityweb/upload/ca9a10a02c1007bf2626e65df2df7381.png" 116 | }, 117 | { 118 | "name": "米游姬-休息", 119 | "icon": "https://img-static.mihoyo.com/communityweb/upload/c35e095d32d704ed85302d6fac6e8ca8.png" 120 | }, 121 | { 122 | "name": "米游兔-OK", 123 | "icon": "https://img-static.mihoyo.com/communityweb/upload/db9c29ac19e22a5d9a60195803c3276b.png" 124 | }, 125 | { 126 | "name": "米游姬-卖萌", 127 | "icon": "https://img-static.mihoyo.com/communityweb/upload/3831379d9e92f4050e640080e73be1bb.gif" 128 | }, 129 | { 130 | "name": "米游姬-感谢", 131 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/02/03/a16450f92757f9d45588a8541fbbab09_4901912883842187271.gif" 132 | }, 133 | { 134 | "name": "米游姬-干杯", 135 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/01/18/8a1c5e858456b720f2c3c45982729628_5411981834900996336.gif" 136 | }, 137 | { 138 | "name": "米游姬-开心", 139 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/01/18/67ac33a760e1b8a4d1ed346bd69a9467_6056156571882767008.gif" 140 | }, 141 | { 142 | "name": "偶像姬-WINK", 143 | "icon": "https://img-static.mihoyo.com/communityweb/upload/13042dc38b444c3dab24bce0b83d35d7.png" 144 | }, 145 | { 146 | "name": "偶像姬-应援", 147 | "icon": "https://img-static.mihoyo.com/communityweb/upload/96934755c76f0abe4ebbdfb34950beaa.png" 148 | }, 149 | { 150 | "name": "偶像姬-达咩", 151 | "icon": "https://img-static.mihoyo.com/communityweb/upload/f289ff3ea6828d1ab47b8e66fef20815.png" 152 | }, 153 | { 154 | "name": "偶像姬-糖葫芦", 155 | "icon": "https://img-static.mihoyo.com/communityweb/upload/23cbc98f6908928ed4b4495421310664.png" 156 | }, 157 | { 158 | "name": "偶像姬-闪亮登场", 159 | "icon": "https://img-static.mihoyo.com/communityweb/upload/53d2ad52039025a469d2c5c39d85b2a0.png" 160 | }, 161 | { 162 | "name": "偶像姬-期待", 163 | "icon": "https://img-static.mihoyo.com/communityweb/upload/5cb972102e1492c61aa2a404953956bd.png" 164 | }, 165 | { 166 | "name": "偶像姬-大声bb", 167 | "icon": "https://img-static.mihoyo.com/communityweb/upload/59fe234835e808c41b0208c50be44914.png" 168 | }, 169 | { 170 | "name": "阿姬-开心", 171 | "icon": "https://upload-bbs.mihoyo.com/upload/2022/11/14/4b4fb49d4bd402c530600e758ebb06bd_6955490723450463851.png" 172 | }, 173 | { 174 | "name": "偶像姬-超凶", 175 | "icon": "https://img-static.mihoyo.com/communityweb/upload/99dad3dfda99a78517532cd36a5c01c5.png" 176 | }, 177 | { 178 | "name": "偶像姬-沮丧", 179 | "icon": "https://img-static.mihoyo.com/communityweb/upload/6bdba3c03727d9e42ff5432d3e422751.png" 180 | }, 181 | { 182 | "name": "偶像姬-沮丧", 183 | "icon": "https://img-static.mihoyo.com/communityweb/upload/045a3b4140d74bd4c091f7e557d26213.png" 184 | }, 185 | { 186 | "name": "偶像姬-鸽子", 187 | "icon": "https://img-static.mihoyo.com/communityweb/upload/850ed1c7790db7beffd44156ddb3173e.png" 188 | }, 189 | { 190 | "name": "偶像姬-咕咕", 191 | "icon": "https://img-static.mihoyo.com/communityweb/upload/059e5d62858227e9e9660ca669187cec.png" 192 | }, 193 | { 194 | "name": "偶像姬-撒花", 195 | "icon": "https://img-static.mihoyo.com/communityweb/upload/7085182d437642634c95e6fecee9ad01.png" 196 | }, 197 | { 198 | "name": "米游姬-哼", 199 | "icon": "https://img-static.mihoyo.com/communityweb/upload/047350694eb6b3b68fbb02d31c1a91d1.png" 200 | }, 201 | { 202 | "name": "米游姬-乖巧", 203 | "icon": "https://img-static.mihoyo.com/communityweb/upload/2f2e3096743e864e75fa2ff50d36fa50.png" 204 | }, 205 | { 206 | "name": "米游姬-吃瓜", 207 | "icon": "https://img-static.mihoyo.com/communityweb/upload/613a2b262af0319edde21587b88a9c6e.png" 208 | }, 209 | { 210 | "name": "米游姬-呆滞", 211 | "icon": "https://img-static.mihoyo.com/communityweb/upload/03c78415a3318d527d307c52a188a5ad.png" 212 | }, 213 | { 214 | "name": "米游姬-疑问", 215 | "icon": "https://img-static.mihoyo.com/communityweb/upload/cfc6b254655ee9b2a08202020d898f87.png" 216 | }, 217 | { 218 | "name": "米游姬-睡觉", 219 | "icon": "https://img-static.mihoyo.com/communityweb/upload/dac9d3391a4a0b5efc0c0acd589d3b30.png" 220 | }, 221 | { 222 | "name": "米游姬-暗中观察", 223 | "icon": "https://img-static.mihoyo.com/communityweb/upload/d4d609d5af6bc85a0c3bba6b4b59fffa.png" 224 | }, 225 | { 226 | "name": "米游姬-惊", 227 | "icon": "https://img-static.mihoyo.com/communityweb/upload/9dec3e01e834018c63b1f6710d4f1b8e.png" 228 | }, 229 | { 230 | "name": "米游姬-点赞", 231 | "icon": "https://img-static.mihoyo.com/communityweb/upload/d6ec212722aa079647bb233a3853f775.png" 232 | }, 233 | { 234 | "name": "米游姬-求求你啦", 235 | "icon": "https://img-static.mihoyo.com/communityweb/upload/1642da910ce6a64b72eb436a008d3af8.png" 236 | }, 237 | { 238 | "name": "米游姬-期待", 239 | "icon": "https://img-static.mihoyo.com/communityweb/upload/6adaac5ed9b16311259d3bbb6c108125.png" 240 | }, 241 | { 242 | "name": "米游姬-打call", 243 | "icon": "https://img-static.mihoyo.com/communityweb/upload/f9c9a65998f81afff034bfbe20087f4a.png" 244 | }, 245 | { 246 | "name": "米游姬-撒花", 247 | "icon": "https://img-static.mihoyo.com/communityweb/upload/19cf3986f2c2e034dd8704641a430cf9.png" 248 | }, 249 | { 250 | "name": "米游姬-来了来了", 251 | "icon": "https://img-static.mihoyo.com/communityweb/upload/e31ca4bc3b36d83ba0095505d4e46972.png" 252 | }, 253 | { 254 | "name": "米游姬-周五啦", 255 | "icon": "https://img-static.mihoyo.com/communityweb/upload/fce537cc087bab80640209c5a2b5c59f.png" 256 | }, 257 | { 258 | "name": "米游姬-累趴", 259 | "icon": "https://img-static.mihoyo.com/communityweb/upload/968635e0f4b0b0fbb6cc3e19aa64ffb3.png" 260 | }, 261 | { 262 | "name": "星穹米游姬-得意", 263 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/01/18/6520fc446c0d2daa7aa0bb0d4947392f_3450328440953691483.png" 264 | }, 265 | { 266 | "name": "星穹米游姬-哭唧唧", 267 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/01/18/9b6cab9f6fe05d7d99888525338b8930_4389753021835428841.png" 268 | }, 269 | { 270 | "name": "星穹米游姬-你好", 271 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/01/18/ca2646288114295698f89b9431db206f_45031739628247345.png" 272 | }, 273 | { 274 | "name": "星穹米游姬-耶!", 275 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/01/18/a0ca769e6f3bfc4291e36287fc9d38ef_8994291476410828569.png" 276 | }, 277 | { 278 | "name": "吃糖葫芦", 279 | "icon": "https://img-static.mihoyo.com/communityweb/upload/ffb9724864fd36eff5df36a3e7145075.png" 280 | }, 281 | { 282 | "name": "春节咕咕", 283 | "icon": "https://img-static.mihoyo.com/communityweb/upload/1f7ac4a1432f8cd2802ad2adb9162199.png" 284 | }, 285 | { 286 | "name": "放鞭炮", 287 | "icon": "https://img-static.mihoyo.com/communityweb/upload/6e68fe14b046b4e0dcadb358f59b57c5.png" 288 | }, 289 | { 290 | "name": "福到啦", 291 | "icon": "https://img-static.mihoyo.com/communityweb/upload/97f16d18549d2649232931a12e4553fb.png" 292 | }, 293 | { 294 | "name": "恭贺新春", 295 | "icon": "https://img-static.mihoyo.com/communityweb/upload/ffa54fd8e3cbf92405b8053462fdfe86.png" 296 | }, 297 | { 298 | "name": "米游姬-发福利", 299 | "icon": "https://img-static.mihoyo.com/communityweb/upload/4a4990c1e1294e40e5078b4ff5518cc4.png" 300 | }, 301 | { 302 | "name": "米游姬-福袋", 303 | "icon": "https://img-static.mihoyo.com/communityweb/upload/906db216f9a2c3bfba0f3c0705cfcc7e.png" 304 | }, 305 | { 306 | "name": "米游姬-哈欠", 307 | "icon": "https://img-static.mihoyo.com/communityweb/upload/9a0d2f4bffa1913f52e19bd738beb41a.png" 308 | }, 309 | { 310 | "name": "米游姬-抢红包", 311 | "icon": "https://img-static.mihoyo.com/communityweb/upload/1bee21e3f57024d72abd4aabeb411315.png" 312 | }, 313 | { 314 | "name": "米游姬-入欧", 315 | "icon": "https://img-static.mihoyo.com/communityweb/upload/366803aa8452bd1a2b2b57b556c7a504.png" 316 | }, 317 | { 318 | "name": "米游姬-唢呐", 319 | "icon": "https://img-static.mihoyo.com/communityweb/upload/3e54210658c07342924cc536caa22c99.png" 320 | }, 321 | { 322 | "name": "米游兔-好耶", 323 | "icon": "https://img-static.mihoyo.com/communityweb/upload/422b384f970bf369dc0083d2fa17ede9.png" 324 | }, 325 | { 326 | "name": "米游兔-脱非", 327 | "icon": "https://img-static.mihoyo.com/communityweb/upload/a7fa138da244faa48e57e81c656639be.png" 328 | }, 329 | { 330 | "name": "牛气冲天", 331 | "icon": "https://img-static.mihoyo.com/communityweb/upload/02390a2b74d3a9c70b4ff78cdb9d608a.png" 332 | }, 333 | { 334 | "name": "受到惊吓", 335 | "icon": "https://img-static.mihoyo.com/communityweb/upload/0b568ef852feeb12f72c1b7b6123461f.png" 336 | }, 337 | { 338 | "name": "我的心好痛", 339 | "icon": "https://img-static.mihoyo.com/communityweb/upload/8e53cb4b90a9bbb67db92a9ca95cfd19.png" 340 | }, 341 | { 342 | "name": "魔游姬-哼哼", 343 | "icon": "https://img-static.mihoyo.com/communityweb/upload/0fb1f674bffda4fa9ab8b078171c53d9.png" 344 | }, 345 | { 346 | "name": "魔游姬-得意", 347 | "icon": "https://img-static.mihoyo.com/communityweb/upload/5a97d54c56fcb87e041c29eb0e6a5f27.png" 348 | }, 349 | { 350 | "name": "魔游姬-害怕", 351 | "icon": "https://img-static.mihoyo.com/communityweb/upload/e907f9fe3609227517f70697835e0cae.png" 352 | }, 353 | { 354 | "name": "魔游姬-鸽子", 355 | "icon": "https://img-static.mihoyo.com/communityweb/upload/03564ea172a338ac1e4e977792d31f3f.png" 356 | }, 357 | { 358 | "name": "魔游姬-生气", 359 | "icon": "https://img-static.mihoyo.com/communityweb/upload/45ca6788363e15288193244bb5493a9b.png" 360 | }, 361 | { 362 | "name": "魔游姬-糖葫芦", 363 | "icon": "https://img-static.mihoyo.com/communityweb/upload/ebd96478a64bb7ac193806be15913198.png" 364 | }, 365 | { 366 | "name": "米游姬-比心", 367 | "icon": "https://img-static.mihoyo.com/communityweb/upload/4d41193fa02ac31e2daf7436ba5e12cf.png" 368 | }, 369 | { 370 | "name": "米游姬糖-葫芦", 371 | "icon": "https://img-static.mihoyo.com/communityweb/upload/6744fdc365a234f152bc3fcc6443c7a9.png" 372 | }, 373 | { 374 | "name": "米游姬-咕咕", 375 | "icon": "https://img-static.mihoyo.com/communityweb/upload/8568e2f26acbd1b86ae03dbf4b62f1b7.png" 376 | }, 377 | { 378 | "name": "米游姬-疑惑", 379 | "icon": "https://img-static.mihoyo.com/communityweb/upload/611e5b672d31a4fd203b46253c1f2348.png" 380 | }, 381 | { 382 | "name": "米游姬-得意", 383 | "icon": "https://img-static.mihoyo.com/communityweb/upload/068a8b611fae7cde48470ebfe5e21847.png" 384 | }, 385 | { 386 | "name": "米游姬-观察", 387 | "icon": "https://img-static.mihoyo.com/communityweb/upload/251a9bb6d4a4f1a39a430159919ed5fc.png" 388 | }, 389 | { 390 | "name": "米游姬-抱抱", 391 | "icon": "https://img-static.mihoyo.com/communityweb/upload/b37d75bce678aaed49a93a911299aa39.png" 392 | }, 393 | { 394 | "name": "米游姬-大哭", 395 | "icon": "https://img-static.mihoyo.com/communityweb/upload/521b9f657d4265d2299641aa6552e7f3.png" 396 | }, 397 | { 398 | "name": "米游姬-喝茶", 399 | "icon": "https://img-static.mihoyo.com/communityweb/upload/01ecd5b3b0f65c59ec46e2dc2538ba79.png" 400 | }, 401 | { 402 | "name": "米游姬流-鼻血", 403 | "icon": "https://img-static.mihoyo.com/communityweb/upload/8bcf809e4bdc770476544b1276acece1.png" 404 | }, 405 | { 406 | "name": "米游姬-委屈", 407 | "icon": "https://img-static.mihoyo.com/communityweb/upload/dc9830d58892a88244364ce51394011c.png" 408 | }, 409 | { 410 | "name": "米游兔-加油", 411 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/01/18/5857b8a3d4023bd05954225b0d578845_8473504187038159665.png" 412 | }, 413 | { 414 | "name": "米游兔-无奈", 415 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/01/18/3597ff62b268362f91576dda93e6d58b_2427201235588485119.png" 416 | }, 417 | { 418 | "name": "米游兔-烟雾弹", 419 | "icon": "https://upload-bbs.miyoushe.com/upload/2023/01/18/3d0feb3d67c59658d1a084a8eabd00af_2634705373803711026.png" 420 | }, 421 | { 422 | "name": "米游兔-笔心", 423 | "icon": "https://img-static.mihoyo.com/communityweb/upload/2416bcd2bd669db70ea8e8db923a81eb.png" 424 | }, 425 | { 426 | "name": "米游兔-糖葫芦", 427 | "icon": "https://img-static.mihoyo.com/communityweb/upload/befed8e6dd7a9d00918dd257cdd1a71d.png" 428 | }, 429 | { 430 | "name": "米游兔-害怕", 431 | "icon": "https://img-static.mihoyo.com/communityweb/upload/a03798f27603dd3d94eb83786f75b71d.png" 432 | }, 433 | { 434 | "name": "米游兔-暗中观察", 435 | "icon": "https://img-static.mihoyo.com/communityweb/upload/c81b4153b1b7e687aaea76fc1e4c5dc2.png" 436 | }, 437 | { 438 | "name": "米游兔-可怜兮兮", 439 | "icon": "https://img-static.mihoyo.com/communityweb/upload/d57ac9b36df80b48b1acb5769ac349ac.png" 440 | }, 441 | { 442 | "name": "米游兔-星星", 443 | "icon": "https://img-static.mihoyo.com/communityweb/upload/43e6636cada23e5411a4e82546ee9f22.png" 444 | }, 445 | { 446 | "name": "米游兔-递茶", 447 | "icon": "https://img-static.mihoyo.com/communityweb/upload/ba72f75be849cf28e55c9dce548967f0.png" 448 | }, 449 | { 450 | "name": "米游兔-吃惊", 451 | "icon": "https://img-static.mihoyo.com/communityweb/upload/378cdb8ccf15a49f0e7b3aba18acf2ff.png" 452 | }, 453 | { 454 | "name": "米游兔-乖巧", 455 | "icon": "https://img-static.mihoyo.com/communityweb/upload/d93728df6c1ea1fb2a0ae381824a970d.png" 456 | }, 457 | { 458 | "name": "米游兔-自闭", 459 | "icon": "https://img-static.mihoyo.com/communityweb/upload/77645095c66d46eb001231934c28be1a.png" 460 | }, 461 | { 462 | "name": "米游兔-安详", 463 | "icon": "https://img-static.mihoyo.com/communityweb/upload/fd58c953ab1b2b886cc524ff1f636aa8.png" 464 | }, 465 | { 466 | "name": "米游兔-吨吨", 467 | "icon": "https://img-static.mihoyo.com/communityweb/upload/2a935e275eaa9b7d7f50523f56adadb6.png" 468 | }, 469 | { 470 | "name": "米游兔-举牌子", 471 | "icon": "https://img-static.mihoyo.com/communityweb/upload/577a81a5ede754faa4d92882c5525fff.png" 472 | }, 473 | { 474 | "name": "米游兔-吃瓜", 475 | "icon": "https://img-static.mihoyo.com/communityweb/upload/5ea22ea42dc918b8263006ef3c2fd7e0.png" 476 | }, 477 | { 478 | "name": "米游兔-飞翔", 479 | "icon": "https://img-static.mihoyo.com/communityweb/upload/cb81bf30de24cf79829961cca5b11428.png" 480 | }, 481 | { 482 | "name": "米游兔-期待", 483 | "icon": "https://img-static.mihoyo.com/communityweb/upload/8ae8647b41a08ffec4e5eb2239fc30c1.png" 484 | } 485 | ] 486 | } 487 | --------------------------------------------------------------------------------