如何从零实现一个todo list(3)

项目地址 喜欢的话 可以给我一个小小的star

一、检查所有需要补充的功能

我们已经把布局还有核心功能增加完毕,再根据导图查看有什么缺失的功能并把它列出来

  1. 事项分类列表(侧边栏)的增删改查
  2. 主界面-查看往日设置的未完成的事项-设置回今日
  3. 待办事项输入框可选项功能编辑
  4. 待办事项点击时(右边弹出抽屉形式)编辑详情
  5. 右键的菜单

最近比较忙就有空就写,加上了上面的功能 改动了亿点点代码 其实到这一步 基本的功能就做完了 还有优化的地方,放在electron版本里。

二、首先我们先对侧边栏再次进行封装

我们在对侧边栏增加分类的功能前 看功能发现还会有两个侧边栏 但不是同一个方向 又有相同的功能 随宽度的变化他是固定的还是隐藏的情况

html 复制代码
// components/Sidebar.vue
// 修改事件监听形式为 v-resize vuetify自带的指令
<template>
  <v-navigation-drawer
    v-model="drawer"
    v-resize="responseWidth"
    :temporary="isAbsolute"
    absolute
    :app="app"
    :class="{top:isAbsolute}"
    disable-resize-watcher
    mobile-breakpoint="0"
    v-bind="$attrs"
    :right="right"
    clipped
    :fixed="isAbsolute"
    color="#FAFAFA"
  >
    <slot :isAbsolute="isAbsolute"></slot>
  </v-navigation-drawer>
</template>

<script>
export default {
  props: ["right", "value", "app"],
  model: {
    prop: "value",
    event: "change",
  },
  computed: {
    // 侧边栏是否是绝对布局
    isAbsolute() {
      return this.width <= 960;
    },
    // 封装的v-model
    drawer: {
      get() {
        return this.value;
      },
      set(value) {
        this.$emit("change", value);
      },
    },
    // 是否是黑暗模式
    dark() {
      return this.$store.state.dark;
    },
  },
  data() {
    return {
      width: "",
    };
  },
  mounted() {
    // 初始化drawer是否显示 并且监听窗口宽度变化情况
    this.responseWidth();
  },
  methods: {
    // 监听窗口宽度变化
    responseWidth(event) {
      let width = window.innerWidth;
      // 右侧的小于960的时候要隐藏
      if (this.width != width && width < 960 && this.right) {
        this.drawer = false;
      }
      // 左侧的大于960要显示出来
      if (this.width != width && width > 960 && !this.right) {
        this.drawer = true;
      }
      this.width = width;
    },
  },
};
</script>

三、对菜单栏进行改动

使用封装好的侧边栏组件预留的默认插槽位置编写布局组件

修改与新增:

  1. 新增了头像栏 和菜单项的图标让菜单栏更加美观
  2. 新增了分类添加按钮
  3. 新增对非固定菜单的右键菜单
  4. 对原来的菜单数据进行修改 以id的形式 不直接以数组 防止:key重复 以及重名报错的问题

为啥不用自带的v-menu去编写? 因为我比较懒 直接使用了现成的库 vue-contextmenujs

javascript 复制代码
// 下载 cnpm i vue-contextmenujs --save
// 引入
import Vue from "vue";
import Contextmenu from "vue-contextmenujs";
Vue.use(Contextmenu);

仓库数据修改 store/index.js

