{T}

项目开发

使用 ChatGPT 和 Copilot 快速生成代码,完成项目开发

路由模板

直接写 顶部栏 代码

ChatGPT 提问:

请用 vue3 和 ElementPlus 写一个顶部导航栏,宽度 100% 。 导航栏分左右两部分。左侧是 logo 和导航菜单。右侧是搜索组件和登录菜单。 导航菜单有三个:前端 java 和 python,使用 vue-router 实现链接。每个菜单前面加上合适的 icon

把代码贴过来测试,效果并不好

html
<template>
  <el-header class="header" height="80px">
    <div class="container">
      <div class="logo">
        <img src="logo.png" alt="logo" />
        <span>我的博客</span>
      </div>
      <el-menu
        class="nav-menu"
        mode="horizontal"
        background-color="#fff"
        text-color="#333"
        active-text-color="#409eff"
      >
        <el-menu-item index="/front-end">
          <i class="el-icon-s-home"></i>
          前端
        </el-menu-item>
        <el-menu-item index="/java">
          <i class="el-icon-s-coffee"></i>
          Java
        </el-menu-item>
        <el-menu-item index="/python">
          <i class="el-icon-s-data"></i>
          Python
        </el-menu-item>
      </el-menu>
      <div class="right-menu">
        <el-input
          class="search-input"
          placeholder="搜索"
        />
        <el-dropdown trigger="click">
          <span class="el-dropdown-link">
            <i class="el-icon-user"></i>
            登录
          </span>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item>登录</el-dropdown-item>
            <el-dropdown-item>注册</el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
      </div>
    </div>
  </el-header>
</template>

<script>
export default {
  name: 'Header',
};
</script>

<style scoped>
.header {
  width: 100%;
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 100%;
  padding: 0 20px;
}

.logo {
  display: flex;
  align-items: center;
}

.logo img {
  width: 40px;
  height: 40px;
  margin-right: 10px;
}

.nav-menu {
  flex: 1;
}

.right-menu {
  display: flex;
  align-items: center;
}

.search-input {
  width: 200px;
  margin-right: 20px;
}
</style>

说明

  • 使用 el-header 组件作为导航栏
  • 使用 el-menu 组件作为导航菜单
  • 使用 el-icon 组件作为菜单图标
  • 使用 el-input 组件作为搜索组件
  • 使用 el-dropdown 组件作为登录菜单
  • 使用 vue-router 实现链接跳转

注意事项

  • 需要安装 element-plusvue-router 依赖
  • 需要配置 vue-router 路由
  • 需要替换 logo.png 为您的 logo 图片

以上代码可以实现一个宽度为 100% 的顶部导航栏,包括 logo、导航菜单、搜索组件和登录菜单。

ChatGPT 按照你的文字要求写出了代码。但这明显不是我们预期的 UI 效果。这不是 ChatGPT 的错误,而是我们无法准确表达自己心理的想法 (人类的语言是抽象的)

所以遇到复杂的功能、设计,要去拆分任务,一步一步去执行 —— 这和你带团队、带项目是一样的

换一种方式 ChatGPT 提问:

请用 vue3 和 ElementPlus 写一个组件,组件包括两个部分: 第一,顶部导航栏。宽度 100% 。左右结构,左侧是 log ,右侧是“登录”链接 第二,主体部分。宽度 1000px ,左右居中。主体部分要包含 <RouterView /> 组件

直接手写

  • 使用 <router-link> 链接到首页
  • 设置样式 text-decoration: none;

修改网页标题

  • index.html 写默认标题 TechBlog
  • 自定义修改标题,可想 ChatGPT 提问,代码参考 hooks/usePageTitle.js

使用 vue3 composition API 写一个 hook ,可以在多个组件中使用,用来修改网页标题

修改网页标题的 hook

javascript
import { isRef, onBeforeUnmount, onMounted, ref, watchEffect } from 'vue'

const NAME = 'TechBlog'

function usePageTitle(title) {
  const originalTitle = ref(document.title)

  // 更新网页标题
  const updatePageTitle = () => {
    const titleValue = isRef(title) ? title.value : title
    if (!titleValue) {
      return
    }
    document.title = titleValue + ' - ' + NAME
  }

  if (isRef(title)) {
    watchEffect(updatePageTitle)
  }

  // 在组件挂载时更新网页标题
  onMounted(() => {
    updatePageTitle()
  })

  // 在组件卸载时恢复原始网页标题
  onBeforeUnmount(() => {
    document.title = originalTitle.value
  })

  return {
    updatePageTitle
  }
}

export default usePageTitle

使用示例

Vue SFC
<template>
  <div>
    <!-- 其他组件内容 -->
  </div>
