├── .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 |
2 |
3 | {{ props.text }}
4 |
5 |
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 |
2 |
3 |
4 |
5 | {{
6 | props.msg.message === '0'
7 | ? '通话未接通'
8 | : `通话时长 : ${formatTimingTime(props.msg.message)}`
9 | }}
10 |
11 |
12 |
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 |
2 |
7 |
8 |
9 |
10 |
11 |
21 |
22 |
35 |
--------------------------------------------------------------------------------
/src/components/LinyuModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
37 |
--------------------------------------------------------------------------------
/src/components/Msg/LinyuReferenceContent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ props.msg.fromInfo?.name }} :
4 |
5 |
6 |
7 |
8 |
9 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/Msg/MsgContent/TimeMsg.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ formatTime(props.content) }}
5 |
6 |
7 |
8 |
15 |
33 |
--------------------------------------------------------------------------------
/src/components/LinyuLabel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
18 |
19 |
33 |
--------------------------------------------------------------------------------
/src/components/LinyuTextButton.vue:
--------------------------------------------------------------------------------
1 |
2 | emit('click', e)">
3 | {{ props.text }}
4 |
5 |
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 |
2 |
3 |
4 |
{{ props.msg.fromInfo.name }}
5 | 撤回一条消息
6 |
7 |
8 |
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 |
2 | emit('click', e)" :style="{ width: size, height: size }">
3 |
4 |
5 |
6 |
7 |
24 |
25 |
41 |
--------------------------------------------------------------------------------
/src/components/Msg/MsgContent/EmojiMsg.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 | ·
5 |
6 |
7 |
8 |
9 |
36 |
37 |
55 |
--------------------------------------------------------------------------------
/src/components/Msg/LinyuChatListContent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ `${msg?.fromInfo?.name} : ` }}
4 |
5 |
6 |
7 |
撤回一条消息
8 |
9 |
10 |
11 |
[音视频通话]
12 |
13 |
14 |
27 |
34 |
--------------------------------------------------------------------------------
/src/components/ToastProvider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/components/LinyuLoading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
{{ props.label }}
9 |
10 |
11 |
16 |
17 |
62 |
--------------------------------------------------------------------------------
/src/components/ChatSkeleton.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
28 |
29 |
63 |
--------------------------------------------------------------------------------
/src/components/BorderGradientButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
28 |
29 |
68 |
--------------------------------------------------------------------------------
/src/components/LinyuToast.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ props.message }}
5 |
6 |
7 |
8 |
26 |
27 |
76 |
--------------------------------------------------------------------------------
/src/components/Msg/MsgContent/TextMsg.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ `@${getUserInfo(item.content).name}` }}
6 |
7 |
8 | {{ item.content }}
9 |
10 |
11 |
12 |
13 | {{ props.msg.message }}
14 |
15 |
16 |
17 |
45 |
46 |
62 |
--------------------------------------------------------------------------------
/src/components/LinyuDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
{{ props.title }}
7 |
8 |
9 |
{{ props.content }}
10 |
11 |
12 | 取 消
13 | 确 定
14 |
15 |
16 |
17 |
18 |
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 |
2 |
3 |
4 | {{ props.src }}
5 |
6 |
7 |
8 |
9 |
10 |
44 |
45 |
76 |
--------------------------------------------------------------------------------
/src/components/LinyuCircleProgress.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
25 |
26 |
{{ progress }}%
27 |
28 |
29 |
30 |
53 |
54 |
84 |
--------------------------------------------------------------------------------
/src/components/LinyuAvatar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ displayCharacter }}
6 |
7 |
8 |
9 |
10 |
60 |
61 |
78 |
--------------------------------------------------------------------------------
/src/components/LinyuPopup.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
83 |
84 |
100 |
--------------------------------------------------------------------------------
/src/components/LinyuEmojiBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | handlerEmoji(emoji.icon)"
8 | :src="emoji.icon"
9 | width="32px"
10 | height="32px"
11 | />
12 |
13 |
14 |
15 |
16 |
17 |
18 |
25 |
26 |
27 |
28 |
29 |
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 |
2 |
3 |
4 |
5 |
6 | {{ `@${getUserInfo(item.content).name}` }}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
64 |
65 |
102 |
--------------------------------------------------------------------------------
/src/components/ModifyUserInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
20 |
28 |
29 | 取消
30 | 确定
31 |
32 |
33 |
34 |
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 |
2 |
29 |
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 |
2 |
16 |
17 |
18 |
109 |
110 |
178 |
--------------------------------------------------------------------------------
/src/components/Notify.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
{{ notify?.notifyTitle }}
6 |
7 |
8 |
9 | 我知道了
10 |
11 |
12 |
13 |
22 |
23 |
24 |
25 |
26 |
68 |
69 |
149 |
--------------------------------------------------------------------------------
/src/components/LinyuTooltip.vue:
--------------------------------------------------------------------------------
1 |
2 |
33 |
34 |
35 |
143 |
144 |
161 |
--------------------------------------------------------------------------------
/src/components/Msg/LinyuMsg.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
14 |
15 |
16 |
17 |
18 | {{ msgStore.userListMap.get(props.msg.fromId).name }}
19 |
20 |
21 | [{{ props.msg.fromInfo.ipOwnership ?? '未知' }}]
22 |
23 |
24 |
25 |
26 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
74 |
152 |
--------------------------------------------------------------------------------
/src/components/Msg/LinyuMsgContent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
42 |
43 |
44 |
45 |
46 |
118 |
119 |
173 |
--------------------------------------------------------------------------------
/src/components/LinyuDraggableWindow.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
12 |
13 |
14 |
20 |
21 |
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 |
2 |
3 |
4 | openUrl('https://space.bilibili.com/135427028/channel/series')"
6 | icon="icon-bilibili"
7 | />
8 | openUrl('https://github.com/linyu-im')" icon="icon-github" />
9 | toggleDark(e, 'dark')"
12 | icon="icon-taiyang"
13 | />
14 | toggleDark(e, 'light')"
17 | icon="icon-yueliang"
18 | />
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 | Linyu在线聊天群
30 |
31 |
32 | 欢迎使用林语mini
33 |
34 |
35 |
36 |
42 |
43 |
44 | {{ !logging ? '验 证' : '登 录 中' }}
45 |
46 |
47 |
48 |
49 |
50 | 填写个人信息
51 |
52 |
53 |
54 |
60 |
61 |
62 |
63 | {{ !logging ? '进 入' : '请 等 待' }}
64 |
65 |
66 |
67 |
Author : Heath
68 |
QQ群 : 729158695
69 |
70 |
71 |
72 |
73 |
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 |
2 |
35 |
36 |
37 |
364 |
365 |
429 |
--------------------------------------------------------------------------------
/src/components/FileTransfer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
{{ props.targetInfo.name }}
10 |
11 | {{ props.isSend ? '等待对方接收' : `请求发送文件` }}
12 |
13 |
14 |
15 |
16 |
17 | {{ formatSize(props.file.size) }}
18 |
19 |
20 | {{ props.file.name }}
21 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {{ progress >= 100 ? '传输完成' : '传输中' }}
49 |
50 |
51 |
52 |
58 |
59 |
60 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
320 |
321 |
398 |
--------------------------------------------------------------------------------
/src/components/VideoChat.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
{{ props.targetInfo.name }}
10 |
11 | {{
12 | props.isSend
13 | ? '正在等待对方接听'
14 | : `邀请你${props.isOnlyAudio ? '语音' : '视频'}通话`
15 | }}
16 |
17 |
18 |
19 |
20 |
21 |
27 |
28 |
29 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
46 |
47 |
48 |
49 |
50 |
51 | {{ props.targetInfo.name }}
52 |
53 | {{ formatTimingTime(time) }}
54 |
55 |
56 |
57 | {{ formatTimingTime(time) }}
58 |
59 |
60 |
61 |
62 |
67 |
71 |
72 |
77 |
78 |
79 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
100 |
106 |
107 |
108 | {{ formatTimingTime(time) }}
109 |
110 |
111 |
116 |
120 |
121 |
126 |
127 |
128 |
133 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
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 |
--------------------------------------------------------------------------------