javascript 复制代码
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    // 侧边栏点击的是否显示
    drawer: false,
    // 当前侧边栏选择的项
    active_key: null,
    // 侧边栏固定的目录
    fixed_menu: [
      { id: 1, name: "我的一天", affix: true, icon: "mdi-home" },
      { id: 2, name: "我的任务", affix: true, icon: "mdi-content-paste" },
    ],
    // 侧边栏可添加的目录
    sort_menu: [{ id: 3, name: "未命名列表", affix: false, icon: "mdi-menu" }],
    // 当前完整列表
    current_list: [
      {
        id: "1",
        title: "测试数据1",
        complete: false,
        create_time: "2024-03-28 16:43:00",
        category: -1,
      },
      {
        id: "2",
        title: "测试数据1",
        complete: true,
        create_time: "2024-03-28 16:43:00",
        category: -1,
      },
      {
        id: "3",
        title: "测试数据2",
        complete: false,
        create_time: "2024-03-29 16:43:00",
        category: -1,
      },
    ],
    // 编辑框的显示
    edit_drawer: false,
    // 建议框的显示
    suggest_drawer: false,
    // 编辑表单
    form: null,
    // 黑暗主题
    dark: true,
  },
  mutations: {
    // 设置侧边栏显示
    setDrawer(state, show) {
      state.drawer = show;
    },
    // 设置当前选中的侧边栏值
    setActiveKey(state, key) {
      state.active_key = key;
    },
    // 设置当前的分类目录
    setSortMenu(state, data) {
      state.sort_menu = data;
    },
    // 设置列表
    setList(state, list) {
      state.current_list = list;
    },
    // 设置编辑栏显示
    setEditDrawer(state, show) {
      state.edit_drawer = show;
    },
    // 设置编辑栏显示
    setSuggestDrawer(state, show) {
      state.suggest_drawer = show;
    },
    // 设置编辑栏显示
    setForm(state, form) {
      state.form = form;
    },
    // 设置编辑栏显示
    setDark(state, value) {
      state.dark = value;
    },
  },
  actions: {},
  modules: {},
});

Mixin修改 mixins/store.js

javascript 复制代码
import moment from "moment";
export default {
  data() {
    return {
      // 菜单选项
      deadline_options: ["今天", "明天", "下周"],
      repeat_options: ["每天", "工作日", "每周", "每月", "每年"],
    };
  },
  computed: {
    // 操作侧边栏的打开
    drawer: {
      get() {
        return this.$store.state.drawer;
      },
      set(value) {
        this.$store.commit("setDrawer", value);
      },
    },
    // 当前侧边栏选了什么
    active_key: {
      get() {
        return this.$store.state.active_key;
      },
      set(value) {
        this.$store.commit("setActiveKey", value);
      },
    },
    // 侧边栏固定的项目
    fixed_menu() {
      return this.$store.state.fixed_menu;
    },
    // 分类项目
    sort_menu: {
      get() {
        return this.$store.state.sort_menu;
      },
      set(value) {
        this.$store.commit("setSortMenu", value);
      },
    },
    // 当前列表
    current_list: {
      get() {
        return this.$store.state.current_list;
      },
      set(value) {
        this.$store.commit("setList", value);
      },
    },
    // 为了能用v-for 渲染写的列表目录 根据不同的key去获取它的待办列表和已完成列表
    lists() {
      return (item) => {
        if (!item) return [];
        switch (item.name) {
          case "我的一天":
            let current_date = moment(new Date()).format("yyyy-MM-DD");
            return [
              {
                name: "待办列表",
                data: this.current_list.filter(
                  (list_item) => !list_item.complete && moment(list_item.create_time).format("yyyy-MM-DD") == current_date
                ),
              },
              {
                name: "已完成",
                data: this.current_list.filter(
                  (list_item) => list_item.complete && moment(list_item.create_time).format("yyyy-MM-DD") == current_date
                ),
              },
            ];
          case "我的任务":
            return [
              {
                name: "待办列表",
                data: this.current_list.filter((list_item) => !list_item.complete),
              },
              {
                name: "已完成",
                data: this.current_list.filter((list_item) => list_item.complete),
              },
            ];
          default:
            return [
              {
                name: "待办列表",
                data: this.current_list.filter((list_item) => !list_item.complete && list_item.category == item.id),
              },
              {
                name: "已完成",
                data: this.current_list.filter((list_item) => list_item.complete && list_item.category == item.id),
              },
            ];
        }
      };
    },
    // 操作侧边栏的打开
    edit_drawer: {
      get() {
        return this.$store.state.edit_drawer;
      },
      set(value) {
        this.$store.commit("setEditDrawer", value);
      },
    },
    // 建议的侧边栏
    suggest_drawer: {
      get() {
        return this.$store.state.suggest_drawer;
      },
      set(value) {
        this.$store.commit("setSuggestDrawer", value);
      },
    },
    // 编辑表单
    form: {
      get() {
        return this.$store.state.form;
      },
      set(value) {
        this.$store.commit("setForm", value);
      },
    },
    dark: {
      get() {
        return this.$store.state.dark;
      },
      set(value) {
        this.$store.commit("setDark", value);
      },
    },
    // 可选菜单列表
    sortOptions() {
      let options = [{ label: "任务", value: -1, icon: "mdi-content-paste" }].concat(
        this.sort_menu.map((item) => {
          return {
            label: item.name,
            value: item.id,
            icon: item.icon,
          };
        })
      );
      return options;
    },
    // 显示subtitle 所属分类用的字典 因为绑定的是id
    menusMap() {
      let map = {};
      this.sortOptions.forEach((item) => {
        map[item.value] = item.label;
      });
      return map;
    },
  },
};