</template>

<script setup>
import usePageTitle from './usePageTitle'

usePageTitle('首页')
</script>

顶部栏

顶部栏 包括导航、搜索、登录/用户信息

导航组件

ChatGPT 提问

使用 vue3 和 ElementPlus 写一个菜单组件,包含:前端、Java、Python、小程序。要求如下:

  1. 每个菜单点击时,都跳转到首页,并加一个 query ?category=xxx
  2. activeMenu 要根据 url query category 参数实时变化,默认值是空
  3. 使用 vue3 setup script 语法

新建组件 components/NavMenu.vue 把代码贴上。最后把 <NavMenu/> 组件引用到 App.vue

html
<template>
  <el-menu
    :default-active="activeMenu"
    @select="handleSelect"
    background-color="#fff"
    text-color="#333"
    active-text-color="#409eff"
  >
    <el-menu-item index="/?category=front-end">
      <i class="el-icon-s-home"></i>前端
    </el-menu-item>
    <el-menu-item index="/?category=java">
      <i class="el-icon-s-coffee"></i>Java
    </el-menu-item>
    <el-menu-item index="/?category=python">
      <i class="el-icon-s-data"></i>Python
    </el-menu-item>
    <el-menu-item index="/?category=mini-program">
      <i class="el-icon-s-cooperation"></i>小程序
    </el-menu-item>
  </el-menu>
</template>

<script setup>
import { useRoute } from 'vue-router'

const route = useRoute()
const activeMenu = route.query.category || ''

const handleSelect = (index) => {
  const category = index.split('?category=')[1]
  const url = category ? `/?category=${category}` : '/'
  router.push(url)
}
</script>

以上代码可以实现一个根据 category 查询参数实时变化的菜单组件,并支持跳转到首页并添加查询参数

搜索框组件

ChatGPT 提问

使用 vue3 和 ElementPlus 写一个 input search 组件。要求:

  1. 搜索时跳转到当前 path ,并加一个 query ?keyword=xxx ,且保留现有的 url query
  2. 实时监听 url query keyword 并讲内容显示到 input 中
  3. 使用 vue3 setup script 语法

新建组件 components/SearchInput.vue ,把代码贴上。最后把 <SearchInput/> 组件引用到 App.vue

下面是使用 Vue 3 和 Element Plus 编写搜索组件的示例代码,满足您的需求:

html
<template>
  <div>
    <el-input v-model="keyword" placeholder="请输入搜索关键字" @keyup.enter="handleSearch"clearable>
    </el-input>
  </div>
</template>

<script setup>
  import { ref, watch, onMounted } from 'vue'
  import { useRouter } from 'vue-router'

  const router = useRouter()
  const keyword = ref('')

  // 监听URL中的keyword参数变化,实时更新搜索框的内容
  watch(
    () => router.currentRoute.value.query.keyword,
    (newValue) => {
      keyword.value = newValue || ''
    }
  )

  // 在组件挂载后,将URL中的keyword参数赋值给搜索框的内容
  onMounted(() => {
    keyword.value = router.currentRoute.value.query.keyword || ''
  })

  // 处理搜索事件
  const handleSearch = () => {
    const query = { ...router.currentRoute.value.query, keyword: keyword.value }
    router.push({ path: router.currentRoute.value.path, query })
  }
</script>

登录/用户信息

新建组件 components/UserInfo.vue

先写一个“登录”链接,后面再补充用户信息

注册页面

使用 vue3 和 ElementPlus 写一个注册页面,包含一个标题“注册新用户”和一个表单。要求:

  1. 页面内容水平垂直居中
  2. 注册表单包含:用户名(必填)、密码(必填)、确认密码、昵称、注册按钮
  3. 注册表单要根据 item 数据类型设置校验规则
  4. 使用 vue3 setup script 语法

把代码拷贝到 views/UserRegisterView.vue

下面是使用Vue 3和Element Plus编写注册页面的示例代码,满足您的需求:

