这是使用vuetify3可以开发的一个待办事项管理实例。
它主要包含事项概览、我的项目、我的团队。其中事项概览中以列表的方式列出所有的事项,在这里可以添加事项、对事项进行排序。

vuetify官网的组件部分有大量控件,我想绝大部分应用使用这些控件拼凑就足够酷了。 一个快捷的方式是:到此官网找控件,把代码拷贝出来,修改一下直接使用。

请注意: 事项数据是存储在google的firebase中的,如果访问不便,可以使用代码中也提供的本地数据。 firebase界面

新建项目

可以使用之前的文章:用最快捷的方法创建vuetify3项目 的方法新建项目。

事项概览

事项概览是使用栅格(Grid)实现的,当然也可以用数据表格(Data tables)实现。使用栅格(Grid)对格式的控制可以更灵活一些。
可以在 veutify官网 选择一款栅格(Grid),可以在网页上直接查看实际效果,也可以直接把代码拷贝出来使用。
选择栅格(Grid)控件

修改 pages 文件夹中的 index.vue,代码如下:

<template>
  <h1 class="text-grey">事项概览</h1>

  <v-container fluid class="my-5">
    <v-layout row justify-start class="mb-3">
      <v-btn small flat color="grey" @click="sortBy('title')">
        <v-icon left small>mdi-folder</v-icon>
        <span class="text-lowercase">按事项名称</span>
        <v-tooltip activator="parent" location="top"
          ><span>按事项名称重新排序</span></v-tooltip
        >
      </v-btn>
      <v-btn small flat color="grey" @click="sortBy('person')" class="ml-3">
        <v-icon small left>mdi-account</v-icon>
        <span class="text-lowercase">按人员</span>
        <v-tooltip activator="parent" location="top"
          ><span>按事项负责人重新排序</span></v-tooltip
        >
      </v-btn>
    </v-layout>

    <v-card flat v-for="project in projects" :key="project.title">
      <v-layout :class="`pa-3 project ${project.status}`">
        <v-row wrap>
          <v-col cols="6">
            <div class="text-caption text-grey">事项名称</div>
            <div>{{ project.title }}</div>
          </v-col>
          <v-col cols="2">
            <div class="text-caption text-grey">负责人</div>
            <div>{{ project.person }}</div>
          </v-col>
          <v-col cols="2">
            <div class="text-caption text-grey">期限</div>
            <div>{{ project.due }}</div>
          </v-col>
          <v-col cols="2">
            <div class="text-caption text-grey">状态</div>
            <v-chip :class="`${project.status} text-white my-2`">{{
              project.status
            }}</v-chip>
          </v-col>
        </v-row>
      </v-layout>
      <v-divider />
    </v-card>
  </v-container>
</template>

<script setup>
import { ref } from "vue";

// 这是静态数据,请根据实际情况修改
/*
const projects = ref([
  {
    title: "设计网站的新主题",
    person: "火云",
    due: "2024-3-1",
    status: "overdue",
    content:
      "为小微企业客户XXX设计一个新的网站主题,包括首页,产品页,关于页等多个页面。",
  },
  {
    title: "实现首页的产品展示",
    person: "八戒",
    due: "2024-3-10",
    status: "complete",
    content:
      "编码实现首页的产品展示功能。",
  },
  {
    title: "实现二级页面的产品展示",
    person: "悟空",
    due: "2024-3-20",
    status: "ongoing",
    content:
      "编码实现网站的二级页面的产品展示功能。",
  },
  {
    title: "产品测试",
    person: "悟净",
    due: "2024-4-2",
    status: "overdue",
    content:
      "测试产品的新功能,确保产品的稳定性。",
  },
]);
*/

import { getAllProjects } from "@/fb";

const projects = ref([]);

const getAll = () => {
  getAllProjects().then((querySnapshot) => {
    querySnapshot.forEach((doc) => {
      // doc.data() is never undefined for query doc snapshots
      console.log(doc.id, " => ", doc.data());
      projects.value.push(doc.data());
    });
  });
};

getAll();

