diff --git a/frontend/video-frontend-web/.env b/frontend/video-frontend-web/.env index 107d503..6f646b3 100644 --- a/frontend/video-frontend-web/.env +++ b/frontend/video-frontend-web/.env @@ -1,3 +1,5 @@ # 应用配置 VITE_APP_TITLE=HAOVP视频平台 VITE_API_BASE_URL=/api +VITE_PROXY_TARGET=http://192.168.1.101:9000 +VITE_MEDIA_BASE_URL=http://192.168.1.101:9100/video-platform diff --git a/frontend/video-frontend-web/.env.mock b/frontend/video-frontend-web/.env.mock index 95d4c1c..87bed87 100644 --- a/frontend/video-frontend-web/.env.mock +++ b/frontend/video-frontend-web/.env.mock @@ -3,4 +3,6 @@ VITE_APP_TITLE=HAOVP视频平台 VITE_API_BASE_URL=/api +VITE_PROXY_TARGET=http://192.168.1.101:9000 +VITE_MEDIA_BASE_URL=http://192.168.1.101:9100/video-platform VITE_MOCK=true diff --git a/frontend/video-frontend-web/.eslintignore b/frontend/video-frontend-web/.eslintignore new file mode 100644 index 0000000..bdf2bb9 --- /dev/null +++ b/frontend/video-frontend-web/.eslintignore @@ -0,0 +1,4 @@ +dist +node_modules +src/auto-imports.d.ts +src/components.d.ts diff --git a/frontend/video-frontend-web/.eslintrc.cjs b/frontend/video-frontend-web/.eslintrc.cjs new file mode 100644 index 0000000..18fd4e3 --- /dev/null +++ b/frontend/video-frontend-web/.eslintrc.cjs @@ -0,0 +1,29 @@ +module.exports = { + root: true, + env: { + browser: true, + es2021: true, + node: true + }, + extends: [ + 'eslint:recommended', + 'plugin:vue/vue3-recommended', + 'plugin:@typescript-eslint/recommended' + ], + parser: 'vue-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser', + ecmaVersion: 'latest', + sourceType: 'module', + extraFileExtensions: ['.vue'] + }, + rules: { + 'no-console': 'off', + 'no-empty': ['error', { allowEmptyCatch: true }], + 'vue/multi-word-component-names': 'off', + 'vue/no-template-shadow': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }] + } +} diff --git a/frontend/video-frontend-web/package-lock.json b/frontend/video-frontend-web/package-lock.json index a49f44c..cf724d0 100644 --- a/frontend/video-frontend-web/package-lock.json +++ b/frontend/video-frontend-web/package-lock.json @@ -14,6 +14,7 @@ "artplayer-plugin-danmuku": "^5.1.0", "axios": "^1.6.8", "dayjs": "^1.11.10", + "dplayer": "^1.27.1", "nprogress": "^0.2.0", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", @@ -2617,6 +2618,12 @@ "dev": true, "license": "MIT" }, + "node_modules/balloon-css": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/balloon-css/-/balloon-css-1.2.0.tgz", + "integrity": "sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==", + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.8", "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", @@ -3040,6 +3047,28 @@ "node": ">=6.0.0" } }, + "node_modules/dplayer": { + "version": "1.27.1", + "resolved": "https://registry.npmmirror.com/dplayer/-/dplayer-1.27.1.tgz", + "integrity": "sha512-2laBMXs5V1B9zPwJ7eAIw/OBo+Xjvy03i4GHTk3Cg+IWbrq8rKMFO0fFr6ClAYotYOCcFGOvaJDkOZcgKllsCA==", + "license": "MIT", + "dependencies": { + "axios": "1.2.3", + "balloon-css": "^1.0.3", + "promise-polyfill": "8.3.0" + } + }, + "node_modules/dplayer/node_modules/axios": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.2.3.tgz", + "integrity": "sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4886,6 +4915,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", diff --git a/frontend/video-frontend-web/package.json b/frontend/video-frontend-web/package.json index 31752a6..ffe76ca 100644 --- a/frontend/video-frontend-web/package.json +++ b/frontend/video-frontend-web/package.json @@ -8,39 +8,40 @@ "dev:mock": "vite --mode mock", "build": "vue-tsc && vite build", "preview": "vite preview", - "lint": "eslint --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "lint": "eslint --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix src", "format": "prettier --write src" }, "dependencies": { - "vue": "^3.4.21", - "vue-router": "^4.3.0", - "pinia": "^2.1.7", - "pinia-plugin-persistedstate": "^3.2.1", - "axios": "^1.6.8", - "vant": "^4.8.4", + "@iconify/vue": "^4.1.1", "@vant/use": "^1.6.0", "artplayer": "^5.1.7", "artplayer-plugin-danmuku": "^5.1.0", + "axios": "^1.6.8", "dayjs": "^1.11.10", + "dplayer": "^1.27.1", "nprogress": "^0.2.0", - "@iconify/vue": "^4.1.1" + "pinia": "^2.1.7", + "pinia-plugin-persistedstate": "^3.2.1", + "vant": "^4.8.4", + "vue": "^3.4.21", + "vue-router": "^4.3.0" }, "devDependencies": { - "@vitejs/plugin-vue": "^5.0.4", - "@vue/tsconfig": "^0.5.1", - "vite": "^5.2.0", - "vite-plugin-vue-devtools": "^7.0.25", - "typescript": "~5.4.0", - "vue-tsc": "^2.0.6", - "sass": "^1.72.0", - "unplugin-auto-import": "^0.17.5", - "unplugin-vue-components": "^0.26.0", "@types/node": "^20.11.30", "@types/nprogress": "^0.2.3", - "eslint": "^8.57.0", - "eslint-plugin-vue": "^9.24.0", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", - "prettier": "^3.2.5" + "@vitejs/plugin-vue": "^5.0.4", + "@vue/tsconfig": "^0.5.1", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.24.0", + "prettier": "^3.2.5", + "sass": "^1.72.0", + "typescript": "~5.4.0", + "unplugin-auto-import": "^0.17.5", + "unplugin-vue-components": "^0.26.0", + "vite": "^5.2.0", + "vite-plugin-vue-devtools": "^7.0.25", + "vue-tsc": "^2.0.6" } } diff --git a/frontend/video-frontend-web/public/home-banner/0095303b06f64e05ba354655c0a6c83fb618f8e7.png b/frontend/video-frontend-web/public/home-banner/0095303b06f64e05ba354655c0a6c83fb618f8e7.png new file mode 100644 index 0000000..e0e8c1a Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/0095303b06f64e05ba354655c0a6c83fb618f8e7.png differ diff --git a/frontend/video-frontend-web/public/home-banner/0415c092aa2c61c1ca25ba69b8c77130f1ab37c3.png b/frontend/video-frontend-web/public/home-banner/0415c092aa2c61c1ca25ba69b8c77130f1ab37c3.png new file mode 100644 index 0000000..0f4ec4e Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/0415c092aa2c61c1ca25ba69b8c77130f1ab37c3.png differ diff --git a/frontend/video-frontend-web/public/home-banner/0546ed600f8dcfcf4c2eb3212ed1e12178abdb86.png b/frontend/video-frontend-web/public/home-banner/0546ed600f8dcfcf4c2eb3212ed1e12178abdb86.png new file mode 100644 index 0000000..75f7152 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/0546ed600f8dcfcf4c2eb3212ed1e12178abdb86.png differ diff --git a/frontend/video-frontend-web/public/home-banner/0b9b00f62fd5760dc64fa16c564591957c3ace05.png b/frontend/video-frontend-web/public/home-banner/0b9b00f62fd5760dc64fa16c564591957c3ace05.png new file mode 100644 index 0000000..30f24e6 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/0b9b00f62fd5760dc64fa16c564591957c3ace05.png differ diff --git a/frontend/video-frontend-web/public/home-banner/0cfe34374720653a8f67d68af761d239eca15da7.png b/frontend/video-frontend-web/public/home-banner/0cfe34374720653a8f67d68af761d239eca15da7.png new file mode 100644 index 0000000..fdade5a Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/0cfe34374720653a8f67d68af761d239eca15da7.png differ diff --git a/frontend/video-frontend-web/public/home-banner/1fb1501cd66d326f296e78a4561f7cc89c48d719.png b/frontend/video-frontend-web/public/home-banner/1fb1501cd66d326f296e78a4561f7cc89c48d719.png new file mode 100644 index 0000000..28c2d39 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/1fb1501cd66d326f296e78a4561f7cc89c48d719.png differ diff --git a/frontend/video-frontend-web/public/home-banner/2a4e5f40421d467e841b24eed228910bf965d4a1.png b/frontend/video-frontend-web/public/home-banner/2a4e5f40421d467e841b24eed228910bf965d4a1.png new file mode 100644 index 0000000..21ee925 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/2a4e5f40421d467e841b24eed228910bf965d4a1.png differ diff --git a/frontend/video-frontend-web/public/home-banner/34c3621fc6fed4e16eab30d0448b35554711bfeb.png b/frontend/video-frontend-web/public/home-banner/34c3621fc6fed4e16eab30d0448b35554711bfeb.png new file mode 100644 index 0000000..8892ca5 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/34c3621fc6fed4e16eab30d0448b35554711bfeb.png differ diff --git a/frontend/video-frontend-web/public/home-banner/3505e4538c81fce91fa5e6dacd4703d8277df021.png b/frontend/video-frontend-web/public/home-banner/3505e4538c81fce91fa5e6dacd4703d8277df021.png new file mode 100644 index 0000000..ea0d916 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/3505e4538c81fce91fa5e6dacd4703d8277df021.png differ diff --git a/frontend/video-frontend-web/public/home-banner/3c711c23ae57db96468a23e2beca49ce66f12c73.png b/frontend/video-frontend-web/public/home-banner/3c711c23ae57db96468a23e2beca49ce66f12c73.png new file mode 100644 index 0000000..b41a97e Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/3c711c23ae57db96468a23e2beca49ce66f12c73.png differ diff --git a/frontend/video-frontend-web/public/home-banner/3d4a9667931a9a20011a384b7d699c3b286b82ab.png b/frontend/video-frontend-web/public/home-banner/3d4a9667931a9a20011a384b7d699c3b286b82ab.png new file mode 100644 index 0000000..5122039 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/3d4a9667931a9a20011a384b7d699c3b286b82ab.png differ diff --git a/frontend/video-frontend-web/public/home-banner/446466499bd9615bc950ec4d8ec63647defc05b6.png b/frontend/video-frontend-web/public/home-banner/446466499bd9615bc950ec4d8ec63647defc05b6.png new file mode 100644 index 0000000..9947b15 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/446466499bd9615bc950ec4d8ec63647defc05b6.png differ diff --git a/frontend/video-frontend-web/public/home-banner/48bee957f95b7bc5f0e1b7fc14b25a4cca26901a.png b/frontend/video-frontend-web/public/home-banner/48bee957f95b7bc5f0e1b7fc14b25a4cca26901a.png new file mode 100644 index 0000000..5fa12a5 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/48bee957f95b7bc5f0e1b7fc14b25a4cca26901a.png differ diff --git a/frontend/video-frontend-web/public/home-banner/4e2110157727360d88357840903ed05b6b94c453.png b/frontend/video-frontend-web/public/home-banner/4e2110157727360d88357840903ed05b6b94c453.png new file mode 100644 index 0000000..2d70e16 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/4e2110157727360d88357840903ed05b6b94c453.png differ diff --git a/frontend/video-frontend-web/public/home-banner/62aee277e1d935580f66d296f349bd358cd956a0.png b/frontend/video-frontend-web/public/home-banner/62aee277e1d935580f66d296f349bd358cd956a0.png new file mode 100644 index 0000000..f1003ad Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/62aee277e1d935580f66d296f349bd358cd956a0.png differ diff --git a/frontend/video-frontend-web/public/home-banner/6970b12a96d85dd28a0aeb803eb3428dfec771bb.png b/frontend/video-frontend-web/public/home-banner/6970b12a96d85dd28a0aeb803eb3428dfec771bb.png new file mode 100644 index 0000000..cde40ce Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/6970b12a96d85dd28a0aeb803eb3428dfec771bb.png differ diff --git a/frontend/video-frontend-web/public/home-banner/752ba46c080dca88401aac470f134ac0e6444705.png b/frontend/video-frontend-web/public/home-banner/752ba46c080dca88401aac470f134ac0e6444705.png new file mode 100644 index 0000000..8515bdc Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/752ba46c080dca88401aac470f134ac0e6444705.png differ diff --git a/frontend/video-frontend-web/public/home-banner/82a15b3fda5ce240bac44b8d0b793b13c3bafc4e.png b/frontend/video-frontend-web/public/home-banner/82a15b3fda5ce240bac44b8d0b793b13c3bafc4e.png new file mode 100644 index 0000000..4696ae0 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/82a15b3fda5ce240bac44b8d0b793b13c3bafc4e.png differ diff --git a/frontend/video-frontend-web/public/home-banner/83615a69b1ecab29163a54912260fd624a886906.png b/frontend/video-frontend-web/public/home-banner/83615a69b1ecab29163a54912260fd624a886906.png new file mode 100644 index 0000000..84cc066 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/83615a69b1ecab29163a54912260fd624a886906.png differ diff --git a/frontend/video-frontend-web/public/home-banner/83e19c7274daec8f0e7d97cd6ad888e3e43b8083.png b/frontend/video-frontend-web/public/home-banner/83e19c7274daec8f0e7d97cd6ad888e3e43b8083.png new file mode 100644 index 0000000..801c4ae Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/83e19c7274daec8f0e7d97cd6ad888e3e43b8083.png differ diff --git a/frontend/video-frontend-web/public/home-banner/85e5279a1d53d4cb46033e1619d78628415f98c4.png b/frontend/video-frontend-web/public/home-banner/85e5279a1d53d4cb46033e1619d78628415f98c4.png new file mode 100644 index 0000000..15e1b7b Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/85e5279a1d53d4cb46033e1619d78628415f98c4.png differ diff --git a/frontend/video-frontend-web/public/home-banner/8f51a0828e322ca0576f449ceb45cca453735506.png b/frontend/video-frontend-web/public/home-banner/8f51a0828e322ca0576f449ceb45cca453735506.png new file mode 100644 index 0000000..f53ce23 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/8f51a0828e322ca0576f449ceb45cca453735506.png differ diff --git a/frontend/video-frontend-web/public/home-banner/9316d140598d5dc7e86d72e4a99ed903953d2e76.png b/frontend/video-frontend-web/public/home-banner/9316d140598d5dc7e86d72e4a99ed903953d2e76.png new file mode 100644 index 0000000..0780338 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/9316d140598d5dc7e86d72e4a99ed903953d2e76.png differ diff --git a/frontend/video-frontend-web/public/home-banner/9afdb972babc0fed529e241176bad847a8094774.png b/frontend/video-frontend-web/public/home-banner/9afdb972babc0fed529e241176bad847a8094774.png new file mode 100644 index 0000000..f626ca4 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/9afdb972babc0fed529e241176bad847a8094774.png differ diff --git a/frontend/video-frontend-web/public/home-banner/b3ee9e71fbcc0520a1ac32e2b3a2d0dc6086bb9b.png b/frontend/video-frontend-web/public/home-banner/b3ee9e71fbcc0520a1ac32e2b3a2d0dc6086bb9b.png new file mode 100644 index 0000000..7dcadc0 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/b3ee9e71fbcc0520a1ac32e2b3a2d0dc6086bb9b.png differ diff --git a/frontend/video-frontend-web/public/home-banner/d81527e08e5031badd870819fa801f6fd41fc021.png b/frontend/video-frontend-web/public/home-banner/d81527e08e5031badd870819fa801f6fd41fc021.png new file mode 100644 index 0000000..9b88107 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/d81527e08e5031badd870819fa801f6fd41fc021.png differ diff --git a/frontend/video-frontend-web/public/home-banner/e3607ba671317e2b1e4990a9225cbf9981a7a414.png b/frontend/video-frontend-web/public/home-banner/e3607ba671317e2b1e4990a9225cbf9981a7a414.png new file mode 100644 index 0000000..8500001 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/e3607ba671317e2b1e4990a9225cbf9981a7a414.png differ diff --git a/frontend/video-frontend-web/public/home-banner/e52d6aecd75db0e310c73185304fd927834ac716.png b/frontend/video-frontend-web/public/home-banner/e52d6aecd75db0e310c73185304fd927834ac716.png new file mode 100644 index 0000000..d190850 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/e52d6aecd75db0e310c73185304fd927834ac716.png differ diff --git a/frontend/video-frontend-web/public/home-banner/eca4ed09885bef57072526002c8900467a07dc3f.png b/frontend/video-frontend-web/public/home-banner/eca4ed09885bef57072526002c8900467a07dc3f.png new file mode 100644 index 0000000..734338d Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/eca4ed09885bef57072526002c8900467a07dc3f.png differ diff --git a/frontend/video-frontend-web/public/home-banner/f14b431627327dda93f34b13392a3a773be5e788.png b/frontend/video-frontend-web/public/home-banner/f14b431627327dda93f34b13392a3a773be5e788.png new file mode 100644 index 0000000..4a79459 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/f14b431627327dda93f34b13392a3a773be5e788.png differ diff --git a/frontend/video-frontend-web/public/home-banner/f8720df769d23c497096f57b73528265fd6b554a.png b/frontend/video-frontend-web/public/home-banner/f8720df769d23c497096f57b73528265fd6b554a.png new file mode 100644 index 0000000..ae0e410 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/f8720df769d23c497096f57b73528265fd6b554a.png differ diff --git a/frontend/video-frontend-web/public/home-banner/fa622f4b21c7fb66f84ebf87d36faaaa993363f0.png b/frontend/video-frontend-web/public/home-banner/fa622f4b21c7fb66f84ebf87d36faaaa993363f0.png new file mode 100644 index 0000000..45afd38 Binary files /dev/null and b/frontend/video-frontend-web/public/home-banner/fa622f4b21c7fb66f84ebf87d36faaaa993363f0.png differ diff --git a/frontend/video-frontend-web/src/api/comment.ts b/frontend/video-frontend-web/src/api/comment.ts index c395f31..3c308c0 100644 --- a/frontend/video-frontend-web/src/api/comment.ts +++ b/frontend/video-frontend-web/src/api/comment.ts @@ -8,7 +8,7 @@ export const commentApi = { /** * 发表评论 */ - addComment(params: CommentSendParams): Promise { + addComment(params: CommentSendParams): Promise { return request.post('/comment', params) }, @@ -50,15 +50,15 @@ export const commentApi = { /** * 置顶评论 */ - topComment(id: number): Promise { - return request.post(`/comment/top/${id}`) + topComment(id: number, videoId: number): Promise { + return request.post(`/comment/top/${id}?videoId=${videoId}`) }, /** * 取消置顶评论 */ - untopComment(id: number): Promise { - return request.delete(`/comment/top/${id}`) + untopComment(id: number, videoId: number): Promise { + return request.delete(`/comment/top/${id}`, { videoId }) }, /** @@ -73,7 +73,7 @@ export const commentApi = { /** * 添加回复 */ - addReply(params: { commentId: number; content: string; replyId?: number }): Promise { + addReply(params: { commentId: number; videoId: number; content: string; replyToId?: number }): Promise { return request.post('/comment/reply', params) }, diff --git a/frontend/video-frontend-web/src/api/user.ts b/frontend/video-frontend-web/src/api/user.ts index eacc49c..b6619b6 100644 --- a/frontend/video-frontend-web/src/api/user.ts +++ b/frontend/video-frontend-web/src/api/user.ts @@ -73,14 +73,14 @@ export const userApi = { /** * 获取关注列表 */ - getFollowing(userId: number, params?: PageParams): Promise { + getFollowing(userId: number, params?: PageParams): Promise> { return request.get(`/user/follow/following/${userId}`, params) }, /** * 获取粉丝列表 */ - getFollowers(userId: number, params?: PageParams): Promise { + getFollowers(userId: number, params?: PageParams): Promise> { return request.get(`/user/follow/followers/${userId}`, params) }, @@ -105,21 +105,21 @@ export const userApi = { /** * 发送私信 */ - sendMessage(params: MessageSendParams): Promise { + sendMessage(params: MessageSendParams): Promise { return request.post('/user/message', params) }, /** * 获取私信会话列表 */ - getMessageList(params?: PageParams): Promise { + getMessageList(params?: PageParams): Promise> { return request.get('/user/message/list', params) }, /** * 获取与指定用户的私信记录 */ - getMessageDetail(targetId: number, params?: PageParams): Promise { + getMessageDetail(targetId: number, params?: PageParams): Promise> { return request.get(`/user/message/detail/${targetId}`, params) }, @@ -142,7 +142,7 @@ export const userApi = { /** * 获取通知列表 */ - getNotifications(type?: number, params?: PageParams): Promise { + getNotifications(type?: number, params?: PageParams): Promise> { return request.get('/user/notification/list', { type, ...params }) }, diff --git a/frontend/video-frontend-web/src/api/video.ts b/frontend/video-frontend-web/src/api/video.ts index d68b9e2..bcc0a0e 100644 --- a/frontend/video-frontend-web/src/api/video.ts +++ b/frontend/video-frontend-web/src/api/video.ts @@ -5,6 +5,7 @@ import type { VideoUpdateParams, VideoQueryParams, VideoUploadParams, + VideoUploadResult, PlayRecord, PageResult } from '@/types' @@ -18,14 +19,14 @@ export const videoApi = { /** * 上传视频文件 */ - uploadVideo(params: VideoUploadParams): Promise<{ videoUrl: string; coverUrl: string }> { + uploadVideo(params: VideoUploadParams): Promise { return request.upload('/content/video/upload', params.file, params.onProgress) }, /** * 创建视频记录 */ - createVideo(params: VideoCreateParams): Promise { + createVideo(params: VideoCreateParams): Promise { return request.post('/content/video', params) }, @@ -101,8 +102,8 @@ export const videoApi = { /** * 投币 */ - coinVideo(videoId: number): Promise { - return request.post(`/content/video/coin/${videoId}`) + coinVideo(videoId: number, amount = 1): Promise { + return request.post(`/content/video/coin/${videoId}?amount=${amount}`) }, /** @@ -122,8 +123,8 @@ export const videoApi = { /** * 分享视频 */ - shareVideo(videoId: number): Promise { - return request.post(`/content/video/share/${videoId}`) + shareVideo(videoId: number, platform = 'web'): Promise { + return request.post(`/content/video/share/${videoId}?platform=${platform}`) }, /** diff --git a/frontend/video-frontend-web/src/components.d.ts b/frontend/video-frontend-web/src/components.d.ts index 9bbc208..73c8aa4 100644 --- a/frontend/video-frontend-web/src/components.d.ts +++ b/frontend/video-frontend-web/src/components.d.ts @@ -34,14 +34,12 @@ declare module 'vue' { VanPopover: typeof import('vant/es')['Popover'] VanPopup: typeof import('vant/es')['Popup'] VanProgress: typeof import('vant/es')['Progress'] - VanPullRefresh: typeof import('vant/es')['PullRefresh'] VanRadio: typeof import('vant/es')['Radio'] VanRadioGroup: typeof import('vant/es')['RadioGroup'] - VanSearch: typeof import('vant/es')['Search'] - VanSidebar: typeof import('vant/es')['Sidebar'] - VanSidebarItem: typeof import('vant/es')['SidebarItem'] VanSkeleton: typeof import('vant/es')['Skeleton'] VanSlider: typeof import('vant/es')['Slider'] + VanSwipe: typeof import('vant/es')['Swipe'] + VanSwipeItem: typeof import('vant/es')['SwipeItem'] VanSwitch: typeof import('vant/es')['Switch'] VanTab: typeof import('vant/es')['Tab'] VanTabbar: typeof import('vant/es')['Tabbar'] diff --git a/frontend/video-frontend-web/src/components/comment/CommentSection.vue b/frontend/video-frontend-web/src/components/comment/CommentSection.vue index b768816..c5baf7e 100644 --- a/frontend/video-frontend-web/src/components/comment/CommentSection.vue +++ b/frontend/video-frontend-web/src/components/comment/CommentSection.vue @@ -5,24 +5,46 @@
- - 发表评论 + + 取消回复 + + + {{ replyTarget ? '发送回复' : '发表评论' }}
- - - + + + -
+
- +
{{ comment.userNickname }} {{ formatRelativeTime(comment.createTime) }}
-
{{ comment.content }}
+
+ {{ comment.content }} +
@@ -51,6 +83,27 @@ 回复
+
+
+ {{ reply.userNickname }} + 回复 {{ reply.replyToNickname }} + :{{ reply.content }} + 回复 +
+
@@ -79,6 +132,7 @@ const loading = ref(false) const finished = ref(false) const submitting = ref(false) const currentPage = ref(1) +const replyTarget = ref(null) const defaultAvatar = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Ccircle cx="50" cy="50" r="50" fill="%23e0e0e0"/%3E%3Ctext x="50%25" y="50%25" fill="%23999" text-anchor="middle" dy=".3em"%3EU%3C/text%3E%3C/svg%3E' @@ -133,12 +187,22 @@ const submitComment = async () => { submitting.value = true try { - await commentApi.addComment({ - videoId: props.videoId, - content: content.value - }) + if (replyTarget.value) { + await commentApi.addReply({ + commentId: replyTarget.value.id, + videoId: props.videoId, + content: content.value, + replyToId: replyTarget.value.userId + }) + } else { + await commentApi.addComment({ + videoId: props.videoId, + content: content.value + }) + } content.value = '' - showSuccessToast('评论成功') + replyTarget.value = null + showSuccessToast('发送成功') loadComments(true) } catch (error: any) { showToast(error.message || '评论失败') @@ -166,8 +230,14 @@ const toggleLike = async (comment: Comment) => { } } -const replyTo = (comment: Comment) => { - content.value = `@${comment.userNickname} ` +const replyTo = (comment: Comment, reply?: Comment) => { + replyTarget.value = reply ?? comment + content.value = '' +} + +const cancelReply = () => { + replyTarget.value = null + content.value = '' } onMounted(() => { @@ -255,6 +325,31 @@ onMounted(() => { } } } + + .reply-list { + margin-top: 10px; + display: grid; + gap: 6px; + padding: 10px 12px; + background: #f8fafc; + border-radius: 12px; + } + + .reply-item { + font-size: 12px; + line-height: 1.6; + color: #64748b; + } + + .reply-name, + .reply-to { + color: #0f766e; + } + + .reply-action { + margin-left: 8px; + color: #00a1d6; + } } } } diff --git a/frontend/video-frontend-web/src/components/danmaku/DanmakuList.vue b/frontend/video-frontend-web/src/components/danmaku/DanmakuList.vue index f94e09d..ed8bd5f 100644 --- a/frontend/video-frontend-web/src/components/danmaku/DanmakuList.vue +++ b/frontend/video-frontend-web/src/components/danmaku/DanmakuList.vue @@ -1,35 +1,64 @@ diff --git a/frontend/video-frontend-web/src/components/video/VideoPlayer.vue b/frontend/video-frontend-web/src/components/video/VideoPlayer.vue index f6e6d59..d17272b 100644 --- a/frontend/video-frontend-web/src/components/video/VideoPlayer.vue +++ b/frontend/video-frontend-web/src/components/video/VideoPlayer.vue @@ -1,25 +1,23 @@ @@ -46,11 +161,39 @@ watch(() => props.src, () => { background: #000; } -.native-player { +.video-player:deep(.dplayer) { width: 100%; height: 100%; - aspect-ratio: 16 / 9; - display: block; + border-radius: 24px; + overflow: hidden; background: #000; } + +.video-player:deep(.dplayer-controller) { + padding-bottom: 4px; +} + +.video-player:deep(.dplayer-danmaku-bar) { + background: rgba(24, 25, 28, 0.78); + border-radius: 999px; + margin: 0 10px 10px; +} + +.video-player:deep(.dplayer-comment-input) { + color: #fff; +} + +.video-player:deep(.dplayer-comment-input::placeholder) { + color: rgba(255, 255, 255, 0.6); +} + +.video-player:deep(.dplayer-menu), +.video-player:deep(.dplayer-setting-box) { + border-radius: 12px; + overflow: hidden; +} + +.video-player:deep(.dplayer-controller .dplayer-icons .dplayer-icon:hover) { + color: #00aeec; +} diff --git a/frontend/video-frontend-web/src/mock/index.ts b/frontend/video-frontend-web/src/mock/index.ts index 2816ec7..3682e2b 100644 --- a/frontend/video-frontend-web/src/mock/index.ts +++ b/frontend/video-frontend-web/src/mock/index.ts @@ -376,7 +376,7 @@ const mockHandlers: Record Promise> 'GET /comment/public/list': async (_url: string, options: any) => { await delay() const params = options.params || {} - let comments = mockComments.filter(c => c.videoId === params.videoId) + const comments = mockComments.filter(c => c.videoId === params.videoId) if (params.sortBy === 'hot') { comments.sort((a, b) => b.likeCount - a.likeCount) } else { @@ -526,7 +526,7 @@ const mockHandlers: Record Promise> // 匹配 Mock 处理器 function matchHandler(url: string, method: string): ((url: string, options: any) => Promise) | null { // 标准化 URL(移除查询参数和 /api 前缀) - let normalizedUrl = url.replace(/^\/api/, '').split('?')[0] + const normalizedUrl = url.replace(/^\/api/, '').split('?')[0] // 构建匹配模式 const patterns = Object.keys(mockHandlers) diff --git a/frontend/video-frontend-web/src/stores/favorite.ts b/frontend/video-frontend-web/src/stores/favorite.ts index 6db1cdb..ca3dd65 100644 --- a/frontend/video-frontend-web/src/stores/favorite.ts +++ b/frontend/video-frontend-web/src/stores/favorite.ts @@ -30,7 +30,7 @@ export const useFavoriteStore = defineStore('favorite', () => { } // 创建收藏夹 - async function createFavorite(params: { name: string; description?: string; visibility: number }) { + async function createFavorite(params: { name: string; description?: string; isPrivate: number }) { try { const result = await favoriteApi.createFavorite(params) favorites.value.unshift(result) diff --git a/frontend/video-frontend-web/src/stores/user.ts b/frontend/video-frontend-web/src/stores/user.ts index 2fc1c79..695ff8e 100644 --- a/frontend/video-frontend-web/src/stores/user.ts +++ b/frontend/video-frontend-web/src/stores/user.ts @@ -70,20 +70,23 @@ export const useUserStore = defineStore('user', () => { } catch (error) { console.error('登出请求失败:', error) } finally { - // 清除本地状态 - token.value = null - refreshToken.value = null - userInfo.value = null - authUser.value = null - removeToken() - removeRefreshToken() + clearAuth() } } + function clearAuth() { + token.value = null + refreshToken.value = null + userInfo.value = null + authUser.value = null + removeToken() + removeRefreshToken() + } + // 刷新 Token async function refreshAccessToken() { if (!refreshToken.value) { - logout() + clearAuth() return false } @@ -96,7 +99,7 @@ export const useUserStore = defineStore('user', () => { return true } catch (error) { console.error('刷新Token失败:', error) - logout() + clearAuth() return false } } @@ -194,6 +197,7 @@ export const useUserStore = defineStore('user', () => { login, register, logout, + clearAuth, refreshAccessToken, fetchAuthUser, fetchUserInfo, diff --git a/frontend/video-frontend-web/src/stores/video.ts b/frontend/video-frontend-web/src/stores/video.ts index 1c42649..863f661 100644 --- a/frontend/video-frontend-web/src/stores/video.ts +++ b/frontend/video-frontend-web/src/stores/video.ts @@ -57,12 +57,15 @@ export const useVideoStore = defineStore('video', () => { // 记录播放 async function recordPlay(videoId: number, progress: number, duration: number) { + const playTime = Math.max(0, Math.min(Math.round(progress), Math.round(duration))) + const isFinished = duration > 0 && playTime >= duration * 0.9 ? 1 : 0 + try { - await videoApi.recordPlay({ videoId, progress, duration }) + await videoApi.recordPlay({ videoId, playTime, isFinished }) // 更新本地播放历史 const index = playHistory.value.findIndex(item => item.videoId === videoId) - const record: PlayRecord = { videoId, progress, duration } + const record: PlayRecord = { videoId, playTime, isFinished } if (index >= 0) { playHistory.value[index] = record @@ -81,7 +84,7 @@ export const useVideoStore = defineStore('video', () => { // 获取播放进度 function getPlayProgress(videoId: number): number { const record = playHistory.value.find(item => item.videoId === videoId) - return record?.progress || 0 + return record?.playTime || 0 } // 清除当前视频 diff --git a/frontend/video-frontend-web/src/styles/index.scss b/frontend/video-frontend-web/src/styles/index.scss index b96104c..608a059 100644 --- a/frontend/video-frontend-web/src/styles/index.scss +++ b/frontend/video-frontend-web/src/styles/index.scss @@ -11,17 +11,19 @@ html, body { width: 100%; min-height: 100vh; - font-family: 'PingFang SC', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, - 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; + font-family: 'Microsoft YaHei', 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; line-height: 1.5; color: #18191c; - background-color: #f6f7fb; + background-color: #f4f5f7; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +body { + min-width: 320px; +} + a { color: inherit; text-decoration: none; @@ -85,56 +87,54 @@ select { // 主题变量 :root { - // 主色调 --primary-color: #fb7299; --primary-hover-color: #ff85ad; - - // 辅助色 + --link-color: #00a1d6; + --link-hover-color: #33b4de; --success-color: #52c41a; --warning-color: #faad14; --error-color: #ff4d4f; --info-color: #1890ff; - - // 粉色(B站风格) --pink-color: #fb7299; --pink-hover-color: #ff85ad; - - // 文字颜色 --text-primary: #18191c; --text-secondary: #61666d; --text-tertiary: #9499a0; --text-white: #fff; - - // 背景颜色 - --bg-primary: #f6f7fb; + --bg-primary: #f4f5f7; --bg-secondary: #fff; - --bg-tertiary: #f1f2f3; - - // 边框颜色 + --bg-tertiary: #f6f7f8; + --bg-soft-blue: #edf7ff; + --bg-soft-pink: #fff2f6; --border-color: #e3e5e7; --border-light: #f1f2f3; - - // 阴影 - --shadow-sm: 0 4px 16px rgba(15, 23, 42, 0.04); - --shadow-md: 0 10px 28px rgba(15, 23, 42, 0.08); - --shadow-lg: 0 18px 48px rgba(15, 23, 42, 0.12); - - // 圆角 - --radius-sm: 4px; - --radius-md: 8px; - --radius-lg: 12px; - - // 间距 + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 18px 44px rgba(0, 0, 0, 0.08); + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; --spacing-xs: 4px; --spacing-sm: 8px; --spacing-md: 16px; --spacing-lg: 24px; --spacing-xl: 32px; - - // 布局 - --header-height: 60px; + --header-top-height: 42px; + --header-main-height: 74px; + --header-height: 74px; + --header-total-height: 116px; + --home-banner-height: 190px; --sidebar-width: 200px; - --max-content-width: 1400px; + --max-content-width: 1380px; +} + +@media (max-width: 768px) { + :root { + --header-main-height: 56px; + --header-height: 56px; + --header-total-height: 56px; + --home-banner-height: 132px; + } } // 暗色主题 @@ -203,6 +203,92 @@ select { box-shadow: var(--shadow-sm); } +.page-shell { + width: min(var(--max-content-width), calc(100% - 24px)); + margin: 0 auto; +} + +// Vant feedback layer overrides +:root { + --van-dialog-width: 420px; + --van-dialog-radius: 20px; + --van-dialog-background: rgba(255, 255, 255, 0.98); + --van-dialog-has-title-message-text-color: #61666d; + --van-dialog-message-font-size: 14px; + --van-dialog-confirm-button-text-color: #fb7299; + --van-dialog-cancel-button-text-color: #61666d; + --van-toast-radius: 18px; + --van-toast-default-width: auto; + --van-toast-background: rgba(24, 25, 28, 0.88); + --van-toast-text-color: #fff; + --van-notify-radius: 0 0 18px 18px; +} + +.van-overlay { + backdrop-filter: blur(4px); +} + +.van-dialog { + border-radius: 20px !important; + overflow: hidden; + box-shadow: 0 20px 44px rgba(15, 23, 42, 0.18); + + .van-dialog__header { + padding: 22px 24px 6px; + font-size: 20px; + font-weight: 700; + color: #18191c; + text-align: left; + } + + .van-dialog__message { + padding: 0 24px 18px; + line-height: 1.7; + text-align: left; + } + + .van-dialog__footer { + padding: 0 16px 16px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + + &::after { + display: none; + } + } + + .van-dialog__confirm, + .van-dialog__cancel { + height: 42px; + margin: 0; + border-radius: 14px; + border: 1px solid #e9edf2; + font-size: 15px; + font-weight: 600; + } + + .van-dialog__cancel { + background: #f6f7f8; + } + + .van-dialog__confirm { + background: #fff2f6; + border-color: #ffd5e2; + } +} + +.van-toast { + padding: 12px 16px; + box-shadow: 0 16px 36px rgba(15, 23, 42, 0.18); + font-size: 14px; + line-height: 1.5; +} + +.van-notify { + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12); +} + // 骨架屏动画 .skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); diff --git a/frontend/video-frontend-web/src/types/dplayer.d.ts b/frontend/video-frontend-web/src/types/dplayer.d.ts new file mode 100644 index 0000000..72b1548 --- /dev/null +++ b/frontend/video-frontend-web/src/types/dplayer.d.ts @@ -0,0 +1 @@ +declare module 'dplayer'; diff --git a/frontend/video-frontend-web/src/types/index.ts b/frontend/video-frontend-web/src/types/index.ts index 66d2918..dd9c344 100644 --- a/frontend/video-frontend-web/src/types/index.ts +++ b/frontend/video-frontend-web/src/types/index.ts @@ -7,6 +7,8 @@ export interface ApiResponse { // 分页请求参数 export interface PageParams { + page?: number + size?: number pageNum?: number pageSize?: number } @@ -146,6 +148,17 @@ export interface VideoCreateParams { tagIds?: number[] } +export interface VideoUploadResult { + videoId?: number + videoUrl: string + coverUrl: string + duration: number + width: number + height: number + format: string + size: number +} + // 视频更新参数 export interface VideoUpdateParams { id: number @@ -222,6 +235,11 @@ export interface Comment { userNickname?: string userAvatar?: string isLiked?: boolean + topStatus?: number + replyToId?: number + replyToNickname?: string + replyToAvatar?: string + hotReplies?: Comment[] replies?: Comment[] } @@ -246,8 +264,7 @@ export interface Favorite { userId: number name: string description: string - coverUrl: string - visibility: number + isPrivate: number videoCount: number createTime: string } @@ -256,8 +273,7 @@ export interface Favorite { export interface FavoriteCreateParams { name: string description?: string - coverUrl?: string - visibility: number + isPrivate: number } // 收藏视频参数 @@ -328,8 +344,8 @@ export interface UserProfileUpdateParams { // 播放记录 export interface PlayRecord { videoId: number - progress: number - duration: number + playTime: number + isFinished?: number } // 推荐视频 diff --git a/frontend/video-frontend-web/src/utils/media.ts b/frontend/video-frontend-web/src/utils/media.ts new file mode 100644 index 0000000..c1e5fc7 --- /dev/null +++ b/frontend/video-frontend-web/src/utils/media.ts @@ -0,0 +1,13 @@ +const MEDIA_BASE_URL = (import.meta.env.VITE_MEDIA_BASE_URL || '').replace(/\/$/, '') + +export function resolveMediaUrl(url?: string | null) { + if (!url) return '' + if (/^default\//i.test(url)) return '' + if (/^(data:|blob:|https?:\/\/)/i.test(url)) return url + if (!MEDIA_BASE_URL) return url + return `${MEDIA_BASE_URL}/${url.replace(/^\/+/, '')}` +} + +export function resolveAvatarUrl(url?: string | null, fallback = '') { + return resolveMediaUrl(url) || fallback +} diff --git a/frontend/video-frontend-web/src/utils/request.ts b/frontend/video-frontend-web/src/utils/request.ts index e5d731a..7ae5e24 100644 --- a/frontend/video-frontend-web/src/utils/request.ts +++ b/frontend/video-frontend-web/src/utils/request.ts @@ -7,6 +7,7 @@ import { mockRequest, isMockEnabled } from '@/mock' // API 基础配置 const BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api' const TIMEOUT = 30000 +let handlingUnauthorized = false // 创建 axios 实例 const service: AxiosInstance = axios.create({ @@ -42,15 +43,21 @@ service.interceptors.response.use( // 业务状态码判断 if (data.code === 200) { + handlingUnauthorized = false return data.data } // Token 过期 if (data.code === 401) { const userStore = useUserStore() - userStore.logout() - router.push('/login') - showToast('登录已过期,请重新登录') + if (!handlingUnauthorized) { + handlingUnauthorized = true + userStore.clearAuth() + if (router.currentRoute.value.path !== '/login') { + router.push('/login') + } + showToast('登录已过期,请重新登录') + } return Promise.reject(new Error('Token expired')) } @@ -69,12 +76,18 @@ service.interceptors.response.use( } else if (error.response) { const status = error.response.status switch (status) { - case 401: + case 401: { const userStore = useUserStore() - userStore.logout() - router.push('/login') - showToast('登录已过期,请重新登录') + if (!handlingUnauthorized) { + handlingUnauthorized = true + userStore.clearAuth() + if (router.currentRoute.value.path !== '/login') { + router.push('/login') + } + showToast('登录已过期,请重新登录') + } break + } case 403: showToast('没有权限访问') break @@ -102,14 +115,20 @@ async function mockFetch(method: string, url: string, data?: any, params?: an }) if (result.code === 200) { + handlingUnauthorized = false return result.data } if (result.code === 401) { const userStore = useUserStore() - userStore.logout() - router.push('/login') - showToast('登录已过期,请重新登录') + if (!handlingUnauthorized) { + handlingUnauthorized = true + userStore.clearAuth() + if (router.currentRoute.value.path !== '/login') { + router.push('/login') + } + showToast('登录已过期,请重新登录') + } throw new Error('Token expired') } diff --git a/frontend/video-frontend-web/src/views/error/404.vue b/frontend/video-frontend-web/src/views/error/404.vue index 97b77f9..a8c518a 100644 --- a/frontend/video-frontend-web/src/views/error/404.vue +++ b/frontend/video-frontend-web/src/views/error/404.vue @@ -3,7 +3,12 @@