html
<template>
  <div class="register-page">
    <div class="register-container">
      <h2 class="register-title">注册新用户</h2>
      <el-form ref="registerForm" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="用户名" prop="username">
          <el-input v-model="form.username"></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input type="password" v-model="form.password"></el-input>
        </el-form-item>
        <el-form-item label="确认密码" prop="confirmPassword">
          <el-input type="password" v-model="form.confirmPassword"></el-input>
        </el-form-item>
        <el-form-item label="昵称" prop="nickname">
          <el-input v-model="form.nickname"></el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleRegister">注册</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script setup>
  import { ref, reactive } from 'vue'
  import { ElForm, ElFormItem, ElInput, ElButton } from 'element-plus'

  const form = reactive({
    username: '',
    password: '',
    confirmPassword: '',
    nickname: ''
  })

  const rules = {
    username: [
      { required: true, message: '请输入用户名', trigger: 'blur' }
    ],
    password: [
      { required: true, message: '请输入密码', trigger: 'blur' }
    ],
    confirmPassword: [
      { required: true, message: '请再次输入密码', trigger: 'blur' },
      { validator: validateConfirmPassword, trigger: 'blur' }
    ],
    nickname: [
      { required: true, message: '请输入昵称', trigger: 'blur' }
    ]
  }

  const validateConfirmPassword = (rule, value, callback) => {
    if (value !== form.password) {
      callback(new Error('两次输入的密码不一致'))
    } else {
      callback()
    }
  }

  const handleRegister = () => {
    // 表单校验
    const formRef = refs.registerForm
    formRef.validate((valid) => {
      if (valid) {
        // 提交注册逻辑
        console.log('注册成功')
      } else {
        console.log('表单校验失败')
        return false
      }
    })
  }
</script>

<style scoped>
  .register-page {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
  }

  .register-container {
    width: 400px;
    padding: 20px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }

  .register-title {
    text-align: center;
    margin-bottom: 20px;
  }
</style>

接口

修改 submitForm 函数执行注册,使用 Copilot 可快速生成代码。在 mock-server 中添加注册 API 调试

登录页

登录,传入用户名和密码,服务端校验。校验成功返回 token 加密了用户信息。客户端下次请求带着 token 表明用户信息

ChatGPT 提问

使用 vue3 和 ElementPlus 写一个登录页面,包含一个标题“用户登录”和一个表单。要求:

  1. 页面内容水平垂直居中。标题和表单之前有 20px 间隔
  2. 登录表单包含:用户名(必填)、密码(必填)、登录按钮。登录按钮后加一个链接“新用户请注册”,点击跳转到注册页
  3. 登录表单要根据 item 数据类型设置校验规则
  4. 使用 vue3 setup script 语法

遇到问题再继续修改,例如 以上代码有两个问题:1. form 的 label 宽度没有固定;2.页面内容没有垂直居中;

把代码拷贝到 views/UserLoginView.vue 并做如下调整:

  • 高度太大有滚动条,改成 80vh
  • form 加一个属性 label-width="100px"

下面是一个使用 Vue 3 和 Element Plus 编写的登录页面示例:

Vue SFC
<template>
  <div class="login-container">
    <h2>用户登录</h2>
    <el-form :model="formData" :rules="rules" ref="loginForm" label-width="80px">
      <el-form-item label="用户名" prop="username">
        <el-input v-model="formData.username"></el-input>
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input type="password" v-model="formData.password"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="login">登录</el-button>
        <router-link to="/register">新用户请注册</router-link>
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { ElForm, ElFormItem, ElInput, ElButton } from 'element-plus';

const formData = ref({
  username: '',
  password: ''
});

const rules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' }
  ]
};

const login = () => {
  const form = $refs.loginForm;
  form.validate((valid) => {
    if (valid) {
      // 进行登录操作
      console.log('登录成功');
    } else {
      console.log('表单校验失败');
      return false;
    }
  });
};
</script>

<style scoped>
.login-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  margin-top: 20px;
}
</style>

在这个示例中:

  • 使用了Vue 3的<script setup>语法,简化了组件的定义和导入
  • 使用了Element Plus提供的表单组件和样式
  • 使用了ref来创建对表单的引用,以便在登录时执行表单验证
  • 设置了校验规则,根据usernamepassword字段是否为空进行校验
  • 点击登录按钮时,会触发login方法进行表单验证,并根据验证结果执行相应的操作

获取用户信息

App.vue 中请求用户信息,并存储到 Pinia。在登录、注册页获取用户信息,并判断跳转

components/UserInfo.vue 获取用户信息

javascript
// stores/user.js

import { reactive } from 'vue'
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  // 定义 userInfo ,包括 username nickname
  const userInfo = reactive({
    username: '',
    nickname: ''
  })

  // 设置用户信息
  function setUserInfo({ username, nickname }) {
    userInfo.username = username
    userInfo.nickname = nickname
  }

  // 清空用户信息
  function clearUserInfo() {
    userInfo.username = ''
    userInfo.nickname = ''
  }

  return { userInfo, setUserInfo, clearUserInfo }
})

登录、注册页

使用 watchEffect 监测变化,并跳转 —— 可抽离单独的 Hook useNavToLogin.js

javascript
import { watch } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';

