├── .babelrc ├── .commitlintrc.js ├── .gitignore ├── .postcssrc ├── .prettierrc ├── README.md ├── jest.config.js ├── package.json ├── public ├── favicon.ico ├── img │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ └── msapplication-icon-144x144.png ├── index.html ├── manifest.json └── sw.js ├── src ├── App.vue ├── api │ ├── feedback.ts │ ├── index.ts │ ├── sync.ts │ └── user.ts ├── assets │ ├── 404.jpg │ ├── iconfont.js │ └── logo.png ├── components │ ├── Footer.vue │ ├── Header.vue │ └── common │ │ ├── Card │ │ └── Card.vue │ │ ├── Circle │ │ ├── Circle.vue │ │ └── style.scss │ │ ├── ClockPopup │ │ ├── ClockPopup.vue │ │ └── style.scss │ │ ├── DateBlock │ │ ├── DateBlock.vue │ │ └── style.scss │ │ ├── HabitList │ │ ├── List.vue │ │ └── style.scss │ │ ├── Icon │ │ ├── FooterIcon.ts │ │ ├── HeaderIcon.ts │ │ ├── Icon.vue │ │ └── style.scss │ │ ├── Popup │ │ ├── Popup.vue │ │ └── style.scss │ │ └── Skeleton │ │ ├── SkeletonCircle.vue │ │ └── SkeletonList.vue ├── config.ts ├── main.ts ├── registerServiceWorker.ts ├── router.ts ├── shims.d.ts ├── store │ ├── actions.ts │ ├── getters.ts │ ├── index.ts │ ├── mutations.ts │ ├── state.ts │ └── types.ts ├── style │ ├── common.css │ └── mixin.scss ├── sw.js ├── typings │ └── ajax.d.ts ├── utils.ts └── views │ ├── 404 │ └── error.vue │ ├── Card │ ├── Card.vue │ ├── Receive │ │ └── Card.vue │ ├── __test__ │ │ └── Card.test.ts │ └── style.scss │ ├── Edit │ ├── Calendar │ │ ├── Calendar.vue │ │ └── style.scss │ ├── Edit.vue │ ├── IconSetting │ │ ├── IconSetting.vue │ │ └── style.scss │ ├── Manage │ │ ├── Manage.vue │ │ └── style.scss │ ├── Recycle │ │ ├── Recycle.vue │ │ └── style.scss │ ├── Remind │ │ ├── Remind.vue │ │ └── style.scss │ ├── Times │ │ ├── Times.vue │ │ └── style.scss │ └── style.scss │ ├── Feedback │ └── Feedback.vue │ ├── Habit │ └── Habit.vue │ ├── Home │ └── Home.vue │ ├── Login │ └── Login.vue │ ├── New │ ├── Habit │ │ ├── Habit.vue │ │ └── style.scss │ ├── Library │ │ ├── Library.vue │ │ └── style.scss │ ├── New.vue │ └── style.scss │ ├── Setting │ └── Setting.vue │ └── UpdateLog │ └── UpdateLog.vue ├── tests └── e2e │ ├── custom-assertions │ └── elementCount.js │ └── specs │ └── test.js ├── tsconfig.json ├── tslint.json ├── vue.config.js └── workbox-config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@vue/app" 4 | ], 5 | "plugins": [ 6 | ["import", { "libraryName": "vant", "style": true }], 7 | "lodash" 8 | ] 9 | } -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@commitlint/config-conventional' 4 | ], 5 | rules: { 6 | } 7 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /tests/e2e/reports/ 6 | selenium-debug.log 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "autoprefixer": {} 4 | } 5 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "trailingComma": "all", 5 | "singleQuote": true, 6 | "overrides": [{ 7 | "files": [ 8 | "*.json", 9 | ".eslintrc", 10 | ".tslintrc", 11 | ".prettierrc", 12 | ".tern-project" 13 | ], 14 | "options": { 15 | "parser": "json", 16 | "tabWidth": 2 17 | } 18 | }, 19 | { 20 | "files": "*.{css,sass,scss,less}", 21 | "options": { 22 | "parser": "postcss", 23 | "tabWidth": 4 24 | } 25 | }, 26 | { 27 | "files": "*.ts", 28 | "options": { 29 | "parser": "typescript" 30 | } 31 | }, 32 | { 33 | "files": "*.tsx", 34 | "options": { 35 | "parser": "typescript" 36 | } 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-ts-daily 2 | 3 | 基于Vue.js的2.5.13版本和TypeScript编写的模仿原生应用的WebApp. 4 | 5 | [项目演示地址](http://day.xiaomuzhu.top/) 6 | 7 | ![扫描二维码](http://omrbgpqyl.bkt.clouddn.com/18-5-15/73469590.jpg) 8 | 9 | 建议直接添加到主屏幕(ios端体验差一些). 10 | 11 | ![](http://omrbgpqyl.bkt.clouddn.com/18-5-16/49737422.jpg) 12 | 13 | ## 前言 14 | 15 | ### 1.为什么做这个项目? 16 | 17 | 1. 学习vue全家桶,本人很长一段时间在用React。 18 | 2. 利用PWA技术来模仿原生应用,来探究PWA与原生的差异。 19 | 3. 作者声称2.5之后vue增强了对TS的支持,探究TS在vue中的支持情况。 20 | 4. github上缺乏Typescript编写的vue项目,个别项目也不是完整的,这可能是第一款。 21 | 22 | ### 2.那么为什么模仿一款"习惯养成APP"而不是饿了么、美团、头条、xx音乐等著名APP? 23 | 24 | 1. 原生APP与WebApp最大的区别就是离线能力,我们希望做一款以离线能力为主的app,这种类型的app绝大多数都是工具类的,例如番茄闹钟、效率工具等等,诸如美团、头条这种app离线场景价值有限(离线怎么点餐看新闻啊?缓存里的应该叫旧闻了)。 25 | 2. 其实最主要的一点是本人自制力差,仿饿了么、xx音乐的项目太多了,怕遇到困难直接抄人家源码,少了思考过程。 26 | 27 | ### 3.这个项目跟其他Vue仿饿了么和xx音乐的项目有何不同? 28 | 29 | 1. 我们全程Typescript编写,组件完全Class化,ts是构建健壮应用的必备良药,众多团队在ts化自己的项目了,而github上我找不到任何一个ts+vue的完整app,此项目可以供你学习. 30 | 31 | 2. 我们利用了pwa技术,pwa目前已经被ios支持(虽然支持得烂),所以,开花结果是迟早的事情,vue+pwa的项目也是十分有限,尤其是在vue-cli 3.0之后就没有相关的项目供参考了. 32 | 33 | ## 技术栈 34 | 35 | vue2.5 + Typescript + vuex + vue-router 36 | 37 | ## 项目启动 38 | ``` 39 | git clone https://github.com/xiaomuzhu/vue-ts-daily 40 | npm i && npm run dev 41 | 42 | ``` 43 | 44 | ## 开发环境 45 | > MacOS 10.12.6 node10.0.0 46 | 47 | # 目标功能 48 | 49 | - [x] 习惯新建 -- 完成 50 | - [x] 习惯编辑 -- 完成 51 | - [x] 习惯归档 -- 完成 52 | - [x] 习惯删除 -- 完成 53 | - [x] 习惯激活 -- 完成 54 | - [x] vuex持久化 -- 完成 55 | - [x] 当日习惯展示 -- 完成 56 | - [x] 对之前习惯的补签和取消 -- 完成 57 | - [x] 默认习惯选择列表 -- 完成 58 | - [x] 习惯图标与背景颜色的编辑 -- 完成 59 | - [x] 习惯的重复日期、激励语、重复时间段的编辑-- 完成 60 | - [x] 奖励卡领取 -- 完成 61 | - [x] 不同时间段不同习惯的tab筛选 -- 完成 62 | - [x] 习惯的总天数、当前连续天数、历史最高纪录等记录逻辑 -- 完成 63 | - [x] 登录 -- 完成 64 | - [x] 反馈 -- 完成 65 | - [x] 更新日志 -- 完成 66 | - [x] 远程同步信息 -- 完成 67 | - [ ] 开启https实现pwa 68 | - [ ] 加入后台推送功能 69 | - [ ] 加入主题更换 70 | - [ ] 丰富动画效果 71 | 72 | ## 预览 73 | 74 | 习惯管理、习惯首页、设置 75 | 76 | ![](http://omrbgpqyl.bkt.clouddn.com/18-5-17/32237664.jpg) 77 | 78 | 时间段编辑、补签记录 79 | 80 | ![](http://omrbgpqyl.bkt.clouddn.com/18-5-17/30859792.jpg) 81 | 82 | 习惯库、图标设置 83 | 84 | ![](http://omrbgpqyl.bkt.clouddn.com/18-5-17/92185459.jpg) 85 | 习惯管理 86 | 87 | ![](http://omrbgpqyl.bkt.clouddn.com/18-5-15/60061652.jpg) 88 | 89 | 习惯记录 90 | 91 | ![](http://omrbgpqyl.bkt.clouddn.com/18-5-16/88199965.jpg) 92 | 93 | 新建习惯 94 | 95 | ![](http://omrbgpqyl.bkt.clouddn.com/18-5-16/36512417.jpg) 96 | 97 | 编辑习惯 98 | 99 | ![](http://omrbgpqyl.bkt.clouddn.com/18-5-16/55413901.jpg) 100 | 101 | 102 | ## 最后 103 | 104 | 本项目是还原了APP Store一个精选习惯管理app,叫"小日常"。 105 | 106 | 整体功能还原了90%以上,身为工具类的app还是以逻辑为主,有两个点比较难处理. 107 | 1. 逻辑耦合严重,例如一个习惯成功打卡或者取消打卡后,相关的连续天数、总天数、当前天数、习惯当前的ui、日历ui、弹窗逻辑全部要响应. 108 | 2. 时间处理,习惯养成工具最主要的还是要处理时间,例如日历组件,当天之后的补签是不能响应的,因此需要做一个时间上的判断,而补签之前的相关连续记录要做改变,这个时候需要计算这个补签是否改变了连续的记录,其中又得涉及时间的处理,整个逻辑就是处理跟时间的关系. 109 | 110 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'ts', 4 | 'tsx', 5 | 'js', 6 | 'jsx', 7 | 'json', 8 | 'vue' 9 | ], 10 | transform: { 11 | '^.+\\.vue$': 'vue-jest', 12 | '^.+\\.tsx?$': 'ts-jest' 13 | }, 14 | moduleNameMapper: { 15 | '^@/(.*)$': '/src/$1' 16 | }, 17 | snapshotSerializers: [ 18 | 'jest-serializer-vue' 19 | ] 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daily", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vue-cli-service serve --open", 7 | "build": "vue-cli-service build && workbox injectManifest", 8 | "test": "vue-cli-service test", 9 | "e2e": "vue-cli-service e2e", 10 | "lint": "vue-cli-service lint", 11 | "serve": "serve dist/", 12 | "commit": "git-cz", 13 | "prettier": "prettier --config ./.prettierrc --write 'src/**/*.ts' 'src/**/*.vue' " 14 | }, 15 | "config": { 16 | "commitizen": { 17 | "path": "node_modules/cz-conventional-changelog" 18 | } 19 | }, 20 | "husky": { 21 | "hooks": { 22 | "commit-msg": "commitlint -e $GIT_PARAMS", 23 | "pre-commit": "lint-staged" 24 | } 25 | }, 26 | "lint-staged": { 27 | "src/**/*.{ts,vue}": [ 28 | "prettier --write", 29 | "tslint --fix", 30 | "git add" 31 | ] 32 | }, 33 | "dependencies": { 34 | "@xunlei/vue-lazy-component": "^1.1.3", 35 | "axios": "^0.18.0", 36 | "circular-json": "^0.5.9", 37 | "fastclick": "^1.0.6", 38 | "moment": "^2.22.2", 39 | "normalize.css": "^8.0.1", 40 | "register-service-worker": "^1.5.2", 41 | "vant": "^1.4.7", 42 | "vue": "^2.5.17", 43 | "vue-class-component": "^6.3.2", 44 | "vue-event-calendar-pro": "^0.0.5", 45 | "vue-icon-font-pro": "^1.0.1", 46 | "vue-property-decorator": "^7.2.0", 47 | "vue-router": "^3.0.2", 48 | "vue-skeleton-loading": "^1.0.2", 49 | "vue2-animate": "^2.1.0", 50 | "vuex": "^3.0.1", 51 | "vuex-class": "^0.3.1", 52 | "vuex-persist": "^2.0.0" 53 | }, 54 | "devDependencies": { 55 | "@commitlint/cli": "^7.2.1", 56 | "@commitlint/config-conventional": "^7.1.2", 57 | "@types/better-scroll": "^1.12.1", 58 | "@types/jest": "^23.3.10", 59 | "@vue/cli-plugin-babel": "^3.2.0", 60 | "@vue/cli-plugin-e2e-nightwatch": "^3.2.0", 61 | "@vue/cli-plugin-pwa": "^3.2.0", 62 | "@vue/cli-plugin-typescript": "^3.2.0", 63 | "@vue/cli-plugin-unit-jest": "^3.2.0", 64 | "@vue/cli-service": "^3.2.0", 65 | "@vue/test-utils": "^1.0.0-beta.26", 66 | "babel-core": "^7.0.0-0", 67 | "babel-plugin-import": "^1.11.0", 68 | "babel-plugin-lodash": "^3.3.4", 69 | "babel-plugin-transform-runtime": "^6.23.0", 70 | "commitizen": "^3.0.5", 71 | "glob-all": "^3.1.0", 72 | "husky": "^1.2.0", 73 | "lint-staged": "^8.1.0", 74 | "node-sass": "^4.10.0", 75 | "prerender-spa-plugin": "^3.4.0", 76 | "prettier": "^1.15.3", 77 | "purgecss-webpack-plugin": "^1.4.0", 78 | "sass-loader": "^7.1.0", 79 | "serve": "^10.1.1", 80 | "ts-jest": "^23.10.5", 81 | "typescript": "^3.2.1", 82 | "vue-template-compiler": "^2.5.17", 83 | "workbox-cli": "^3.6.3", 84 | "workbox-webpack-plugin": "^3.6.3" 85 | }, 86 | "browserslist": [ 87 | "> 1%", 88 | "last 2 versions", 89 | "not ie <= 8" 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/public/favicon.ico -------------------------------------------------------------------------------- /public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 小日常 11 | 16 | 17 | 18 | 19 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "小日常--一个简洁的习惯管理工具", 3 | "short_name": "小日常", 4 | "icons": [{ 5 | "src": "/img/icons/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/img/icons/android-chrome-512x512.png", 11 | "sizes": "512x512", 12 | "type": "image/png" 13 | } 14 | ], 15 | "start_url": "/", 16 | "display": "standalone", 17 | "background_color": "#fff", 18 | "theme_color": "#ffb95c", 19 | "gcm_sender_id": "my_gcm_sender_id", 20 | "gcm_user_visible_only": true 21 | } -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.6.1/workbox-sw.js'); 2 | 3 | workbox.loadModule('workbox-strategies'); 4 | 5 | const cacheVersion = '20181225v1' 6 | const staticCacheName = 'static' + cacheVersion 7 | const staticAssetsCacheName = '/' + cacheVersion 8 | const vendorCacheName = 'verdor' + cacheVersion 9 | const contentCacheName = 'content' + cacheVersion 10 | const maxEntries = 100 11 | 12 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 13 | 14 | workbox.precaching.suppressWarnings(); 15 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 16 | 17 | if (workbox) { 18 | console.log(`Workbox is loaded`); 19 | workbox.precaching.precacheAndRoute(self.__precacheManifest); 20 | } 21 | else { 22 | console.log(`Workbox didn't load`); 23 | } 24 | 25 | // workbox.routing.registerRoute( 26 | // new RegExp('\\.css$'), 27 | // workbox.strategies.cacheFirst() 28 | // ); 29 | 30 | 31 | workbox.precaching.precacheAndRoute([]); -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | 27 | 62 | -------------------------------------------------------------------------------- /src/api/feedback.ts: -------------------------------------------------------------------------------- 1 | import { _post } from './index'; 2 | 3 | // 反馈 4 | export const feedback = (data: any) => { 5 | const req = { 6 | data, 7 | url: 'feedback', 8 | }; 9 | return _post(req); 10 | }; 11 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import * as Axios from 'axios'; 2 | 3 | import config from '@/config'; 4 | import router from '@/router'; 5 | import { removeInfo } from '@/utils'; 6 | 7 | const baseURL = config.url.basicUrl; 8 | const axios = Axios.default.create({ 9 | baseURL, // api请求的baseURL 10 | timeout: 0, 11 | // withCredentials: true, // 允许跨域 cookie 12 | headers: { 'X-Requested-With': 'XMLHttpRequest' }, 13 | maxContentLength: 2000, 14 | transformResponse: [ 15 | data => { 16 | try { 17 | data = JSON.parse(data); 18 | } catch (e) { 19 | data = {}; 20 | } 21 | if (data.status === 403) { 22 | removeInfo(); 23 | router.push('/login'); 24 | } 25 | return data; 26 | }, 27 | ], 28 | }); 29 | 30 | // get 31 | export const _get = (req: any) => { 32 | return axios.get(req.url, { params: req.data }); 33 | }; 34 | 35 | // post 36 | export const _post = (req: any) => { 37 | return axios({ method: 'post', url: `/${req.url}`, data: req.data }); 38 | }; 39 | 40 | // patch 41 | export const _put = (req: any) => { 42 | return axios({ method: 'put', url: `/${req.url}`, data: req.data }); 43 | }; 44 | 45 | // delete 46 | export const _delete = (req: any) => { 47 | return axios({ method: 'delete', url: `/${req.url}`, data: req.data }); 48 | }; 49 | -------------------------------------------------------------------------------- /src/api/sync.ts: -------------------------------------------------------------------------------- 1 | import { _post } from './index'; 2 | 3 | // 同步 4 | export const sync = (data: any) => { 5 | console.log(data); 6 | const req = { 7 | data, 8 | url: 'sync', 9 | }; 10 | return _post(req); 11 | }; 12 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import { _get, _post, _delete } from './index'; 2 | 3 | // 登录 4 | export const login = (data: any) => { 5 | const req = { 6 | data, 7 | url: 'admin/user_login', 8 | }; 9 | return _post(req); 10 | }; 11 | 12 | // 获取用户信息 13 | export const userInfo = (data: any) => { 14 | const req = { 15 | data, 16 | url: 'admin/user_info', 17 | }; 18 | return _get(req); 19 | }; 20 | 21 | // 改变用户头像 22 | export const changeAvatar = (data: any) => { 23 | const req = { 24 | data, 25 | url: 'admin/change_avatar', 26 | }; 27 | return _post(req); 28 | }; 29 | -------------------------------------------------------------------------------- /src/assets/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/src/assets/404.jpg -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 24 | 25 | 48 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 88 | 89 | 106 | -------------------------------------------------------------------------------- /src/components/common/Card/Card.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | 26 | 57 | -------------------------------------------------------------------------------- /src/components/common/Circle/Circle.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | 35 | 46 | -------------------------------------------------------------------------------- /src/components/common/Circle/style.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaomuzhu/vue-ts-daily/b2e32473fe4a612a9dd1f41ed9a4298eeacf28f8/src/components/common/Circle/style.scss -------------------------------------------------------------------------------- /src/components/common/ClockPopup/ClockPopup.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 49 | 50 | 52 | -------------------------------------------------------------------------------- /src/components/common/ClockPopup/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/mixin'; 2 | .van-popup { 3 | border: solid 0.1rem; 4 | @include borderRadius(3%); 5 | .clock { 6 | width: 60vw; 7 | height: 55vh; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: space-between; 11 | align-items: center; 12 | p { 13 | border: solid 0.1rem; 14 | width: 60%; 15 | height: 1.5rem; 16 | margin: 0; 17 | background-color: hsl(60, 97%, 65%); 18 | } 19 | svg { 20 | width: 2.5rem; 21 | height: 2.5rem; 22 | } 23 | .van-field { 24 | height: 50%; 25 | background-color: transparent; 26 | width: 100%; 27 | } 28 | div { 29 | width: 70%; 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: center; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/components/common/DateBlock/DateBlock.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 19 | -------------------------------------------------------------------------------- /src/components/common/DateBlock/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/mixin'; 2 | .checked { 3 | background-color: $green; 4 | } 5 | 6 | div { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | border: solid 0.05rem; 11 | @include borderRadius(5%); 12 | width: 6rem; 13 | height: 6rem; 14 | @include font(1rem); 15 | } -------------------------------------------------------------------------------- /src/components/common/HabitList/List.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 44 | 45 | 46 | 48 | -------------------------------------------------------------------------------- /src/components/common/HabitList/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/mixin'; 2 | .habit { 3 | box-sizing: border-box; 4 | margin: 1.5rem 0; 5 | aside { 6 | height: 100%; 7 | background-color: $warn; 8 | width: 3.5rem; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | color: $font-o; 13 | font-weight: bold; 14 | } 15 | .edit { 16 | background-color: $edit; 17 | } 18 | } 19 | 20 | .habitList { 21 | margin: 0.5rem 0; 22 | } 23 | 24 | .listGroup { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | } 29 | 30 | .listCell { 31 | border: solid; 32 | width: 95%; 33 | border-width: 0.08rem; 34 | justify-content: space-between; 35 | align-items: center; 36 | @include font(1.2rem, 1rem); 37 | svg { 38 | width: 2rem; 39 | height: 2rem; 40 | margin-right: 1rem; 41 | } 42 | span { 43 | display: inline-block; 44 | transform: translateY(-35%); 45 | @include font(1.2rem, 1rem); 46 | } 47 | } -------------------------------------------------------------------------------- /src/components/common/Icon/FooterIcon.ts: -------------------------------------------------------------------------------- 1 | import { Component, Prop, Vue, Emit } from 'vue-property-decorator'; 2 | import { Mutation } from 'vuex-class'; 3 | 4 | import { PageInfo } from '@/store/state'; 5 | import template from './Icon.vue'; 6 | 7 | @Component({ 8 | name: 'FooterIcon', 9 | mixins: [template], 10 | }) 11 | export default class FooterIcon extends Vue { 12 | @Prop() private name!: object; 13 | @Prop() private path!: string; 14 | @Prop() private id!: number; 15 | @Prop() private isActived!: boolean; 16 | @Prop() private tagName!: string; 17 | @Mutation private getActivePage!: (pageName: number) => void; 18 | @Mutation private changeHeaderState!: (pageName: number) => void; 19 | 20 | private changeActivePage() { 21 | const id = this.id; 22 | 23 | if (!this.isActived) { 24 | this.getActivePage(id); 25 | this.changeHeaderState(id); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/common/Icon/HeaderIcon.ts: -------------------------------------------------------------------------------- 1 | import { Component, Prop, Vue } from 'vue-property-decorator'; 2 | import template from './Icon.vue'; 3 | 4 | @Component({ 5 | name: 'HeaderIcon', 6 | mixins: [template], 7 | }) 8 | export default class FooterIcon extends Vue { 9 | @Prop() private name!: string; 10 | @Prop() private path!: string; 11 | 12 | private data() { 13 | return { 14 | isTouched: false, 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/common/Icon/Icon.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 18 | -------------------------------------------------------------------------------- /src/components/common/Icon/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/mixin'; 2 | p { 3 | font-size: 50%; 4 | margin: 0.2rem 0 0 0; 5 | color: grey; 6 | } 7 | 8 | .headerIcon { 9 | margin: 0 1rem 0; 10 | } 11 | 12 | .active { 13 | color: $font; 14 | } 15 | 16 | .router-link-active { 17 | text-decoration: none; 18 | } 19 | 20 | a { 21 | text-decoration: none; 22 | box-sizing: border-box 23 | } -------------------------------------------------------------------------------- /src/components/common/Popup/Popup.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 41 | 42 | 44 | -------------------------------------------------------------------------------- /src/components/common/Popup/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/mixin'; 2 | .van-popup { 3 | @include wh(100%, 100%); 4 | main { 5 | @include wh(100%, 100%); 6 | } 7 | } 8 | 9 | .van-nav-bar { 10 | height: 3.5rem; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | font-family: 'Microsoft YaHei'; 15 | svg { 16 | @include iconSize(1.4rem); 17 | } 18 | } -------------------------------------------------------------------------------- /src/components/common/Skeleton/SkeletonCircle.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 19 | -------------------------------------------------------------------------------- /src/components/common/Skeleton/SkeletonList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 30 | 31 | 35 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | navTitle: { 3 | habit: { 4 | habitLibrary: '习惯库', 5 | newHabit: '新的习惯', 6 | }, 7 | }, 8 | newHabit: { 9 | id: 0, 10 | name: 'diy', 11 | title: '自定义习惯', 12 | }, 13 | habitLibrary: [ 14 | { 15 | id: 1, 16 | name: 'yingtao', 17 | title: '吃水果', 18 | }, 19 | { 20 | id: 2, 21 | name: 'gongshangyinhang', 22 | title: '记账', 23 | }, 24 | { 25 | id: 3, 26 | name: 'yinliao', 27 | title: '按时喝水', 28 | }, 29 | { 30 | id: 4, 31 | name: 'tang', 32 | title: '煲汤', 33 | }, 34 | { 35 | id: 5, 36 | name: 'pijiu', 37 | title: '不喝酒', 38 | }, 39 | { 40 | id: 6, 41 | name: 'pisa', 42 | title: '做西餐', 43 | }, 44 | { 45 | id: 7, 46 | name: 'hanbao', 47 | title: '不吃垃圾食品', 48 | }, 49 | { 50 | id: 8, 51 | name: 'lanqiu', 52 | title: '打篮球', 53 | }, 54 | { 55 | id: 9, 56 | name: 'zuqiu', 57 | title: '踢足球', 58 | }, 59 | { 60 | id: 10, 61 | name: 'yumaoqiu', 62 | title: '打羽毛球', 63 | }, 64 | { 65 | id: 11, 66 | name: 'yingyu', 67 | title: '学英语', 68 | }, 69 | { 70 | id: 12, 71 | name: 'tiyu', 72 | title: '做运动', 73 | }, 74 | { 75 | id: 13, 76 | name: 'yinle', 77 | title: '唱歌', 78 | }, 79 | { 80 | id: 14, 81 | name: 'jingji', 82 | title: '股票', 83 | }, 84 | { 85 | id: 15, 86 | name: 'wudao', 87 | title: '跳舞', 88 | }, 89 | { 90 | id: 16, 91 | name: 'taolun', 92 | title: '开会', 93 | }, 94 | { 95 | id: 17, 96 | name: 'liyi', 97 | title: '保持仪表', 98 | }, 99 | { 100 | id: 18, 101 | name: 'shuxue', 102 | title: '学习数学', 103 | }, 104 | { 105 | id: 19, 106 | name: 'xiaoche', 107 | title: '低碳出行', 108 | }, 109 | { 110 | id: 20, 111 | name: 'zihangche', 112 | title: '骑自行车', 113 | }, 114 | { 115 | id: 21, 116 | name: 'zixi', 117 | title: '自习', 118 | }, 119 | { 120 | id: 22, 121 | name: 'xiezuo', 122 | title: '写作', 123 | }, 124 | ], 125 | 126 | iconSetting: [ 127 | 'yingtao', 128 | 'gongshangyinhang', 129 | 'yinliao', 130 | 'tang', 131 | 'pijiu', 132 | 'pisa', 133 | 'hanbao', 134 | 'lanqiu', 135 | 'zuqiu', 136 | 'yumaoqiu', 137 | 'yingyu', 138 | 'tiyu', 139 | 'yinle', 140 | 'jingji', 141 | 'wudao', 142 | 'taolun', 143 | 'liyi', 144 | 'shuxue', 145 | 'xiaoche', 146 | 'zihangche', 147 | 'zixi', 148 | 'xiezuo', 149 | 'handaccount', 150 | 'miao1', 151 | 'miao', 152 | 'juice', 153 | 'yue-pin', 154 | 'yue-pin', 155 | 'yin-yang', 156 | 'kite', 157 | 'zhuyishixiang', 158 | 'tianqi', 159 | 'feijihangban', 160 | '-cunqianguan', 161 | '-qianbao', 162 | '-baoxiangui', 163 | '-biji', 164 | '-diannao', 165 | '-gouwuche', 166 | 'guitar', 167 | 'plant', 168 | 'shenghuo', 169 | 'tuokouxiu', 170 | 'donghua', 171 | 'dianying', 172 | 'zhibo', 173 | 'wuli', 174 | ], 175 | colorSetting: [ 176 | '#DDDDDD', 177 | ' #AAAAAA', 178 | '#888888', 179 | '#666666', 180 | '#444444', 181 | '#000000', 182 | '#FFB7DD', 183 | '#FF88C2', 184 | '#FF44AA', 185 | '#FF0088', 186 | '#C10066', 187 | '#A20055', 188 | '#8C0044', 189 | '#FFCCCC', 190 | '#FF8888', 191 | '#FF3333', 192 | '#FF0000', 193 | '#CC0000', 194 | '#AA0000', 195 | '#880000', 196 | '#FFC8B4', 197 | '#FFA488', 198 | '#FF7744', 199 | '#FF5511', 200 | '#E63F00', 201 | '#C63300', 202 | '#A42D00', 203 | '#FFDDAA', 204 | '#FFBB66', 205 | '#FFAA33', 206 | '#FF8800', 207 | '#EE7700', 208 | '#CC6600', 209 | '#BB5500', 210 | '#FFEE99', 211 | '#FFDD55', 212 | '#FFCC22', 213 | '#FFBB00', 214 | '#DDAA00', 215 | '#AA7700', 216 | '#886600', 217 | '#FFFFBB', 218 | '#FFFF77', 219 | '#FFFF33', 220 | '#FFFF00', 221 | '#EEEE00', 222 | '#BBBB00', 223 | '#888800', 224 | '#EEFFBB', 225 | '#DDFF77', 226 | '#CCFF33', 227 | '#BBFF00', 228 | '#99DD00', 229 | '#88AA00', 230 | '#668800', 231 | '#CCFF99', 232 | '#BBFF66', 233 | '#99FF33', 234 | '#77FF00', 235 | '#66DD00', 236 | '#55AA00', 237 | '#227700', 238 | '#99FF99', 239 | '#66FF66', 240 | '#33FF33', 241 | '#00FF00', 242 | '#00DD00', 243 | '#00AA00', 244 | '#008800', 245 | '#BBFFEE', 246 | '#77FFCC', 247 | '#33FFAA', 248 | '#00FF99', 249 | '#00DD77', 250 | '#00AA55', 251 | '#008844', 252 | '#AAFFEE', 253 | '#77FFEE', 254 | '#33FFDD', 255 | '#00FFCC', 256 | '#00DDAA', 257 | '#00AA88', 258 | '#008866', 259 | '#99FFFF', 260 | '#66FFFF', 261 | '#33FFFF', 262 | '#00FFFF', 263 | '#00DDDD', 264 | '#00AAAA', 265 | '#008888', 266 | '#CCEEFF', 267 | '#77DDFF', 268 | '#33CCFF', 269 | '#00BBFF', 270 | '#009FCC', 271 | '#0088A8', 272 | '#007799', 273 | '#CCDDFF', 274 | '#99BBFF', 275 | '#5599FF', 276 | '#0066FF', 277 | '#0044BB', 278 | '#003C9D', 279 | '#003377', 280 | '#CCCCFF', 281 | '#9999FF', 282 | '#5555FF', 283 | '#0000FF', 284 | '#0000CC', 285 | '#0000AA', 286 | '#000088', 287 | '#CCBBFF', 288 | '#9F88FF', 289 | '#7744FF', 290 | '#5500FF', 291 | '#4400CC', 292 | '#2200AA', 293 | '#220088', 294 | '#D1BBFF', 295 | '#B088FF', 296 | '#9955FF', 297 | '#7700FF', 298 | '#5500DD', 299 | '#4400B3', 300 | '#3A0088', 301 | '#E8CCFF', 302 | '#D28EFF', 303 | '#B94FFF', 304 | '#9900FF', 305 | '#7700BB', 306 | '#66009D', 307 | '#550088', 308 | '#F0BBFF', 309 | '#E38EFF', 310 | '#E93EFF', 311 | '#CC00FF', 312 | '#A500CC', 313 | '#7A0099', 314 | '#660077', 315 | '#FFB3FF', 316 | '#FF77FF', 317 | '#FF3EFF', 318 | '#FF00FF', 319 | '#CC00CC', 320 | '#990099', 321 | '#770077', 322 | ], 323 | url: { 324 | basicUrl: 325 | process.env.NODE_ENV === 'development' 326 | ? 'http://xiaomuzhu.top/api/' 327 | : 'http://xiaomuzhu.top/api/', 328 | }, 329 | }; 330 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import FastClick from 'fastclick'; 4 | import VueIconFont from 'vue-icon-font-pro'; 5 | import vueEventCalendar from 'vue-event-calendar-pro'; 6 | import VueLazyComponent from '@xunlei/vue-lazy-component'; 7 | import VueSkeletonLoading from 'vue-skeleton-loading'; 8 | 9 | import 'normalize.css'; 10 | import 'vue2-animate/dist/vue2-animate.min.css'; 11 | import 'vue-event-calendar-pro/dist/style.css'; 12 | 13 | import App from './App.vue'; 14 | import router from './router'; 15 | import store from './store'; 16 | import './registerServiceWorker'; 17 | import '@/assets/iconfont.js'; 18 | 19 | // 兼容毒瘤ios的300ms延迟问题 20 | if ('addEventListener' in document) { 21 | document.addEventListener( 22 | 'DOMContentLoaded', 23 | () => { 24 | (FastClick as any).attach(document.body); 25 | }, 26 | false, 27 | ); 28 | } 29 | 30 | Vue.use(VueLazyComponent); 31 | Vue.use(VueSkeletonLoading); 32 | Vue.use(vueEventCalendar, { locale: 'zh', weekStartOn: 1 }); 33 | Vue.use(VueIconFont); 34 | 35 | Vue.config.productionTip = false; 36 | 37 | new Vue({ 38 | router, 39 | store, 40 | render: (h) => h(App), 41 | }).$mount('#app'); 42 | -------------------------------------------------------------------------------- /src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-console */ 2 | 3 | import { register } from 'register-service-worker'; 4 | 5 | // if (process.env.NODE_ENV !== 'production') { 6 | register(`${process.env.BASE_URL}sw.js`, { 7 | ready() {}, 8 | cached() { 9 | console.log('Content has been cached for offline use.'); 10 | }, 11 | updated() { 12 | console.log('New content is available; please refresh.'); 13 | }, 14 | offline() { 15 | console.log( 16 | 'No internet connection found. App is running in offline mode.', 17 | ); 18 | }, 19 | error(error) { 20 | console.error('Error during service worker registration:', error); 21 | }, 22 | }); 23 | // } 24 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Home from './views/Home/Home.vue'; 4 | import Habit from './views/Habit/Habit.vue'; 5 | import Setting from './views/Setting/Setting.vue'; 6 | 7 | const New = (r: any) => 8 | (require as any).ensure([], () => r(require('@/views/New/New'), 'New')); // 新建 9 | 10 | const Edit = (r: any) => 11 | (require as any).ensure([], () => r(require('@/views/Edit/Edit'), 'Edit')); // 编辑 12 | 13 | const Library = (r: any) => 14 | (require as any).ensure([], () => 15 | r(require('@/views/New/Library/Library'), 'Library'), 16 | ); // 习惯库 17 | 18 | const NewHabit = (r: any) => 19 | (require as any).ensure([], () => 20 | r(require('@/views/New/Habit/Habit'), 'Habit'), 21 | ); // 新建习惯 22 | 23 | const Calendar = (r: any) => 24 | (require as any).ensure([], () => 25 | r(require('@/views/Edit/Calendar/Calendar'), 'Calendar'), 26 | ); // 日历 27 | 28 | const Times = (r: any) => 29 | (require as any).ensure([], () => 30 | r(require('@/views/Edit/Times/Times'), 'Times'), 31 | ); // 时间段 32 | 33 | const Manage = (r: any) => 34 | (require as any).ensure([], () => 35 | r(require('@/views/Edit/Manage/Manage'), 'Manage'), 36 | ); // 管理 37 | 38 | const Remind = (r: any) => 39 | (require as any).ensure([], () => 40 | r(require('@/views/Edit/Remind/Remind'), 'Remind'), 41 | ); // 提醒 42 | 43 | const IconSetting = (r: any) => 44 | (require as any).ensure([], () => 45 | r(require('@/views/Edit/IconSetting/IconSetting'), 'IconSetting'), 46 | ); // 图标设置 47 | 48 | const Recycle = (r: any) => 49 | (require as any).ensure([], () => 50 | r(require('@/views/Edit/Recycle/Recycle'), 'Recycle'), 51 | ); // 回收站 52 | 53 | const Card = (r: any) => 54 | (require as any).ensure([], () => r(require('@/views/Card/Card'), 'Card')); // 打卡 55 | 56 | const Receive = (r: any) => 57 | (require as any).ensure([], () => 58 | r(require('@/views/Card/Receive/Card'), 'Receive'), 59 | ); // 打卡 60 | 61 | const Feedback = (r: any) => 62 | (require as any).ensure([], () => 63 | r(require('@/views/Feedback/Feedback'), 'Feedback'), 64 | ); // 反馈 65 | 66 | const UpdateLog = (r: any) => 67 | (require as any).ensure([], () => 68 | r(require('@/views/UpdateLog/UpdateLog'), 'UpdateLog'), 69 | ); // 更新日志 70 | 71 | const Login = (r: any) => 72 | (require as any).ensure([], () => r(require('@/views/Login/Login'), 'Login')); // 登录 73 | 74 | const Error = (r: any) => 75 | (require as any).ensure([], () => r(require('@/views/404/error'), 'Error')); // 错误 76 | 77 | Vue.use(Router); 78 | 79 | export default new Router({ 80 | mode: 'history', 81 | routes: [ 82 | { 83 | path: '/', 84 | name: 'today', 85 | component: Home, 86 | meta: { main: true }, 87 | }, 88 | { 89 | path: '/habit', 90 | name: 'habit', 91 | component: Habit, 92 | meta: { main: true }, 93 | children: [ 94 | // { 95 | // path: 'new', 96 | // name: 'new', 97 | // component: NewHabit, 98 | // }, 99 | ], 100 | }, 101 | { 102 | path: '/setting', 103 | name: '设置', 104 | component: Setting, 105 | meta: { main: true }, 106 | }, 107 | { 108 | path: '/feedback', 109 | name: '反馈', 110 | component: Feedback, 111 | }, 112 | { 113 | path: '/update', 114 | name: '更新日志', 115 | component: UpdateLog, 116 | }, 117 | { 118 | path: '/login', 119 | name: '登录', 120 | component: Login, 121 | }, 122 | { 123 | path: '/card', 124 | name: '卡片管理', 125 | component: Card, 126 | children: [ 127 | { 128 | path: 'receive', 129 | name: '今日卡片', 130 | component: Receive, 131 | }, 132 | ], 133 | }, 134 | { 135 | path: '/new', 136 | name: '新建习惯', 137 | component: New, 138 | children: [ 139 | { 140 | path: 'library', 141 | name: '习惯库', 142 | component: Library, 143 | }, 144 | { 145 | path: 'habit', 146 | name: '习惯', 147 | component: NewHabit, 148 | }, 149 | ], 150 | }, 151 | { 152 | path: '/edit', 153 | name: '编辑习惯', 154 | component: Edit, 155 | children: [ 156 | { 157 | path: 'calendar', 158 | name: '习惯记录', 159 | component: Calendar, 160 | }, 161 | { 162 | path: 'recycle', 163 | name: '已归档习惯', 164 | component: Recycle, 165 | }, 166 | { 167 | path: 'icon', 168 | name: '图表设置', 169 | component: IconSetting, 170 | }, 171 | { 172 | path: 'times', 173 | name: '选择习惯时段', 174 | component: Times, 175 | }, 176 | { 177 | path: 'manage', 178 | name: '时段管理', 179 | component: Manage, 180 | }, 181 | { 182 | path: 'remind', 183 | name: '提醒设置', 184 | component: Remind, 185 | }, 186 | { 187 | path: 'habit', 188 | name: '编辑习惯', 189 | component: NewHabit, 190 | }, 191 | ], 192 | }, 193 | { 194 | path: '/error', 195 | name: '找不到该页面', 196 | component: Error, 197 | }, 198 | { 199 | path: '*', 200 | redirect: '/error', 201 | }, 202 | ], 203 | }); 204 | -------------------------------------------------------------------------------- /src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | 6 | declare module 'fastclick'; 7 | 8 | declare module 'vue-lazyload'; 9 | 10 | declare module 'vue-icon-font-pro'; 11 | 12 | declare module 'vue-event-calendar-pro'; 13 | declare module '@xunlei/vue-lazy-component'; 14 | declare module 'vue-skeleton-loading'; -------------------------------------------------------------------------------- /src/store/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree } from 'vuex'; 2 | import axios from 'axios'; 3 | 4 | import config from '@/config'; 5 | import { login } from '@/api/user'; 6 | import { sync } from '@/api/sync'; 7 | 8 | const actions: ActionTree = { 9 | // 发起登录 10 | async login({ state, commit }, data) { 11 | const res: Ajax.AjaxResponse = await login(data) 12 | .then(res => res.data) 13 | .catch((e: string) => console.error(e)); 14 | if (res) { 15 | commit('loginSuccess', res); 16 | } 17 | }, 18 | 19 | // 数据同步 20 | async sync({ state, commit }, data) { 21 | const res: Ajax.AjaxResponse = await sync(data) 22 | .then(res => res.data) 23 | .catch((e: string) => console.error(e)); 24 | if (res) { 25 | commit('sync', 1); 26 | } 27 | }, 28 | }; 29 | 30 | export default actions; 31 | -------------------------------------------------------------------------------- /src/store/getters.ts: -------------------------------------------------------------------------------- 1 | import { GetterTree } from 'vuex'; 2 | 3 | const getters: GetterTree = { 4 | syncData(state) { 5 | const { activePage, headerInfo, card, habitList, today, setting } = state; 6 | return { 7 | activePage, 8 | headerInfo, 9 | card, 10 | habitList, 11 | today, 12 | setting, 13 | }; 14 | }, 15 | }; 16 | 17 | export default getters; 18 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import * as Vuex from 'vuex'; 3 | import VuexPersistence from 'vuex-persist'; 4 | 5 | import state, { State } from './state'; 6 | import mutations from './mutations'; 7 | import actions from './actions'; 8 | import getters from './getters'; 9 | 10 | Vue.use(Vuex); 11 | 12 | const vuexLocal = new VuexPersistence({ 13 | storage: window.localStorage, 14 | }); 15 | 16 | export default new Vuex.Store({ 17 | state, 18 | mutations, 19 | actions, 20 | getters, 21 | plugins: [vuexLocal.plugin], 22 | }); 23 | -------------------------------------------------------------------------------- /src/store/mutations.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { State, HabitList } from './state'; 4 | import config from '@/config'; 5 | import _ from '@/utils'; 6 | 7 | export default { 8 | // 切换活动图标的状态 9 | getActivePage(state: State, id: number) { 10 | state.activePage.map(item => { 11 | // 将当前活动的页脚图表点亮 12 | if (item.id !== id) { 13 | item.isActived = false; 14 | } else { 15 | item.isActived = true; 16 | } 17 | }); 18 | }, 19 | 20 | // 切换header上图标 21 | changeHeaderState(state: State, id: number) { 22 | const { headerInfo } = state; 23 | switch (id) { 24 | case 0: 25 | headerInfo.left = 'letter'; 26 | headerInfo.right = ''; // filter 27 | headerInfo.title = 'TODAY'; 28 | break; 29 | case 1: 30 | headerInfo.left = 'file'; 31 | headerInfo.right = 'new'; 32 | headerInfo.title = '我的习惯'; 33 | break; 34 | case 2: 35 | headerInfo.left = ''; 36 | headerInfo.right = ''; // skin 37 | headerInfo.title = '设置'; 38 | break; 39 | } 40 | }, 41 | 42 | // 创建习惯 43 | createHabit(state: State, habit: HabitList) { 44 | state.habitList.push(habit); 45 | }, 46 | // 删除未定义好的习惯 47 | RemoveHabit(state: State) { 48 | state.habitList.pop(); 49 | }, 50 | // 选择执行的星期 51 | selectDate(state: State, payload: { habitId: number; id: number }) { 52 | const list = state.habitList; 53 | const len = list.length; 54 | const { RepeatingDate } = _.find(list, payload.habitId)!.habitInfo; 55 | 56 | (RepeatingDate as any[]).forEach(element => { 57 | if (element.id === payload.id) { 58 | element.checked = !element.checked; 59 | } 60 | }); 61 | }, 62 | // 切换练习的时间段 63 | changeTimes(state: State, payload: { habitId: number; id: number }) { 64 | const list = state.habitList; 65 | const habit = _.find(list, payload.habitId); 66 | 67 | habit!.habitInfo.activeTimes = payload.id; 68 | }, 69 | // 选择图标背景 70 | selectColor(state: State, payload: { id: number; color: string }) { 71 | const list = state.habitList; 72 | const habit = _.find(list, payload.id); 73 | 74 | habit!.color = payload.color; 75 | }, 76 | // 选择图标 77 | selectIcon(state: State, payload: { id: number; icon: string }) { 78 | const list = state.habitList; 79 | const habit = _.find(list, payload.id); 80 | 81 | habit!.iconName = payload.icon; 82 | }, 83 | // 切换提醒时间 84 | switchRemind(state: State, payload: { habitId: number; id: number }) { 85 | const list = state.habitList; 86 | const { remind } = _.find(list, payload.habitId)!.habitInfo; 87 | (remind as any[]).forEach(item => { 88 | if (item.id === payload.id) { 89 | item.isOpen = !item.isOpen; 90 | } 91 | }); 92 | }, 93 | // 习惯名称 94 | changeName(state: State, payload: { id: number; value: string }) { 95 | const list = state.habitList; 96 | const habit = _.find(list, payload.id); 97 | habit!.habitInfo.habitName = payload.value; 98 | }, 99 | // 绑定激励的话 100 | changInspire(state: State, payload: { id: number; value: string }) { 101 | const list = state.habitList; 102 | const habit = _.find(list, payload.id); 103 | habit!.habitInfo.inspire = payload.value; 104 | }, 105 | // 切换习惯当前的状态 106 | changeMode(state: State, payload: { id: number; value: string }) { 107 | const list = state.habitList; 108 | const habit = _.find(list, payload.id); 109 | habit!.isActive = true; 110 | habit!.mode = payload.value; 111 | }, 112 | // 将此习惯归档 113 | deleteHabit(state: State, id: number) { 114 | const list = state.habitList; 115 | const habit = _.find(list, id); 116 | habit!.isActive = false; 117 | }, 118 | // 删除此习惯 119 | removeHabit(state: State, id: number) { 120 | const list: HabitList[] = state.habitList; 121 | state.habitList = list.filter(item => item.id !== id); 122 | }, 123 | // 重新激活此习惯 124 | activateHabit(state: State, id: number) { 125 | const list = state.habitList; 126 | const habit = _.find(list, id); 127 | habit!.isActive = true; 128 | }, 129 | // 获取需要当天执行的习惯 130 | changeCollapse(state: State, activeNames: number[] | never[]) { 131 | const today = state.today; 132 | today.active = activeNames; 133 | }, 134 | // 未添加当日任务的习惯列表进行更新 135 | updateHabits(state: State, updateList: number[]) { 136 | const today = moment(); 137 | const newId = _.getDaysId(); 138 | const list = state.habitList; 139 | for (let index = 0; index < updateList.length; index++) { 140 | const id = updateList[index]; 141 | const habit = _.find(list, id); 142 | 143 | habit!.habitLog.date.push({ 144 | id: newId, 145 | time: today, 146 | isFinished: false, 147 | message: '', 148 | }); 149 | } 150 | }, 151 | // 对习惯的打卡信息进行补签 152 | supplementHabits(state: State, payload: { id: number; daysId: number }) { 153 | const list = state.habitList; 154 | const today = _.getMoment(payload.daysId); 155 | const habit = _.find(list, payload.id); 156 | // 储存date信息的数组 157 | const dateList = habit!.habitLog.date; 158 | if (dateList.length > 0) { 159 | for (let index = 0; index < dateList.length; index++) { 160 | const element = dateList[index]; 161 | if (element.id > payload.daysId) { 162 | dateList.splice(index, 0, { 163 | id: payload.daysId, 164 | time: today, 165 | isFinished: true, 166 | message: '', 167 | }); 168 | habit!.habitLog.currentConsecutiveDays = _.getCurrentMaxDays( 169 | dateList, 170 | ); 171 | 172 | habit!.habitLog.totalHabitDays++; 173 | console.log(_.getMaxDays(dateList)); 174 | 175 | habit!.habitLog.mostConsecutiveDays = _.getMaxDays(dateList); 176 | return; 177 | } 178 | } 179 | } else { 180 | dateList.push({ 181 | id: payload.daysId, 182 | time: today, 183 | isFinished: true, 184 | message: '', 185 | }); 186 | } 187 | }, 188 | // 切换当前习惯是否完成 189 | changeFinished(state: State, payload: { id: number; daysId: number }) { 190 | const list = state.habitList; 191 | const habit = _.find(list, payload.id); 192 | // 储存date信息的数组 193 | const dateList = habit!.habitLog.date; 194 | const len = dateList.length; 195 | // 找到id相关信息 196 | const date = dateList.find(item => item.id === payload.daysId); 197 | // 切换完成状态 198 | date!.isFinished = !date!.isFinished; 199 | 200 | // 当当前信息被切换成"已完成" 201 | if (date!.isFinished) { 202 | // 当当前打卡信息属于当天的时候 203 | if (dateList[len - 1].id === payload.daysId) { 204 | habit!.habitLog.currentConsecutiveDays++; 205 | } else { 206 | habit!.habitLog.currentConsecutiveDays = _.getCurrentMaxDays(dateList); 207 | } 208 | habit!.habitLog.totalHabitDays++; 209 | } else { 210 | // 当当前打卡信息属于当天的时候 211 | if (dateList[len - 1].id === payload.daysId) { 212 | habit!.habitLog.currentConsecutiveDays--; 213 | } else { 214 | habit!.habitLog.currentConsecutiveDays = _.getCurrentMaxDays(dateList); 215 | } 216 | habit!.habitLog.totalHabitDays--; 217 | date!.message = ''; 218 | } 219 | habit!.habitLog.mostConsecutiveDays = _.getMaxDays(dateList); 220 | }, 221 | // 储存打卡日志 222 | saveLog( 223 | state: State, 224 | payload: { id: number; daysId: number; message: string }, 225 | ) { 226 | const list = state.habitList; 227 | const habit = _.find(list, payload.id); 228 | const day = habit!.habitLog.date.find(item => item.id === payload.daysId); 229 | day!.message = payload.message; 230 | }, 231 | 232 | // 领取卡片 233 | receiveCard(state: State) { 234 | const today = moment(); 235 | // @ts-ignore 236 | state.today.finishedDate.push(today); 237 | state.today.isReceived = true; 238 | }, 239 | // 登陆成功后执行 240 | loginLoading(state: State, data: any) { 241 | state.user!.isLogin = 0; 242 | }, 243 | // 登陆成功后执行 244 | loginSuccess(state: State, data: any) { 245 | const currentState = JSON.parse(data.content); 246 | 247 | state.activePage = currentState.activePage; 248 | state.headerInfo = currentState.headerInfo; 249 | state.card = currentState.card; 250 | state.habitList = currentState.habitList; 251 | state.today = currentState.today; 252 | state.setting = currentState.setting; 253 | 254 | state.user!.id = data.id; 255 | state.user!.username = data.username; 256 | state.user!.url = data.url; 257 | state.user!.isLogin = 1; 258 | }, 259 | // 退出登录 260 | logoutSuccess(state: State) { 261 | state.user!.id = null; 262 | state.user!.username = ''; 263 | state.user!.url = 264 | 'https://is4-ssl.mzstatic.com/image/thumb/Purple71/v4/be/13/06/be1306d8-e343-2adb-2b04-9a6884300499/pr_source.jpg/1200x630bb.jpg'; 265 | state.user!.isLogin = -1; 266 | }, 267 | // 是否开启整点报时 268 | changeHourly(state: State, checked: boolean) { 269 | state.setting.checked = checked; 270 | }, 271 | // 是否同步成功 272 | sync(state: State, isSync: number) { 273 | state.user!.isSync = isSync; 274 | }, 275 | }; 276 | -------------------------------------------------------------------------------- /src/store/state.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | export interface ClockLog { 3 | id: number; 4 | time?: moment.Moment; 5 | isFinished: boolean; 6 | message?: string; 7 | } 8 | export interface UserState { 9 | username: string | undefined; 10 | id: number | null; 11 | createdTime: string | undefined; 12 | url: string; 13 | isLogin: number; 14 | isSync: number; 15 | } 16 | 17 | export interface SettingState { 18 | checked: boolean; 19 | url: string; 20 | } 21 | 22 | export interface TimeSlotList { 23 | id: number; 24 | title: string; 25 | } 26 | 27 | export interface RemindState { 28 | id: number; 29 | remind: string; 30 | isOpen: boolean; 31 | } 32 | 33 | export interface RepeatingDateState { 34 | id: number; 35 | date: string; 36 | checked: boolean; 37 | } 38 | 39 | // 单个习惯的状态信息 40 | export interface HabitList { 41 | id: number; 42 | iconName: string; 43 | color: string; 44 | mode: string; 45 | // 是否可用,否则是被归档了 46 | isActive: boolean; 47 | // 关于习惯的基本信息 48 | habitInfo: { 49 | // 习惯名称 50 | habitName: string; 51 | // 重复练习的日期 52 | RepeatingDate: RepeatingDateState[] | never[]; 53 | // 练习的时间段 54 | activeTimes: number; 55 | timeSlotList: TimeSlotList[] | never[]; 56 | // 提醒的时间 57 | remind: RemindState[] | never[]; 58 | // 激励自己的话 59 | inspire: string; 60 | }; 61 | // 习惯日志 62 | habitLog: { 63 | // 总共坚持练习了多少天 64 | totalHabitDays: number; 65 | // 当前连续联系了多少天 66 | currentConsecutiveDays: number; 67 | // 历史上最多连续练习多少天 68 | mostConsecutiveDays: number; 69 | // 创建日期 70 | createdTime: string; 71 | // 创建此习惯至今多少天 72 | totalDays: number; 73 | date: ClockLog[]; 74 | }; 75 | } 76 | 77 | export interface Card { 78 | src: string; 79 | content?: string; 80 | } 81 | export interface PageInfo { 82 | id: number; 83 | isActived: boolean; 84 | name: { 85 | defaultName: string; 86 | activedName: string; 87 | }; 88 | path: string; 89 | tagName: string; 90 | } 91 | 92 | export interface HeaderInfo { 93 | left?: string; 94 | title: string; 95 | right?: string; 96 | } 97 | 98 | export interface State { 99 | activePage: PageInfo[]; 100 | headerInfo: HeaderInfo; 101 | card: Card; 102 | habitList: HabitList[]; 103 | today: { 104 | active: string[] | never[] | number[]; 105 | finishedDate: moment.Moment[] | never[]; 106 | isReceived: boolean; 107 | }; 108 | setting: SettingState; 109 | user?: UserState; 110 | } 111 | 112 | // 初始状态 113 | const state: State = { 114 | activePage: [ 115 | { 116 | id: 0, 117 | isActived: true, 118 | name: { 119 | defaultName: 'today-o', 120 | activedName: 'today', 121 | }, 122 | path: '/', 123 | tagName: '日常', 124 | }, 125 | { 126 | id: 1, 127 | isActived: false, 128 | name: { 129 | defaultName: 'habit-o', 130 | activedName: 'habit', 131 | }, 132 | path: '/habit', 133 | tagName: '习惯', 134 | }, 135 | { 136 | id: 2, 137 | isActived: false, 138 | name: { 139 | defaultName: 'setting-o', 140 | activedName: 'setting', 141 | }, 142 | path: '/setting', 143 | tagName: '更多', 144 | }, 145 | ], 146 | headerInfo: { 147 | left: 'letter', 148 | title: 'TODAY', 149 | right: '', // filter 150 | }, 151 | today: { 152 | active: [0], 153 | finishedDate: [], 154 | isReceived: false, 155 | }, 156 | setting: { 157 | checked: false, 158 | url: 159 | 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=4216091012,4283409120&fm=27&gp=0.jpg', 160 | }, 161 | card: { 162 | src: 163 | 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-5xlxmMc1UjkLOsMSPPX9sKgNr3XuCNHCCCwI__iXCx2zftWo', 164 | content: '1', 165 | }, 166 | habitList: [ 167 | { 168 | id: 1524822339790, 169 | iconName: 'taiyang', 170 | color: '#ffe884', 171 | mode: 'done', 172 | isActive: true, 173 | habitInfo: { 174 | // 习惯名称 175 | habitName: '背单词', 176 | // 重复练习的日期 177 | RepeatingDate: [ 178 | { id: 0, date: '星期一', checked: true }, 179 | { id: 1, date: '星期二', checked: true }, 180 | { id: 2, date: '星期三', checked: true }, 181 | { id: 3, date: '星期四', checked: true }, 182 | { id: 4, date: '星期五', checked: true }, 183 | { id: 5, date: '星期六', checked: true }, 184 | { id: 6, date: '星期日', checked: true }, 185 | ], 186 | // 练习的时间段 187 | activeTimes: 0, 188 | // 目前已存在的时间段 189 | timeSlotList: [ 190 | { 191 | id: 0, 192 | title: '起床之后', 193 | }, 194 | { 195 | id: 1, 196 | title: '晨间习惯', 197 | }, 198 | { 199 | id: 2, 200 | title: '中午时分', 201 | }, 202 | { 203 | id: 3, 204 | title: '午间习惯', 205 | }, 206 | { 207 | id: 4, 208 | title: '晚间习惯', 209 | }, 210 | { 211 | id: 5, 212 | title: '睡觉之前', 213 | }, 214 | { 215 | id: 6, 216 | title: '任意时间', 217 | }, 218 | ], 219 | // 提醒的时间 220 | remind: [{ id: 0, remind: '12:00', isOpen: false }], 221 | // 激励自己的话 222 | inspire: '坚持的路上没有捷径', 223 | }, 224 | habitLog: { 225 | // 总共坚持练习了多少天 226 | totalHabitDays: 0, 227 | // 当前连续联系了多少天 228 | currentConsecutiveDays: 0, 229 | // 历史上最多连续练习多少天 230 | mostConsecutiveDays: 0, 231 | // 创建日期 232 | createdTime: '0', 233 | // 创建此习惯至今多少天 234 | totalDays: 0, 235 | // 坚持的日期 236 | date: [], 237 | }, 238 | }, 239 | ], 240 | user: { 241 | isLogin: -1, 242 | username: '', 243 | id: null, 244 | createdTime: '', 245 | isSync: -1, 246 | url: 247 | 'https://is4-ssl.mzstatic.com/image/thumb/Purple71/v4/be/13/06/be1306d8-e343-2adb-2b04-9a6884300499/pr_source.jpg/1200x630bb.jpg', 248 | }, 249 | }; 250 | 251 | export default state; 252 | -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | // export const GET_ACTIVE_PAGE = 'GET_ACTIVE_PAGE'; 2 | -------------------------------------------------------------------------------- /src/style/common.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | } -------------------------------------------------------------------------------- /src/style/mixin.scss: -------------------------------------------------------------------------------- 1 | $font: #333; 2 | $font-o: #fff; 3 | $grey: #e6e6e6; 4 | $bc: #e4e4e4; 5 | $edit:#444; 6 | $warn: #ff4149; 7 | $green: #00a267; 8 | // 背景图片地址和大小 9 | @mixin bis($url) { 10 | background-image: url($url); 11 | background-repeat: no-repeat; 12 | background-size: 100% 100%; 13 | } 14 | 15 | @mixin borderRadius($radius) { 16 | -webkit-border-radius: $radius; 17 | -moz-border-radius: $radius; 18 | -ms-border-radius: $radius; 19 | -o-border-radius: $radius; 20 | border-radius: $radius; 21 | } 22 | 23 | //定位全屏 24 | @mixin shadow { 25 | position: absolute; 26 | top: 0; 27 | right: 0; 28 | } 29 | 30 | //定位上下左右居中 31 | @mixin center { 32 | position: absolute; 33 | top: 50%; 34 | left: 50%; 35 | transform: translate(-50%, -50%); 36 | } 37 | 38 | //定位上下居中 39 | @mixin ct { 40 | position: absolute; 41 | top: 50%; 42 | transform: translateY(-50%); 43 | } 44 | 45 | //定位上下居中 46 | @mixin cl { 47 | position: absolute; 48 | left: 50%; 49 | transform: translateX(-50%); 50 | } 51 | 52 | // 图标大小 53 | @mixin iconSize($size: 1rem) { 54 | width: $size; 55 | height: $size; 56 | } 57 | 58 | //宽高 59 | @mixin wh($width, $height) { 60 | width: $width; 61 | height: $height; 62 | } 63 | 64 | //字体大小、行高、字体 65 | @mixin font($size: 1rem, $line-height: 100%, $family: 'Microsoft YaHei') { 66 | font: #{$size}/#{$line-height} $family; 67 | } 68 | 69 | //字体大小,颜色 70 | @mixin sc($size, $color) { 71 | font-size: $size; 72 | color: $color; 73 | } 74 | 75 | //flex 布局和 子元素 对其方式 76 | @mixin fj($type: space-between) { 77 | display: flex; 78 | justify-content: $type; 79 | } -------------------------------------------------------------------------------- /src/sw.js: -------------------------------------------------------------------------------- 1 | importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.6.1/workbox-sw.js'); 2 | 3 | workbox.loadModule('workbox-strategies'); 4 | 5 | const cacheVersion = '20181225v1' 6 | const staticCacheName = 'static' + cacheVersion 7 | const staticAssetsCacheName = '/' + cacheVersion 8 | const vendorCacheName = 'verdor' + cacheVersion 9 | const contentCacheName = 'content' + cacheVersion 10 | const maxEntries = 100 11 | 12 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 13 | 14 | workbox.precaching.suppressWarnings(); 15 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 16 | 17 | if (workbox) { 18 | console.log(`Workbox is loaded`); 19 | workbox.precaching.precacheAndRoute(self.__precacheManifest); 20 | } 21 | else { 22 | console.log(`Workbox didn't load`); 23 | } 24 | 25 | // workbox.routing.registerRoute( 26 | // new RegExp('\\.css$'), 27 | // workbox.strategies.cacheFirst() 28 | // ); 29 | 30 | 31 | workbox.precaching.precacheAndRoute([]); -------------------------------------------------------------------------------- /src/typings/ajax.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Ajax { 2 | // axios 返回数据 3 | export interface AxiosResponse { 4 | data: AjaxResponse; 5 | } 6 | 7 | // 请求接口数据 8 | export interface AjaxResponse { 9 | ID: number; 10 | /** 11 | * 消息 12 | * @type { string } 13 | */ 14 | message?: string; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { State, HabitList, RepeatingDateState, ClockLog } from '@/store/state'; 4 | 5 | const userInfo = 'xiaomuzhu'; 6 | 7 | export function getInfo() { 8 | return localStorage.getItem(userInfo); 9 | } 10 | 11 | export function setInfo(username: string) { 12 | return localStorage.setItem(userInfo, username); 13 | } 14 | 15 | export function removeInfo() { 16 | return localStorage.removeItem(userInfo); 17 | } 18 | 19 | function getDateList(arr: RepeatingDateState[]) { 20 | const list: string[] = []; 21 | for (let index = 0; index < arr.length; index++) { 22 | const element = arr[index]; 23 | if (element.checked) { 24 | list.push(element.date); 25 | } 26 | } 27 | return list; 28 | } 29 | 30 | // 星期与数字相互转化 31 | function transformDate(date: string | number) { 32 | const weekday = new Map([ 33 | [0, '星期日'], 34 | [1, '星期一'], 35 | [2, '星期二'], 36 | [3, '星期三'], 37 | [4, '星期四'], 38 | [5, '星期五'], 39 | [6, '星期六'], 40 | ]); 41 | 42 | if (typeof date === 'number') { 43 | return weekday.get(date); 44 | } else if (typeof date === 'string') { 45 | for (const [key, value] of weekday) { 46 | if (value === date) { 47 | return key; 48 | } 49 | } 50 | } else { 51 | return null; 52 | } 53 | } 54 | 55 | const utils = { 56 | getDate(str: string) { 57 | return str.replace(/['星期']/g, ' '); 58 | }, 59 | 60 | getStr(str: string, min: number = 2, max: number = 6) { 61 | const reg = /^[a-zA-Z0-9_\u4e00-\u9fa5]{min,max}$/; 62 | if (!reg.test(str.trim())) { 63 | return false; 64 | } else { 65 | return true; 66 | } 67 | }, 68 | getNewDate(str: string) { 69 | const newList = str.split('/'); 70 | const year = newList.shift(); 71 | newList.push(year!); 72 | return newList.join('/'); 73 | }, 74 | getDateList, 75 | transformDate, 76 | 77 | // 通过id查找相关习惯对象 78 | find(arr: HabitList[], id: number) { 79 | const index = utils.findIndex(arr, id); 80 | if (index >= 0) { 81 | return arr[index]; 82 | } 83 | }, 84 | 85 | // 通过id查找相关习惯对象的Index 86 | findIndex(arr: HabitList[], id: number) { 87 | let low = 0; 88 | let high = arr.length - 1; 89 | let mid: number; 90 | let currentId: number; 91 | while (low <= high) { 92 | mid = Math.floor(low + (high - low) / 2); 93 | currentId = arr[mid].id; 94 | if (currentId < id) { 95 | low = mid + 1; 96 | } else if (currentId > id) { 97 | high = mid - 1; 98 | } else { 99 | return mid; 100 | } 101 | } 102 | return -1; 103 | }, 104 | 105 | /** 106 | * 获取符合日期的习惯 107 | * 108 | * @param {HabitList[]} arr 存放习惯相关信息的数组 109 | * @param {number} [preDay] 默认是当天日期,如果加上数字1就是一天前(昨天) 110 | * @returns 111 | */ 112 | dateComparison(arr: HabitList[], preDay: number = 0) { 113 | let day: number; 114 | if (!preDay) { 115 | day = moment().day(); 116 | } else { 117 | day = moment() 118 | .add(preDay, 'days') 119 | .day(); 120 | } 121 | const current = transformDate(day); 122 | const currentList: HabitList[] = []; 123 | for (let index = 0; index < arr.length; index++) { 124 | const habit = arr[index]; 125 | const element = arr[index].habitInfo.RepeatingDate; 126 | // @ts-ignore 127 | if (getDateList(element).indexOf(current) >= 0) { 128 | currentList.push(habit); 129 | } 130 | } 131 | 132 | return currentList; 133 | }, 134 | 135 | // 以天为单位设置打卡的id 136 | getDaysId(time?: number) { 137 | const now = !time ? new Date().valueOf() : time; 138 | 139 | // 之所以+8 是因为得转换成天朝的东八区 140 | const hours = moment.duration(now).as('hours') + 8; 141 | return Math.floor(hours / 24); 142 | }, 143 | 144 | // 以天为单位设置打卡的id 145 | getMoment(days: number) { 146 | moment.locale('zh-cn'); 147 | return moment('1-1-1970', 'MM-DD-YYYY').add(days, 'd'); 148 | }, 149 | 150 | // 获取当天需要更新的打卡对象的index 151 | getHabitLogDateIndex(dateList: ClockLog[], days: number) { 152 | let low = 0; 153 | let high = dateList.length - 1; 154 | let mid: number; 155 | let currentId: number; 156 | while (low <= high) { 157 | mid = Math.floor(low + (high - low) / 2); 158 | currentId = dateList[mid].id; 159 | if (currentId < days) { 160 | low = mid + 1; 161 | } else if (currentId > days) { 162 | high = mid - 1; 163 | } else { 164 | return mid; 165 | } 166 | } 167 | return -1; 168 | }, 169 | 170 | // 通过id查找相关习惯对象 171 | getHabitLogDate(dateList: ClockLog[], days: number) { 172 | const index = utils.getHabitLogDateIndex(dateList, days); 173 | if (index >= 0) { 174 | return dateList[index]; 175 | } 176 | }, 177 | 178 | /** 179 | * 查找打卡信息史上最长连续打卡的长度 180 | * @param dateList ClockLog[] 储存打卡信息的数组 181 | */ 182 | getMaxDays(dateList: ClockLog[]) { 183 | const list = dateList.filter(item => item.isFinished === true); 184 | if (list.length === 0) { 185 | return 0; 186 | } 187 | if (list.length === 1) { 188 | return 1; 189 | } 190 | let max = 1; 191 | let current = 1; 192 | for (let index = 0; index < list.length; index++) { 193 | const element = list[index]; 194 | const next = list[index + 1]; 195 | if (next && element.id + 1 === next.id) { 196 | current++; 197 | max = Math.max(max, current); 198 | } else { 199 | current = 1; 200 | } 201 | } 202 | 203 | return max; 204 | }, 205 | 206 | /** 207 | * 查找打卡信息当前最长连续打卡的长度 208 | * @param dateList ClockLog[] 储存打卡信息的数组 209 | */ 210 | getCurrentMaxDays(dateList: ClockLog[]) { 211 | const list = dateList.filter(item => item.isFinished === true); 212 | if (list.length === 0) { 213 | return 0; 214 | } 215 | if (list.length === 1) { 216 | return 1; 217 | } 218 | if (!dateList[dateList.length - 1].isFinished) { 219 | return 0; 220 | } 221 | 222 | let current = 1; 223 | for (let index = list.length - 1; index > 0; index--) { 224 | const element = list[index]; 225 | const pre = list[index - 1]; 226 | if (element.id - 1 === pre.id) { 227 | current++; 228 | // max = Math.max(max, current); 229 | } else { 230 | return current; 231 | } 232 | } 233 | 234 | return current; 235 | }, 236 | 237 | // 获取isFinished 238 | getIsFinished(habit: HabitList) { 239 | const { date } = habit.habitLog; 240 | const { length } = date; 241 | return date[length - 1].isFinished; 242 | }, 243 | }; 244 | 245 | export default utils; 246 | -------------------------------------------------------------------------------- /src/views/404/error.vue: -------------------------------------------------------------------------------- 1 | 6 | 21 | -------------------------------------------------------------------------------- /src/views/Card/Card.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | 39 | 41 | -------------------------------------------------------------------------------- /src/views/Card/Receive/Card.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 98 | 99 | 117 | -------------------------------------------------------------------------------- /src/views/Card/__test__/Card.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import Card from '../Card.vue'; 3 | 4 | describe('card', () => { 5 | const wrapper = mount(Card); 6 | 7 | it('渲染正确的标记', () => { 8 | expect(wrapper.contains('h3')).toBe(true); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/views/Card/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/mixin'; 2 | .van-popup { 3 | @include wh(100%, 100%); 4 | } 5 | 6 | .van-nav-bar { 7 | height: 3.5rem; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | svg { 12 | @include iconSize(1.4rem); 13 | } 14 | } 15 | 16 | // .van-popup { 17 | // display: flex; 18 | // flex-direction: column; 19 | // align-items: center; 20 | // .van-field { 21 | // margin: 3rem 0; 22 | // } 23 | // } 24 | // .time { 25 | // @include wh(100%, 30%); 26 | // display: flex; 27 | // flex-direction: column-reverse; 28 | // align-items: center; 29 | // .van-datetime-picker { 30 | // .van-picker { 31 | // width: 50rem; 32 | // } 33 | // } 34 | // } -------------------------------------------------------------------------------- /src/views/Edit/Calendar/Calendar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 182 | 183 | 185 | -------------------------------------------------------------------------------- /src/views/Edit/Calendar/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/mixin'; 2 | .edit { 3 | margin-top: 1rem; 4 | section { 5 | margin: 1rem 0; 6 | } 7 | .panel { 8 | li { 9 | display: flex; 10 | justify-content: space-between; 11 | padding: 0 1rem; 12 | @include font(1rem, 300%); 13 | svg { 14 | margin-right: 1rem; 15 | @include iconSize(1.2rem); 16 | } 17 | } 18 | } 19 | } 20 | 21 | .highlight { 22 | background-color: red; 23 | } 24 | 25 | .van-popup { 26 | border: solid 0.1rem; 27 | @include borderRadius(3%); 28 | .clock { 29 | width: 60vw; 30 | height: 55vh; 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: space-between; 34 | align-items: center; 35 | p { 36 | border: solid 0.1rem; 37 | width: 60%; 38 | height: 1.5rem; 39 | margin: 0; 40 | background-color: hsl(60, 97%, 65%); 41 | } 42 | svg { 43 | width: 2.5rem; 44 | height: 2.5rem; 45 | } 46 | .van-field { 47 | height: 50%; 48 | background-color: transparent; 49 | width: 100%; 50 | } 51 | div { 52 | width: 70%; 53 | display: flex; 54 | flex-direction: column; 55 | justify-content: center; 56 | } 57 | } 58 | } 59 | 60 | .button { 61 | background-color: $edit; 62 | color: #fff; 63 | } -------------------------------------------------------------------------------- /src/views/Edit/Edit.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 119 | 120 | 122 | -------------------------------------------------------------------------------- /src/views/Edit/IconSetting/IconSetting.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 92 | 93 | 95 | -------------------------------------------------------------------------------- /src/views/Edit/IconSetting/style.scss: -------------------------------------------------------------------------------- 1 | .iconSetting { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-around; 5 | align-items: center; 6 | .icon { 7 | display: flex; 8 | justify-content: center; 9 | width: 100%; 10 | height: 4rem; 11 | margin-top: 1rem; 12 | .cir { 13 | border-radius: 50%; 14 | border: solid black 2px; 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | overflow: hidden; 19 | svg { 20 | margin: 0; 21 | width: 95%; 22 | height: 95%; 23 | } 24 | } 25 | } 26 | .alternative { 27 | margin: 2rem; 28 | height: 10rem; 29 | width: 100%; 30 | overflow: auto; 31 | .alternativeIcon { 32 | display: inline-block; 33 | svg { 34 | width: 2rem; 35 | height: 2rem; 36 | border: none; 37 | margin: 0.8rem; 38 | } 39 | } 40 | } 41 | .colorSetting { 42 | margin: 2rem; 43 | height: 15rem; 44 | width: 100%; 45 | overflow: auto; 46 | .background { 47 | display: inline-block; 48 | width: 2rem; 49 | height: 2rem; 50 | div { 51 | display: block; 52 | width: 2rem; 53 | height: 2rem; 54 | border-radius: 50%; 55 | margin: 0.5rem; 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/views/Edit/Manage/Manage.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 57 | 58 | 60 | -------------------------------------------------------------------------------- /src/views/Edit/Manage/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/mixin'; 2 | aside { 3 | height: 100%; 4 | background-color: $edit; 5 | width: 3.5rem; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | color: $font-o; 10 | font-weight: bold; 11 | } 12 | 13 | .delete { 14 | background-color: $warn; 15 | } 16 | 17 | section { 18 | width: 95%; 19 | justify-content: space-between; 20 | align-items: center; 21 | svg { 22 | width: 1.5rem; 23 | height: 1.5rem; 24 | margin-right: 1rem; 25 | } 26 | span { 27 | display: inline-block; 28 | transform: translateY(-35%); 29 | } 30 | } -------------------------------------------------------------------------------- /src/views/Edit/Recycle/Recycle.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 51 | 52 | 78 | -------------------------------------------------------------------------------- /src/views/Edit/Recycle/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/mixin'; -------------------------------------------------------------------------------- /src/views/Edit/Remind/Remind.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 74 | 75 | 77 | -------------------------------------------------------------------------------- /src/views/Edit/Remind/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/mixin'; 2 | .van-cell-group { 3 | @include font(1.5rem); 4 | } -------------------------------------------------------------------------------- /src/views/Edit/Times/Times.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 81 | 82 | 84 | -------------------------------------------------------------------------------- /src/views/Edit/Times/style.scss: -------------------------------------------------------------------------------- 1 | section { 2 | margin: 2rem 0; 3 | } 4 | 5 | .van-radio { 6 | display: flex; 7 | flex-direction: row-reverse; 8 | justify-content: space-between; 9 | align-items: center; 10 | height: 2rem; 11 | } -------------------------------------------------------------------------------- /src/views/Edit/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/mixin'; 2 | .van-popup { 3 | @include wh(100%, 100%); 4 | } 5 | 6 | .van-nav-bar { 7 | height: 3.5rem; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | svg { 12 | @include iconSize(1.4rem); 13 | } 14 | } 15 | 16 | .van-popup { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | .van-field { 21 | margin: 3rem 0; 22 | } 23 | } 24 | 25 | .time { 26 | @include wh(100%, 30%); 27 | display: flex; 28 | flex-direction: column-reverse; 29 | align-items: center; 30 | .van-datetime-picker { 31 | .van-picker { 32 | width: 50rem; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/views/Feedback/Feedback.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 74 | 75 | 87 | -------------------------------------------------------------------------------- /src/views/Habit/Habit.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 95 | 96 | 106 | -------------------------------------------------------------------------------- /src/views/Home/Home.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 180 | 181 | 224 | -------------------------------------------------------------------------------- /src/views/Login/Login.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 77 | 78 | 101 | -------------------------------------------------------------------------------- /src/views/New/Habit/Habit.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 194 | 195 | 197 | -------------------------------------------------------------------------------- /src/views/New/Habit/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/mixin'; 2 | svg { 3 | @include iconSize(5rem); 4 | } 5 | 6 | .button { 7 | width: 80%; 8 | height: 3rem; 9 | background-color: $edit; 10 | color: #fff; 11 | position: relative; 12 | top: 5rem; 13 | } 14 | 15 | .van-cell { 16 | height: 3.5rem; 17 | display: flex; 18 | text-align: left; 19 | justify-content: space-between; 20 | @include font(1rem); 21 | } 22 | 23 | .field { 24 | width: 100%; 25 | margin: 0 auto; 26 | text-align: text; 27 | overflow: hidden; 28 | } 29 | 30 | .van-popup { 31 | width: 100%; 32 | height: 100%; 33 | display: flex; 34 | flex-direction: column; 35 | justify-content: space-around; 36 | align-items: center; 37 | p { 38 | margin-top: -3rem; 39 | @include font(0.8rem); 40 | } 41 | aside { 42 | display: inherit; 43 | justify-content: space-around; 44 | flex: none; 45 | flex-wrap: wrap; 46 | width: 60vw; 47 | height: 70vh; 48 | } 49 | .van-button { 50 | width: 80%; 51 | height: 3rem; 52 | background-color: $edit; 53 | color: #fff; 54 | } 55 | } 56 | 57 | .icon { 58 | width: 100%; 59 | height: 5rem; 60 | display: flex; 61 | justify-content: center; 62 | margin-top: 1rem; 63 | .cir { 64 | border-radius: 50%; 65 | border: solid black 2px; 66 | overflow: hidden; 67 | svg { 68 | margin: 0; 69 | width: 100%; 70 | height: 100%; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/views/New/Library/Library.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 145 | 146 | 148 | -------------------------------------------------------------------------------- /src/views/New/Library/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/mixin'; 2 | p { 3 | @include font(1rem, 120%); 4 | height: 2.5rem; 5 | padding: 0.3rem; 6 | } 7 | 8 | h4 { 9 | height: 1.6rem; 10 | background-color: $grey; 11 | margin: 0; 12 | display: flex; 13 | align-items: center; 14 | padding: 0 1.2rem; 15 | } 16 | 17 | .van-list { 18 | overflow: scroll; 19 | height: calc(100vh - 16rem); 20 | } 21 | 22 | .van-cell { 23 | display: flex; 24 | justify-content: flex-start; 25 | flex-direction: row; 26 | align-items: center; 27 | flex: none; 28 | height: 3.5rem; 29 | @include font(1rem, 120%); 30 | svg { 31 | @include iconSize(2.6rem); 32 | margin-right: 1rem; 33 | } 34 | } -------------------------------------------------------------------------------- /src/views/New/New.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 41 | 42 | 44 | -------------------------------------------------------------------------------- /src/views/New/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/mixin'; 2 | .van-popup { 3 | @include wh(100%, 100%); 4 | main { 5 | @include wh(100%, 100%); 6 | } 7 | } 8 | 9 | .van-nav-bar { 10 | height: 3.5rem; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | svg { 15 | @include iconSize(1.4rem); 16 | } 17 | } -------------------------------------------------------------------------------- /src/views/Setting/Setting.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 106 | 146 | -------------------------------------------------------------------------------- /src/views/UpdateLog/UpdateLog.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 45 | 46 | 58 | -------------------------------------------------------------------------------- /tests/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // The assertion name is the filename. 3 | // Example usage: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // For more information on custom assertions see: 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | 10 | exports.assertion = function elementCount (selector, count) { 11 | this.message = `Testing if element <${selector}> has count: ${count}` 12 | this.expected = count 13 | this.pass = val => val === count 14 | this.value = res => res.value 15 | function evaluator (_selector) { 16 | return document.querySelectorAll(_selector).length 17 | } 18 | this.command = cb => this.api.execute(evaluator, [selector], cb) 19 | } 20 | -------------------------------------------------------------------------------- /tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': browser => { 6 | browser 7 | .url(process.env.VUE_DEV_SERVER_URL) 8 | .waitForElementVisible('#app', 5000) 9 | .assert.elementPresent('.hello') 10 | .assert.containsText('h1', 'Welcome to Your Vue.js + TypeScript App') 11 | .assert.elementCount('img', 1) 12 | .end() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "allowSyntheticDefaultImports": true, 11 | "sourceMap": true, 12 | // "lib": [ 13 | // "es6", 14 | // "webworker", 15 | // "dom" 16 | // ], 17 | "baseUrl": ".", 18 | "types": [ 19 | "node", 20 | "jest" 21 | ], 22 | "paths": { 23 | "@/*": [ 24 | "src/*" 25 | ] 26 | } 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "src/**/*.vue", 31 | "tests/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "rules": { 7 | "quotemark": [true, "single"], 8 | "indent": [true, "spaces", 2], 9 | "semicolon":false, 10 | "max-line-length": false, 11 | "interface-name": false, 12 | "ordered-imports": false, 13 | "object-literal-sort-keys": false, 14 | "no-consecutive-blank-lines": false, 15 | "prefer-for-of":false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const PrerenderSPAPlugin = require('prerender-spa-plugin') 4 | 5 | const PurgecssPlugin = require('purgecss-webpack-plugin'); 6 | const glob = require('glob-all'); 7 | 8 | module.exports = { 9 | lintOnSave: false, 10 | pwa: { 11 | name: 'My App', 12 | themeColor: '#4DBA87', 13 | msTileColor: '#000000', 14 | appleMobileWebAppCapable: 'yes', 15 | appleMobileWebAppStatusBarStyle: 'black', 16 | 17 | // configure the workbox plugin 18 | workboxPluginMode: 'InjectManifest', 19 | workboxOptions: { 20 | // swSrc is required in InjectManifest mode. 21 | swSrc: 'src/sw.js', 22 | // ...other Workbox options... 23 | } 24 | }, 25 | configureWebpack(config) { 26 | if (process.env.NODE_ENV === 'production') { 27 | return { 28 | 29 | plugins: [ 30 | new PrerenderSPAPlugin({ 31 | // Required - The path to the webpack-outputted app to prerender. 32 | staticDir: path.join(__dirname, 'dist'), 33 | // Required - Routes to render. 34 | routes: ['/', '/habit', '/setting'], 35 | }), 36 | ], 37 | } 38 | }; 39 | }, 40 | 41 | chainWebpack: config => { 42 | config.optimization 43 | .clear('splitChunks') 44 | .splitChunks({ 45 | cacheGroups: { 46 | vue: { 47 | name: 'vue', 48 | test: /[\\/]node_modules[\\/]vue[\\/]/, 49 | priority: 0, 50 | chunks: 'initial' 51 | }, 52 | vendors: { 53 | name: 'chunk-vendors', 54 | test: /[\\/]node_modules[\\/]/, 55 | priority: -10, 56 | chunks: 'initial' 57 | }, 58 | common: { 59 | name: 'chunk-common', 60 | minChunks: 2, 61 | priority: -20, 62 | chunks: 'initial', 63 | reuseExistingChunk: true 64 | } 65 | } 66 | }) 67 | } 68 | } -------------------------------------------------------------------------------- /workbox-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "globDirectory": "dist/", 3 | "globPatterns": [ 4 | "**/*.{css,ico,png,html,js,json}" 5 | ], 6 | "swDest": "dist/sw.js", 7 | "swSrc": "src/sw.js" 8 | }; --------------------------------------------------------------------------------