const sortBy = (prop) => {
  projects.value.sort((a, b) => (a[prop] < b[prop] ? -1 : 1));
};
</script>

<style scoped>
.project.complete {
  border-left: 4px solid #3cd1c2;
}
.project.ongoing {
  border-left: 4px solid #ffaa2c;
}
.project.overdue {
  border-left: 4px solid #f83e70;
}
.v-chip.complete {
  background: #3cd1c2;
}
.v-chip.ongoing {
  background: #ffaa2c;
}
.v-chip.overdue {
  background: #f83e70;
}
</style>

这里使用getAllfirebase中加载数据,如果使用firebase不方便,可以直接使用const projects定义的数据。

我的项目

component文件夹中添加projects.vue
此处使用 计算属性:myProjects 筛选当前用户的事项,它使用v-expansion-panels事项条目列表,点击每一行可以展开显示详情。

<template>
  <h1 class="text-grey">项目</h1>
  <div class="ma-8">
    <v-expansion-panels variant="accordion">
      <v-expansion-panel v-for="project in myProjects" :key="project.title">
        <v-expansion-panel-title>{{ project.title }}</v-expansion-panel-title>
        <v-expansion-panel-text>
          <v-card>
            <v-card-text class="px-4 text-grey">
              <div class="font-weight-bold">期限 {{ project.due }}</div>
              <div>{{ project.content }}</div>
            </v-card-text>
          </v-card>
        </v-expansion-panel-text>
      </v-expansion-panel>
    </v-expansion-panels>
  </div>
</template>

<script setup>

import { ref,computed } from "vue";

// 这是静态数据,请根据实际情况修改
/*
const projects = ref([
  {
    title: "设计网站的新主题",
    person: "火云",
    due: "2024-3-1",
    status: "overdue",
    content:
      "为小微企业客户XXX设计一个新的网站主题,包括首页,产品页,关于页等多个页面。",
  },
  {
    title: "实现首页的产品展示",
    person: "八戒",
    due: "2024-3-10",
    status: "complete",
    content:
      "编码实现首页的产品展示功能。",
  },
  {
    title: "实现二级页面的产品展示",
    person: "悟空",
    due: "2024-3-20",
    status: "ongoing",
    content:
      "编码实现网站的二级页面的产品展示功能。",
  },
  {
    title: "产品测试",
    person: "悟净",
    due: "2024-4-2",
    status: "overdue",
    content:
      "测试产品的新功能,确保产品的稳定性。",
  },
]);
*/

import { getAllProjects } from "@/fb";

const projects = ref([]);

const getAll = () => {
  getAllProjects().then((querySnapshot) => {
    querySnapshot.forEach((doc) => {
      // doc.data() is never undefined for query doc snapshots
      console.log(doc.id, " => ", doc.data());
      projects.value.push(doc.data());
    });
  });
};

getAll();

//自动筛选项目
const myProjects = computed(() => {
  return projects.value.filter((project) => project.person === "火云");
});

</script>

我的团队

component文件夹中添加team.vue。 此页面用于显示团队内容,它使用栅格(Grid)做整体模具,每一个格子里面用一个v-card显示一个成员信息。

<template>
  <h1 class="text-grey">团队</h1>
  <v-container class="my-5">
    <v-row wrap>
      <v-col xs12 sm6 md4 lg3 v-for="person in team" :key="person.name">
        <v-card flat class="text-center ma-3" elevation="16">
          <v-responsive class="pt-4">
            <v-avatar size="100" color="grey">
              <img :src="person.avatar" />
            </v-avatar>
          </v-responsive>
          <v-card-text>
            <div class="text-subtitle-1">{{ person.name }}</div>
            <div class="text-grey">{{ person.role }}</div>
          </v-card-text>
          <v-card-actions>
            <v-btn flat color="grey">
              <v-icon>mdi-message</v-icon>
              <span>消息</span>
            </v-btn>
          </v-card-actions>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<script setup>