function useNavToLogin() {
  const router = useRouter();
  const { userInfo } = useUserStore()
  // 监听 userInfo.username ,如果没有值,说明用户未登录,跳转到登录页
  watch(() => userInfo.username, (val) => {
    if (!val) {
      router.push('/login');
    }
  }, { immediate: true });
}

export default useNavToLogin

【注意】这里没有使用导航守卫,有问题 (代码参考 router/index.js

  • 使用 <router-link> 跳转没问题
  • 但直接访问 /login 无法第一时间获取到 pinia store

显示用户信息

修改 components/UserInfo.vue 代码

ChatGPT 提问:

使用 vue3 和 ElementPlus 写一个下拉菜单,标题是 nickname 下拉菜单有两个:

  1. 创建博客,点击跳转到 /create-edit-blog
  2. 我的博客,点击跳转到 /my-blogs
  3. 注销,点击执行一个函数

ChatGPT 写的不好,再用 Copilot 提问 使用 element-plus 实现一个下拉菜单 ,也写的不好

最后不得已从官网拷贝 https://element-plus.org/zh-CN/component/dropdown.html

另外,ElementPlus 的 icon 需要单独安装和注册,参考 main.js

javascript
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)

for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

完善代码(结合 Copilot)

  • 使用 v-ifv-else 判断显示内容
  • 跳转到“我的博客”页面
  • 注销,清空 store 内容,清空 token
html
<script setup>
  import { RouterLink, useRouter } from 'vue-router'
  import { useUserStore } from '@/stores/user'

  const router = useRouter()

  const { userInfo, clearUserInfo } = useUserStore()

  const goToCreateEditBlog = () => {
    router.push('/create')
  }

  const goToMyBlogs = () => {
    router.push('/my-blogs')
  }

  const logout = () => {
    clearUserInfo()
    localStorage.removeItem('token')

    // 跳转到登录页或其他页面
    router.push('/login')
  }
</script>

<template>
  <div>
    <!-- 判断 userInfo.username ,如果有则显示 nickname ,没有则显示登录链接 -->
    <div class="dropdown" v-if="userInfo.username">
      <el-dropdown>
        <span class="el-dropdown-link">
          {{ userInfo.nickname || userInfo.username }}
          <el-icon class="el-icon--right">
            <arrow-down />
          </el-icon>
        </span>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item @click="goToCreateEditBlog">
              <el-icon>
                <Edit />
              </el-icon>
              创建博客
            </el-dropdown-item>
            <el-dropdown-item @click="goToMyBlogs">
              <el-icon>
                <Files />
              </el-icon>
              我的博客
            </el-dropdown-item>
            <el-dropdown-item @click="logout">
              <el-icon>
                <Close />
              </el-icon>
              注销
            </el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </div>
    <RouterLink v-else to="/login">登录</RouterLink>
  </div>
</template>

首页

梳理需求

  • 博客列表:分类,搜索,分页
  • 单个博客:显示博客信息,点击进入详情页

mock

获取博客列表的接口 /api/blogs

ChatGPT 提问

生成 10 条博客数据,JSON 格式,用于 mock 服务。 编程技术类博客,如前端、Java、Python、小程序等,内容要贴近真实的技术博客内容。 每条博客内容如下:

  • id (唯一,不可重复)
  • 标题
  • 简介(50-100字)
  • 分类(随机选择:前端, java, python, 小程序)要和标题、简介对应好
  • 作者
  • 点赞数量
  • 收藏数量
  • 评论数量
  • 创建时间
  • 更新时间

PS:ChatGPT 可能会使用 JS 函数生成,直接停止,并要求 不要使用 JS 函数,直接生成静态 JSON 数据格式

考虑到分页,返回的 data 应该改为 { list: [], total: 100 }

最后,分页后 id 也不能重复,所以使用 MockJS 来随机生成 id ,使用 Random.id()