侧边栏改动

引入sidebar组件 并且新增 增删改事件方法 并且加入右键菜单的方法

html 复制代码
<template>
  <side-bar
    v-model="drawer"
    :app="true"
    #default="{isAbsolute}"
  >
    <v-app-bar-nav-icon
      @click="drawer=!drawer"
      v-if="isAbsolute"
    ></v-app-bar-nav-icon>
    <v-list v-if="active_key">
      <v-list-item>
        <v-list-item-avatar>
          <v-img src="https://pic.imeitou.com/uploads/allimg/2019090208/j1lm1xn2ibf.jpg"></v-img>
        </v-list-item-avatar>
        <v-list-item-content>
          <v-list-item-title class="text-h6">
            羊驼
          </v-list-item-title>
        </v-list-item-content>
      </v-list-item>
      <v-list-item
        class="list-item text-h6"
        v-for="(item,index) in menus"
        :key="index"
        :class="{'active':item.id==active_key.id}"
        @click="changeKey(item)"
        @contextmenu.prevent="(event)=>{onContextmenu(event,item)}"
      >
        <v-list-item-icon>
          <v-icon>{{item.icon}}</v-icon>
        </v-list-item-icon>
        <v-list-item-content>
          <span
            v-if="!item.edit"
            @dblclick="rename(item)"
          > {{item.name}}</span>
          <v-text-field
            v-if="item.edit"
            v-model="item.name"
            dense
            autofocus
            @blur="checkInput(item)"
          />
        </v-list-item-content>
        <v-list-item-action>
          <v-chip
            class="ml-4"
            v-if="lists(item)[0].data.length"
          >{{lists(item)[0].data.length}}</v-chip>
        </v-list-item-action>
      </v-list-item>
      <v-divider></v-divider>
    </v-list>
    <v-btn
      block
      text
      @click="pushMenu"
    >
      <v-icon left>
        mdi-plus
      </v-icon>
      新建列表</v-btn>
  </side-bar>
</template>