const team = [
  { name: "玄奘", role: "客户经理", avatar: "/image/avatar-1.png" },
  { name: "悟空", role: "主程", avatar: "/image/avatar-2.png" },
  { name: "八戒", role: "程序员", avatar: "/image/avatar-3.png" },
  { name: "悟净", role: "程序员", avatar: "/image/avatar-4.png" },
  { name: "小白龙", role: "测试", avatar: "/image/avatar-5.png" },
];
</script>

头像放在了public文件夹内,这样是为了方便javascript使用。


到此为止,主要页面都已经准备好了,下面再用一些其它控件把这些页面串联起来。


添加事项控件

component中新增Popup.vue。 这里使用v-dialog弹窗实现添加事项的主要逻辑功能,当然,其中也展示了:

  1. 前端数据校验以及提交项目信息时的提交按钮遮罩效果;
  2. 日历控件的使用以及日期的格式化;
  3. 在提交后发出projectAdded事件,订阅者可以根据此事件做个性化处理;
  4. 默认情况下只显示 添加事项 按钮,对话框默认情况下不显示,这样下一步该控件放在NavBar控件中。

<!--npm install date-fns-->
<template>
  <v-dialog max-width="600" v-model="dialog">
    <template v-slot:activator="{ props: activatorProps }">
      <v-btn
        v-bind="activatorProps"
        color="success"
        text="添加事项"
        variant="flat"
      ></v-btn>
    </template>

    <template v-slot:default="{ isActive }">
      <v-card title="添加新事项">
        <v-card-text>
          <v-form class="px-3" @submit.prevent>
            <v-text-field
              v-model="form_data.title"
              label="名称"
              prepend-icon="mdi-folder"
              :rules="[rules.required, rules.min, rules.max]"
            ></v-text-field>
            <v-textarea
              v-model="form_data.content"
              label="详情"
              prepend-icon="mdi-note"
              :rules="[rules.required, rules.min, rules.max]"
            ></v-textarea>

            <v-menu v-model="menu" :close-on-content-click="false">
              <template v-slot:activator="{ props }">
                <v-text-field
                  v-model="formatedDate"
                  label="期限"
                  prepend-icon="mdi-calendar-month"
                  v-bind="props"
                  readonly
                ></v-text-field>
              </template>
              <v-date-picker v-model="form_data.due"></v-date-picker>
            </v-menu>

            <div align="right" class="mt-4">
              <v-btn
                type="submit"
                flat
                @click="submit"
                color="success"
                :loading="loading"
                ><span>保存新项目</span></v-btn
              >
              <v-btn
                flat
                text="关闭"
                @click="isActive.value = false"
                class="ml-2"
              ></v-btn>
            </div>
          </v-form>
        </v-card-text>
      </v-card>
    </template>
  </v-dialog>
</template>

<script setup>

import { ref, computed,watch,defineEmits} from "vue";
import format from "date-fns/format";

import { addProject } from "@/fb";

const form_data = ref({
  title: "",
  content: "",
  due: null,
});

const loading = ref(false);
const dialog = ref(false);
const menu = ref(false);

//监控due的数据变化,如果变化了就关闭菜单
watch(
  () => form_data.value.due,
  () => {
    menu.value = false;
  }
);

// 格式化日期
const formatedDate = computed(() => {
  if (form_data.value && form_data.value.due) {
    //console.log(form_data.value.due);
    return format(form_data.value.due, "yyyy-MM-dd");
  } else {
    return "";
  }
});

//校验规则
const rules = {
  required: (value) => !!value || "不能为空",
  min: (value) => value.length >= 3 || "最少3个字符",
  max: (value) => value.length <= 20 || "最多20个字符",
};

const emits = defineEmits(["projectAdded"]);

const submit = () => {
  loading.value = true;

  if (
    !form_data.value.title ||
    !form_data.value.content ||
    !form_data.value.due
  ) {
    loading.value = false;
    return;
  }

  const newProject = {
    title: form_data.value.title,
    content: form_data.value.content,
    person: "火云",
    due: format(form_data.value.due, "yyyy-MM-dd"),
    status: "ongoing",
  };

  addProject(newProject)
    .then(() => {
      console.log("事项添加成功!");
      console.log(JSON.stringify(form_data.value));
      loading.value = false;
      dialog.value = false;
      emits("projectAdded");
    })
    .catch((error) => {
      console.error("Error writing document: ", error);
      loading.value = false;
    });
};
</script>