json
[
  {
    "id": "1",
    "title": "深入理解JavaScript闭包",
    "summary": "本文介绍了JavaScript闭包的概念、原理和应用场景,帮助读者更好地理解JavaScript中的作用域链和闭包机制。",
    "category": "前端",
    "author": "张三",
    "likes": 100,
    "favorites": 50,
    "comments": 20,
    "created_at": "2024-05-09T08:00:00Z",
    "updated_at": "2024-05-09T10:00:00Z"
  },
  {
    "id": "2",
    "title": "使用Spring Boot构建RESTful API",
    "summary": "本文详细介绍了如何使用Spring Boot框架构建RESTful API,包括项目的初始化、依赖管理、控制器编写等内容。",
    "category": "java",
    "author": "李四",
    "likes": 120,
    "favorites": 60,
    "comments": 30,
    "created_at": "2024-05-08T09:00:00Z",
    "updated_at": "2024-05-08T11:30:00Z"
  },
  {
    "id": "3",
    "title": "Python爬虫实战:爬取豆瓣电影Top250",
    "summary": "本文介绍了使用Python编写爬虫程序,爬取豆瓣电影Top250的数据,并进行数据分析和可视化展示。",
    "category": "python",
    "author": "王五",
    "likes": 80,
    "favorites": 40,
    "comments": 25,
    "created_at": "2024-05-07T10:00:00Z",
    "updated_at": "2024-05-07T12:45:00Z"
  },
  {
    "id": "4",
    "title": "小程序开发实践:仿写微信运动",
    "summary": "本文介绍了如何使用小程序开发工具开发一个仿微信运动的小程序应用,包括界面设计、数据存储、运动计步等功能。",
    "category": "小程序",
    "author": "赵六",
    "likes": 90,
    "favorites": 55,
    "comments": 15,
    "created_at": "2024-05-06T11:00:00Z",
    "updated_at": "2024-05-06T13:20:00Z"
  },
  {
    "id": "5",
    "title": "Vue 3新特性解读",
    "summary": "本文介绍了Vue 3框架的新特性和改进,包括响应式API、组件生命周期、编译优化等方面的更新。",
    "category": "前端",
    "author": "张三",
    "likes": 110,
    "favorites": 65,
    "comments": 22,
    "created_at": "2024-05-05T12:00:00Z",
    "updated_at": "2024-05-05T14:10:00Z"
  },
  {
    "id": "6",
    "title": "Java并发编程实战",
    "summary": "本文介绍了Java并发编程的基本概念、常见问题和解决方案,包括线程、锁、并发集合等内容。",
    "category": "java",
    "author": "李四",
    "likes": 130,
    "favorites": 70,
    "comments": 28,
    "created_at": "2024-05-04T13:00:00Z",
    "updated_at": "2024-05-04T15:25:00Z"
  },
  {
    "id": "7",
    "title": "Python数据分析:探索性数据分析入门",
    "summary": "本文介绍了Python数据分析的基本流程和常用工具,通过实例演示了如何进行探索性数据分析和可视化。",
    "category": "python",
    "author": "王五",
    "likes": 85,
    "favorites": 45,
    "comments": 18,
    "created_at": "2024-05-03T14:00:00Z",
    "updated_at": "2024-05-03T16:40:00Z"
  },
  {
    "id": "8",
    "title": "小程序云开发实战:搭建在线商城",
    "summary": "本文介绍了如何使用小程序云开发搭建一个简单的在线商城应用,包括用户身份认证、商品管理、订单处理等功能。",
    "category": "小程序",
    "author": "赵六",
    "likes": 95,
    "favorites": 60,
    "comments": 20,
    "created_at": "2024-05-02T15:00:00Z",
    "updated_at": "2024-05-02T17:15:00Z"
  },
  {
    "id": "9",
    "title": "Vue 3与TypeScript结合实践",
    "summary": "本文介绍了如何在Vue 3项目中结合使用TypeScript,提高代码的可维护性和类型安全性。",
    "category": "前端",
    "author": "张三",
    "likes": 115,
    "favorites": 70,
    "comments": 25,
    "created_at": "2024-05-01T16:00:00Z",
    "updated_at": "2024-05-01T18:30:00Z"
  },
  {
    "id": "10",
    "title": "Java微服务架构实践",
    "summary": "本文介绍了微服务架构的概念、优势和实践经验,以及在Java平台下如何设计和构建微服务应用。",
    "category": "java",
    "author": "李四",
    "likes": 140,
    "favorites": 75,
    "

comments": 30,
    "created_at": "2024-04-30T17:00:00Z",
    "updated_at": "2024-04-30T19:50:00Z"
  }
]

渲染博客列表

需要考虑的条件:

  • 分类 category
  • 搜索 keyword
  • 分页 page pageSize

需要改造一下 blogApi.js 中的 getBlogList ,增加参数

在首页 HomeView.vue 中请求数据。使用 Copilot 生成代码。

渲染博客列表,使用 Copilot 生成代码 <!-- 遍历显示 blogList 的内容,每个 item 使用 div -->

博客卡片组件

ChatGPT 提问

用 vue3 和 ElementPlus 写一个博客卡片组件(BlogCard.vue),用于在博客列表中显示单个博客信息 该组件接受一个属性 blog ,是一个 js 对象

该组件的 UI 分为上中下三层结构:

  • 上层显示:博客作者(blog.author)并在前面加 icon ,分类(blog.category),时间(blog.updatedAt)
  • 中层显示:博客标题(blog.title),博客简介(blog.summary),且点击标题跳转到 /blog/:id
  • 下层显示:点赞数量(blog.likes),收藏数量(blog.favorites)、评论数量(blog.comments)。每个使用合适的 icon 来表示