<script>
import SideBar from "./SideBar.vue";
import mixin from "@/mixins/store";
import { uuid } from "vue-uuid";
export default {
  mixins: [mixin],
  components: { SideBar },
  computed: {
    // 目录列表
    menus() {
      return this.fixed_menu.concat(this.sort_menu);
    },
  },
  mounted() {
    if (!this.active_key) {
      this.active_key = this.fixed_menu[0];
    }
  },
  methods: {
    // 切换侧边栏选中项 如果小于960 代表是在移动端的情况下 要把侧边栏收起来
    changeKey(key) {
      this.active_key = key;
      if (this.width < 960) {
        this.drawer = false;
      }
    },
    // 增加分类
    pushMenu() {
      this.sort_menu.push({
        id: uuid.v4(),
        name: "未命名列表",
        affix: false,
        edit: true,
        icon: "mdi-menu",
      });
    },
    // 检查输出结果
    checkInput(item) {
      let name_set = new Set();
      let exist = false;
      this.sort_menu.forEach((list_item) => {
        name_set.add(list_item.name);
        if (list_item != item && list_item.name == item.name) {
          exist = true;
        }
      });
      let [name, i] = [item.name, 1];
      while (exist) {
        name = `${item.name}(${i++})`;
        exist = name_set.has(name);
      }
      item.name = name;

      item.edit = false;
      this.$forceUpdate();
    },
    // 重命名
    rename(item) {
      if (item.affix) return;
      item.edit = true;
      this.$forceUpdate();
    },
    // 右键菜单
    onContextmenu(event, item) {
      if (item.affix) return;
      this.$contextmenu({
        items: [
          {
            label: "重命名",
            icon: "v-icon notranslate mdi mdi-pencil",
            onClick: () => {
              this.rename(item);
            },
          },
          {
            label: "删除",
            icon: "v-icon notranslate mdi mdi-close",
            onClick: () => {
              this.deleteMenu(item);
            },
          },
        ],
        event, // 鼠标事件信息
        customClass: "custom-class", // 自定义菜单 class
        zIndex: 999, // 菜单样式 z-index
        minWidth: 230, // 主菜单最小宽度
      });
      return false;
    },
    // 删除菜单
    deleteMenu(item) {
      // 清除存在分类中的事项
      this.current_list.forEach((list_item) => {
        if (list_item.category == item.id) {
          list_item.category = "";
        }
      });
      let index = this.sort_menu.findIndex((x) => x.id == item.id);
      index != -1 && this.sort_menu.splice(index, 1);
      // 切换选中的栏目
      if (this.active_key.id == item.id) {
        this.active_key = this.fixed_menu[0];
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.list-item {
  cursor: pointer;
  &:hover {
    background-color: #eee;
  }
  &.active {
    background: #eee;
  }
}
</style>

四、对任务列表进行修改

修改与新增:

  1. 新增待办事项按钮菜单设置功能
  2. 将待办子项抽出变成单独的组件并且添加上右键菜单
  3. 新增完成事项与待办事项为空的UI

新增输入时显示配置按钮

我们想要的是一个tooltip+v-menu的组合 所以需要封装一个组件MenuButton.vue

这里涉及到了一个插槽同名叠加的问题 通过官方的示例去解决

xml 复制代码
// components/MenuButton
<template> html
  <!-- 注意两个插槽同名事件要写入同一个按钮的话 记得{...插槽数据1,插槽数据2} -->
  <v-menu
    offset-y
    :dark="dark"
  >
    <template v-slot:activator="{attrs:toolAttr,on:toolOn}">
      <v-tooltip top>
        <template v-slot:activator="{ on, attrs }">
          <v-btn
            v-bind="{...toolAttr,...attrs}"
            v-on="{...on,...toolOn}"
            text
            dark
          >
            <v-icon left>
              {{icon}}
            </v-icon>
            {{model&&model.label||model}}
          </v-btn>
        </template>
        <span>{{tips}}</span>
      </v-tooltip>
    </template>
    <v-list
      dense
      :dark="dark"
    >
      <v-list-item
        class="pr-10"
        v-for="(item, index) in options"
        :key="index"
        @click="model=item"
      >
        <v-list-item-icon v-if="item.icon||item_icon">
          <v-icon>{{item.icon||item_icon}}</v-icon>
        </v-list-item-icon>
        <v-list-item-title>{{ item&&item.label||item }}</v-list-item-title>
      </v-list-item>
      <v-list-item
        @click="model='无'"
        v-if="model!='无'&&append"
      >
        <v-list-item-title class="red--text">{{append}}</v-list-item-title>
      </v-list-item>
    </v-list>
  </v-menu>

</template>

<script>
export default {
  props: ["options", "icon", "tips", "value", "item_icon", "append", "dark"],
  model: {
    prop: "value",
    event: "input",
  },
  computed: {
    model: {
      get() {
        return this.value;
      },
      set(value) {
        this.$emit("input", value);
      },
    },
  },
};
</script>

将任务项变成组件的形式 后续在添加建议栏的时候会用到 能复用的情况下最好抽成组件 但是也不要过度组件化

新增components/TaskItem.vue

功能一览

  1. 右键菜单 用于删除 标记完成 移动分类
  2. 被点击时 弹出编辑框
  3. 在不同地方显示时 右侧图标有不同功能
  4. 重新设置回今天

删除自身的功能

html 复制代码
<template>
  <v-list-item
    class="task-item mb-4"
    :class="{complete:item.complete}"
    @contextmenu.prevent="(event)=>{onContextmenu(event,item)}"
    @click="selectItem(item)"
  >
    <v-list-item-avatar>
      <v-checkbox
        v-model="item.complete"
        :dark="dark"
      />
    </v-list-item-avatar>
    <v-list-item-content>
      <v-list-item-title class="text-subtitle-1">{{item.title}}</v-list-item-title>
      <v-list-item-subtitle>{{menusMap[item.category]||'任务'}}</v-list-item-subtitle>
    </v-list-item-content>
    <v-list-item-action>
      <template v-if="mode=='delete'">
        <v-icon
          v-if="!item.complete"
          @click="deleteItem(item.id)"
        >mdi-close</v-icon>
      </template>
      <template v-else-if="mode=='add'">
        <v-icon
          v-if="!item.complete"
          @click="restoreItem(item)"
        >mdi-plus</v-icon>
      </template>
    </v-list-item-action>
  </v-list-item>
</template>

<script>
import mixin from "@/mixins/store";
import moment from "moment";
export default {
  mixins: [mixin],
  props: {
    value: {
      type: Object,
    },
    mode: {
      type: String,
      default: "delete",
    },
  },
  model: {
    event: "input",
    prop: "value",
  },
  computed: {
    item: {
      get() {
        return this.value;
      },
      set(value) {
        this.$emit("input", value);
      },
    },
  },
  methods: {
    // 删除事项
    deleteItem(id) {
      this.current_list.splice(
        this.current_list.findIndex((item) => item.id == id),
        1
      );
      if (this.form.id == id) {
        this.edit_drawer = false;
      }
    },
    // 右键菜单
    onContextmenu(event, item) {
      if (item.affix) return;
      let items = [
        {
          label: !item.complete ? "标记为完成" : "标记为未完成",
          icon: `v-icon notranslate mdi ${
            item.complete ? "mdi-circle-outline" : "mdi-check"
          }`,
          onClick: () => {
            item.complete = !item.complete;
          },
        },
        {
          label: "删除",
          icon: "v-icon notranslate mdi mdi-close",
          onClick: () => {
            this.deleteItem(item.id);
          },
        },
      ];
      if (this.sortOptions.length > 1) {
        items.splice(1, 0, {
          label: "移动",
          icon: `v-icon notranslate mdi mdi-apps`,
          children: this.sortOptions
            .filter((sort) => sort.value != item.category)
            .map((sort) => {
              return {
                label: sort.label,
                icon: `v-icon notranslate mdi ${sort.icon}`,
                onClick: () => {
                  item.category = sort.value;
                },
              };
            }),
        });
      }
      this.$contextmenu({
        items,
        event, // 鼠标事件信息
        customClass: "custom-class", // 自定义菜单 class
        zIndex: 999, // 菜单样式 z-index
        minWidth: 230, // 主菜单最小宽度
      });
      return false;
    },
    // 选择要编辑的todo
    selectItem(item) {
      // 深拷贝对象
      this.form = item;
      this.edit_drawer = true;
    },
    // 修改创建时间为今天
    restoreItem(item) {
      item.create_time = moment(new Date()).format("yyyy-MM-DD HH:mm:ss");
    },
  },
};
</script>

修改任务列表

将定义的好的组件加回去里面 并且清除掉以前没用的方法 并且添加空提示 去掉了重复的代码 增加了可读性 剩下一些关于提示重复的功能会放在electron版本

html 复制代码
// components/TaskList.vue
<template>
  <div class="py-4 pr-8 px-6">
    <v-text-field
      v-model="todo"
      placeholder="请输入待办事项"
      label="待办事项"
      outlined
      single-line
      persistent-placeholder
      dense
      prepend-inner-icon="mdi-plus"
      :dark="dark"
      clearable
      class="todo"
      @keydown.enter="addTodo"
    >
    </v-text-field>
    <div v-if="todo&&todo.length">
      <div class="mb-2 title pl-2">待办设置</div>
      <div>
        <!-- 分类设置 -->
        <menu-button
          icon="mdi-menu"
          v-model="category"
          :options="sortOptions"
          tips="分类设置"
          v-if="active_key&&[1,2].includes(active_key.id)"
          :dark="dark"
        />
        <!-- 截止日期设置 -->
        <menu-button
          v-model="deadline"
          :options="deadline_options"
          tips="截止日期"
          item_icon="mdi-calendar-month"
          icon="mdi-calendar-month"
          append="删除截止日期"
          :dark="dark"
        />
        <!-- 提醒日期 -->
        <menu-button
          v-model="remind_time"
          :options="deadline_options"
          tips="提醒日期"
          item_icon="mdi-calendar-month"
          icon="mdi-clock-alert"
          append="删除提醒日期"
          :dark="dark"
        />
        <!--  -->
        <menu-button
          v-model="repeat_type"
          :options="repeat_options"
          tips="重复"
          item_icon="mdi-calendar-month"
          icon="mdi-calendar-refresh"
          append="从不重复"
          :dark="dark"
        />
      </div>
    </div>
    <!-- 任务列表 -->
    <v-list
      dense
      color="transparent"
      :dark="dark"
    >
      <div
        v-for="
          list
          in
          tasks"
        :key="list.name"
      >
        <template v-if="list.data.length">
          <v-subheader class="text-h6 mb-2 title">{{list.name}}<v-chip
              small
              class="ml-2"
            >{{list.data.length}}</v-chip></v-subheader>
          <task-item
            v-for="(item,index) in list.data"
            :key="item.id"
            v-model="list.data[index]"
          />
        </template>
      </div>
    </v-list>
    <div
      v-if="isEmpty"
      class="v-empty"
    >
      <v-card
        max-width="344"
        color="transparent"
        flat
      >
        <v-card-text>
          <v-img
            class="mx-auto"
            width="80"
            height="80"
            :src="require('@/assets/empty.png')"
          ></v-img>
          <div class="title mt-4">待办列表为空</div>
        </v-card-text>
      </v-card>
    </div>
  </div>
</template>

<script>
import { uuid } from "vue-uuid";
import mixin from "@/mixins/store";
import moment from "moment";
import MenuButton from "./MenuButton.vue";
import SideBar from "./SideBar.vue";
import TaskItem from "./TaskItem.vue";
export default {
  components: { MenuButton, SideBar, TaskItem },
  mixins: [mixin],
  data() {
    return {
      // 待办事项输入值
      todo: "",
      // 追加功能
      defaultCategory: { label: "任务", value: -1, icon: "mdi-content-paste" },
      // 分类
      category: null,
      // 截止日期
      deadline: "无",
      // 提醒时间
      remind_time: "无",
      // 重复类型
      repeat_type: "无",
    };
  },
  computed: {
    // 任务列表
    tasks() {
      return this.lists(this.active_key);
    },
    // 是否是空的
    isEmpty() {
      return this.tasks.every((x) => !x.data.length);
    },
  },
  mounted() {
    this.category = this.defaultCategory;
  },
  methods: {
    // 新增todolist
    addTodo() {
      if (!this.todo) return;
      let category = this.category.value;
      if (![1, 2].includes(this.active_key.id)) {
        category = this.active_key.id;
      }
      this.current_list.push({
        // 增加id是因为使用了computed 它的index是不准的
        id: uuid.v4(),
        title: this.todo,
        complete: false,
        create_time: moment(new Date()).format("yyyy-MM-DD HH:mm:ss"),
        category,
        deadline: "",
        remind_time: "",
        repeat_type: "",
        remark: "",
      });
      this.todo = "";
      this.category = this.defaultCategory;
      // TODO 处理接收的所有参数
    },
  },
};
</script>

<style lang="scss" scoped>
.v-empty {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}
.todo::v-deep input {
  margin-top: 1px;
}
</style>

五、新增右侧编辑栏

我们之前在store里加的form就派上用场了 因为object是地址引用的关系 所以唤醒时把任务项的数据源指定给Form即可 和之前一样 我们使用sidebar组件 并且要指定为右边 是不是app的模式 取决于右边打开了几个抽屉 不然右侧两个app的话 v-main布局的偏移宽度会计算两次

还要保证名称不能被清空 清空的时候要还原回去

html 复制代码
// components/EditBar.vue
<template>
  <!-- 编辑抽屉 -->
  <side-bar
    v-model="edit_drawer"
    :right="true"
    width="350px"
    class="edit-drawer"
    :app="suggest_drawer?false:true"
  >
    <div class="d-flex justify-space-between mt-12 mt-md-4 px-4 pr-2">
      <span class="text-h6">事项编辑</span>
      <v-btn
        text
        fab
        x-small
        @click="edit_drawer=false"
      > <v-icon>mdi-close</v-icon></v-btn>
    </div>
    <v-form
      v-if="form"
      class="px-6"
    >
      <div class="d-flex align-center mb-4">
        <v-checkbox v-model="form.complete" />
        <v-text-field
          v-model="form.title"
          dense
          hide-details
          outlined
          label="待办事项"
          @focus="getCacheName"
          @blur="checkName"
        />
      </div>
      <v-select
        v-model="form.category"
        :items="sortOptions"
        item-text="label"
        item-value="value"
        dense
        label="分类设置"
        outlined
        persistent-placeholder
      />
      <v-select
        v-model="form.deadline"
        :items="deadline_options"
        clearable
        dense
        label="截止日期"
        outlined
        persistent-placeholder
      />
      <v-select
        v-model="form.remind_time"
        :items="deadline_options"
        clearable
        dense
        label="提醒日期"
        persistent-placeholder
        outlined
      />
      <v-select
        v-model="form.repeat_type"
        :items="repeat_options"
        clearable
        dense
        label="重复"
        persistent-placeholder
        outlined
      />
      <v-textarea
        v-model="form.remark"
        label="备注"
        clearable
        persistent-placeholder
        outlined
        dense
        no-resize
      />
    </v-form>
  </side-bar>
</template>

<script>
import SideBar from "./SideBar.vue";
import mixin from "@/mixins/store";
export default {
  mixins: [mixin],
  components: { SideBar },
  data() {
    return {
      // 防止清空item的名称
      cache_name: "",
    };
  },
  methods: {
    // 缓存名称
    getCacheName(event) {
      this.cache_name = event.target.value;
    },
    // 检查名称是否被清空
    checkName() {
      if (!this.form.title) {
        this.form.title = this.cache_name;
        this.cache_name = "";
      }
    },
  },
};
</script>

六、新增右侧建议栏

建议栏的功能就是把所有已经添加过的但是未完成的添加回今天的一天的功能 所以没有什么很多的功能 唯一注意的是 层级要在编辑栏之下 所以当点击建议处于编辑的情况下 要关闭编辑栏

html 复制代码
//components/SuggestBar.vue
<template>
  <side-bar
    :right="true"
    v-model="suggest_drawer"
    width="350px"
    :app="true"
  >
    <div class="d-flex justify-space-between  mt-12 mt-md-4  px-4 pr-2">
      <span class="text-h6">建议</span>
      <v-btn
        text
        fab
        x-small
        @click="suggest_drawer=false"
      > <v-icon>mdi-close</v-icon></v-btn>
    </div>
    <v-list>
      <div
        v-for="(value,key) in unComplete"
        :key="key"
      >
        <v-subheader>{{key}}</v-subheader>
        <task-item
          v-for="(item,index) in value"
          :key="item.id"
          mode="add"
          v-model="value[index]"
        />
      </div>
    </v-list>
  </side-bar>
</template>

<script>
import SideBar from "./SideBar.vue";
import mixin from "@/mixins/store";
import moment from "moment";
import TaskItem from "./TaskItem.vue";
export default {
  mixins: [mixin],
  components: { SideBar, TaskItem },
  computed: {
    unComplete() {
      let today = moment().format("yyyy-MM-DD");
      let unCompelete = this.current_list.filter((item) => {
        return (
          !item.complete &&
          moment(item.create_time).format("yyyy-MM-DD") != today
        );
      });
      let dates = {};
      unCompelete = unCompelete.sort((a, b) =>
        moment(a.create_time).isBefore(b.create_time) ? 1 : -1
      );
      unCompelete.forEach((item) => {
        let date = moment(item.create_time).format("yyyy-MM-DD dddd");
        if (!dates[date]) {
          dates[date] = [];
        }
        dates[date].push(item);
      });
      return dates;
    },
  },
  watch: {
    suggest_drawer(value) {
      if (value) {
        this.edit_drawer = false;
      }
    },
  },
};
</script>

七、App.vue布局调整

修改上方的系统栏加上app 与window 标签 与布局符合 加入修改的三个侧边栏

当在我的一天的时候显示今天的日期与建议栏的唤醒图标

html 复制代码
// APP.vue
<template>
  <v-app :class="{dark}">
    <v-system-bar
      color="#EFEFEF"
      height="36px"
      window
      app
    >
      <v-icon>mdi-infinity</v-icon>
      <div class="app-name">Yangtuo To Do</div>
      <v-spacer></v-spacer>
      <v-icon>mdi-window-minimize</v-icon>
      <v-icon>mdi-window-maximize</v-icon>
      <v-icon>mdi-close</v-icon>
    </v-system-bar>
    <!-- 这里使用我们自己封装的drawer 要根据页面宽度去解决侧边栏常驻 -->
    <left-bar />
    <!-- 编辑栏 -->
    <edit-bar />
    <!-- 建议栏 -->
    <suggest-bar />
    <v-main v-if="active_key">
      <v-toolbar
        :dark="dark"
        color="transparent"
        flat
        :height="active_key.id==1?'120px':'84px'"
        class="pt-2 pr-4"
      >
        <v-toolbar-title>
          <v-app-bar-nav-icon
            @click="drawer=!drawer"
            class="d-sm-flex d-md-none"
          ></v-app-bar-nav-icon>
          <div class="pl-3 text-h5">{{active_key&&active_key.name}}</div>
          <div
            class="pl-3 mt-2 date"
            v-if="active_key.id==1"
          >{{today}}</div>
        </v-toolbar-title>
        <v-spacer></v-spacer>
        <v-btn
          text
          fab
          small
          v-if="active_key&&active_key.id==1"
          @click="suggest_drawer=!suggest_drawer"
        >
          <v-icon>mdi-lightbulb-outline</v-icon>
        </v-btn>

      </v-toolbar>

      <task-list />

    </v-main>
  </v-app>
</template>

<script>
import TaskList from "./components/TaskList.vue";
import mixin from "@/mixins/store";
import LeftBar from "./components/LeftBar.vue";
import SuggestBar from "./components/SuggestBar.vue";
import EditBar from "./components/EditBar.vue";
import moment from "moment";
moment.locale("zh-cn");

export default {
  mixins: [mixin],
  name: "App",
  components: {
    TaskList,
    LeftBar,
    SuggestBar,
    EditBar,
  },
  computed: {
    today() {
      return moment().format("M月D日 dddd");
    },
  },
};
</script>,

<style lang="scss">
.task-item {
  background-color: #eee;
  border-radius: 10px;
  cursor: pointer;
  &:hover {
    background-color: #ccc;
  }
}
.task-item.complete {
  .v-list-item__title,
  .v-list-item__subtitle {
    text-decoration: line-through;
    color: #ccc;
  }
}
.menu_item_icon {
  margin-right: 20px !important;
}
.edit-drawer {
  z-index: 1001 !important;
}
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  width: 100vw;
  height: 100vh;
  overflow-y: auto;
  overflow-x: hidden;
  background-color: #6478c7;
  .v-system-bar {
    z-index: 1002;
  }
  .menu_item_label {
    margin-left: 10px !important;
    font-size: 16px;
  }
  .app-name {
    font-size: 16px;
    user-select: none;
  }
  &.dark {
    .todo .v-input__slot {
      background-color: #fff !important;
      input {
        color: #000 !important;
        &::placeholder {
          color: #ccc !important;
        }
      }
      .v-icon,
      .white-text {
        color: #616161 !important;
      }
    }
    .title {
      color: #fff;
    }
    $subtitle: #616161;
    .task-item {
      background-color: #f6f6f6;
      color: #717171;
      .v-list-item__subtitle {
        color: $subtitle;
      }
      .v-input--selection-controls__input .v-icon {
        color: $subtitle !important;
      }
      .v-icon {
        color: $subtitle !important;
      }
    }
  }
}
</style>

到这里其实不用加数据库 用electron包一包 加上vuex的持久化数据操作 就可以实现 一个应用版本的todolist 下一个版本我会做成electron版本一点点优化 为什么经常有大量代码修改 因为有空就写一点 所以只是对功能定了方向 实际上的结构规划没有定义的很清晰

相关推荐
GISer_Jing6 分钟前
前端算法实战:大小堆原理与应用详解(React中优先队列实现|求前K个最大数/高频元素)
前端·算法·react.js
写代码的小王吧2 小时前
【安全】Web渗透测试(全流程)_渗透测试学习流程图
linux·前端·网络·学习·安全·网络安全·ssh
小小小小宇2 小时前
CSS 渐变色
前端
snow@li3 小时前
前端:开源软件镜像站 / 清华大学开源软件镜像站 / 阿里云 / 网易 / 搜狐
前端·开源软件镜像站
小小小小宇3 小时前
配置 Gemini Code Assist 插件
前端
one 大白(●—●)3 小时前
前端用用jsonp的方式解决跨域问题
前端·jsonp跨域
刺客-Andy3 小时前
前端加密方式 AES对称加密 RSA非对称加密 以及 MD5哈希算法详解
前端·javascript·算法·哈希算法
前端开发张小七4 小时前
13.Python Socket服务端开发指南
前端·python
前端开发张小七4 小时前
14.Python Socket客户端开发指南
前端·python
ElasticPDF-新国产PDF编辑器4 小时前
Vue 项目 PDF 批注插件库在线版 API 示例教程
前端·vue.js·pdf