├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ └── index.ts ├── assets │ ├── icon-loading.gif │ ├── icon-search.svg │ ├── icon-star-empty.png │ ├── icon-star-full.png │ ├── icon-star-half.png │ └── logo.png ├── common │ ├── font │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.woff2 │ └── style │ │ ├── iconfont.css │ │ ├── reset.less │ │ └── variable.less ├── components │ ├── ArticleListComponent.vue │ ├── ArticleTabComponent.vue │ ├── ClassifyHeaderComponent.vue │ ├── CommentListComponent.vue │ ├── MoreHandleComponent.vue │ ├── MovieListComponent.vue │ ├── MovieTabComponent.vue │ ├── MyTabComponent.vue │ ├── VideoListComponent.vue │ └── VideoTabComponent.vue ├── hooks │ ├── useArticleDetailEffect.ts │ ├── useArticleListEffect.ts │ ├── useArticleTabEffect.ts │ ├── useCommentEffect.ts │ ├── useMoreHandleEffect.ts │ ├── useMovieDetailEffect.ts │ ├── useMovieTabEffect.ts │ ├── useMyTabEffect.ts │ ├── useVideoListEffect.ts │ └── useVideoTabEffect.ts ├── main.ts ├── pages │ ├── ArticleDetailPage.vue │ ├── HomePage.vue │ ├── MovieDetaiPage.vue │ └── RecordListPage.vue ├── router │ └── index.ts ├── service │ ├── appService.ts │ ├── articleDetailService.ts │ ├── handleService.ts │ ├── homeService.ts │ ├── movieDetailService.ts │ ├── movieService.ts │ ├── myService.ts │ ├── recordService.ts │ └── videoService.ts ├── shims-vue.d.ts ├── store │ ├── actions.ts │ ├── getters.ts │ ├── index.ts │ ├── mapActions.ts │ ├── mapGetters.ts │ ├── mutation-types.ts │ ├── mutations.ts │ ├── state.ts │ └── stateInterface.ts ├── types │ └── index.ts └── utils │ ├── emitter.ts │ ├── index.ts │ └── setAxios ├── tsconfig.json ├── vue.config.js ├── 明日头条app整体效果图(待更新).jpg ├── 界面预览1.png ├── 界面预览10.png ├── 界面预览2.png ├── 界面预览3.png ├── 界面预览4.png ├── 界面预览5.png ├── 界面预览6.png ├── 界面预览7.png ├── 界面预览8.png └── 界面预览9.png /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 4 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: false, 3 | env: { 4 | node: false 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | '@vue/standard', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue3-toutiao-app-ui 2 | 使用最新流行的vue3+typescript+hook搭建的明日头条webapp项目,数据来自于python自动化爬虫,自动抓取今日头条的数据,存入自己的数据中,包含文章。博客,视频,电影,详情,用户中心,登录等模块,功能完善齐全,持续更新中... 3 | 4 | ================================界面预览================================ 5 | ![手机端预览效果图7](./%E6%98%8E%E6%97%A5%E5%A4%B4%E6%9D%A1app%E6%95%B4%E4%BD%93%E6%95%88%E6%9E%9C%E5%9B%BE%EF%BC%88%E5%BE%85%E6%9B%B4%E6%96%B0%EF%BC%89.jpg) 6 | ![手机端预览效果图1](./%E7%95%8C%E9%9D%A2%E9%A2%84%E8%A7%881.png) 7 | ![手机端预览效果图2](./%E7%95%8C%E9%9D%A2%E9%A2%84%E8%A7%882.png) 8 | ![手机端预览效果图3](./%E7%95%8C%E9%9D%A2%E9%A2%84%E8%A7%883.png) 9 | ![手机端预览效果图4](./%E7%95%8C%E9%9D%A2%E9%A2%84%E8%A7%884.png) 10 | ![手机端预览效果图5](./%E7%95%8C%E9%9D%A2%E9%A2%84%E8%A7%885.png) 11 | ![手机端预览效果图6](./%E7%95%8C%E9%9D%A2%E9%A2%84%E8%A7%886.png) 12 | ![手机端预览效果图7](./%E7%95%8C%E9%9D%A2%E9%A2%84%E8%A7%887.png) 13 | ![手机端预览效果图7](./%E7%95%8C%E9%9D%A2%E9%A2%84%E8%A7%888.png) 14 | ![手机端预览效果图7](./%E7%95%8C%E9%9D%A2%E9%A2%84%E8%A7%889.png) 15 | ![手机端预览效果图7](./%E7%95%8C%E9%9D%A2%E9%A2%84%E8%A7%8810.png) 16 | ================================界面预览================================ 17 | 18 | 19 | 后端接口和sql参见:https://github.com/wuyuanwuhui99/springboot-app-service 20 | 21 | flutter在线电影项目:https://github.com/wuyuanwuhui99/flutter-movie-app-ui 22 | 23 | react-native在线电影项目:https://github.com/wuyuanwuhui99/react-native-app-ui 24 | 25 | 在线音乐项目:https://github.com/wuyuanwuhui99/vue-music-app-ui 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "muvue3", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "ansi-regex": "^6.0.0", 12 | "axios": "^0.21.1", 13 | "better-scroll": "^2.4.1", 14 | "fork-ts-checker-webpack-plugin-v5": "^0.0.1-security", 15 | "mitt": "^3.0.0", 16 | "vue": "^3.0.0", 17 | "vue-router": "^4.0.0-0", 18 | "vuex": "^4.0.0-0" 19 | }, 20 | "devDependencies": { 21 | "@typescript-eslint/eslint-plugin": "^4.18.0", 22 | "@typescript-eslint/parser": "^4.18.0", 23 | "@vue/cli-plugin-eslint": "~4.5.0", 24 | "@vue/cli-plugin-router": "~4.5.0", 25 | "@vue/cli-plugin-typescript": "~4.5.0", 26 | "@vue/cli-plugin-vuex": "~4.5.0", 27 | "@vue/cli-service": "~4.5.0", 28 | "@vue/compiler-sfc": "^3.0.0", 29 | "@vue/eslint-config-standard": "^5.1.2", 30 | "@vue/eslint-config-typescript": "^7.0.0", 31 | "eslint": "^6.7.2", 32 | "eslint-plugin-import": "^2.20.2", 33 | "eslint-plugin-node": "^11.1.0", 34 | "eslint-plugin-promise": "^4.2.1", 35 | "eslint-plugin-standard": "^4.0.0", 36 | "eslint-plugin-vue": "^7.0.0", 37 | "less": "^3.13.1", 38 | "less-loader": "^5.0.0", 39 | "typescript": "~4.1.5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 35 | 36 | 57 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | getUserData: "/service/toutiao/getUserData",//获取用户信息 3 | getFavoriteChannels: "/service/toutiao-getway/getArticleFavoriteChannels",//获取用户收藏的频道 4 | getAllChannels: "/service/toutiao/getFavoriteChannels",//获取所有频道 5 | getArticleList: "/service/toutiao/getArticleList",//获取文章列表 6 | getVideoFavoriteChannels:"/service/video-getway/getFavoriteChannels",//获取视频分类 7 | getVideoList: "/service/video/getVideoList",//获取视频列表 8 | getMovieList: "/service/movie/search",//获取视频列表 9 | getArticleDetail:"/service/toutiao/getArticleDetail",//获取文章详情 10 | getRecordList:"/service/{type}-getway/getRecordList",//获取文章浏览记录 11 | isFavorite:"/service/{type}-getway/isFavorite",//获取是否收藏过的文章和视频 12 | insertFavorite:"/service/{type}-getway/insertFavorite",//插入收藏 13 | deleteFavorite:"/service/{type}-getway/deleteFavorite",//删除收藏 14 | isLike:"/service/{type}-getway/isLike",//获取是是否点赞过的文章和视频 15 | insertLike:"/service/{type}-getway/insertLike",//插入点赞 16 | deleteLike:"/service/{type}-getway/deleteLike",//删除点赞 17 | getFavorite:"/service/{type}-getway/getFavorite",//获取文章浏览记录或视频播放记录或电影观看记录 18 | isFocus:"/service/{type}-getway/isFocus",//查询是否已经关注 19 | insertFocus:"/service/{type}-getway/insertFocus",//新增关注 20 | deleteFocus:"/service/{type}-getway/deleteFocus",//取消关注 21 | getCommentCount:"/service/{type}/getCommentCount",//获取评论总条数 22 | getTopCommentList:"/service/{type}/getTopCommentList",//获取一级评论列表 23 | insertComment:"/service/{type}-getway/insertComment",//新增评论 24 | deleteComment:"/service/{type}-getway/deleteComment/{id}",//删除评论 25 | getReplyCommentList:"/service/{type}/getReplyCommentList",//获取回复列表 26 | getCommentItem:"/service/{type}/getCommentItem",//获取新增的单条评论或者回复 27 | getMovieDetail:"/service/movie/getMovieDetail/",//获取电影详情 28 | getStar:"/service/movie/getStar/",//获取演员表 29 | getYourLikes:"/service/movie/getYourLikes",//获取猜你喜欢的电影 30 | getMovieListByType:"/service/movie/getMovieListByType",//按类型获取获取电影 31 | getRecommend:"/service/movie/getRecommend",//获取推荐的电影 32 | } 33 | -------------------------------------------------------------------------------- /src/assets/icon-loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/src/assets/icon-loading.gif -------------------------------------------------------------------------------- /src/assets/icon-search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icon-star-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/src/assets/icon-star-empty.png -------------------------------------------------------------------------------- /src/assets/icon-star-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/src/assets/icon-star-full.png -------------------------------------------------------------------------------- /src/assets/icon-star-half.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/src/assets/icon-star-half.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/src/assets/logo.png -------------------------------------------------------------------------------- /src/common/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/src/common/font/iconfont.ttf -------------------------------------------------------------------------------- /src/common/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/src/common/font/iconfont.woff -------------------------------------------------------------------------------- /src/common/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/src/common/font/iconfont.woff2 -------------------------------------------------------------------------------- /src/common/style/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 87269 */ 3 | src: url('../font/iconfont.woff2?t=1624079947675') format('woff2'), 4 | url('../font/iconfont.woff?t=1624079947675') format('woff'), 5 | url('../font/iconfont.ttf?t=1624079947675') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 1.6rem; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-edit:before { 17 | content: "\e63c"; 18 | } 19 | 20 | .icon-edit:before { 21 | content: "\e63c"; 22 | } 23 | 24 | .iconfont-arrow:before{ 25 | content: "\e615"; 26 | } 27 | 28 | .iconfont-arrow{ 29 | color: #888; 30 | font-size:0.5rem; 31 | padding: 0 0.3rem; 32 | } 33 | 34 | .iconfont-has-collection:before{ 35 | content: "\e62a"; 36 | color: #ff9900; 37 | } 38 | 39 | .iconfont-set:before{ 40 | content: "\e605"; 41 | } 42 | 43 | .iconfont-play:before { 44 | content: "\e701"; 45 | color: #fff; 46 | } 47 | 48 | .iconfont-tab-my:before { 49 | content: "\e763"; 50 | } 51 | 52 | .iconfont-tab-home:before { 53 | content: "\e603"; 54 | } 55 | 56 | .iconfont-search:before { 57 | content: "\e7d2"; 58 | } 59 | 60 | .iconfont-tab-play:before { 61 | content: "\e61b"; 62 | } 63 | 64 | .iconfont-tab-video:before { 65 | content: "\e6f8"; 66 | } 67 | 68 | .iconfont-user:before { 69 | content: "\e646"; 70 | } 71 | 72 | .iconfont-user:before { 73 | content: "\e646"; 74 | } 75 | 76 | .iconfont-comment:before { 77 | content: "\e604"; 78 | } 79 | 80 | .iconfont-no-collection:before { 81 | content: "\e629"; 82 | } 83 | 84 | .iconfont-like:before { 85 | content: "\e61c"; 86 | } 87 | 88 | .iconfont-like-active:before { 89 | content: "\e60c"; 90 | color: #ff9900; 91 | } 92 | 93 | .iconfont-views:before { 94 | content: "\e68c"; 95 | } 96 | 97 | .iconfont-more:before { 98 | content: "\e639"; 99 | color: #bbb; 100 | } 101 | 102 | .iconfont-jiantou:before { 103 | content: "\e616"; 104 | } 105 | -------------------------------------------------------------------------------- /src/common/style/reset.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/) 3 | * http://cssreset.com 4 | */ 5 | html, body, div, span, applet, object, iframe, h1, 6 | h2, h3, h4, h5, h6, p, blockquote, pre, a, 7 | abbr, acronym, address, big, cite, code, del, 8 | dfn, em, img, ins, kbd, q, s, samp, small, 9 | strike, strong, sub, sup, tt, var, b, 10 | u, i, center, dl, 11 | dt, dd, ol, ul, li, fieldset, 12 | form, label, legend, table, 13 | caption, tbody, tfoot, thead, tr, th, td, article, 14 | aside, canvas, details, embed, figure, 15 | figcaption, footer, header, menu, 16 | nav, output, ruby, section, summary, time, 17 | mark, audio, video, input { 18 | margin: 0; 19 | padding: 0; 20 | border: 0; 21 | font-size: 100%; 22 | font-weight: normal; 23 | vertical-align: baseline; 24 | } 25 | 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, footer, 28 | header, menu, nav, section { 29 | display: block; 30 | } 31 | body,html{ 32 | width: 100%; 33 | height: 100%; 34 | } 35 | blockquote, q { 36 | quotes: none; 37 | } 38 | 39 | blockquote:before, blockquote:after, 40 | q:before, q:after { 41 | content: none; 42 | } 43 | 44 | table { 45 | border-collapse: collapse; 46 | border-spacing: 0; 47 | } 48 | 49 | /* custom */ 50 | 51 | a { 52 | color: #7e8c8d; 53 | -webkit-backface-visibility: hidden; 54 | text-decoration: none; 55 | } 56 | 57 | li { 58 | list-style: none; 59 | } 60 | 61 | body { 62 | -webkit-text-size-adjust: none; 63 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 64 | } 65 | -------------------------------------------------------------------------------- /src/common/style/variable.less: -------------------------------------------------------------------------------- 1 | @color-active: #ff9900; 2 | @color-icon:#bbb; 3 | @iconfont-size: 1.5rem; 4 | @iconfont-margin:0 0.5rem; 5 | @article-title-font-size:1.3rem; 6 | @article-footer-font-size:0.9rem; 7 | @article-footer-color:#bbb; 8 | @border:1px solid #eee; 9 | @small-margin: 0.5rem; 10 | @big-margin:1rem; 11 | @border-raduis:0.3rem; 12 | @big-avater-size:4rem; 13 | @middle-avater-size:2rem; 14 | @small-avater-size:1rem; 15 | @circle-avater:50%; 16 | @img-border-raduis:0.5rem; 17 | -------------------------------------------------------------------------------- /src/components/ArticleListComponent.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 51 | 52 | 103 | -------------------------------------------------------------------------------- /src/components/ArticleTabComponent.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 45 | 46 | 131 | -------------------------------------------------------------------------------- /src/components/ClassifyHeaderComponent.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 28 | 29 | 49 | -------------------------------------------------------------------------------- /src/components/CommentListComponent.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 70 | 71 | 184 | -------------------------------------------------------------------------------- /src/components/MoreHandleComponent.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 41 | 42 | 80 | -------------------------------------------------------------------------------- /src/components/MovieListComponent.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 41 | 42 | 103 | -------------------------------------------------------------------------------- /src/components/MovieTabComponent.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 44 | 45 | 172 | -------------------------------------------------------------------------------- /src/components/MyTabComponent.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 99 | 100 | 356 | -------------------------------------------------------------------------------- /src/components/VideoListComponent.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 41 | 42 | 176 | -------------------------------------------------------------------------------- /src/components/VideoTabComponent.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 44 | 45 | 261 | -------------------------------------------------------------------------------- /src/hooks/useArticleDetailEffect.ts: -------------------------------------------------------------------------------- 1 | import {reactive,ref} from "vue" 2 | import {useRoute} from "vue-router"; 3 | import { 4 | getArticleService, 5 | isFocusService, 6 | insertFocusService, 7 | deleteFocusService, 8 | } from "../service/articleDetailService" 9 | import {ArticleInterface} from "../types"; 10 | 11 | export default ()=> { 12 | 13 | const articleDetail = reactive({}); 14 | const isFocus = ref(false); 15 | const route:any = useRoute(); 16 | 17 | const useInitArticleDetail = async () => { 18 | const result = await getArticleService(route.params.id); 19 | Object.assign(articleDetail,result); 20 | isFocusService(articleDetail.authorId,"toutiao").then((res)=>{ 21 | isFocus.value = res > 0; 22 | }) 23 | }; 24 | 25 | const useHandleFocus =()=>{ 26 | if(isFocus.value){ 27 | deleteFocusService(articleDetail.authorId,"toutiao").then((res)=>{ 28 | isFocus.value = !(res > 0) 29 | }); 30 | }else{ 31 | insertFocusService(articleDetail.authorId,"toutiao").then((res)=>{ 32 | isFocus.value = res > 0; 33 | }); 34 | } 35 | }; 36 | 37 | return { 38 | useInitArticleDetail, 39 | articleDetail, 40 | isFocus, 41 | useHandleFocus 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/hooks/useArticleListEffect.ts: -------------------------------------------------------------------------------- 1 | import {useRouter} from "vue-router"; 2 | import {ref,onUnmounted} from "vue"; 3 | import emitter from "../utils/emitter" 4 | import {fomatTime,getImgHtml} from "../utils"; 5 | export default ()=> { 6 | const showHandleIndex = ref(-1); 7 | const router = useRouter(); 8 | const useGoArticleDetail = (id:number)=>{ 9 | router.push(`/articleDetail/${id}`); 10 | }; 11 | /** 12 | * @author: wuwenqiang 13 | * @description: 显示点赞评论收藏的操作框 14 | * @date: 2021-08-15 16:20 15 | */ 16 | const useShowHandle = (index:number)=>{ 17 | showHandleIndex.value = index; 18 | }; 19 | /** 20 | * @author: wuwenqiang 21 | * @description: 点击了其他地方,因此点赞评论操作框 22 | * @date: 2021-08-15 16:20 23 | */ 24 | const useHandleArticle = ()=>{ 25 | showHandleIndex.value = -1; 26 | }; 27 | 28 | emitter.$on("bodyClick",useHandleArticle); 29 | 30 | onUnmounted(()=>{ 31 | emitter.$off("bodyClick",useHandleArticle) 32 | }); 33 | 34 | return {fomatTime,useGoArticleDetail,showHandleIndex,useShowHandle,getImgHtml} 35 | } 36 | -------------------------------------------------------------------------------- /src/hooks/useArticleTabEffect.ts: -------------------------------------------------------------------------------- 1 | import {ref,reactive,nextTick,toRefs,onUnmounted} from "vue" 2 | import { 3 | ArticleStateInterface, 4 | ArticleChannelInterface 5 | } from "../types"; 6 | import { 7 | getFavoriteChannelsListService, 8 | getArticleListService 9 | } from "../service/homeService"; 10 | import BScroll from "better-scroll"; 11 | import { useRouter } from "vue-router"; 12 | import emitter from "../utils/emitter"; 13 | 14 | export default ()=> { 15 | const articleState = reactive({ 16 | isInit: true, 17 | activeId:-1, 18 | isEnd: false, 19 | loading:false, 20 | params:{ 21 | pageNum: 1, 22 | pageSize: 20, 23 | channelId: "", 24 | }, 25 | channels:[], 26 | list:[], 27 | bscroll:null 28 | }); 29 | const articleNavScroll = ref(null); 30 | const articleScrollWrapper = ref(null); 31 | 32 | /** 33 | * @author: wuwenqiang 34 | * @description: 切换频道 35 | * @date: 2020-06-27 21:29 36 | */ 37 | const useTabArticleChannel = async (navItem: ArticleChannelInterface) => { 38 | articleState.activeId = navItem.id; 39 | articleState.params = { 40 | pageNum: 1, 41 | pageSize: 20, 42 | channelId: navItem.channelId, 43 | }; 44 | articleState.isEnd = false; 45 | articleState.list.splice(0, articleState.list.length); 46 | if (navItem.channelName == "西瓜视频") articleState.params.type = "video"; 47 | let res = await getArticleListService(articleState.params); 48 | articleState.list.push(...res); 49 | nextTick(()=>{ 50 | articleState.bscroll.refresh(); 51 | }) 52 | }; 53 | 54 | /** 55 | * @author: wuwenqiang 56 | * @description: 初始化方法 57 | * @date: 2020-06-30 23:28 58 | */ 59 | const useInitArticle = async ()=>{ 60 | const res = await getFavoriteChannelsListService();//获取频道信息 61 | articleState.channels.push(...res); 62 | const activeChannel = articleState.channels.find((item: ArticleChannelInterface) => item.status == 1); 63 | const {channelId,id} = activeChannel as ArticleChannelInterface; 64 | articleState.params.channelId = channelId; 65 | articleState.activeId = id; 66 | const reuslt = await getArticleListService(articleState.params).finally(()=>{ 67 | articleState.isInit = true 68 | }); 69 | articleState.list.push(...reuslt); 70 | setTimeout(()=>{ 71 | new BScroll(articleNavScroll.value as HTMLElement, { 72 | probeType: 1, 73 | click: true, 74 | scrollX: true, 75 | scrollY: false, 76 | eventPassthrough: "vertical" 77 | }); 78 | 79 | articleState.bscroll = new BScroll(articleScrollWrapper.value as HTMLElement, { 80 | probeType: 1, 81 | click: true, 82 | }); 83 | articleState.bscroll.on("scroll",()=>{ 84 | emitter.emit("scroll"); 85 | }); 86 | articleState.bscroll.on('scrollEnd', async () => { 87 | if (articleState.bscroll.y <= (articleState.bscroll.maxScrollY + 100) && !articleState.isEnd && !articleState.loading) { 88 | articleState.params.pageNum++; 89 | let result = await getArticleListService(articleState.params).finally(()=>{ 90 | articleState.isInit = true 91 | }); 92 | if(result.length == 0){ 93 | articleState.isEnd = true; 94 | return; 95 | }else{ 96 | articleState.isEnd = false; 97 | } 98 | articleState.list.push(...reuslt); 99 | nextTick(() => { 100 | articleState.bscroll.refresh(); 101 | }); 102 | } 103 | }) 104 | },500); 105 | }; 106 | 107 | const router = useRouter(); 108 | 109 | const useGoArticleDetail = (id:number)=>{ 110 | router.push(`/articleDetail/${id}`); 111 | }; 112 | 113 | /** 114 | * @author: wuwenqiang 115 | * @description: 弹出评论对话框禁止滚动页面 116 | * @date: 2020-08-23 23:11 117 | */ 118 | const disableScroll =()=>{ 119 | articleState.bscroll.disable() 120 | }; 121 | 122 | /** 123 | * @author: wuwenqiang 124 | * @description: 弹出评论对话框禁止滚动页面 125 | * @date: 2020-08-23 23:11 126 | */ 127 | const enableScroll = ()=>{ 128 | articleState.bscroll.enable() 129 | }; 130 | 131 | emitter.on("disable-scroll",disableScroll); 132 | emitter.on("enable-scroll",enableScroll); 133 | 134 | onUnmounted(()=>{ 135 | emitter.off("disable-scroll",disableScroll); 136 | emitter.off("enable-scroll",disableScroll); 137 | }); 138 | 139 | return { 140 | ...toRefs(articleState), 141 | useTabArticleChannel, 142 | articleNavScroll, 143 | articleScrollWrapper, 144 | useInitArticle, 145 | useGoArticleDetail 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/hooks/useCommentEffect.ts: -------------------------------------------------------------------------------- 1 | import {ReplyCommentInterface, TopCommentInterface} from "../types"; 2 | import {ref,reactive} from "vue"; 3 | import { 4 | getCommentCountService, 5 | getTopCommentListService, 6 | insertCommentService, 7 | getCommentItemService, 8 | getReplyCommentListService 9 | } from "../service/handleService"; 10 | export default (type:string,id:number)=> { 11 | const commentCount = ref(0);//评论条数 12 | const isDone = ref(true);//是否已经加载所有与评论 13 | const topCommentList = reactive>([]);//一级评论 14 | const placeholder = ref("有爱评论,说点好听的~"); 15 | const content = ref(""); 16 | let replyCommentItem:ReplyCommentInterface = {}; 17 | let pageNum:number = 1;//评论的分页 18 | let pageSize:number = 20; 19 | 20 | /** 21 | * @author: wuwenqiang 22 | * @description: 显示评论 23 | * @date: 2021-08-21 14:16 24 | */ 25 | getCommentCountService(type,id).then((res)=>{ 26 | commentCount.value = res; 27 | }); 28 | getTopCommentListService(type,id,pageNum,pageSize).then((res)=>{ 29 | topCommentList.push(...res); 30 | if(res.length < pageSize)isDone.value = true; 31 | }); 32 | 33 | /** 34 | * @author: wuwenqiang 35 | * @description: 发送评论 36 | * @date: 2021-08-22 14:10 37 | */ 38 | const sendComment = async () => { 39 | const commentItem: ReplyCommentInterface = {}; 40 | commentItem.parentId = replyCommentItem.id; 41 | commentItem.topId = replyCommentItem.topId || replyCommentItem.id; 42 | commentItem.content = content.value; 43 | if(type == "toutiao"){ 44 | commentItem.articleId = id; 45 | }else if(type == "video"){ 46 | commentItem.videoId = id; 47 | }else if(type == "movie"){ 48 | commentItem.movieId = id; 49 | } 50 | commentItem.replyUserId = replyCommentItem.userId; 51 | let commmentId:number = -1;//新增之后返回的评论的id 52 | await insertCommentService(type, commentItem).then((res) => { 53 | if (res) content.value = ""; 54 | commmentId = res; 55 | replyCommentItem = {}; 56 | }); 57 | getCommentItemService(type,commmentId).then((res)=>{ 58 | if(res.topId){//如果是回复的评论 59 | let topCommentItem:any = topCommentList.find((item)=>item.id == res.topId); 60 | topCommentItem.replyList = topCommentItem.replyList || []; 61 | topCommentItem.replyList.push(res); 62 | }else{//如果是一级评论 63 | topCommentList.push(res); 64 | } 65 | placeholder.value = "有爱评论,说点好听的~"; 66 | }); 67 | }; 68 | 69 | /** 70 | * @author: wuwenqiang 71 | * @description: 发送评论 72 | * @date: 2021-08-22 17:59 73 | */ 74 | const useReply = (currentReplyComment:TopCommentInterface)=>{ 75 | replyCommentItem = currentReplyComment; 76 | placeholder.value = `回复:${currentReplyComment.username}` 77 | }; 78 | 79 | 80 | const useGetReplyCommentList = (commentItem:TopCommentInterface)=>{ 81 | commentItem.pageNum = commentItem.pageNum || 1; 82 | commentItem.pageSize = commentItem.pageSize || 20; 83 | commentItem.replyList = commentItem.replyList || []; 84 | getReplyCommentListService(type,commentItem.id as number,commentItem.pageNum,commentItem.pageSize).then((res)=>{ 85 | commentItem.isLoadAllReply = res.length < 20; 86 | commentItem.replyList.push(...res); 87 | }); 88 | }; 89 | 90 | return { 91 | commentCount,isDone,topCommentList,placeholder,sendComment,content,useReply,useGetReplyCommentList 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/hooks/useMoreHandleEffect.ts: -------------------------------------------------------------------------------- 1 | import {MixinInterface} from "../types"; 2 | import {ref,onUnmounted,watch,nextTick} from "vue"; 3 | import { 4 | isFavoriteService, 5 | isLikeService, 6 | deleteLikeService, 7 | insertLikeService, 8 | deleteFavoriteService, 9 | insertFavoriteService 10 | } from "../service/handleService"; 11 | import emitter from "../utils/emitter"; 12 | export default (item:MixinInterface,type:string)=> { 13 | const isFavorite = ref(false); 14 | const isLike = ref(false); 15 | const showComment = ref(false); 16 | const commentContainer = ref(null); 17 | let loading:boolean = false; 18 | /** 19 | * @author: wuwenqiang 20 | * @description: 显示点赞评论收藏的操作框 21 | * @date: 2021-08-15 16:20 22 | */ 23 | const useShowHandle = ()=>{ 24 | isFavoriteService(type,item.id).then(res=> isFavorite.value = res > 0); 25 | isLikeService(type,item.id).then(res => isLike.value = res > 0); 26 | }; 27 | 28 | /** 29 | * @author: wuwenqiang 30 | * @description: 点赞 31 | * @date: 2021-08-15 16:20 32 | */ 33 | const useHandleLike =()=>{ 34 | if(loading)return; 35 | loading = true; 36 | if(isLike.value){ 37 | deleteLikeService(type,item.id).then(res=> isLike.value = !(res > 0)).finally(()=>loading = false); 38 | }else{ 39 | insertLikeService(type,item.id).then(res=> isLike.value = res > 0).finally(()=>loading = false); 40 | } 41 | }; 42 | 43 | /** 44 | * @author: wuwenqiang 45 | * @description: 点赞 46 | * @date: 2021-08-18 19:57 47 | */ 48 | const useHandleFavorite = ()=>{ 49 | if(loading)return; 50 | loading = true; 51 | if(isFavorite.value){ 52 | deleteFavoriteService(type,item.id).then(res=> isFavorite.value = !(res > 0)).finally(()=>loading = false); 53 | }else{ 54 | insertFavoriteService(type,item.id).then(res=> isFavorite.value = res > 0).finally(()=>loading = false); 55 | } 56 | }; 57 | 58 | /** 59 | * @author: wuwenqiang 60 | * @description: 显示评论 61 | * @date: 2021-08-21 14:16 62 | */ 63 | const useShowComment = ()=>{ 64 | showComment.value = true; 65 | }; 66 | 67 | /** 68 | * @author: wuwenqiang 69 | * @description: 显示评论 70 | * @date: 2021-08-21 14:16 71 | */ 72 | const useHideComment = ()=>{ 73 | showComment.value = false; 74 | }; 75 | 76 | useShowHandle(); 77 | 78 | emitter.on("scroll",useHideComment); 79 | 80 | onUnmounted(()=>{ 81 | emitter.off("scroll",useHideComment); 82 | }); 83 | 84 | watch(showComment,(value)=>{ 85 | if(value){ 86 | nextTick(()=>{ 87 | document.body.appendChild(commentContainer.value) 88 | }); 89 | } 90 | 91 | }); 92 | 93 | return { 94 | useHandleFavorite,useHandleLike,isFavorite,isLike,useShowComment,showComment,useHideComment,commentContainer 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/hooks/useMovieDetailEffect.ts: -------------------------------------------------------------------------------- 1 | import {reactive,toRefs,ref} from "vue"; 2 | import {getMovieDetailService,getStarService,getMovieListByTypeService,getYourLikesService,getRecommendService} from "../service/movieDetailService"; 3 | import {useRoute} from "vue-router"; 4 | import {MovieInterface,StarInterface} from "@/types"; 5 | export default async () => { 6 | const scores = reactive>([]); 7 | const stars = reactive>([]); 8 | const route: any = useRoute(); 9 | const movieDetail = reactive({}); 10 | const youLikes = reactive>([]); 11 | const recommendList = reactive>([]); 12 | const sameTypeList = reactive>([]); 13 | const showMoreHandle = ref(false); 14 | /** 15 | * @author: wuwenqiang 16 | * @description: 显示点赞评论收藏的操作框 17 | * @date: 2021-08-25 21:44 18 | */ 19 | const getStar = (score: number) => { 20 | for (let i = 0; i < 5; i++) { 21 | if ((i + 1) * 2 < score) { 22 | scores.push("full"); 23 | } else if ((i + 1) * 2 > score && i * 2 < score) { 24 | scores.push("half"); 25 | } else { 26 | scores.push("empty"); 27 | } 28 | } 29 | }; 30 | 31 | const onMoreHandle = ()=>{ 32 | showMoreHandle.value = true; 33 | }; 34 | 35 | await getMovieDetailService(route.params.movieId).then((res) => { 36 | Object.assign(movieDetail, res); 37 | getStar(res.score); 38 | }); 39 | 40 | getStarService(route.params.movieId).then((res)=>{ 41 | stars.push(...res); 42 | }); 43 | 44 | if(movieDetail.label){ 45 | getYourLikesService(movieDetail.label,movieDetail.classify).then(res=>{ 46 | youLikes.push(...res); 47 | }); 48 | } 49 | 50 | if(movieDetail.type){ 51 | getMovieListByTypeService(movieDetail.type,movieDetail.classify).then(res=>{ 52 | sameTypeList.push(...res); 53 | }); 54 | } 55 | 56 | getRecommendService(movieDetail.classify).then(res=>{ 57 | recommendList.push(...res); 58 | }); 59 | 60 | return { 61 | scores, 62 | stars, 63 | youLikes, 64 | sameTypeList, 65 | recommendList, 66 | showMoreHandle, 67 | movieDetail, 68 | onMoreHandle, 69 | ...toRefs(movieDetail) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/hooks/useMovieTabEffect.ts: -------------------------------------------------------------------------------- 1 | import {ref,reactive,nextTick,Ref,toRefs} from "vue" 2 | import { 3 | MovieInterface, 4 | MovieStateInterface, 5 | } from "../types"; 6 | import BScroll from "better-scroll"; 7 | import {getMovieListService} from "../service/movieService"; 8 | 9 | export default ()=> { 10 | 11 | const movieState = reactive({ 12 | isInit: false, 13 | activeClassify:"电影", 14 | isEnd: false, 15 | loading:false, 16 | params: { 17 | pageNum:1, 18 | pageSize:20, 19 | classify:"电影", 20 | keyword:"" 21 | }, 22 | classifies:["电影","电视剧","动漫","综艺","新片场","福利","恐怖"], 23 | list:[], 24 | bscroll:null 25 | }); 26 | const movieNavScroll:Ref = ref(null); 27 | const movieScrollWrapper: Ref = ref(null); 28 | 29 | 30 | /** 31 | * @author: wuwenqiang 32 | * @description: 切换频道 33 | * @date: 2020-06-27 21:29 34 | */ 35 | const useTabMovieChannel = async (classify:string) => { 36 | movieState.params = { 37 | pageNum: 1, 38 | pageSize: 20, 39 | classify 40 | }; 41 | movieState.isEnd = false; 42 | movieState.activeClassify = classify; 43 | movieState.list.splice(0, movieState.list.length); 44 | const result:Array = await getMovieListService(movieState.params); 45 | movieState.list.push(...result); 46 | nextTick(()=>{ 47 | movieState.bscroll.refresh(); 48 | }) 49 | }; 50 | 51 | /** 52 | * @author: wuwenqiang 53 | * @description: 初始化视频方法 54 | * @date: 2020-07-02 00:11 55 | */ 56 | const useInitMovie = async ()=>{ 57 | movieState.params.classify = "电影"; 58 | const result:Array = await getMovieListService(movieState.params); 59 | movieState.list.push(...result); 60 | movieState.isInit = true; 61 | movieState.loading = false; 62 | setTimeout(()=>{ 63 | new BScroll(movieNavScroll.value as HTMLElement, { 64 | probeType: 1, 65 | click: true, 66 | scrollX: true, 67 | scrollY: false, 68 | eventPassthrough: "vertical" 69 | }); 70 | movieState.bscroll = new BScroll(movieScrollWrapper.value as HTMLElement, { 71 | probeType: 1, 72 | click: true 73 | }); 74 | movieState.bscroll.on('scrollEnd', async () => { 75 | if (movieState.bscroll.y <= (movieState.bscroll.maxScrollY + 100) && !movieState.isEnd && !movieState.loading) { 76 | movieState.params.pageNum++ 77 | let result:Array = await getMovieListService(movieState.params); 78 | movieState.list.push(...result); 79 | nextTick(() => { 80 | movieState.bscroll.refresh(); 81 | }) 82 | } 83 | }) 84 | },100) 85 | } 86 | 87 | 88 | return { 89 | ...toRefs(movieState), 90 | useTabMovieChannel, 91 | movieNavScroll, 92 | movieScrollWrapper, 93 | useInitMovie 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/hooks/useMyTabEffect.ts: -------------------------------------------------------------------------------- 1 | import {useRouter} from "vue-router"; 2 | 3 | export default ()=> { 4 | const router = useRouter(); 5 | const goRouter =(name:string)=>{ 6 | router.push(`recordList?type=${name}`); 7 | }; 8 | 9 | return { 10 | goRouter 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/hooks/useVideoListEffect.ts: -------------------------------------------------------------------------------- 1 | import {ref,onUnmounted} from "vue"; 2 | import emitter from "../utils/emitter"; 3 | import {fomatTime} from "../utils"; 4 | export default ()=> { 5 | const showHandleIndex = ref(-1); 6 | 7 | /** 8 | * @author: wuwenqiang 9 | * @description: 显示点赞评论收藏的操作框 10 | * @date: 2021-08-15 16:20 11 | */ 12 | const useShowHandle = (index:number)=>{ 13 | showHandleIndex.value = index; 14 | }; 15 | 16 | /** 17 | * @author: wuwenqiang 18 | * @description: 点击了其他地方,因此点赞评论操作框 19 | * @date: 2021-08-15 16:20 20 | */ 21 | const useHandleVideo = ()=>{ 22 | showHandleIndex.value = -1; 23 | }; 24 | 25 | emitter.$on("bodyClick",useHandleVideo); 26 | 27 | onUnmounted(()=>{ 28 | emitter.$off("bodyClick",useHandleVideo) 29 | }); 30 | 31 | return {useShowHandle,showHandleIndex,fomatTime} 32 | } 33 | -------------------------------------------------------------------------------- /src/hooks/useVideoTabEffect.ts: -------------------------------------------------------------------------------- 1 | import {ref,reactive,nextTick,toRefs,Ref} from "vue" 2 | import { 3 | VideoInterface, 4 | VideoChannelInterface, 5 | VideoStateInterface, 6 | } from "../types"; 7 | import { 8 | getVideoListService, 9 | getVideoFavoriteChannelsService 10 | } from "../service/videoService"; 11 | import BScroll from "better-scroll"; 12 | import emitter from "../utils/emitter"; 13 | 14 | export default ()=> { 15 | 16 | const videoState = reactive({ 17 | isInit: false, 18 | activeChannelId:"", 19 | isEnd: false, 20 | loading:false, 21 | showHandleIndex:-1, 22 | params: { 23 | pageSize:20, 24 | pageNum:1,//页码 25 | star:"",//主演 26 | channelId:"",//分类 27 | type:"",//类型 28 | label:"",//标签 29 | authorId:"",//用户 30 | keyword:"",//关键字 31 | }, 32 | channels:[], 33 | list:[], 34 | bscroll:null 35 | }); 36 | const videoNavScroll:Ref = ref(null); 37 | const videoScrollWrapper = ref(); 38 | 39 | 40 | /** 41 | * @author: wuwenqiang 42 | * @description: 切换频道 43 | * @date: 2020-06-27 21:29 44 | */ 45 | const useTabVideoChannel = async (navItem: VideoChannelInterface) => { 46 | videoState.activeChannelId = navItem.channelId; 47 | videoState.params = { 48 | pageNum: 1, 49 | pageSize: 20, 50 | channelId: navItem.channelId, 51 | }; 52 | videoState.isEnd = false; 53 | videoState.list.splice(0, videoState.list.length); 54 | let res = await getVideoListService(videoState.params); 55 | videoState.list.push(...res as Array); 56 | nextTick(()=>{ 57 | videoState.bscroll.refresh() 58 | }) 59 | }; 60 | 61 | /** 62 | * @author: wuwenqiang 63 | * @description: 初始化视频方法 64 | * @date: 2020-07-02 00:11 65 | */ 66 | const useInitVideo = async ()=>{ 67 | let res = await getVideoFavoriteChannelsService(); 68 | videoState.channels.push(...res as Array); 69 | videoState.params.channelId = videoState.channels[0].channelId; 70 | let result = await getVideoListService(videoState.params); 71 | videoState.list.push(...result as Array); 72 | videoState.isInit = true; 73 | videoState.loading = false; 74 | videoState.activeChannelId = videoState.channels[0].channelId; 75 | setTimeout(()=>{ 76 | new BScroll(videoNavScroll.value as HTMLElement, { 77 | probeType: 1, 78 | click: true, 79 | scrollX: true, 80 | scrollY: false, 81 | eventPassthrough: "vertical" 82 | }); 83 | videoState.bscroll = new BScroll(videoScrollWrapper.value as HTMLElement, { 84 | probeType: 1, 85 | click: true, 86 | }); 87 | videoState.bscroll.on("scroll",()=>{ 88 | emitter.emit("scroll"); 89 | }); 90 | videoState.bscroll.on('scrollEnd', async () => { 91 | if (videoState.bscroll.y <= (videoState.bscroll.maxScrollY + 100) && !videoState.isEnd && !videoState.loading) { 92 | videoState.params.pageNum++; 93 | let result:Array = await getVideoListService(videoState.params); 94 | videoState.list.push(...result); 95 | nextTick(() => { 96 | videoState.bscroll.refresh() 97 | }); 98 | } 99 | }) 100 | },100) 101 | }; 102 | 103 | 104 | return { 105 | ...toRefs(videoState), 106 | useTabVideoChannel, 107 | videoNavScroll, 108 | videoScrollWrapper, 109 | useInitVideo 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue,{ createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import store from './store'; 5 | import "./utils/setAxios"; 6 | import "./common/style/iconfont.css"; 7 | import "./common/style/reset.less"; 8 | 9 | createApp(App).use(store).use(router).mount('#app'); 10 | -------------------------------------------------------------------------------- /src/pages/ArticleDetailPage.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 37 | 45 | 91 | -------------------------------------------------------------------------------- /src/pages/HomePage.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 63 | 119 | 153 | -------------------------------------------------------------------------------- /src/pages/MovieDetaiPage.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 84 | 85 | 214 | -------------------------------------------------------------------------------- /src/pages/RecordListPage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | 40 | 43 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import {createRouter, createWebHashHistory, RouteRecordRaw} from 'vue-router' 2 | 3 | const routes: Array = [ 4 | { 5 | path: '/', 6 | name: 'Home', 7 | meta: {keepAlive: true}, 8 | component: () => import('../pages/HomePage.vue') 9 | }, 10 | { 11 | path: '/articleDetail/:id', 12 | name: 'detail', 13 | meta: {keepAlive: false}, 14 | component: () => import('../pages/ArticleDetailPage.vue') 15 | }, 16 | { 17 | path: '/recordList', 18 | name: 'recordList', 19 | meta: {keepAlive: false}, 20 | component: () => import('../pages/RecordListPage.vue') 21 | }, 22 | { 23 | path: '/movieDetail/:movieId', 24 | name: 'movieDetail/:movieId', 25 | meta: {keepAlive: false}, 26 | component: () => import('../pages/MovieDetaiPage.vue') 27 | } 28 | ]; 29 | 30 | const router = createRouter({ 31 | history: createWebHashHistory(), 32 | routes 33 | }); 34 | 35 | export default router 36 | -------------------------------------------------------------------------------- /src/service/appService.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosResponse} from "axios"; 2 | import api from "../api"; 3 | import store from "../store"; 4 | import {UserDataInterface} from "../types"; 5 | import {USER_DATA} from "../store/mutation-types"; 6 | export const getUserDataService = async () => { 7 | return new Promise((async (resolve) => { 8 | let data = await axios.get(api.getUserData).then((res: AxiosResponse) => res.data) 9 | .catch(()=>resolve({})); 10 | store.commit(USER_DATA, data); 11 | resolve(data); 12 | })); 13 | } 14 | -------------------------------------------------------------------------------- /src/service/articleDetailService.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosResponse} from "axios"; 2 | import api from "../api"; 3 | import {ArticleInterface} from "../types"; 4 | import {getUrl} from "../utils" 5 | 6 | export const getArticleService = async (id:number) => { 7 | return axios.get(`${api.getArticleDetail}/${id}`).then((res:AxiosResponse)=>res.data); 8 | }; 9 | 10 | export const isFocusService = async (authorId:string,type:string) => { 11 | return axios.get(getUrl(api.isFocus,type),{params:{authorId}}).then((res:AxiosResponse)=>res.data); 12 | }; 13 | 14 | export const insertFocusService = async (authorId:string,type:string) => { 15 | return axios.post(getUrl(api.insertFocus,type),{authorId}).then((res:AxiosResponse)=>res.data); 16 | }; 17 | 18 | export const deleteFocusService = async (authorId:string,type:string) => { 19 | return axios.delete(getUrl(api.deleteFocus,type),{params:{authorId}}).then((res:AxiosResponse)=>res.data); 20 | }; 21 | -------------------------------------------------------------------------------- /src/service/handleService.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosResponse} from "axios"; 2 | import api from "../api"; 3 | import {getUrl,getParams} from "../utils" 4 | import {TopCommentInterface,ReplyCommentInterface} from "@/types"; 5 | 6 | export const isFavoriteService = (type:string,id:number) => { 7 | return axios.get(getUrl(api.isFavorite,type), {params:getParams(type,id)}).then((res:AxiosResponse)=>res.data); 8 | }; 9 | 10 | export const insertFavoriteService = (type:string,id:number) => { 11 | return axios.post(getUrl(api.insertFavorite,type), getParams(type,id)).then((res:AxiosResponse)=>res.data); 12 | }; 13 | 14 | export const deleteFavoriteService = (type:string,id:number) => { 15 | return axios.delete(getUrl(api.deleteFavorite,type), {params:getParams(type,id)}).then((res:AxiosResponse)=>res.data); 16 | }; 17 | 18 | export const isLikeService = (type:string,id:number) => { 19 | return axios.get(getUrl(api.isLike,type), { params:getParams(type,id)}).then((res:AxiosResponse)=>res.data); 20 | }; 21 | 22 | export const insertLikeService = (type:string,id:number) => { 23 | return axios.post(getUrl(api.insertLike,type), getParams(type,id)).then((res:AxiosResponse)=>res.data); 24 | }; 25 | 26 | export const deleteLikeService = (type:string,id:number) => { 27 | return axios.delete(getUrl(api.deleteLike,type), {params:getParams(type,id)}).then((res:AxiosResponse)=>res.data); 28 | }; 29 | 30 | export const getCommentCountService = (type:string,id:number) => { 31 | return axios.get(getUrl(api.getCommentCount,type), {params:getParams(type,id)}).then((res:AxiosResponse)=>res.data); 32 | }; 33 | 34 | export const getTopCommentListService = (type:string,id:number,pageNum:number,pageSize:number) => { 35 | return axios.get(getUrl(api.getTopCommentList,type), {params:getParams(type,id,{pageNum,pageSize})}).then((res:AxiosResponse>)=>res.data); 36 | }; 37 | 38 | export const insertCommentService = (type:string,params:ReplyCommentInterface) => { 39 | return axios.post(getUrl(api.insertComment,type), params).then((res:AxiosResponse)=>res.data); 40 | }; 41 | 42 | export const deleteCommentService = (type:string,id:number) => { 43 | return axios.delete(getUrl(api.insertComment,type).replace("{id}",id+"")).then((res:AxiosResponse)=>res.data); 44 | }; 45 | 46 | export const getReplyCommentListService = (type:string,topId:number,pageNum:number,pageSize:number) => { 47 | return axios.get(getUrl(api.getReplyCommentList,type),{params:{topId,pageNum,pageSize}}).then((res:AxiosResponse>)=>res.data); 48 | }; 49 | 50 | export const getCommentItemService = (type:string,id:number) => { 51 | return axios.get(getUrl(api.getCommentItem,type), {params:{id}}).then((res:AxiosResponse)=>res.data); 52 | }; 53 | -------------------------------------------------------------------------------- /src/service/homeService.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosResponse} from "axios"; 2 | import api from "../api"; 3 | import { ArticleParamsInterface, ArticleInterface, ArticleChannelInterface} from "../types"; 4 | import {getImg} from "../utils"; 5 | 6 | export const getFavoriteChannelsListService = async () => { 7 | return axios.get(api.getFavoriteChannels).then((res:AxiosResponse>)=>res.data); 8 | }; 9 | 10 | export const getArticleListService = (params:ArticleParamsInterface) => { 11 | return axios.get(api.getArticleList, { 12 | params 13 | }).then((res:AxiosResponse>)=>{ 14 | return res.data.map((item:ArticleInterface)=>{ 15 | item.imgList = getImg(item); 16 | return item; 17 | }); 18 | }) 19 | }; 20 | -------------------------------------------------------------------------------- /src/service/movieDetailService.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosResponse} from "axios"; 2 | import api from "../api"; 3 | import {MovieInterface, StarInterface} from "../types"; 4 | 5 | export const getMovieDetailService = async (movieId:number) => { 6 | return axios.get(api.getMovieDetail+movieId).then((res:AxiosResponse)=>res.data); 7 | }; 8 | 9 | export const getStarService = async (movieId:number)=>{ 10 | return axios.get(api.getStar+movieId).then((res:AxiosResponse>)=>res.data); 11 | }; 12 | 13 | export const getYourLikesService = async (labels:string,classify:string)=>{ 14 | return axios.get(api.getYourLikes,{params:{labels,classify}}).then((res:AxiosResponse>)=>res.data); 15 | }; 16 | 17 | export const getMovieListByTypeService = async (types:string,classify:string)=>{ 18 | return axios.get(api.getMovieListByType,{params:{types,classify}}).then((res:AxiosResponse>)=>res.data); 19 | }; 20 | 21 | export const getRecommendService = async (classify:string)=>{ 22 | return axios.get(api.getRecommend,{params:{classify}}).then((res:AxiosResponse>)=>res.data); 23 | }; 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/service/movieService.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosResponse} from "axios"; 2 | import api from "../api"; 3 | import {MovieParamsInterface,MovieInterface} from "../types"; 4 | 5 | export const getMovieListService = (params:MovieParamsInterface) => { 6 | return axios.get(api.getMovieList, { 7 | params 8 | }).then((res:AxiosResponse>)=>res.data); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/service/myService.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosResponse} from "axios"; 2 | import api from "../api"; 3 | import {MixinInterface} from "../types"; 4 | 5 | export const getRecordListService = (type:string) => { 6 | return axios.get(api.getRecordList,{params:{type}}).then((res:AxiosResponse>)=>res.data); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/service/recordService.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosResponse} from "axios"; 2 | import api from "../api"; 3 | import {ArticleInterface, MixinInterface} from "../types"; 4 | import {getImg,getUrl} from "../utils"; 5 | 6 | export const getRecordListService = (type:string) => { 7 | return axios.get(getUrl(api.getRecordList,type)).then((res:AxiosResponse>)=>{ 8 | if(type == "toutiao"){ 9 | return res.data.map((item:any)=>{ 10 | item.imgList = getImg(item as ArticleInterface); 11 | return item; 12 | }); 13 | } 14 | return res.data 15 | }) 16 | }; 17 | 18 | -------------------------------------------------------------------------------- /src/service/videoService.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosResponse} from "axios"; 2 | import api from "../api"; 3 | import {VideoParamsInterface, VideoInterface, VideoChannelInterface, ArticleInterface, MovieInterface} from "../types"; 4 | 5 | export const getVideoFavoriteChannelsService = ()=>{ 6 | return axios.get(api.getVideoFavoriteChannels).then((res:AxiosResponse>)=>res.data) 7 | } 8 | 9 | export const getVideoListService = (params:VideoParamsInterface) => { 10 | return axios.get(api.getVideoList, { 11 | params 12 | }).then((res:AxiosResponse>)=>res.data); 13 | }; 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /src/store/actions.ts: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types' 2 | import {UserDataInterface} from "../types"; 3 | 4 | export const setToken = ({ commit }:any, myData:string):void => { 5 | commit(types.TOKEN, myData) 6 | } 7 | 8 | export const setUserData = ({ commit }:any, userData:UserDataInterface):void => { 9 | commit(types.USER_DATA, userData) 10 | } 11 | -------------------------------------------------------------------------------- /src/store/getters.ts: -------------------------------------------------------------------------------- 1 | import stateInterface from './stateInterface' 2 | 3 | export const token = (state: stateInterface) => state.token; 4 | export const userData = (state: stateInterface) => state.userData; 5 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | import * as getters from "./getters"; 3 | import * as actions from "./actions"; 4 | import mutations from "./mutations"; 5 | import state from "./state"; 6 | 7 | export default createStore({ 8 | state, 9 | mutations, 10 | actions, 11 | getters 12 | }) 13 | -------------------------------------------------------------------------------- /src/store/mapActions.ts: -------------------------------------------------------------------------------- 1 | import {useStore} from "vuex" 2 | 3 | export default (arr:Array):any=>{ 4 | const data: any = {}; 5 | const state = useStore(); 6 | arr.forEach((item: string) => { 7 | data[item] = (payload:any) => { 8 | state.dispatch(item,payload) 9 | } 10 | }); 11 | return data; 12 | } 13 | -------------------------------------------------------------------------------- /src/store/mapGetters.ts: -------------------------------------------------------------------------------- 1 | import {computed} from "vue"; 2 | import store from "../store" 3 | 4 | export default (arr:Array):any => { 5 | const data:any = {}; 6 | arr.forEach((item:string)=>{ 7 | data[item] = computed(()=>{ 8 | return store.getters[item]; 9 | }) 10 | }) 11 | return data; 12 | } 13 | -------------------------------------------------------------------------------- /src/store/mutation-types.ts: -------------------------------------------------------------------------------- 1 | export const TOKEN = 'TOKEN' 2 | export const USER_DATA = 'USER_DATA' 3 | -------------------------------------------------------------------------------- /src/store/mutations.ts: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-types' 2 | import stateInterface from "./stateInterface"; 3 | import {UserDataInterface} from "../types" 4 | 5 | export default { 6 | [types.TOKEN](state: stateInterface, token: string): void { 7 | state.token = token 8 | }, 9 | [types.USER_DATA](state: stateInterface, userData: UserDataInterface): void { 10 | state.userData = userData 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /src/store/state.ts: -------------------------------------------------------------------------------- 1 | import stateInterface from './stateInterface' 2 | 3 | export default { 4 | token: '', 5 | userData: null, 6 | } 7 | -------------------------------------------------------------------------------- /src/store/stateInterface.ts: -------------------------------------------------------------------------------- 1 | import {UserDataInterface} from "../types" 2 | 3 | export default interface StateInterface { 4 | token: string, 5 | userData: UserDataInterface | null, 6 | } 7 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface UserDataInterface { 2 | userId:string, 3 | create_date:string, 4 | update_date:string, 5 | username:string, 6 | telephone:string, 7 | email:string, 8 | avater:string, 9 | birthday:string, 10 | sex:string, 11 | role:string, 12 | sign:string, 13 | region:string 14 | } 15 | 16 | export interface ArticleInterface { 17 | id:number,//主键 18 | channelId:string,//频道id 19 | content:string,//内容 20 | duration:string,//视频播放时长 21 | href:string,//链接地址 22 | img:string,//视频图片地址 23 | imgNum:string,//图片长度 24 | type:string,//类型,视频:video, 文章: article,博客:blog 25 | isTop:string,//是否置顶,0表示否,1表示是 26 | title:string,//主标题 27 | createTime:string,//创建时间 28 | updateTime:string,//更新时间 29 | source:string,//来源 30 | commentId:string,//评论id 31 | labels:string,//标签 32 | authorId:string,//用户id 33 | authorAvatar:string,//头像 34 | authorHref:string,//用户主页 35 | ranks:string//排名 36 | imgList?:Array,//预览图片列表 37 | } 38 | 39 | export interface ArticleChannelInterface { 40 | id:number,//主键 41 | channelId:string,//频道id 42 | channelName:string,//频道名称 43 | href:string,//链接地址 44 | createTime:string,//创建时间 45 | updateTime:string,//最近更新时间 46 | userId:string,//用户id 47 | disabled:number,//是否禁用 48 | status:number,//状态,公开:0,推荐:1,默认:2 49 | sequence:number,//主标题 50 | } 51 | 52 | export interface ArticleParamsInterface { 53 | channelId?:string,//频道id 54 | authorId?:string,//用户id 55 | keyword?:string, 56 | pageNum:number,//页码 57 | pageSize: number,//每页长度 58 | isTop?:string //是否置顶 59 | type?:string 60 | } 61 | 62 | export interface ArticleStateInterface { 63 | isInit: boolean, 64 | activeId:number, 65 | isEnd: boolean, 66 | loading:boolean, 67 | params:ArticleParamsInterface, 68 | channels:Array, 69 | list:Array, 70 | bscroll:any 71 | } 72 | 73 | export interface VideoInterface { 74 | id:number, //主键, 75 | albumId:string,//视频id, 76 | channelId:string,//频道id, 77 | title:string,//电影名称, 78 | director:string,//导演, 79 | star:string,//主演, 80 | type:string,//类型, 81 | countryLanguage: string,//国家/语言, 82 | publishTime:string,//上映时间, 83 | plot:string,//剧情, 84 | isRecommend:string //是否推荐,0:不推荐,1:推荐, 85 | classify:string ,//分类 电影,电视剧,动漫,综艺,新片库,福利,午夜,恐怖,其他, 86 | sourceName: string,//来源名称,本地,骑士影院,爱奇艺, 87 | sourceUrl:string,//来源地址, 88 | label:string,//标签, 89 | originaHref: string,//源地址, 90 | description :string,//简单描述, 91 | targetHref:string,//链接地址, 92 | status:string,//0代表未使用,1表示正在使用,是banner和carousel图的才有, 93 | score: number,//评分, 94 | category:string,//类目,值为banner首屏,carousel:滚动轮播, 95 | ranks:string,//排名, 96 | authorId:string,//用户名,这这个表不需要,为了跟记录叫和收藏表的结构一致, 97 | duration:string,//时长, 98 | img:string,// 图片地址, 99 | createTime:string,//创建时间, 100 | updateTime:string//更新时间, 101 | authorInfo:AuthorInfo,//用户信息 102 | } 103 | 104 | export interface AuthorInfo { 105 | id:number,//主键 106 | authorId:string,//用户id 107 | name:string,//名称 108 | authorDesc:string,//描述 109 | avatarUrl:string,//头像地址 110 | description:string,//描述 111 | followersCount:string,//粉丝数量 112 | verifiedContent:string,//主要内容 113 | createTime:string,//创建时间 114 | updateTime:string,//更新时间 115 | } 116 | 117 | export interface VideoParamsInterface { 118 | pageSize:number,//每页显示条数 119 | pageNum:number,//页码 120 | star?:string,//主演 121 | channelId?:string,//分类 122 | type?:string,//类型 123 | label?:string,//标签 124 | authorId?:string,//用户 125 | keyword?:string,//关键字 126 | } 127 | 128 | export interface VideoChannelInterface { 129 | id:number, 130 | channelName:string, 131 | channelId:string, 132 | createTime:string, 133 | updateTime:string, 134 | sequence:number, 135 | status:number, 136 | userId:string 137 | } 138 | 139 | export interface VideoStateInterface { 140 | isInit: boolean, 141 | activeChannelId:string, 142 | isEnd: boolean, 143 | loading:boolean, 144 | params: VideoParamsInterface, 145 | channels:Array, 146 | list:Array, 147 | bscroll:any 148 | showHandleIndex:number,//显示点赞评论收藏操作框的下标 149 | } 150 | 151 | export interface MovieStateInterface { 152 | isInit: boolean, 153 | activeClassify:string, 154 | isEnd: boolean, 155 | loading:boolean, 156 | params: MovieParamsInterface, 157 | classifies:Array, 158 | list:Array, 159 | bscroll:any 160 | } 161 | export interface MovieInterface { 162 | id:number,//主键', 163 | movieId:number,//电影id 164 | movieName:string,//电影名称 165 | director:string,//导演 166 | star:string,//主演 167 | type:string,//类型 168 | countryLanguage:string,//国家/语言 169 | viewingState:string,//观看状态 170 | releaseTime:string,//上映时间 171 | plot:string,//剧情 172 | updateTime:string,//更新时间 173 | isRecommend:number,//是否推荐,0:不推荐,1:推荐 174 | img:number,//电影海报 175 | classify:string,//分类 电影,电视剧,动漫,综艺,新片库,福利,午夜,恐怖,其他 176 | sourceName:string,//来源名称,本地,骑士影院,爱奇艺 177 | sourceUrl:string,//来源地址 178 | createTime:string,//创建时间 179 | localImg:string,//本地图片 180 | label:string,//标签 181 | originalHref:string,//源地址 182 | description:string,//简单描述 183 | targetHref:string,//链接地址 184 | useStatus:string,//0代表未使用,1表示正在使用,是banner和carousel图的才有 185 | score:number,//评分 186 | category:string,//类目,值为banner首屏,carousel:滚动轮播 187 | ranks:string,//排名 188 | userId:string,//用户名,这这个表不需要,为了跟记录叫和收藏表的结构一致 189 | doubanUrl:string,//对应豆瓣网的地址 190 | } 191 | export interface MovieParamsInterface { 192 | id?:number, 193 | pageNum:number, 194 | pageSize:number 195 | classify?:string, 196 | star?:string, 197 | type?:string, 198 | label?:string, 199 | director?:string,//导员 200 | keyword?:string 201 | } 202 | 203 | export interface HandlePropsInterface { 204 | type:string, 205 | item:MixinInterface 206 | } 207 | 208 | export type MixinInterface = ArticleInterface|MovieInterface|VideoInterface 209 | 210 | export interface ReplyCommentInterface { 211 | id?:number, 212 | content:string, 213 | parentId?:number, 214 | topId?:number, 215 | articleId?:number, 216 | videoId?:number, 217 | movieId?:number, 218 | userId:string, 219 | username?:string, 220 | avater?:string, 221 | replyUserId?:string, 222 | replyUserName?:string, 223 | createTime?:string, 224 | updateTime?:string, 225 | } 226 | 227 | export interface TopCommentInterface extends ReplyCommentInterface{ 228 | replyCount:number, 229 | pageNum?:number,//第某页回复 230 | pageSize?:number,//每页回复数量 231 | isLoadAllReply?:boolean,//是否已经加载了所有回复 232 | replyList:Array 233 | } 234 | 235 | export interface CommentListPropsInterface { 236 | type:string, 237 | id:number 238 | } 239 | 240 | export interface StarInterface { 241 | id:number, 242 | starName:string, 243 | img:string, 244 | localImg:string, 245 | createTime:string, 246 | update_time:string, 247 | movie_id:number, 248 | role:string, 249 | href:string 250 | works:string 251 | } 252 | 253 | 254 | 255 | -------------------------------------------------------------------------------- /src/utils/emitter.ts: -------------------------------------------------------------------------------- 1 | import Mitt from 'mitt'; 2 | 3 | const emitter = new Mitt(); 4 | 5 | emitter.$on = emitter.on; 6 | emitter.$off = emitter.off; 7 | emitter.$emit = emitter.emit; 8 | 9 | export default emitter; 10 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import {ArticleInterface} from "../types"; 2 | 3 | export const zorefull=(value:number):string|number=>{ 4 | return value < 9 ? "0"+value:value 5 | } 6 | 7 | export const fomatTime=(value:any):string=>{ 8 | var date =new Date(value); 9 | var nowDate = new Date() 10 | let diff = Math.ceil((nowDate.getTime()-date.getTime())/1000); 11 | if(diff < 60){ 12 | return diff + "秒前" 13 | }else if(diff < 60*60){ 14 | return Math.ceil(diff/60) + "分前" 15 | }else if(diff < 60*60*24){ 16 | return Math.ceil(diff/(60*60))+"小时前" 17 | }else if(diff < 60*60*24*30){ 18 | return Math.ceil(diff/(60*60*24))+"天前" 19 | }else if(diff < 60*60*24*30*12){ 20 | return Math.ceil(diff/(60*60*24*30))+"个月前" 21 | } 22 | const year = zorefull(date.getFullYear()); 23 | const month = zorefull(date.getMonth()+1); 24 | const dates = zorefull(date.getDate()); 25 | const hour = zorefull(date.getHours()); 26 | const minutes = zorefull(date.getMinutes()); 27 | const seconds = zorefull(date.getSeconds()); 28 | return `${year}-${month}-${dates} ${hour}:${minutes}:${seconds}` 29 | }; 30 | 31 | /** 32 | * @author: wuwenqiang 33 | * @description: 获取图片html 34 | * @date: 2020-06-27 21:29 35 | */ 36 | export const getImg = (article:ArticleInterface)=>{ 37 | if (article.type == "video")return article.img ? [`
${article.duration}
`] : [] 38 | return article.content.match(/]+>/g) || [] 39 | }; 40 | 41 | export const isMobile = ()=> { 42 | var sUserAgent = navigator.userAgent.toLowerCase(); 43 | if (/ipad|iphone|midp|rv:1.2.3.4|ucweb|android|windows ce|windows mobile/.test(sUserAgent)) { 44 | return true;//手机端 45 | } else { 46 | return false;//pc端 47 | } 48 | }; 49 | 50 | /** 51 | * @author: wuwenqiang 52 | * @description: 获取图片数量 53 | * @date: 2020-06-27 21:29 54 | */ 55 | export const getImgHtml = (htmlStr:string,length:number,index:number) =>{ 56 | if (index == 3 && length > 4){ 57 | return `
+${length-index-1}
${htmlStr}`; 58 | }else{ 59 | return htmlStr; 60 | } 61 | }; 62 | 63 | /** 64 | * @author: wuwenqiang 65 | * @description: 根据类型获取api地址 66 | * @date: 2020-06-27 21:29 67 | */ 68 | export const getUrl = (url:string,type:string) => url.replace("{type}",type); 69 | 70 | /** 71 | * @author: wuwenqiang 72 | * @description: 根据类型转换参数 73 | * @date: 2020-06-27 21:29 74 | */ 75 | export const getParams = (type:string,id:number,params?:object) => { 76 | switch (type) { 77 | case "toutiao": 78 | return {articleId:id,...params}; 79 | case "video": 80 | return {videoId:id,...params}; 81 | case "movieId": 82 | return {movieId:id,...params}; 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /src/utils/setAxios: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import store from "../store"; 3 | import {TOKEN} from "../store/mutation-types" 4 | 5 | axios.interceptors.request.use(config => { 6 | //config是axios配置对象 7 | //每次发送请求前都会进入此拦截器处理函数,可以在此处统一携带上token,每次请求都会带有 8 | config.headers.common.Authorization = store.state.token || localStorage.getItem('token'); 9 | return config 10 | },err => { 11 | //请求出错的处理函数 12 | return Promise.reject(err) 13 | }); 14 | 15 | axios.interceptors.response.use((res)=> { 16 | if(res.data.status=="SUCCESS" && res.data.token){ 17 | store.commit(TOKEN, res.data.token); 18 | localStorage.setItem("token",res.data.token); 19 | } 20 | return res.data; 21 | },err => { 22 | // 在请求错误时要做的事儿 23 | // 该返回的数据则是axios.catch(err)中接收的数据 24 | return Promise.reject(err) 25 | }); 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "esnext", 24 | "dom", 25 | "dom.iterable", 26 | "scripthost" 27 | ] 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue", 33 | "tests/**/*.ts", 34 | "tests/**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false, 3 | devServer: { 4 | overlay: { 5 | warning: false, 6 | errors: false 7 | }, 8 | port: '8080', 9 | open: true, 10 | proxy: { 11 | '^/service/toutiao': { 12 | target: 'http://localhost:8000', 13 | }, 14 | '^/service/video': { 15 | target: 'http://localhost:8001', 16 | }, 17 | '^/service/movie': { 18 | target: 'http://localhost:5000', 19 | }, 20 | '/static': { 21 | target: 'http://localhost:5001', 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /明日头条app整体效果图(待更新).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/明日头条app整体效果图(待更新).jpg -------------------------------------------------------------------------------- /界面预览1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/界面预览1.png -------------------------------------------------------------------------------- /界面预览10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/界面预览10.png -------------------------------------------------------------------------------- /界面预览2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/界面预览2.png -------------------------------------------------------------------------------- /界面预览3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/界面预览3.png -------------------------------------------------------------------------------- /界面预览4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/界面预览4.png -------------------------------------------------------------------------------- /界面预览5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/界面预览5.png -------------------------------------------------------------------------------- /界面预览6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/界面预览6.png -------------------------------------------------------------------------------- /界面预览7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/界面预览7.png -------------------------------------------------------------------------------- /界面预览8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/界面预览8.png -------------------------------------------------------------------------------- /界面预览9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyuanwuhui99/vue3-ts-toutiao-app-ui/a5c2409f027113d9ce1bff4776141a77be28fcb0/界面预览9.png --------------------------------------------------------------------------------