还要求:

  • 当鼠标 hover 卡片,设置浅灰色背景
  • 点击卡片跳转到 /blog/:id
  • CSS 样式要美观,可参考当前博客网站的样式
  • 使用 vue3 setup script 语法

新建组件 components/BlogCard.vue 并粘贴相应代码

样式有点问题,继续调整一下:

你给出的代码非常好。不过有几个样式的地方需要优化:

  1. 内容和边框的间距太大了,再小一些
  2. 上层显示的三个信息,全部靠左显示,中间要有间隙。
  3. 上层显示的三个信息,“时间”和“分类”使用灰色

代码进行如下调整

  • 卡片之间增加间隙,在 HomeView.vue 中增加 margin-bottom
  • 修改 icon 未显示的问题(代码生成的是老旧写法)

处理时间格式,新建 utils/date.js ,然后用 Copilot 快速生成。页面导入使用 import { formatDate } from '@/utils/date'

javascript
export function formatDate(date) {
  // 获取当前时间
  const now = Date.now()
  // 获取传入时间
  const time = new Date(date).getTime()
  // 获取时间差
  const diff = (now - time) / 1000
  // 判断时间差
  if (diff < 30) {
    return '刚刚'
  } else if (diff < 60) {
    return Math.ceil(diff) + '秒前'
  } else if (diff < 60 * 60) {
    return Math.ceil(diff / 60) + '分钟前'
  } else if (diff < 60 * 60 * 24) {
    return Math.ceil(diff / (60 * 60)) + '小时前'
  } else if (diff < 60 * 60 * 24 * 30) {
    return Math.ceil(diff / (60 * 60 * 24)) + '天前'
  } else if (diff < 60 * 60 * 24 * 30 * 12) {
    return Math.ceil(diff / (60 * 60 * 24 * 30)) + '月前'
  } else {
    return Math.ceil(diff / (60 * 60 * 24 * 30 * 12)) + '年前'
  }
}

使用

html
<script setup>
  // import { ref } from 'vue';
  import { formatDate } from '@/utils/date'

  defineProps({
    blog: {
      type: Object,
      required: true
    }
  })
</script>

<template>
  <div class="blog-card">
    <div class="blog-card-header">
      <div class="blog-category">{{ blog.category }}</div>
      <div class="blog-time">{{ formatDate(blog.updatedAt) }}</div>
    </div>
  </div>
</template>

分页

ChatGPT 提问

使用 vue3 和 ElementPlus 写一个分页组件,水平居中显示 可用的变量有 total page pageSize 使用 vue3 setup script

下面是一个水平居中显示的分页组件 Pagination.vue,使用了 Vue 3 和 Element Plus:

Vue SFC
<template>
  <div class="pagination-container">
    <el-pagination
      :total="total"
      :page-size="pageSize"
      @current-change="handleCurrentChange"
      layout="prev, pager, next"
    >
    </el-pagination>
  </div>
</template>

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

const total = ref(0);
const page = ref(1);
const pageSize = ref(10);

const handleCurrentChange = (val) => {
  page.value = val;
  // 在这里可以触发父组件的方法,传递当前页数val
};
</script>

<style scoped>
.pagination-container {
  display: flex;
  justify-content: center;
  margin-top: 20px;
}
</style>

在这个分页组件中:

  • 使用了 totalpagepageSize 作为可用的变量,分别表示总条目数、当前页码和每页显示条目数。
  • 使用了 ref 创建响应式变量。
  • 通过 el-pagination 组件实现了分页功能,使用 layout 属性指定了布局,只显示了上一页、页码和下一页。
  • 当页码变化时,会触发 handleCurrentChange 方法,更新当前页码 page 的值。

样式部分,使用了 scoped 样式,使得样式只在当前组件内起作用,确保了分页组件水平居中显示。

然后用 Copilot 快速生成 handleCurrentChange 函数代码

博客详情页

梳理需求

  • 展示博客信息
  • 点赞、收藏
  • 评论 (单独讲)

获取博客详情接口 /api/blogs/${id}

用 ChatGPT 生成博客内容

用 JSON 格式写一个真实的编程技术博客,博客包括:

  • id
  • 标题
  • 内容,要求:使用 markdown 格式,包含标题、文本、列表、代码块,1000 字左右
  • 作者
  • 分类(随机选择:前端, java, python, 小程序)要和标题、简介对应好
  • 点赞数量
  • 是否点赞
  • 收藏数量
  • 是否收藏
  • 评论数量
  • 创建时间

