这是一个非常有意思的项目,我们先来看看效果
这个项目所用的技术也比较有意思,它的技术栈为vue2.5 + Typescript + vuex + vue-router
放下博主的项目地址吧,https://github.com/xiaomuzhu/vue-ts-daily
接下来我们一起看项目代码吧,也一起研究ts怎么在vue中进行使用
首先是入口文件main.ts
//main.ts
// 本质上和写js一样
import Vue from 'vue';
// 解决300ms点击延迟问题
import FastClick from 'fastclick';
// 引用图标字体组件
import VueIconFont from 'vue-icon-font-pro';
// 日历组件
import vueEventCalendar from 'vue-event-calendar-pro';
// Vue.js 2.0 组件级懒加载方案:Vue Lazy Component
import VueLazyComponent from '@xunlei/vue-lazy-component';
//骨架loading
import VueSkeletonLoading from 'vue-skeleton-loading';
// Normalize.css是一种CSS reset的替代方案
import 'normalize.css';
// 动画
import 'vue2-animate/dist/vue2-animate.min.css';
import 'vue-event-calendar-pro/dist/style.css';
import App from './App.vue';
import router from './router';
import store from './store';
import './registerServiceWorker';
import '@/assets/iconfont.js';
// 兼容毒瘤ios的300ms延迟问题
if ('addEventListener' in document) {
document.addEventListener(
'DOMContentLoaded',
() => {
(FastClick as any).attach(document.body);
},
false,
);
}
Vue.use(VueLazyComponent);
Vue.use(VueSkeletonLoading);
Vue.use(vueEventCalendar, { locale: 'zh', weekStartOn: 1 });
Vue.use(VueIconFont);
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');
App.vue引入两个组件
<template>
<main id="app">
<div v-if="$route.meta.main">
<Header></Header>
<router-view />
<Footer></Footer>
</div>
<div v-if="!$route.meta.main">
<router-view />
</div>
</main>
</template>
<script lang="ts">
// 引入组件
import { Component, Prop, Vue } from 'vue-property-decorator';
// 引入头部和底部
import Header from './components/Header.vue';
import Footer from './components/Footer.vue';
@Component({
components: {
Header,
Footer,
},
})
export default class App extends Vue {}
</script>
<style lang="scss" scoped>
@import './style/mixin';
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: $font;
display: flex;
text-align: center;
flex-direction: column;
justify-content: space-between;
max- 100vw;
height: 100vh;
}
#nav {
padding: 30px;
a {
font-weight: bold;
color: $font;
&.router-link-exact-active {
color: #42b983;
}
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to
/* .fade-leave-active below version 2.1.8 */
{
opacity: 0;
}
</style>
接下来我们看里面引入的HeaderIcon.ts
//srccomponentscommonIconHeaderIcon.ts
import { Component, Prop, Vue } from 'vue-property-decorator';
import template from './Icon.vue';
@Component({
name: 'HeaderIcon',
mixins: [template],
})
// 使用ts封装的srccomponentscommonIconHeaderIcon.ts组
export default class FooterIcon extends Vue {
@Prop() private name!: string;
@Prop() private path!: string;
private data() {
return {
isTouched: false,
};
}
}
我们来看一下FooterIcon.ts
//srccomponentscommonIconFooterIcon.ts
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
import { Mutation } from 'vuex-class';
import { PageInfo } from '@/store/state';
import template from './Icon.vue';
@Component({
name: 'FooterIcon',
mixins: [template],
})
// 使用ts封装的srccomponentscommonIconFooterIcon.ts组件
export default class FooterIcon extends Vue {
@Prop() private name!: object;
@Prop() private path!: string;
@Prop() private id!: number;
@Prop() private isActived!: boolean;
@Prop() private tagName!: string;
@Mutation private getActivePage!: (pageName: number) => void;
@Mutation private changeHeaderState!: (pageName: number) => void;
private changeActivePage() {
const id = this.id;
if (!this.isActived) {
this.getActivePage(id);
this.changeHeaderState(id);
}
}
}
对icon也做的封装
//srccomponentscommonIconIcon.vue
<template>
<section>
<router-link v-if="!!path" :to="path">
<span @click="changeActivePage">
<icon :name="!isActived ? name.defaultName : name.activedName" style=" 2rem; height:2rem"></icon>
<p :class="{active: isActived}">{{tagName}}</p>
</span>
</router-link>
<div v-else class="headerIcon">
<icon :name="name" style=" 1.6rem; height:1.8rem">
</icon>
</div>
</section>
</template>
<style src="./style.scss" lang="scss" scoped>
</style>
footer.vue中也是对footer进行了封装,感觉和封装普通的组件差别不大,不过在使用vuex,state之类的属性的时候就难起来
//srccomponentsFooter.vue
<template>
<footer>
<Icon v-for="item in activePage" :key="item.id" :tagName="item.tagName" :isActived="item.isActived" :id="item.id" :name="item.name" :path="item.path" >
</Icon>
</footer>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { State } from 'vuex-class';
import { PageInfo } from '@/store/state';
import Icon from './common/Icon/FooterIcon';
@Component({
components: {
Icon,
},
})
export default class Footer extends Vue {
@State private activePage!: PageInfo[];
}
</script>
<style lang="scss" scoped>
@import '../style/mixin';
footer {
100%;
height: 3.5rem;
min-height: 8%;
background-color: $grey;
display: flex;
align-items: center;
justify-content: space-around;
div {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
font-size: 60%;
svg {
margin-bottom: 0.4rem;
}
}
}
</style>
封装的骨架
//srccomponentscommonSkeletonSkeletonList.vue
<template>
<skeleton-loading>
<row
v-for="i in num"
:key="i"
:gutter="{top: '10px', bottom: '10px'}"
>
<column :span="23" :gutter="10">
<square-skeleton
:count="2"
:boxProperties="{
bottom: '15px',
'250px',
height: '15px'
}"
>
</square-skeleton>
</column>
</row>
</skeleton-loading>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
@Component({})
export default class Skeleton extends Vue {
@Prop() private num!: number;
}
</script>
<style lang="scss" scoped>
@import '../../../style/mixin';
</style>
我们看一下项目中的vuex是怎么和ts结合使用的
首先是state
//srcstorestate.ts
//ts里面也可以像平时一样写函数,只不过它要定义一下类型
//在state里面写了很多接口类型
import * as moment from 'moment';
export interface ClockLog {
id: number;
time?: moment.Moment;
isFinished: boolean;
message?: string;
}
export interface UserState {
username: string | undefined;
id: number | null;
createdTime: string | undefined;
url: string;
isLogin: number;
isSync: number;
}
export interface SettingState {
checked: boolean;
url: string;
}
export interface TimeSlotList {
id: number;
title: string;
}
export interface RemindState {
id: number;
remind: string;
isOpen: boolean;
}
export interface RepeatingDateState {
id: number;
date: string;
checked: boolean;
}
// 单个习惯的状态信息
export interface HabitList {
id: number;
iconName: string;
color: string;
mode: string;
// 是否可用,否则是被归档了
isActive: boolean;
// 关于习惯的基本信息
habitInfo: {
// 习惯名称
habitName: string;
// 重复练习的日期
RepeatingDate: RepeatingDateState[] | never[];
// 练习的时间段
activeTimes: number;
timeSlotList: TimeSlotList[] | never[];
// 提醒的时间
remind: RemindState[] | never[];
// 激励自己的话
inspire: string;
};
// 习惯日志
habitLog: {
// 总共坚持练习了多少天
totalHabitDays: number;
// 当前连续联系了多少天
currentConsecutiveDays: number;
// 历史上最多连续练习多少天
mostConsecutiveDays: number;
// 创建日期
createdTime: string;
// 创建此习惯至今多少天
totalDays: number;
date: ClockLog[];
};
}
export interface Card {
src: string;
content?: string;
}
export interface PageInfo {
id: number;
isActived: boolean;
name: {
defaultName: string;
activedName: string;
};
path: string;
tagName: string;
}
export interface HeaderInfo {
left?: string;
title: string;
right?: string;
}
export interface State {
activePage: PageInfo[];
headerInfo: HeaderInfo;
card: Card;
habitList: HabitList[];
today: {
active: string[] | never[] | number[];
finishedDate: moment.Moment[] | never[];
isReceived: boolean;
};
setting: SettingState;
user?: UserState;
}
// 初始状态
const state: State = {
activePage: [
{
id: 0,
isActived: true,
name: {
defaultName: 'today-o',
activedName: 'today',
},
path: '/',
tagName: '日常',
},
{
id: 1,
isActived: false,
name: {
defaultName: 'habit-o',
activedName: 'habit',
},
path: '/habit',
tagName: '习惯',
},
{
id: 2,
isActived: false,
name: {
defaultName: 'setting-o',
activedName: 'setting',
},
path: '/setting',
tagName: '更多',
},
],
headerInfo: {
left: 'letter',
title: 'TODAY',
right: '', // filter
},
today: {
active: [0],
finishedDate: [],
isReceived: false,
},
setting: {
checked: false,
url:
'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=4216091012,4283409120&fm=27&gp=0.jpg',
},
card: {
src:
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-5xlxmMc1UjkLOsMSPPX9sKgNr3XuCNHCCCwI__iXCx2zftWo',
content: '1',
},
habitList: [
{
id: 1524822339790,
iconName: 'taiyang',
color: '#ffe884',
mode: 'done',
isActive: true,
habitInfo: {
// 习惯名称
habitName: '背单词',
// 重复练习的日期
RepeatingDate: [
{ id: 0, date: '星期一', checked: true },
{ id: 1, date: '星期二', checked: true },
{ id: 2, date: '星期三', checked: true },
{ id: 3, date: '星期四', checked: true },
{ id: 4, date: '星期五', checked: true },
{ id: 5, date: '星期六', checked: true },
{ id: 6, date: '星期日', checked: true },
],
// 练习的时间段
activeTimes: 0,
// 目前已存在的时间段
timeSlotList: [
{
id: 0,
title: '起床之后',
},
{
id: 1,
title: '晨间习惯',
},
{
id: 2,
title: '中午时分',
},
{
id: 3,
title: '午间习惯',
},
{
id: 4,
title: '晚间习惯',
},
{
id: 5,
title: '睡觉之前',
},
{
id: 6,
title: '任意时间',
},
],
// 提醒的时间
remind: [{ id: 0, remind: '12:00', isOpen: false }],
// 激励自己的话
inspire: '坚持的路上没有捷径',
},
habitLog: {
// 总共坚持练习了多少天
totalHabitDays: 0,
// 当前连续联系了多少天
currentConsecutiveDays: 0,
// 历史上最多连续练习多少天
mostConsecutiveDays: 0,
// 创建日期
createdTime: '0',
// 创建此习惯至今多少天
totalDays: 0,
// 坚持的日期
date: [],
},
},
],
user: {
isLogin: -1,
username: '',
id: null,
createdTime: '',
isSync: -1,
url:
'https://is4-ssl.mzstatic.com/image/thumb/Purple71/v4/be/13/06/be1306d8-e343-2adb-2b04-9a6884300499/pr_source.jpg/1200x630bb.jpg',
},
};
export default state;
//srcstoregetters.ts
import { GetterTree } from 'vuex';
const getters: GetterTree<any, any> = {
syncData(state) {
const { activePage, headerInfo, card, habitList, today, setting } = state;
return {
activePage,
headerInfo,
card,
habitList,
today,
setting,
};
},
};
export default getters;
//srcstoremutations.ts
import moment from 'moment';
import { State, HabitList } from './state';
import config from '@/config';
import _ from '@/utils';
export default {
// 切换活动图标的状态
getActivePage(state: State, id: number) {
state.activePage.map(item => {
// 将当前活动的页脚图表点亮
if (item.id !== id) {
item.isActived = false;
} else {
item.isActived = true;
}
});
},
// 切换header上图标
changeHeaderState(state: State, id: number) {
const { headerInfo } = state;
switch (id) {
case 0:
headerInfo.left = 'letter';
headerInfo.right = ''; // filter
headerInfo.title = 'TODAY';
break;
case 1:
headerInfo.left = 'file';
headerInfo.right = 'new';
headerInfo.title = '我的习惯';
break;
case 2:
headerInfo.left = '';
headerInfo.right = ''; // skin
headerInfo.title = '设置';
break;
}
},
// 创建习惯
createHabit(state: State, habit: HabitList) {
state.habitList.push(habit);
},
// 删除未定义好的习惯
RemoveHabit(state: State) {
state.habitList.pop();
},
// 选择执行的星期
selectDate(state: State, payload: { habitId: number; id: number }) {
const list = state.habitList;
const len = list.length;
const { RepeatingDate } = _.find(list, payload.habitId)!.habitInfo;
(RepeatingDate as any[]).forEach(element => {
if (element.id === payload.id) {
element.checked = !element.checked;
}
});
},
// 切换练习的时间段
changeTimes(state: State, payload: { habitId: number; id: number }) {
const list = state.habitList;
const habit = _.find(list, payload.habitId);
habit!.habitInfo.activeTimes = payload.id;
},
// 选择图标背景
selectColor(state: State, payload: { id: number; color: string }) {
const list = state.habitList;
const habit = _.find(list, payload.id);
habit!.color = payload.color;
},
// 选择图标
selectIcon(state: State, payload: { id: number; icon: string }) {
const list = state.habitList;
const habit = _.find(list, payload.id);
habit!.iconName = payload.icon;
},
// 切换提醒时间
switchRemind(state: State, payload: { habitId: number; id: number }) {
const list = state.habitList;
const { remind } = _.find(list, payload.habitId)!.habitInfo;
(remind as any[]).forEach(item => {
if (item.id === payload.id) {
item.isOpen = !item.isOpen;
}
});
},
// 习惯名称
changeName(state: State, payload: { id: number; value: string }) {
const list = state.habitList;
const habit = _.find(list, payload.id);
habit!.habitInfo.habitName = payload.value;
},
// 绑定激励的话
changInspire(state: State, payload: { id: number; value: string }) {
const list = state.habitList;
const habit = _.find(list, payload.id);
habit!.habitInfo.inspire = payload.value;
},
// 切换习惯当前的状态
changeMode(state: State, payload: { id: number; value: string }) {
const list = state.habitList;
const habit = _.find(list, payload.id);
habit!.isActive = true;
habit!.mode = payload.value;
},
// 将此习惯归档
deleteHabit(state: State, id: number) {
const list = state.habitList;
const habit = _.find(list, id);
habit!.isActive = false;
},
// 删除此习惯
removeHabit(state: State, id: number) {
const list: HabitList[] = state.habitList;
state.habitList = list.filter(item => item.id !== id);
},
// 重新激活此习惯
activateHabit(state: State, id: number) {
const list = state.habitList;
const habit = _.find(list, id);
habit!.isActive = true;
},
// 获取需要当天执行的习惯
changeCollapse(state: State, activeNames: number[] | never[]) {
const today = state.today;
today.active = activeNames;
},
// 未添加当日任务的习惯列表进行更新
updateHabits(state: State, updateList: number[]) {
const today = moment();
const newId = _.getDaysId();
const list = state.habitList;
for (let index = 0; index < updateList.length; index++) {
const id = updateList[index];
const habit = _.find(list, id);
habit!.habitLog.date.push({
id: newId,
time: today,
isFinished: false,
message: '',
});
}
},
// 对习惯的打卡信息进行补签
supplementHabits(state: State, payload: { id: number; daysId: number }) {
const list = state.habitList;
const today = _.getMoment(payload.daysId);
const habit = _.find(list, payload.id);
// 储存date信息的数组
const dateList = habit!.habitLog.date;
if (dateList.length > 0) {
for (let index = 0; index < dateList.length; index++) {
const element = dateList[index];
if (element.id > payload.daysId) {
dateList.splice(index, 0, {
id: payload.daysId,
time: today,
isFinished: true,
message: '',
});
habit!.habitLog.currentConsecutiveDays = _.getCurrentMaxDays(
dateList,
);
habit!.habitLog.totalHabitDays++;
console.log(_.getMaxDays(dateList));
habit!.habitLog.mostConsecutiveDays = _.getMaxDays(dateList);
return;
}
}
} else {
dateList.push({
id: payload.daysId,
time: today,
isFinished: true,
message: '',
});
}
},
// 切换当前习惯是否完成
changeFinished(state: State, payload: { id: number; daysId: number }) {
const list = state.habitList;
const habit = _.find(list, payload.id);
// 储存date信息的数组
const dateList = habit!.habitLog.date;
const len = dateList.length;
// 找到id相关信息
const date = dateList.find(item => item.id === payload.daysId);
// 切换完成状态
date!.isFinished = !date!.isFinished;
// 当当前信息被切换成"已完成"
if (date!.isFinished) {
// 当当前打卡信息属于当天的时候
if (dateList[len - 1].id === payload.daysId) {
habit!.habitLog.currentConsecutiveDays++;
} else {
habit!.habitLog.currentConsecutiveDays = _.getCurrentMaxDays(dateList);
}
habit!.habitLog.totalHabitDays++;
} else {
// 当当前打卡信息属于当天的时候
if (dateList[len - 1].id === payload.daysId) {
habit!.habitLog.currentConsecutiveDays--;
} else {
habit!.habitLog.currentConsecutiveDays = _.getCurrentMaxDays(dateList);
}
habit!.habitLog.totalHabitDays--;
date!.message = '';
}
habit!.habitLog.mostConsecutiveDays = _.getMaxDays(dateList);
},
// 储存打卡日志
saveLog(
state: State,
payload: { id: number; daysId: number; message: string },
) {
const list = state.habitList;
const habit = _.find(list, payload.id);
const day = habit!.habitLog.date.find(item => item.id === payload.daysId);
day!.message = payload.message;
},
// 领取卡片
receiveCard(state: State) {
const today = moment();
// @ts-ignore
state.today.finishedDate.push(today);
state.today.isReceived = true;
},
// 登陆成功后执行
loginLoading(state: State, data: any) {
state.user!.isLogin = 0;
},
// 登陆成功后执行
loginSuccess(state: State, data: any) {
const currentState = JSON.parse(data.content);
state.activePage = currentState.activePage;
state.headerInfo = currentState.headerInfo;
state.card = currentState.card;
state.habitList = currentState.habitList;
state.today = currentState.today;
state.setting = currentState.setting;
state.user!.id = data.id;
state.user!.username = data.username;
state.user!.url = data.url;
state.user!.isLogin = 1;
},
// 退出登录
logoutSuccess(state: State) {
state.user!.id = null;
state.user!.username = '';
state.user!.url =
'https://is4-ssl.mzstatic.com/image/thumb/Purple71/v4/be/13/06/be1306d8-e343-2adb-2b04-9a6884300499/pr_source.jpg/1200x630bb.jpg';
state.user!.isLogin = -1;
},
// 是否开启整点报时
changeHourly(state: State, checked: boolean) {
state.setting.checked = checked;
},
// 是否同步成功
sync(state: State, isSync: number) {
state.user!.isSync = isSync;
},
};
//srcstoreactions.ts
import { ActionTree } from 'vuex';
import axios from 'axios';
import config from '@/config';
import { login } from '@/api/user';
import { sync } from '@/api/sync';
const actions: ActionTree<any, any> = {
// 发起登录
async login({ state, commit }, data) {
const res: Ajax.AjaxResponse = await login(data)
.then(res => res.data)
.catch((e: string) => console.error(e));
if (res) {
commit('loginSuccess', res);
}
},
// 数据同步
async sync({ state, commit }, data) {
const res: Ajax.AjaxResponse = await sync(data)
.then(res => res.data)
.catch((e: string) => console.error(e));
if (res) {
commit('sync', 1);
}
},
};
export default actions;
接下来看一些views组件
//error.vue
<template>
<div class="error">
<router-link class="redirect" to="/">回到首页</router-link>
</div>
</template>
<style lang="scss" scoped>
@import '../../style/mixin';
.error {
100vw;
height: 100vh;
@include bis('../../assets/404.jpg');
.redirect {
display: block;
position: relative;
top: 70%;
color: antiquewhite;
}
}
</style>
//srcviewsCardCard.vue
<template>
<div class="card">
<!-- 新建页面的导航 -->
<van-nav-bar @click-left="onClickLeft" @click-right="onClickRight">
<icon name="left-arrow" slot="left" />
<h3 v-if="title" slot="title">{{title}}</h3>
</van-nav-bar>
<!-- 渲染各种新建项目 -->
<router-view />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { NavBar, Popup, Field, Button, Toast, DatetimePicker } from 'vant';
import utils from '@/utils';
@Component({
components: {
[NavBar.name]: NavBar,
[Popup.name]: Popup,
},
})
export default class CardPage extends Vue {
private title?: string;
private data() {
return {
title: this.$route.name,
};
}
private onClickLeft() {
this.$router.go(-1);
}
}
</script>
<style src="./style.scss" lang="scss" scoped>
</style>
这些写vuex的方式值得学习
//srcviewsCardReceiveCard.vue
<template>
<div class="card">
<!-- 说明 -->
<section>
<p v-if="num > 0">{{`你还需要完成${num}个习惯来获得此卡片`}}</p>
<p v-else>您已完成全部习惯请领取卡片</p>
</section>
<!-- 卡片 -->
<section>
<Card :saying="saying" :id="id" ></Card>
</section>
<!-- 领取按钮 -->
<section>
<van-button @click="receive" :disabled="today.isReceived" :class="{ done: isDone }">领取</van-button>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import { Mutation, State } from 'vuex-class';
import { Button, Toast } from 'vant';
import moment from 'moment';
import { HabitList as HabitListState } from '@/store/state';
import Card from '@/components/common/Card/Card.vue';
import _ from '@/utils';
@Component({
components: {
[Button.name]: Button,
Card,
},
})
export default class Library extends Vue {
@Mutation private receiveCard!: () => void;
@State private habitList!: HabitListState[];
@State
private today!: {
active: string[] | never[] | number[];
finishedDate: string[] | never[];
isReceived: boolean;
};
private title?: string;
private num!: number;
private isDone!: boolean;
private isReceived!: boolean;
private data() {
return {
saying: '自己打败自己是最可悲的失败,自己战胜自己是最可贵的胜利。',
id: 1,
num: 0,
isDone: false,
isReceived: false,
};
}
private mounted() {
const id = _.getDaysId();
this.habitList.forEach(item => {
item.habitLog.date.filter(ele => ele.id === id).forEach(e => {
if (!e.isFinished) {
this.num++;
}
});
});
if (this.num > 0) {
this.isDone = false;
} else {
this.isDone = true;
}
}
private receive() {
if (!this.isDone) {
Toast('请完成全部任务再来领取~');
} else {
const { length } = this.today.finishedDate;
const today = this.today.finishedDate[length - 1];
if (!length) {
Toast('领取成功~');
this.receiveCard();
this.$router.go(-1);
return;
}
const tip = moment(today).isSame(moment())
? '今天您已经领取过了'
: '领取成功~';
Toast(tip);
this.receiveCard();
this.$router.go(-1);
}
}
}
</script>
<style lang="scss" scoped>
@import '../../../style/mixin';
.card {
margin: 0;
height: calc(100vh - 6rem);
100vw;
display: flex;
flex-direction: column;
justify-content: space-around;
}
p {
@include font(0.8rem, 150%);
color: #c0c0c0;
}
.done {
background-color: #34ba3a;
}
</style>
接下来我们根据router.ts结合页面来看代码
router.ts中有利用懒加载的方式去加载,倒是可以看里面的一些懒加载的写法
首先看home.vue
一般使用组件或者引入vuex会引入
import { Component, Vue, Watch } from 'vue-property-decorator';
import { Mutation, State } from 'vuex-class';
子组件通过@Emit向父组件传递事件
//srccomponentscommonClockPopupClockPopup.vue
<template>
<van-popup v-model="show" @click-overlay="handleHide" :close-on-click-overlay="false">
<div class="clock">
<p>打卡日志</p>
<icon name="target" />
<div>
<van-field v-model="message" type="textarea" rows="5" placeholder="写出你想说的话..." />
<van-button @click="save(message)" class="button" size="small">保存</van-button>
</div>
</div>
</van-popup>
</template>
<script lang="ts">
import { Component, Vue, Watch, Emit, Prop } from 'vue-property-decorator';
import { Field, Popup, Button } from 'vant';
// 组件
@Component({
components: {
[Field.name]: Field,
[Popup.name]: Popup,
[Button.name]: Button,
},
})
export default class ClockPopup extends Vue {
// 这个是父子组件传参数 props的用法
@Prop() private name?: string;
@Prop() private show?: boolean;
private message?: string;
public data() {
return {
message: '',
};
}
// 子组件向父组件传事件有趣有意思
@Emit('save')
private save(messages?: string) {
this.message = '';
}
@Watch('show')
private changeShow(val: boolean, oldVal: boolean) {
this.show = val;
}
@Emit('hide')
private handleHide() {}
}
</script>
<style src="./style.scss" lang="scss" scoped>
</style>
//srcviewsHomeHome.vue
<template>
<div class="today">
<section>
<!-- dayComputed数据循环 -->
<van-collapse v-for="(item, index) in dayComputed.habits" :key="item.id" @change="change" v-model="today.active">
<van-collapse-item :name="index">
<p slot="title">
<!-- icon -->
<icon name="time" />
<!-- 中间内容 -->
{{item.title}}</p>
<aside v-for="ele in item.habits" :key="ele.id" @click="finish(ele.id)">
<Circles radius="3.5rem" v-if="!!ele.habitLog.date.find(item =>(item.id === days))" :activeColor="!!ele.habitLog.date.find(item =>(item.id === days)).isFinished ? ele.color : '#fff'">
<icon :name="ele.iconName" slot="icon" />
</Circles>
</aside>
</van-collapse-item>
</van-collapse>
</section>
<!-- 控制显示隐藏? -->
<!-- 点击van-collapse的时候弹框弹出来 -->
<ClockPopup :show="show" @save="saveLogs" @hide="hide" />
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import { Mutation, State } from 'vuex-class';
import { Collapse, CollapseItem } from 'vant';
import { HabitList as HabitListState, TimeSlotList } from '@/store/state';
import _ from '@/utils';
import Circles from '@/components/common/Circle/Circle.vue';
import ClockPopup from '@/components/common/ClockPopup/ClockPopup.vue';
// 从工具函数中获取
export interface NewList {
habits: HabitListState[] | never[];
title?: string;
id?: number;
}
// 引入组件
@Component({
components: {
[Collapse.name]: Collapse,
[CollapseItem.name]: CollapseItem,
Circles,
ClockPopup,
},
})
export default class Today extends Vue {
// v2ex用法
@Mutation private createHabit!: (habit: HabitListState) => void;
@Mutation
private changeFinished!: (payload: { id: number; daysId: number }) => void;
@Mutation private changeCollapse!: (habit: number[] | never[]) => void;
@Mutation private updateHabits!: (updateList: number[]) => void;
@Mutation
private saveLog!: (
payload: { id: number; daysId: number; message: string },
) => void;
@State private habitList!: HabitListState[];
@State private today!: object;
private show!: boolean;
private currentId!: number;
private isDone!: boolean;
// 今天距离1970年1.1的天数
private days!: number;
private data() {
return {
show: false,
currentId: 0,
days: _.getDaysId(),
isDone: false,
};
}
private mounted() {
const { needUpdate } = this.dayComputed;
console.log('..........dayComputed',needUpdate)
const len = needUpdate.length;
if (len) {
this.updateHabits(needUpdate);
}
this.isDone = true;
}
private get dayComputed() {
const habitsList = this.habitList.filter((item) => item.isActive === true);
// 今天可用的习惯
const current = _.dateComparison(habitsList);
// 检测这些习惯是否产生了当日的任务,如果没有,批量创建
const needUpdate = [];
const timeList = new Set();
for (let index = 0; index < current.length; index++) {
const element = current[index]; // 单个习惯
const { timeSlotList, activeTimes } = element.habitInfo;
const { date } = element.habitLog;
const len = date.length;
// 当习惯的历史任务数组里是空那么放入待更新数组,如果最近的历史任务与今日的数字不匹配,说明没有创建今日任务,也放入更新
if (len === 0) {
needUpdate.push(element.id);
} else if (date[len - 1].id !== this.days) {
needUpdate.push(element.id);
}
// @ts-ignore
const time = timeSlotList.find((e) => e.id === activeTimes);
// time.habits.push(element);
timeList.add(time!.title);
}
// 今天生效的时间段
const list = Array.from(timeList);
// 每一个时间段对应相应的习惯所组成的数组
const currentList = [];
for (let index = 0; index < list.length; index++) {
const element = list[index];
const newList: NewList = {
habits: [],
};
for (let i = 0; i < current.length; ++i) {
const item = current[i];
const { timeSlotList, activeTimes } = item.habitInfo;
const time = (timeSlotList as TimeSlotList[]).find(
(e: TimeSlotList) => e.id === activeTimes,
);
if (time!.title === element) {
if (newList.title === time!.title) {
// @ts-ignore
newList.habits.push(item);
} else {
newList.title = element;
newList.id = index;
// @ts-ignore
newList.habits.push(item);
currentList.push(newList);
}
}
}
}
return {
current,
list,
habits: currentList,
needUpdate,
};
}
// 这个change是做什么啊?
private change(activeNames: number[] | never[]) {
this.changeCollapse(activeNames);
}
private finish(id: number) {
this.currentId = id;
// 如果已经完成那么则是取消操作,否则是标记完成的操作
if (
this.habitList
.find((item) => item.id === id)!
.habitLog.date.find((item) => item.id === this.days)!.isFinished
) {
// TODO/bug 将Finished重新设置为false时不触发视图更新
// this.changeFinished({
// id,
// daysId: this.days,
// });
} else {
this.show = true;
this.changeFinished({
id,
daysId: this.days,
});
}
}
// 点击保存
private saveLogs(message: string) {
const id = _.getDaysId();
this.saveLog({ id: this.currentId, daysId: id, message });
this.show = false;
}
// 一开始show为false
private hide() {
this.show = false;
}
}
</script>
<style lang="scss" scoped>
@import '../../style/mixin';
.today {
height: calc(100vh - 7rem);
display: flex;
justify-content: flex-start;
flex-direction: column;
overflow: auto;
.van-collapse-item {
margin-bottom: 1rem;
p {
display: flex;
justify-content: flex-start;
align-items: center;
@include font(1rem);
svg {
1.2rem;
height: 1.2rem;
margin-right: 0.5rem;
}
}
aside {
min-height: 5rem;
display: inline-flex;
margin: 0 1rem;
div {
border: solid 1px black;
border-radius: 50%;
3rem;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
box-shadow: 0 0 3px 3px rgba(130, 130, 130, 0.3);
svg {
3rem;
height: 3rem;
}
}
}
}
}
</style>
//srcviewsCardReceiveCard.vue
<template>
<div class="card">
<!-- 说明 -->
<section>
<p v-if="num > 0">{{`你还需要完成${num}个习惯来获得此卡片`}}</p>
<p v-else>您已完成全部习惯请领取卡片</p>
</section>
<!-- 卡片 -->
<section>
<Card :saying="saying" :id="id" ></Card>
</section>
<!-- 领取按钮 -->
<section>
<van-button @click="receive" :disabled="today.isReceived" :class="{ done: isDone }">领取</van-button>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import { Mutation, State } from 'vuex-class';
import { Button, Toast } from 'vant';
import moment from 'moment';
import { HabitList as HabitListState } from '@/store/state';
import Card from '@/components/common/Card/Card.vue';
import _ from '@/utils';
@Component({
components: {
[Button.name]: Button,
Card,
},
})
export default class Library extends Vue {
@Mutation private receiveCard!: () => void;
@State private habitList!: HabitListState[];
@State
private today!: {
active: string[] | never[] | number[];
finishedDate: string[] | never[];
isReceived: boolean;
};
private title?: string;
private num!: number;
private isDone!: boolean;
private isReceived!: boolean;
private data() {
return {
saying: '自己打败自己是最可悲的失败,自己战胜自己是最可贵的胜利。',
id: 1,
num: 0,
isDone: false,
isReceived: false,
};
}
private mounted() {
const id = _.getDaysId();
this.habitList.forEach(item => {
item.habitLog.date.filter(ele => ele.id === id).forEach(e => {
if (!e.isFinished) {
this.num++;
}
});
});
if (this.num > 0) {
this.isDone = false;
} else {
this.isDone = true;
}
}
private receive() {
if (!this.isDone) {
Toast('请完成全部任务再来领取~');
} else {
const { length } = this.today.finishedDate;
const today = this.today.finishedDate[length - 1];
if (!length) {
Toast('领取成功~');
this.receiveCard();
this.$router.go(-1);
return;
}
const tip = moment(today).isSame(moment())
? '今天您已经领取过了'
: '领取成功~';
Toast(tip);
this.receiveCard();
this.$router.go(-1);
}
}
}
</script>
<style lang="scss" scoped>
@import '../../../style/mixin';
.card {
margin: 0;
height: calc(100vh - 6rem);
100vw;
display: flex;
flex-direction: column;
justify-content: space-around;
}
p {
@include font(0.8rem, 150%);
color: #c0c0c0;
}
.done {
background-color: #34ba3a;
}
</style>
接下来我们看habit页面
//srccomponentscommonHabitListList.vue
<template>
<div class="habitList">
<van-swipe-cell :right-width="65" :left-width="65" class="listSwipe">
<aside class="edit" v-if="leftValue" slot="left" @click="$emit('click-left', id)" >{{leftValue}}</aside>
<slot v-else slot="left" @click="$emit('click-right', id)" name="act"></slot>
<van-cell-group class="listGroup">
<van-cell class="van-ellipsis listCell" :url="`/edit/calendar?id=${id}`" :value="habitLog.totalHabitDays + '天'" :style="{ background: color }" >
<template slot="title">
<icon :name="iconName" />
<span>{{habitInfo.habitName}}</span>
</template>
</van-cell>
</van-cell-group>
<aside class="delete" v-if="rightValue" slot="right" @click="$emit('click-right', id)">{{rightValue}}</aside>
<slot v-else slot="right" @click="$emit('click-right', id)" name="del"></slot>
</van-swipe-cell>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Progress, Step, Steps, SwipeCell, Cell, CellGroup } from 'vant';
import { HabitList as HabitListState } from '@/store/state';
@Component({
components: {
[Progress.name]: Progress,
[Step.name]: Step,
[Steps.name]: Steps,
[SwipeCell.name]: SwipeCell,
[Cell.name]: Cell,
[CellGroup.name]: CellGroup,
},
})
export default class HabitList extends Vue {
@Prop() private habitInfo!: object;
@Prop() private habitLog!: object;
@Prop() private iconName!: string;
@Prop() private color!: string;
@Prop() private id!: number;
@Prop() private rightValue?: string;
@Prop() private leftValue?: string;
}
</script>
<style src="./style.scss" lang="scss" scoped>
</style>
//srcviewsHabitHabit.vue
<template>
<div class="habit">
<van-tabs @click="changeTitle">
<van-tab v-for="(item, index) in tabsComputed" :title="item" :key="index">
<transition-group name="fade" tag="ul" class="list-group">
<List v-for="item in ChangeTab" :key="item.id" @click-right="del(item.id)" @click-left="edit(item.id)" rightValue="归档" leftValue="编辑" :id="item.id" :color="item.color" :habitLog="item.habitLog" :habitInfo="item.habitInfo" :iconName="item.iconName" />
</transition-group>
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Tab, Tabs } from 'vant';
import { State, Mutation } from 'vuex-class';
import { HabitList as HabitListState } from '@/store/state';
import List from '@/components/common/HabitList/List.vue';
@Component({
components: {
[Tab.name]: Tab,
[Tabs.name]: Tabs,
List,
},
})
export default class Habit extends Vue {
@State private habitList!: HabitListState[];
@Mutation private deleteHabit!: (id: number) => void;
@Mutation
private changeMode!: (payload: { id: number; value: string }) => void;
private currentTitle!: string;
private data() {
return {
currentTitle: '全部',
};
}
private edit(id: number) {
this.$router.push(`/edit/habit?id=${id}`);
this.changeMode({ id, value: 'editing' });
}
private del(id: number) {
this.deleteHabit(id);
}
// 计算出全部tab标签
private get tabsComputed() {
const total: string[] = [];
(this.habitList as any).forEach((item: any) => {
item.habitInfo.timeSlotList.forEach((element: any) => {
if (item.mode === 'done') {
total.push(element.title);
}
});
});
const tabs = [...new Set(total)];
tabs.unshift('全部');
return tabs;
}
private changeTitle(index: number, title: string) {
this.currentTitle = title;
}
// 切换tab后重新计算符合当前标准的列表
private get ChangeTab() {
const total: HabitListState[] = [];
if (this.currentTitle !== '全部') {
(this.habitList as HabitListState[]).forEach((item: HabitListState) => {
if (item.isActive && item.mode === 'done') {
const { activeTimes, timeSlotList } = item.habitInfo;
// @ts-ignore
const timeSolt = timeSlotList.find(
(ele: any) => ele.id === activeTimes,
);
if (timeSolt!.title === this.currentTitle) {
total.push(item);
}
}
});
} else {
(this.habitList as HabitListState[]).forEach((item: HabitListState) => {
if (item.isActive && item.mode === 'done') {
total.push(item);
}
});
}
return total;
}
}
</script>
<style lang="scss" scoped>
@import '../../style/mixin';
.habit {
100%;
height: calc(100vh - 7rem);
display: flex;
justify-content: flex-start;
flex-direction: column;
}
</style>
//srcviewsEditRecycleRecycle.vue
<template>
<div class="recycle">
<List v-for="item in recycleList" :key="item.id" :id="item.id" :color="item.color" :habitLog="item.habitLog" :habitInfo="item.habitInfo" :iconName="item.iconName">
<aside slot="del" @click="remove(item.id)" >删除</aside><aside @click="activate(item.id)" class="act" slot="act">激活</aside>
</List>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { State, Mutation } from 'vuex-class';
import { HabitList as HabitListState } from '@/store/state';
import List from '@/components/common/HabitList/List.vue';
@Component({
components: {
List,
},
})
export default class Recycle extends Vue {
@State private habitList!: HabitListState[];
@Mutation private removeHabit!: (id: number) => void;
@Mutation private activateHabit!: (id: number) => void;
private currentTitle!: string;
public activate(id: number) {
this.activateHabit(id);
}
public remove(id: number) {
this.removeHabit(id);
}
private onClick(index: number, title: string) {
this.currentTitle = title;
}
// 获取被归档的列表
private get recycleList() {
const total: any[] = [];
(this.habitList as any).forEach((item: HabitListState) => {
if (!item.isActive) {
total.push(item);
}
});
return total;
}
}
</script>
<style lang="scss" scoped>
@import '../../../style/mixin';
.recycle {
overflow: auto;
100%;
height: calc(100vh - 7rem);
display: flex;
flex: none;
justify-content: flex-start;
flex-direction: column;
aside {
display: inline-flex;
background-color: $warn;
@include font(0.9rem);
color: #fff;
3rem;
height: 100%;
margin: 0;
justify-content: center;
align-items: center;
}
.act {
background-color: $edit;
}
}
</style>
//srcviewsNewLibraryLibrary.vue
<template>
<div class="habit">
<!-- 新建说明 -->
<section>
<p>
您可以从习惯库中挑选一个习惯,也可以新建一个新的习惯。
</p>
</section>
<!-- 新建习惯 -->
<section>
<h4>自定义习惯</h4>
<router-link :to="{path:'habit',query:{id: newHabit.id}}">
<van-cell @click="create(newHabit.id)" :value="newHabit.title">
<icon :name="newHabit.name" slot="icon" />
</van-cell>
</router-link>
</section>
<!-- 习惯库 -->
<section>
<h4>从库中挑选习惯</h4>
<van-list>
<van-cell v-for="item in habitLibrary" :key="item.id" @click="create(item.id)" :value="item.title">
<icon :name="item.name" slot="icon" />
</van-cell>
</van-list>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { List, Cell } from 'vant';
import { Mutation } from 'vuex-class';
import moment from 'moment';
import config from '@/config';
import { HabitList as HabitListState } from '@/store/state';
@Component({
components: {
[List.name]: List,
[Cell.name]: Cell,
},
})
export default class Library extends Vue {
private title?: string;
private habitLibrary!: object[];
// type Callback = () => void;
// function render(callback: Callback):string{}
@Mutation private createHabit!: (habit: HabitListState) => void;
private data() {
return {
title: this.$route.name,
habitLibrary: (config as any).habitLibrary,
newHabit: (config as any).newHabit,
};
}
private create(id: number) {
const timestamp = new Date().valueOf();
const iconInfo =
id === 0
? config.newHabit
: config.habitLibrary.find(item => item.id === id);
const habit = {
id: timestamp,
iconName: iconInfo!.name,
color: '#ffe884',
mode: 'creating',
isActive: false,
habitInfo: {
// 习惯名称
habitName: iconInfo!.title,
// 重复练习的日期
RepeatingDate: [
{ id: 0, date: '星期一', checked: true },
{ id: 1, date: '星期二', checked: true },
{ id: 2, date: '星期三', checked: true },
{ id: 3, date: '星期四', checked: true },
{ id: 4, date: '星期五', checked: true },
{ id: 5, date: '星期六', checked: true },
{ id: 6, date: '星期日', checked: true },
],
// 练习的时间段
activeTimes: 0,
timeSlotList: [
{
id: 0,
isActive: true,
title: '起床之后',
},
{
id: 1,
isActive: false,
title: '晨间习惯',
},
{
id: 2,
isActive: false,
title: '中午时分',
},
{
id: 3,
isActive: false,
title: '午间习惯',
},
{
id: 4,
isActive: false,
title: '晚间习惯',
},
{
id: 5,
isActive: false,
title: '睡觉之前',
},
{
id: 6,
isActive: false,
title: '任意时间',
},
],
// 提醒的时间
remind: [],
// 激励自己的话
inspire: '',
},
habitLog: {
// 总共坚持练习了多少天
totalHabitDays: 0,
// 当前连续联系了多少天
currentConsecutiveDays: 0,
// 历史上最多连续练习多少天
mostConsecutiveDays: 0,
// 创建日期
createdTime: moment(timestamp).format('YYYY-MM-DD'),
// 创建此习惯至今多少天
totalDays: parseInt(moment(timestamp).fromNow(true)),
// 坚持的日期
date: [],
},
};
this.createHabit(habit);
this.$router.push(`/new/habit?id=${id}`);
}
}
</script>
<style src="./style.scss" lang="scss" scoped>
</style>
//srcviewsNewHabitHabit.vue
<template>
<div class="habit" v-if="!!habitList[index]">
<!-- 习惯图标 -->
<section class="icon">
<router-link v-if="!!colorComputed" :to="{path:'/edit/icon/',query:{mode}}">
<div class="cir">
<Circles radius="3.5rem" :activeColor="colorComputed">
<icon :name="iconComputed" slot="icon" />
</Circles>
</div>
</router-link>
</section>
<!-- 输入习惯名称 -->
<section class="field">
<van-field v-model="nameComputed" placeholder="请输入名称" />
</section>
<!-- 习惯设置 -->
<section>
<van-cell-group>
<van-cell clickable is-link center @click="handleShow" title="习惯的重复" :value="dateComputed.value" />
<router-link :to="{path:'/edit/times/',query:{mode: 'new'}}">
<van-cell center title="重复的时段" :value="repeatComputed" />
</router-link>
<router-link :to="{path:'/edit/remind/',query:{mode: 'new'}}">
<van-cell center title="提醒的时间" :value="`${remindComputed}个提醒`" />
</router-link>
<van-cell center title="激励的话">
<input v-model="inspireComputed" placeholder="输入激励的话" />
</van-cell>
</van-cell-group>
<van-popup v-model="show" position="right">
<h2>选择重复的日期</h2>
<p>您希望在一周里那几天执行这个习惯?</p>
<aside>
<DateBlock v-for="(item) in dateComputed.dates" :key="item.id" :checked="item.checked" :title="item.date" @click.native="select(item.id)" />
</aside>
<van-button @click="handleShow" size="large">保存</van-button>
</van-popup>
</section>
<van-button @click="handleNew" class="button" size="large">{{mode === 'new' ? '新建' : '保存'}}</van-button>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Field, Cell, CellGroup, Popup, Button } from 'vant';
import { State, Mutation } from 'vuex-class';
import Circles from '@/components/common/Circle/Circle.vue';
import DateBlock from '@/components/common/DateBlock/DateBlock.vue';
import config from '@/config';
import _ from '@/utils';
import { HabitList as HabitListState } from '@/store/state';
@Component({
components: {
[Field.name]: Field,
[Cell.name]: Cell,
[CellGroup.name]: CellGroup,
[Popup.name]: Popup,
[Button.name]: Button,
DateBlock,
Circles,
},
})
export default class Habit extends Vue {
@State private habitList!: HabitListState[];
@Mutation
private selectDate!: (payload: { habitId: number; id: number }) => void;
@Mutation
private changeName!: (payload: { id: number; value: string }) => void;
@Mutation
private changInspire!: (payload: { id: number; value: string }) => void;
@Mutation
private changeMode!: (payload: { id: number; value: string }) => void;
private show!: boolean;
private value?: string;
private name?: string;
private habitLibrary!: object[];
private id!: number;
private index!: number;
private mode!: string;
private data() {
const id: number = parseInt((this.$route.query.id) as string, 10);
const mode = id > config.habitLibrary.length ? 'edit' : 'new';
return {
name,
value: '',
show: false,
mode,
};
}
// 获取当前习惯的id
private created() {
if (this.mode === 'edit') {
this.id = parseInt((this.$route.query.id) as string, 10);
// @ts-ignore
const Index = _.findIndex(this.habitList, this.id);
this.index = Index!;
return;
}
const list = this.habitList;
for (let index = 0; index < list.length; index++) {
const element = list[index];
if (element.mode === 'creating') {
this.id = element.id;
this.index = index;
return;
}
}
this.id = -1;
}
private get nameComputed() {
const habit = this.habitList[this.index];
return habit.habitInfo.habitName;
}
private set nameComputed(name) {
this.changeName({ id: this.id, value: name });
}
private get inspireComputed() {
const habit = this.habitList[this.index];
return habit.habitInfo.inspire;
}
private set inspireComputed(name) {
this.changInspire({ id: this.id, value: name });
}
// 计算当前图标
private get iconComputed() {
const habit = this.habitList[this.index];
return habit.iconName;
}
// 计算重复时间段
private get repeatComputed() {
const { activeTimes, timeSlotList } = this.habitList[this.index].habitInfo;
// @ts-ignore
return timeSlotList.find((item: any) => item.id === activeTimes).title;
}
// 计算提醒个数
private get remindComputed() {
const { remind } = this.habitList[this.index].habitInfo;
const num = (remind as any[]).filter((item) => item.open === true).length;
return num;
}
// 计算当前颜色
private get colorComputed() {
const habit = this.habitList[this.index];
const color = habit && habit.color ? habit.color : '#fff';
return color;
}
// 通过计算属性获取当前每周哪几天需要重复训练
private get dateComputed() {
const dates = this.habitList[this.index].habitInfo.RepeatingDate;
const currentDates = [];
let value: string = '';
for (let i = 0; i < dates.length; i++) {
if (dates[i].checked) {
currentDates.push(dates[i]);
const result = _.getDate(dates[i].date);
value += result;
}
}
return {
value,
dates,
currentDates,
};
}
// 对话框控制
private handleShow() {
this.show = !this.show;
}
// 重复的日期选择
private select(id: number) {
this.selectDate({ habitId: this.id, id });
}
// 创建此习惯
private handleNew() {
this.changeMode({ id: this.id, value: 'done' });
if (this.mode === 'edit') {
this.$router.go(-1);
return;
}
this.$router.go(-2);
}
}
</script>
<style src="./style.scss" lang="scss" scoped>
</style>
//srcviewsEditIconSettingIconSetting.vue
<template>
<div class="iconSetting">
<!-- 当前图表 -->
<section class="icon">
<Circles class="cir" radius="3.5rem" :activeColor="colorComputed">
<icon :name="iconComputed" slot="icon" />
</Circles>
</section>
<!-- 备选图标 -->
<section class="alternative">
<div class="alternativeIcon" v-for="(item, index) in iconSetting" :key="index" @click="handleIcon(item)">
<icon :name="item" />
</div>
</section>
<!-- 图标背景 -->
<section class="colorSetting">
<div class="background" v-for="(item, index) in colorSetting" :key="index" @click="handleColor(item)">
<div v-bind:style="{ backgroundColor: item }"></div>
</div>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { SwipeCell, Cell, CellGroup } from 'vant';
import { State, Mutation } from 'vuex-class';
import config from '@/config';
import Circles from '@/components/common/Circle/Circle.vue';
import { HabitList as HabitListState } from '@/store/state';
@Component({
components: {
[SwipeCell.name]: SwipeCell,
[Cell.name]: Cell,
[CellGroup.name]: CellGroup,
Circles,
},
})
export default class IconSetting extends Vue {
@State private habitList!: HabitListState[];
@Mutation
private selectColor!: (payload: { id: number; color: string }) => void;
@Mutation
private selectIcon!: (payload: { id: number; icon: string }) => void;
private id!: number;
private index!: number;
private iconSetting!: string[];
private colorSetting!: string[];
private data() {
return {
iconSetting: (config as any).iconSetting,
colorSetting: (config as any).colorSetting,
};
}
// 获取当前习惯的id
private created() {
const list = this.habitList;
for (let index = 0; index < list.length; index++) {
const element = list[index];
if (element.mode === 'creating' || element.mode === 'editing') {
this.id = element.id;
this.index = index;
return;
}
}
this.id = -1;
}
// 计算当前icon名称
private get iconComputed() {
const len = this.habitList.length;
const iconName = this.habitList[this.index].iconName;
return iconName;
}
// 计算当前背景颜色
private get colorComputed() {
const len = this.habitList.length;
const { color } = this.habitList[this.index];
return color;
}
private handleColor(color: string) {
this.selectColor({ id: this.id, color });
}
private handleIcon(name: string) {
this.selectIcon({ id: this.id, icon: name });
}
}
</script>
<style src="./style.scss" lang="scss" scoped>
</style>
//srcviewsEditTimesTimes.vue
<template>
<div class="times">
<!-- 说明文字 -->
<section class="panel">
<p>大致在一天中哪个时间段执行这个习惯呢?</p>
</section>
<!-- 删除按钮 -->
<section class="list">
<van-radio-group v-model='radio' @change="change">
<van-cell-group>
<van-cell v-for="item in timesComputed.timeSlotList" :key="item.id">
<van-radio :name="item.id">{{item.title}}</van-radio>
</van-cell>
</van-cell-group>
</van-radio-group>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Radio, Cell, CellGroup, RadioGroup } from 'vant';
import { State, Mutation } from 'vuex-class';
import { HabitList as HabitListState } from '@/store/state';
@Component({
components: {
[Radio.name]: Radio,
[Cell.name]: Cell,
[CellGroup.name]: CellGroup,
[RadioGroup.name]: RadioGroup,
},
})
export default class Calendar extends Vue {
@State private habitList!: HabitListState[];
@Mutation
private changeTimes!: (payload: { habitId: number; id: number }) => void;
private radio!: number;
private id!: number;
private index!: number;
public data() {
return {
radio: -1,
};
}
// 加载完毕后将radio重新赋值
public mounted() {
this.radio = this.timesComputed.radio;
const list = this.habitList;
for (let index = 0; index < list.length; index++) {
const element = list[index];
if (element.mode === 'creating' || element.mode === 'editing') {
this.id = element.id;
this.index = index;
return;
}
}
this.id = -1;
}
// 计算当前时间段的状态
private get timesComputed() {
const len = this.habitList.length;
const habit = this.habitList[len - 1];
const activeTimes = habit.habitInfo;
return {
timeSlotList: habit.habitInfo!.timeSlotList,
radio: habit.habitInfo.activeTimes,
};
}
// 选择时段后触发vuex进行变动
private change(id: number) {
this.changeTimes({ habitId: this.id, id });
}
}
</script>
<style src="./style.scss" lang="scss" scoped>
</style>
//srcviewsNewHabitHabit.vue
<section>
<van-cell-group>
<van-cell clickable is-link center @click="handleShow" title="习惯的重复" :value="dateComputed.value" />
<router-link :to="{path:'/edit/times/',query:{mode: 'new'}}">
<van-cell center title="重复的时段" :value="repeatComputed" />
</router-link>
<router-link :to="{path:'/edit/remind/',query:{mode: 'new'}}">
<van-cell center title="提醒的时间" :value="`${remindComputed}个提醒`" />
</router-link>
<van-cell center title="激励的话">
<input v-model="inspireComputed" placeholder="输入激励的话" />
</van-cell>
</van-cell-group>
<van-popup v-model="show" position="right">
<h2>选择重复的日期</h2>
<p>您希望在一周里那几天执行这个习惯?</p>
<aside>
<DateBlock v-for="(item) in dateComputed.dates" :key="item.id" :checked="item.checked" :title="item.date" @click.native="select(item.id)" />
</aside>
<van-button @click="handleShow" size="large">保存</van-button>
</van-popup>
</section>
//srcviewsEditRemindRemind.vue
<template>
<div class="remind">
<!-- 说明文字 -->
<section class="panel">
<p>您打算在那个时间设置提醒呢?</p>
</section>
<!-- 提醒列表 -->
<section class="list">
<van-cell-group v-if="!!remindComputed.length">
<van-cell v-for="item in remindComputed" :key="item.id">
<van-switch-cell :title="item.remind" v-model="item.isOpen" @change="change" />
</van-cell>
</van-cell-group>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { SwitchCell, Cell, CellGroup, Toast } from 'vant';
import { State, Mutation } from 'vuex-class';
import { HabitList as HabitListState } from '@/store/state';
import config from '@/config';
import { Payload } from '_vuex@3.0.1@vuex';
@Component({
components: {
[SwitchCell.name]: SwitchCell,
[Cell.name]: Cell,
[CellGroup.name]: CellGroup,
},
})
export default class Remind extends Vue {
@State private habitList!: HabitListState[];
@Mutation
private switchRemind!: (payload: { habitId: number; id: number }) => void;
private id!: number;
// 获取当前习惯的id
private mounted() {
const list = this.habitList;
for (let index = 0; index < list.length; index++) {
const element = list[index];
// 如果在编辑或者在新建那一定是当前习惯了
if (element.mode === 'creating' || element.mode === 'editing') {
this.id = element.id;
return;
}
}
this.id = -1;
}
// 计算属性得到remind相关数据
private get remindComputed() {
const len = this.habitList.length;
const habit = this.habitList[len - 1];
return habit.habitInfo.remind;
}
// 切换switch按钮的状态
private change(id: number) {
if (this.id < 0) {
Toast({
type: 'fail',
message: '可能出错了',
});
} else {
this.switchRemind({ habitId: this.id, id });
}
}
}
</script>
<style src="./style.scss" lang="scss" scoped>
</style>
//srcviewsSettingSetting.vue
<template>
<div class="setting">
<!-- 头像 -->
<section class="avatar">
<vue-lazy-component :timeout="1000">
<router-link :to="{path:'/login'}">
<div>
<img v-if="user.url" :src="user.url" alt="头像">
<icon v-else name="user" />
<p v-if="user.username">{{user.username}}</p>
</div>
</router-link>
<Skeleton slot="skeleton"/>
</vue-lazy-component>
</section>
<!-- 系统设置 -->
<section>
<van-cell-group>
<van-cell clickable title="数据备份" @click="globelSync" :is-link="false" />
<van-switch-cell v-model="setting.checked" @change="change" title="整点报时" />
</van-cell-group>
</section>
<!-- 反馈与商店 -->
<section>
<van-cell-group>
<van-cell title="主题商店" @click="handleToast" is-link />
<router-link :to="{path:'/feedback'}">
<van-cell title="给作者反馈" is-link />
</router-link>
<router-link :to="{path:'/update'}">
<van-cell title="更新日志" is-link />
</router-link>
</van-cell-group>
</section>
<section>
<van-button v-if="user.id" bottom-action @click="logout" >退出登录</van-button>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Cell, CellGroup, SwitchCell, Toast, Button } from 'vant';
import { State, Mutation, Action, Getter } from 'vuex-class';
import { SettingState, UserState } from '@/store/state';
import Skeleton from '@/components/common/Skeleton/SkeletonCircle.vue';
@Component({
components: {
[Cell.name]: Cell,
[CellGroup.name]: CellGroup,
[SwitchCell.name]: SwitchCell,
[Button.name]: Button,
Skeleton,
},
})
export default class Setting extends Vue {
@State private setting!: SettingState;
@State private user!: UserState;
@Getter private syncData: any;
@Mutation private changeHourly!: (checked: boolean) => void;
@Mutation private logoutSuccess!: () => void;
@Action private sync!: (data: any) => void;
private isOpen!: boolean;
public data() {
return {
isOpen: false,
checked: false,
};
}
private change(checked: boolean) {
this.changeHourly(checked);
}
private globelSync() {
if (!this.user.id) {
Toast('请先登录');
return;
}
this.sync({
syncData: this.syncData,
id: this.user.id,
});
if (this.user.isSync === 1) {
Toast('同步成功');
}
}
private logout() {
// 先同步再退出
this.globelSync();
this.logoutSuccess();
// 清楚本地缓存
localStorage.removeItem('vuex');
}
private handleToast() {
Toast('敬请期待!');
}
}
</script>
<style lang="scss" scoped>
@import '../../style/mixin';
section {
100%;
text-align: left;
}
.setting {
height: calc(100vh - 7rem);
100%;
display: flex;
justify-content: space-around;
align-items: center;
flex-direction: column;
.avatar {
4.5rem;
height: 4.5rem;
margin-bottom: 1rem;
img {
100%;
height: 100%;
@include borderRadius(50%);
}
}
svg {
5rem;
height: 5rem;
}
.van-button {
background-color: $warn;
}
.van-cell {
display: flex;
// justify-content: flex-start;
// flex-direction: row;
// align-items: center;
}
}
</style>
//srcviewsLoginLogin.vue
<template>
<div class="login">
<!-- 导航 -->
<section>
<van-nav-bar @click-left="onClickLeft">
<icon name="left-arrow" slot="left" />
<h3 v-if="title" slot="title">{{title}}</h3>
</van-nav-bar>
</section>
<!-- 登录 -->
<main>
<section>
<van-cell-group>
<van-field v-model="username" label="用户名" icon="clear" placeholder="请输入用户名" required @click-icon="username = ''" />
<van-field v-model="password" type="password" label="密码" placeholder="请输入密码" required />
</van-cell-group>
</section>
<van-button @click="handleLogin" size="small" type="primary">登录</van-button>
<van-loading v-if="user.isLogin===0" type="spinner" color="black" />
</main>
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import { NavBar, Field, CellGroup, Button, Toast, Loading } from 'vant';
import { Mutation, State, Action } from 'vuex-class';
import { UserState } from '@/store/state';
@Component({
components: {
[NavBar.name]: NavBar,
[Field.name]: Field,
[CellGroup.name]: CellGroup,
[Button.name]: Button,
[Loading.name]: Loading,
},
})
export default class Login extends Vue {
@Action public login!: (data: { username: string; password: string }) => void;
@Mutation private loginLoading!: () => void;
@State private user!: UserState;
private message?: string;
private title!: string;
private username!: string;
private password!: string;
public data() {
return {
username: '',
title: this.$route.name,
password: '',
};
}
@Watch('user', { immediate: true, deep: true })
private onUserChanged(val: UserState, oldVal: UserState) {
if (val.isLogin === 1) {
this.$router.go(-1);
}
}
private handleLogin() {
const { username, password } = this;
if (!username || !password) {
Toast('请输入完整的用户名和密码');
} else {
this.login({ username, password });
this.loginLoading();
}
}
private onClickLeft() {
this.$router.go(-1);
}
}
</script>
<style lang="scss" scoped>
@import '../../style/mixin';
.van-nav-bar {
height: 3.5rem;
display: flex;
justify-content: center;
align-items: center;
svg {
@include iconSize(1.4rem);
}
}
main {
section {
margin-bottom: 3rem;
}
height: calc(50vh - 6rem);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>
//srcviewsFeedbackFeedback.vue
<template>
<div class="feedback">
<!-- 导航 -->
<section>
<van-nav-bar @click-left="onClickLeft">
<icon name="left-arrow" slot="left" />
<h3 v-if="title" slot="title">{{title}}</h3>
</van-nav-bar>
</section>
<!-- 输入框 -->
<section>
<p>留言板</p>
<van-field v-model="message" type="textarea" placeholder="请输入留言" rows="4" autosize />
</section>
<!-- 确认发送 -->
<section>
<van-button :loading="loading" @click="send" size="small" >发送</van-button>
</section>
<van-cell title="给作者反馈" is-link />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Field, Button, NavBar, Toast, Cell } from 'vant';
import { State, Mutation } from 'vuex-class';
import { SettingState, UserState } from '@/store/state';
import { feedback } from '@/api/feedback';
@Component({
components: {
[Cell.name]: Cell,
[Field.name]: Field,
[Button.name]: Button,
[NavBar.name]: NavBar,
},
})
export default class Feedback extends Vue {
@State private user!: UserState;
private message?: string;
private title!: string;
private loading!: boolean;
public data() {
return {
message: '',
title: this.$route.name,
loading: false,
};
}
private onClickLeft() {
this.$router.go(-1);
}
private async send() {
if (this.message) {
const createTime = new Date().valueOf();
const res = await feedback({
content: this.message,
createTime,
username: this.user.username,
})
.then(res => res.data)
.catch((e: string) => Toast(e));
if (res.message) {
Toast(res.message);
this.$router.go(-1);
}
} else {
Toast('请补充完反馈信息');
}
}
}
</script>
<style lang="scss" scoped>
@import '../../style/mixin';
.van-nav-bar {
height: 3.5rem;
display: flex;
justify-content: center;
align-items: center;
svg {
@include iconSize(1.4rem);
}
}
</style>
//srcviewsUpdateLogUpdateLog.vue
<template>
<div class="log">
<!-- 导航 -->
<section>
<van-nav-bar @click-left="onClickLeft">
<icon name="left-arrow" slot="left" />
<h3 v-if="title" slot="title">{{title}}</h3>
</van-nav-bar>
</section>
<!-- 日志 -->
<section>
<ul>
<li>0.0.1: 初版,目前已完成90%的功能,即将可以正常使用.</li>
<li></li>
</ul>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { NavBar } from 'vant';
@Component({
components: {
[NavBar.name]: NavBar,
},
})
export default class UpdateLog extends Vue {
private message?: string;
private title!: string;
private loading!: boolean;
public data() {
return {
message: '',
title: this.$route.name,
loading: false,
};
}
private onClickLeft() {
this.$router.go(-1);
}
}
</script>
<style lang="scss" scoped>
@import '../../style/mixin';
.van-nav-bar {
height: 3.5rem;
display: flex;
justify-content: center;
align-items: center;
svg {
@include iconSize(1.4rem);
}
}
</style>
这个算是我看的第一个ts+vue的项目,因为我学过ts的语法,基本ts那部分是看的懂的,不过我看不懂业务逻辑,一些里面的内容我不明白为什么那样写。总之好好加油吧
对了这个里面学到的东西有
1.和react一样使用的是class继承的方式写组件
2.vue中使用了npm包专门用来引入组件和进行vuex部分的处理
import { Component, Vue } from 'vue-property-decorator';
import { State, Mutation } from 'vuex-class';
3.vue中即使用ts语法,也只是增加的类型修饰,逻辑部分其实同es6类似
4.我觉得vue同react相比,就是vue多了一个getter,而react是可以直接setState()进行改变数据的。
5.项目中用到了es6的装饰器语法,结合ts使用,显得技术很高端,还有父子组件传参等一系列的语法改变了。
下面是一些语法的改变,学习自博客:https://juejin.im/post/5c173a84f265da610e7ffe44
在Vue中使用TypeScript时,非常好用的一个库,使用装饰器来简化书写。
1.安装npm install --save vue-property-decorator
@Component (from vue-class-component)
@Prop
@Model
@Watch
@Emit
@Inject
@Provide
Mixins (the helper function named mixins defined at vue-class-component)
2、@Component
import {componentA,componentB} from '@/components';
export default{
components:{
componentA,
componentB,
},
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
}
import {Component,Vue} from 'vue-property-decorator';
import {componentA,componentB} from '@/components';
@Component({
components:{
componentA,
componentB,
},
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
})
export default class YourCompoent extends Vue{
}
3、@Prop 父子组件之间值的传递
export default{
props:{
propA:String, // propA:Number
propB:[String,Number],
propC:{
type:Array,
default:()=>{
return ['a','b']
},
required: true,
validator:(value) => {
return [
'a',
'b'
].indexOf(value) !== -1
}
}
}
}
import {Component,Vue,Prop} from vue-property-decorator;
@Component
export default class YourComponent extends Vue {
@Prop(String)
propA:string;
@Prop([String,Number])
propB:string|number;
@Prop({
type: String, // type: [String , Number]
default: 'default value', // 一般为String或Number
//如果是对象或数组的话。默认值从一个工厂函数中返回
// defatult: () => {
// return ['a','b']
// }
required: true,
validator: (value) => {
return [
'InProcess',
'Settled'
].indexOf(value) !== -1
}
})
propC:string;
}
4、@Model (组件之间,checkbox)
父组件中使用 v-model="checked" 子组件
<input type="checkbox" :checked="checked" @change="change">
js写法 (2.2.0+ 新增)
export default {
model:{
prop:'checked',
event:'change'
},
props:{
checked:{
type:Boolean
}
},
methods:{
change(e){
this.$emit('change', e.target.checked)
}
}
}
import {Vue,Component,Model,Emit} from 'vue-property-decorator';
@Component
export default class YourComponent extends Vue{
@Model('change',{
type:Boolean
})
checked!:boolean;
@Emit('change')
change(e:MouseEvent){}
}
5、@Watch
export default {
watch: {
'person': {
handler: 'onPersonChanged',
immediate: true,
deep: true
}
},
methods: {
onPersonChanged(val, oldVal) { }
}
}
import {Vue, Component, Watch} from 'vue-property-decorator';
@Component
export default class YourComponent extends Vue{
@Watch('person', { immediate: true, deep: true })
onPersonChanged(val: Person, oldVal: Person) { }
}
6、@Emit
由@Emit $emit 定义的函数发出它们的返回值,后跟它们的原始参数。 如果返回值是promise,则在发出之前将其解析。
如果事件的名称未通过事件参数提供,则使用函数名称。 在这种情况下,camelCase名称将转换为kebab-case。
export default {
data() {
return {
count: 0
}
},
methods: {
addToCount(n) {
this.count += n
this.$emit('add-to-count', n)
},
resetCount() {
this.count = 0
this.$emit('reset')
},
returnValue() {
this.$emit('return-value', 10)
},
promise() {
const promise = new Promise(resolve => {
setTimeout(() => {
resolve(20)
}, 0)
})
promise.then(value => {
this.$emit('promise', value)
})
}
}
}
import { Vue, Component, Emit } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
count = 0
@Emit()
addToCount(n: number) {
this.count += n
}
@Emit('reset')
resetCount() {
this.count = 0
}
@Emit()
returnValue() {
return 10
}
@Emit()
promise() {
return new Promise(resolve => {
setTimeout(() => {
resolve(20)
}, 0)
})
}
}
7、@Provide 提供 / @Inject 注入
注:父组件不便于向子组件传递数据,就把数据通过Provide传递下去,然后子组件通过Inject来获取
const symbol = Symbol('baz')
export const MyComponent = Vue.extend({
inject: {
foo: 'foo',
bar: 'bar',
'optional': { from: 'optional', default: 'default' },
[symbol]: symbol
},
data () {
return {
foo: 'foo',
baz: 'bar'
}
},
provide () {
return {
foo: this.foo,
bar: this.baz
}
}
})
import {Vue,Component,Inject,Provide} from 'vue-property-decorator';
const symbol = Symbol('baz')
@Component
export defalut class MyComponent extends Vue{
@Inject()
foo!: string;
@Inject('bar')
bar!: string;
@Inject({
from:'optional',
default:'default'
})
optional!: string;
@Inject(symbol)
baz!: string;
@Provide()
foo = 'foo'
@Provide('bar')
baz = 'bar'
}