404

页面未找到

- 返回首页 + + 返回首页 +
diff --git a/frontend/video-frontend-web/src/views/favorite/detail.vue b/frontend/video-frontend-web/src/views/favorite/detail.vue index a5a5b57..cf29357 100644 --- a/frontend/video-frontend-web/src/views/favorite/detail.vue +++ b/frontend/video-frontend-web/src/views/favorite/detail.vue @@ -1,49 +1,147 @@ @@ -77,42 +219,60 @@ onMounted(() => { diff --git a/frontend/video-frontend-web/src/views/favorite/index.vue b/frontend/video-frontend-web/src/views/favorite/index.vue index aa881c2..fd2f694 100644 --- a/frontend/video-frontend-web/src/views/favorite/index.vue +++ b/frontend/video-frontend-web/src/views/favorite/index.vue @@ -1,64 +1,143 @@ diff --git a/frontend/video-frontend-web/src/views/home/category.vue b/frontend/video-frontend-web/src/views/home/category.vue index 706b3c4..c24a3eb 100644 --- a/frontend/video-frontend-web/src/views/home/category.vue +++ b/frontend/video-frontend-web/src/views/home/category.vue @@ -1,35 +1,50 @@ diff --git a/frontend/video-frontend-web/src/views/home/index.vue b/frontend/video-frontend-web/src/views/home/index.vue index a2bba4c..790f86b 100644 --- a/frontend/video-frontend-web/src/views/home/index.vue +++ b/frontend/video-frontend-web/src/views/home/index.vue @@ -1,276 +1,673 @@ diff --git a/frontend/video-frontend-web/src/views/layout/MainLayout.vue b/frontend/video-frontend-web/src/views/layout/MainLayout.vue index 12c3e8b..47659cd 100644 --- a/frontend/video-frontend-web/src/views/layout/MainLayout.vue +++ b/frontend/video-frontend-web/src/views/layout/MainLayout.vue @@ -3,9 +3,15 @@ -
+
- + @@ -19,9 +25,12 @@ import AppHeader from '@/components/layout/AppHeader.vue' import AppTabbar from '@/components/layout/AppTabbar.vue' import { useAppStore } from '@/stores' -import { onMounted } from 'vue' +import { computed, onMounted } from 'vue' +import { useRoute } from 'vue-router' const appStore = useAppStore() +const route = useRoute() +const isHome = computed(() => route.path === '/') onMounted(() => { appStore.initApp() @@ -38,9 +47,13 @@ onMounted(() => { .main-content { flex: 1; - padding-top: var(--header-height); + padding-top: var(--header-total-height); padding-bottom: 50px; // 移动端 tabbar 高度 + &.is-home { + padding-top: var(--home-banner-height); + } + @media (min-width: 768px) { padding-bottom: 0; } diff --git a/frontend/video-frontend-web/src/views/login/index.vue b/frontend/video-frontend-web/src/views/login/index.vue index 1f5c3c2..0598e79 100644 --- a/frontend/video-frontend-web/src/views/login/index.vue +++ b/frontend/video-frontend-web/src/views/login/index.vue @@ -1,88 +1,146 @@ diff --git a/frontend/video-frontend-web/src/views/login/register.vue b/frontend/video-frontend-web/src/views/login/register.vue index 286c640..627b037 100644 --- a/frontend/video-frontend-web/src/views/login/register.vue +++ b/frontend/video-frontend-web/src/views/login/register.vue @@ -1,80 +1,155 @@ diff --git a/frontend/video-frontend-web/src/views/message/detail.vue b/frontend/video-frontend-web/src/views/message/detail.vue index 4ebc48b..05e2ae2 100644 --- a/frontend/video-frontend-web/src/views/message/detail.vue +++ b/frontend/video-frontend-web/src/views/message/detail.vue @@ -1,46 +1,58 @@ @@ -52,50 +64,21 @@ import { showToast } from 'vant' import { userApi } from '@/api/user.ts' import { useUserStore } from '@/stores/user.ts' import { formatRelativeTime } from '@/utils' +import { resolveAvatarUrl } from '@/utils/media' import type { Message } from '@/types' const router = useRouter() const route = useRoute() const userStore = useUserStore() - const targetId = Number(route.params.id) const messages = ref([]) const content = ref('') const sending = ref(false) const listRef = ref() -const defaultAvatar = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Ccircle cx="50" cy="50" r="50" fill="%23e0e0e0"/%3E%3Ctext x="50%25" y="50%25" fill="%23999" text-anchor="middle" dy=".3em"%3EU%3C/text%3E%3C/svg%3E' - -const loadMessages = async () => { - try { - const data = await userApi.getMessageDetail(targetId) - messages.value = data - await nextTick() - scrollToBottom() - } catch (error) { - showToast('加载失败') - } -} - -const sendMessage = async () => { - if (!content.value.trim()) return - - sending.value = true - try { - const msg = await userApi.sendMessage({ - receiverId: targetId, - content: content.value - }) - messages.value.push(msg) - content.value = '' - await nextTick() - scrollToBottom() - } catch (error) { - showToast('发送失败') - } finally { - sending.value = false - } -} +const defaultAvatar = + 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Ccircle cx="50" cy="50" r="50" fill="%23e0e0e0"/%3E%3Ctext x="50%25" y="50%25" fill="%23999" text-anchor="middle" dy=".3em"%3EU%3C/text%3E%3C/svg%3E' +const getAvatarUrl = (avatar?: string) => resolveAvatarUrl(avatar, defaultAvatar) const scrollToBottom = () => { if (listRef.value) { @@ -103,76 +86,130 @@ const scrollToBottom = () => { } } -onMounted(() => { - loadMessages() -}) +const loadMessages = async () => { + try { + const data = await userApi.getMessageDetail(targetId, { page: 1, size: 50 }) + messages.value = data.records + await userApi.markMessageRead(targetId) + await nextTick() + scrollToBottom() + } catch { + showToast('加载失败') + } +} + +const sendMessage = async () => { + if (!content.value.trim()) return + sending.value = true + try { + await userApi.sendMessage({ + receiverId: targetId, + content: content.value + }) + content.value = '' + await loadMessages() + } catch { + showToast('发送失败') + } finally { + sending.value = false + } +} + +onMounted(loadMessages) diff --git a/frontend/video-frontend-web/src/views/message/index.vue b/frontend/video-frontend-web/src/views/message/index.vue index 9191a2d..c65e007 100644 --- a/frontend/video-frontend-web/src/views/message/index.vue +++ b/frontend/video-frontend-web/src/views/message/index.vue @@ -1,37 +1,45 @@ @@ -41,7 +49,9 @@ import { ref, onMounted } from 'vue' import { useRouter } from 'vue-router' import { showToast } from 'vant' import { userApi } from '@/api/user.ts' +import { useUserStore } from '@/stores/user.ts' import { formatRelativeTime } from '@/utils' +import { resolveAvatarUrl } from '@/utils/media' interface Conversation { targetId: number @@ -53,96 +63,131 @@ interface Conversation { } const router = useRouter() +const userStore = useUserStore() const conversations = ref([]) -const loading = ref(false) -const finished = ref(false) -const refreshing = ref(false) +const defaultAvatar = + 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Ccircle cx="50" cy="50" r="50" fill="%23e0e0e0"/%3E%3Ctext x="50%25" y="50%25" fill="%23999" text-anchor="middle" dy=".3em"%3EU%3C/text%3E%3C/svg%3E' +const getAvatarUrl = (avatar?: string) => resolveAvatarUrl(avatar, defaultAvatar) -const defaultAvatar = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Ccircle cx="50" cy="50" r="50" fill="%23e0e0e0"/%3E%3Ctext x="50%25" y="50%25" fill="%23999" text-anchor="middle" dy=".3em"%3EU%3C/text%3E%3C/svg%3E' - -const loadConversations = async (isRefresh = false) => { - if (isRefresh) { - conversations.value = [] - finished.value = false - } - - loading.value = true +const loadConversations = async () => { try { - const data = await userApi.getMessageList() - conversations.value = data.map((item) => ({ - targetId: item.senderId, - targetNickname: item.senderNickname, - targetAvatar: item.senderAvatar, - lastMessage: item.content, - lastTime: item.createTime, - unreadCount: item.isRead ? 0 : 1 - })) - finished.value = true - } catch (error) { + const data = await userApi.getMessageList({ page: 1, size: 20 }) + const conversationMap = new Map() + data.records.forEach((item) => { + const isSelfSender = item.senderId === userStore.userId + const targetId = isSelfSender ? item.receiverId : item.senderId + const existing = conversationMap.get(targetId) + const unreadCount = item.isRead ? 0 : 1 + const conversation = { + targetId, + targetNickname: isSelfSender ? item.receiverNickname : item.senderNickname, + targetAvatar: isSelfSender ? item.receiverAvatar : item.senderAvatar, + lastMessage: item.content, + lastTime: item.createTime, + unreadCount: (existing?.unreadCount || 0) + unreadCount + } + if (!existing || new Date(item.createTime).getTime() > new Date(existing.lastTime).getTime()) { + conversationMap.set(targetId, conversation) + } else { + conversationMap.set(targetId, { + ...existing, + unreadCount: existing.unreadCount + unreadCount + }) + } + }) + conversations.value = [...conversationMap.values()].sort((a, b) => new Date(b.lastTime).getTime() - new Date(a.lastTime).getTime()) + } catch { showToast('加载失败') - } finally { - loading.value = false - refreshing.value = false } } -const loadMore = () => { - if (!finished.value) loadConversations() -} - -const onRefresh = () => { - loadConversations(true) -} - const goToDetail = (targetId: number) => { router.push(`/messages/${targetId}`) } -onMounted(() => { - loadConversations() -}) +onMounted(loadConversations) diff --git a/frontend/video-frontend-web/src/views/message/notifications.vue b/frontend/video-frontend-web/src/views/message/notifications.vue index 54d75ba..843ca71 100644 --- a/frontend/video-frontend-web/src/views/message/notifications.vue +++ b/frontend/video-frontend-web/src/views/message/notifications.vue @@ -1,84 +1,96 @@ diff --git a/frontend/video-frontend-web/src/views/search/index.vue b/frontend/video-frontend-web/src/views/search/index.vue index b94e94a..cee5b69 100644 --- a/frontend/video-frontend-web/src/views/search/index.vue +++ b/frontend/video-frontend-web/src/views/search/index.vue @@ -1,56 +1,207 @@ diff --git a/frontend/video-frontend-web/src/views/upload/index.vue b/frontend/video-frontend-web/src/views/upload/index.vue index 3ed55a7..6002630 100644 --- a/frontend/video-frontend-web/src/views/upload/index.vue +++ b/frontend/video-frontend-web/src/views/upload/index.vue @@ -1,13 +1,21 @@