其他接口:

  • 点赞
  • 取消点赞
  • 收藏
  • 取消收藏

展示博客内容

根据 id 获取博客数据 —— 使用 Copilot 可快速完成

使用 useTitle 修改博客标题

显示博客信息:标题,作者,时间,内容

  • 使用 Copilot 生成 <!-- 显示 title author createdAt content ,中间留 margin -->
  • 优化 css 样式
  • 修改时间格式

转换 markdown

使用 ChatGPT 提问

用 js 把 markdown 格式转换为 html 格式?有没有第三方库可以使用?

  • 选择 markdown-it
  • 并需要传入字符串格式 toRaw(blogInfo.content)
  • 优化代码块样式,使用 Copilot /* 优化 pre code 样式 */

点赞和收藏

使用 Copilot 生成模板 <!-- 生成一个 div ,用于点赞和收藏,水平居中显示 -->

优化代码

  • icon
  • 显示数量

根据 isLikedisFavorited 判断,显示按钮 active 状态。手写即可

添加点击事件,执行 点赞/取消点赞,收藏/取消收藏。可使用 Copilot 生成代码

  • // 点赞或取消点赞
  • // 收藏或取消收藏
html
<script setup>
import { computed, reactive, onMounted, toRef } from 'vue'
import { useRoute } from 'vue-router'
import MarkdownIt from 'markdown-it'
import { Pointer, Star } from '@element-plus/icons-vue'
import { getBlogDetails, likeBlog, unlikeBlog, favoriteBlog, unfavoriteBlog } from '@/api/blogApi'
import usePageTitle from '@/hooks/usePageTitle'
import { formatDate } from '@/utils/date'
import CommentView from './sub-views/CommentView.vue'

const md = new MarkdownIt()

const $route = useRoute()

const blogInfo = reactive({})

// onMounted 时获取博客详情
const blogId = $route.params.id || ''
onMounted(async () => {
  const res = await getBlogDetails(blogId)
  blogInfo.id = res.id
  blogInfo.title = res.title
  blogInfo.content = res.content
  blogInfo.category = res.category
  blogInfo.author = res.author
  blogInfo.likes = res.likes
  blogInfo.isLiked = res.isLiked
  blogInfo.favorites = res.favorites
  blogInfo.isFavorited = res.isFavorited
  blogInfo.comments = res.comments
  blogInfo.createdAt = res.createdAt
  blogInfo.updatedAt = res.updatedAt
})

// 修改标题
usePageTitle(toRef(blogInfo, 'title'))

// 点赞和取消点赞
const handleLike = async () => {
  if (blogInfo.isLiked) {
    await unlikeBlog(blogId)
    blogInfo.likes -= 1
    blogInfo.isLiked = false
  } else {
    await likeBlog(blogId)
    blogInfo.likes += 1
    blogInfo.isLiked = true
  }
}

// 收藏和取消收藏
const handleFavorite = async () => {
  if (blogInfo.isFavorited) {
    await unfavoriteBlog(blogId)
    blogInfo.favorites -= 1
    blogInfo.isFavorited = false
  } else {
    await favoriteBlog(blogId)
    blogInfo.favorites += 1
    blogInfo.isFavorited = true
  }
}

// 格式化内容
const formattedContent = computed(() => {
  return md.render(blogInfo.content || '')
})
</script>

<template>
  <!-- 显示 title author createdAt Content -->
  <div class="blog-detail">
    <div class="blog-detail__title">{{ blogInfo.title }}</div>
    <div class="blog-detail__author">
      <span class="blog-detail__author-name">
        <el-icon><User /></el-icon>
        {{ blogInfo.author }}
      </span>
      <span class="blog-detail__author-time">{{ formatDate(blogInfo.createdAt) }}</span>
    </div>
    <div class="blog-detail__content" v-html="formattedContent"></div>

    <!-- 生成一个 div ,用于点赞和收藏,水平居中显示 -->
    <div class="blog-detail__actions">
      <el-button
        :type="blogInfo.isLiked ? 'primary' : 'default'"
        :icon="Pointer"
        size="large"
        round
        @click="handleLike"
      >
        点赞 {{ blogInfo.likes }}
      </el-button>
      <el-button
        :type="blogInfo.isFavorited ? 'primary' : 'default'"
        :icon="Star"
        size="large"
        round
        @click="handleFavorite"
      >
        收藏 {{ blogInfo.favorites }}
      </el-button>
    </div>

    <CommentView />
  </div>
</template>