导航栏(NaveBar)

componets中添加NavBar.vue。它的主要功能有:

  1. 使用v-menu实现了 下拉菜单;
  2. 使用v-navigation-drawer实现了左侧菜单;
  3. 使用v-snackbar显示系统消息:当 popup控件添加项目成功时,这里会收到projectAdded事件,改变v-snackbarv-model使得它可以显示提示优雅的信息。
<template >
  <div class="text-center ma-2">
    <v-snackbar
      v-model="snackbar"
      :timeout="4000"
      location="top"
      color="success"
    >
      <span>太棒了项目添加成功</span>
      <template v-slot:actions>
        <v-btn variant="text" @click="snackbar = false"
          >关闭</v-btn
        >
      </template>
    </v-snackbar>
  </div>

  <v-app-bar prominent density="compact" app>
    <v-app-bar-nav-icon
      variant="text"
      @click.stop="drawer = !drawer"
    ></v-app-bar-nav-icon>

    <v-toolbar-title>待办事项</v-toolbar-title>

    <v-spacer></v-spacer>

    <!--dropdown menu-->
    <v-menu open-on-hover>
      <template v-slot:activator="{ props }">
        <v-btn flat color="grey" v-bind="props"> 下拉菜单 </v-btn>
      </template>

      <v-list>
        <v-list-item
          v-for="(item, index) in side_links"
          :key="index"
          :to="item.route"
        >
          <v-list-item-title>{{ item.text }}</v-list-item-title>
        </v-list-item>
      </v-list>
    </v-menu>

    <v-btn append-icon="mdi-exit-to-app"> 退出 </v-btn>
  </v-app-bar>

  <v-navigation-drawer
    :location="$vuetify.display.mobile ? 'bottom' : undefined"
    temporary
    v-model="drawer"
    app
  >
    <v-card class="text-center pa-2" flat>
      <v-col class="mt-5">
        <v-avatar size="100">
          <img src="/image/avatar-1.png" />
        </v-avatar>
        <p class="text-grey mt-1 text-subtitle-1">火云</p>
      </v-col>
    </v-card>
    <v-card flat class="text-center">
      <popup @projectAdded="snackbar = true"></popup>
    </v-card>

    <v-list>
      <v-list-item
        v-for="link in side_links"
        :key="link.text"
        :prepend-icon="link.icon"
        :title="link.text"
        :value="link.text"
        :to="link.route"
      >
      </v-list-item>
    </v-list>
  </v-navigation-drawer>
</template>

<script setup>
import { ref } from "vue";

const drawer = ref(false);
const snackbar = ref(false);

const side_links = [
  { icon: "mdi-view-dashboard", text: "事项概览", route: "/" },
  { icon: "mdi-folder", text: "我的项目", route: "/projects" },
  { icon: "mdi-account", text: "我的团队", route: "/team" },
];
</script>

组合控件,见证成果

修改App.vue,下面是代码:

<!--
<v-app>: Vuetify 的根组件提供应用的主题和基础布局
<v-main>: 包裹主要内容的组件确保应用内容居中且响应式布局
<router-view />: Vue Router 的占位符组件显示当前匹配的路由视图
-->
<template>
  <v-app>
    <nav-bar />
    <v-main >
      <router-view />
    </v-main>
  </v-app>
</template>

<script setup>
//
</script>

nav-bar放在App.vue中使得该工具栏应用于整个应用。

在应用程序的根目录下运行:

pnpm dev

程序启动后会出现下图所示页面,并且自动打开浏览器: 程序启动了

总结

通过几个简单的控件组合,我们就实现了一个五脏俱全的待办事项管理功能,这里面也使用了很多控件可以应用在其它很多场合,相信能对您使用vuetify3起到入门的功效。
这里列出了主要代码逻辑,全部代码请在后面列出的地址下载。

查看完整代码


🪐祝好运🪐