<style scoped>
.blog-detail {
  padding: 20px;
}
.blog-detail__title {
  font-size: 20px;
  font-weight: bold;
  margin-bottom: 10px;
}
.blog-detail__author {
  margin-bottom: 10px;
}
.blog-detail__author-name {
  margin-right: 10px;
}
.blog-detail__content {
  line-height: 1.5;
}
.blog-detail__actions {
  text-align: center;
  margin-top: 20px;
}
</style>

评论

梳理需求

  • 评论列表展示
  • 删除自己的评论
  • 发表评论

评论列表接口 (不考虑分页)

ChatGPT 提问:

使用 JSON 格式,生成一个编程技术博客的评论列表,一共 5 条。评论内容要真实。每条评论包含:

  • id
  • 评论内容
  • 创建时间
  • 作者用户名
  • 作者昵称

然后配合 Copilot 编写 mock 代码

其他接口,使用 Copilot 生成

  • 删除评论
  • 发表评论

评论列表

新建一个 views/subviews/CommentView.vue ,并引入到 views/BlogDetailView.vue

  • 获取博客列表数据,直接使用 Copilot 生成

  • 显示评论列表,直接用 Copilot 生成 <!-- 评论列表 -->

  • CSS 也可自动生成,且样式很好看

  • 最后再加用户名前面加一个 icon

删除自己的评论

前提:该一下 mock 数据,让一条数据是自己的

获取当年用户名:从 pinia store 中引入

删除评论 —— 直接使用 Copilot 生成

  • 显示按钮(和样式)<!-- 如果 comment.username 等于 userInfo.username ,则显示一个“删除”按钮,点击执行删除 -->
  • CSS 也可以生成
  • 删除的方法 // 删除一条评论

发表评论

使用 Copilot 生成

  • UI <!-- 发表评论。左右结构,一个输入框,一个按钮 --> (以及 CSS)
  • 方法 // 发表评论

我的博客列表

需求分析

  • 我的博客列表
  • 删除一条博客
  • 修改一条博客
  • 创建博客

通用功能

  • 修改标题
  • 未登录,跳转到登录页(参考登录页) —— 可使用 Copilot

PS:在此生成“创建博客”按钮

mock

mock 增加:删除博客。使用 Copilot

blogApi.js 获取博客列表,加一个 opt my 默认 false

参数传给服务端,让服务端处理

博客列表

获取博客列表数据,使用 Copilot 生成,参考代码的注释 【注意】加上 my: true 表示“我的博客”

显示列表和操作

  • Copilot 生成模板 <!--用 table 显示博客列表,包括 title category likes favorites comments createdAt,再加两个操作按钮“编辑” “删除”-->
  • Copilot 生成编辑代码 // 跳转到编辑页
  • Copilot 生成删除代码 // 删除博客

分页:可以把“首页”分页,单独抽离一个组件

需要改动

  • 接受两个属性 pageSize total
  • 去掉 path: '/' ,不要只考虑首页

创建按钮。使用 Copilot 生成代码,在 table 上面写 <!-- 左右结构,左侧标题“我的博客”,右侧“创建”按钮 -->

创建编辑博客

需求梳理

  • 编辑博客内容
  • 新建博客

使用 Copilot 生成

  • 创建博客 post
  • 更新博客 put

如果用户未登录,则跳转到登录页。参考“我的博客”页 —— 可抽离单独的 Hook useNavToLogin.js

判断 创建/更新

使用 Coplit 根据注释生成

第一,// 获取 url params id

第二,根据 id 获取博客数据。先引入 api ,再写 // onMounted 根据 id 获取博客详情页,id 为空则不获取

第三,修改标题。先引入 hook ,再写 // 定义标题,有 id 则为编辑,否则为创建// 设置网页标题

渲染 UI

第一,标题栏(和“提交”按钮),ChatGPT 生成

使用 vue3 和 ElementPlus 写一个 div ,左右结构 左侧是 title ,右侧是“提交”按钮,点击执行一个事件 使用 vue3 setup script 语法

第二,博客标题和类型,ChatGPT 生成

使用 vue3 和 ElementPlus 写一个 div ,左右结构 左侧:一个文字“标题”,一个 input ,v-model 对应 blogDetails.title 右侧:一个文字“类型”(可选:前端、Java、Python、小程序),v-model 对应 blogDetails.category 中间要有间隔 使用 vue3 setup script 语法

第三,内容 textarea ,回车使用 Copilot 直接生成

提交数据

第一,confirm 确认是否提交

  • Copilot 一开始给出的代码不好
  • 参考“我的博客”,引入 ElMessageBox 以后(并修改为 async 函数),Copilot 给出的答案就好了

第二,验证 title content category 是否为空 —— 使用 Copilot 即可

第三,(先引入 api)然后使用 Copilot 生成代码 // 如果没有 id ,则创建博客。没有 id 则更新博客