AI书签管理工具开发全记录(七):页面编写与接口对接

AI书签管理工具开发全记录(七):页面编写与接口对接

前言 📝

在上一篇博客中,我们完成了前端基础框架的搭建。现在,我们将实现书签和分类管理的具体页面,并与后端API进行对接,完成整个前后端交互流程。

1. 页面功能规划 📌

我们需要实现以下核心功能页面:

  1. ​书签管理页面​
    • 书签列表展示
    • 书签增删改查​
  2. ​分类管理页面​
    • 分类列表展示
    • 分类增删改查

2. 接口api编写 📡

2.1 创建.env,设置环境变量

VITE_API_BASE_URL=http://localhost:8080

2.2 增加axios拦截器

import axios from 'axios'
import { ElMessage } from 'element-plus'

// 创建 axios 实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000
})

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    // 在这里可以添加请求前的处理逻辑
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  (response) => {
    // 在这里可以添加响应后的处理逻辑
    const res = response.data
    // 如果返回的状态码不是200,说明接口请求有误
    if (response.status !== 200) {
      ElMessage.error(res.message || '请求失败')
      return Promise.reject(new Error(res.message || '请求失败'))
    }
    return res
  },
  (error) => {
    ElMessage.error(error.message || '请求失败')
    return Promise.reject(error)
  }
)

export default service

2.3 创建接口

以分类为例,书签相似。

// src/api/category/index.js
import request from '/@/utils/request'

// 创建分类
export function createCategory(data) {
  return request({
    url: '/api/categories',
    method: 'post',
    data
  })
}

// 获取分类列表
export function listCategories(params) {
  return request({
    url: '/api/categories',
    method: 'get',
    params
  })
}

// 获取单个分类
export function getCategory(id) {
  return request({
    url: `/api/categories/${id}`,
    method: 'get'
  })
}

// 更新分类
export function updateCategory(id, data) {
  return request({
    url: `/api/categories/${id}`,
    method: 'put',
    data
  })
}

// 删除分类
export function deleteCategory(id) {
  return request({
    url: `/api/categories/${id}`,
    method: 'delete'
  })
}

2. 页面编写 📄

2.1 示例代码

以分类为例

<!--/src/views/category/index.vue -->
<template>
  <div class="category-container">
    <div class="header">
      <h2>分类管理</h2>
    </div>

    <div class="toolbar">
      <div class="search-row">
        <el-input
          v-model="searchName"
          placeholder="请输入分类名称"
          class="search-input"
          clearable
          @clear="handleSearch"
          @keyup.enter="handleSearch"
        >
          <template #append>
            <el-button @click="handleSearch">
              <el-icon><Search /></el-icon>
            </el-button>
          </template>
        </el-input>
      </div>
      <div class="button-row">
        <el-button type="primary" @click="handleAdd">新增分类</el-button>
      </div>
    </div>

    <el-table :data="categoryList" style="width: 100%" v-loading="loading">
      <el-table-column prop="id" label="ID" width="80" />
      <el-table-column prop="name" label="分类名称" />
      <el-table-column prop="description" label="描述" />
      <el-table-column label="操作" width="200">
        <template #default="{ row }">
          <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
          <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <div class="pagination">
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :page-sizes="[10, 20, 50, 100]"
        layout="total, sizes, prev, pager, next"
        :total="total"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>

    <!-- 新增/编辑对话框 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogType === 'add' ? '新增分类' : '编辑分类'"
      width="500px"
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="80px"
      >
        <el-form-item label="分类名称" prop="name">
          <el-input v-model="form.name" placeholder="请输入分类名称" />
        </el-form-item>
        <el-form-item label="描述" prop="description">
          <el-input
            v-model="form.description"
            type="textarea"
            placeholder="请输入分类描述"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import { listCategories, createCategory, updateCategory, deleteCategory } from '/@/api/category'

// 数据列表
const categoryList = ref([])
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const searchName = ref('')

// 对话框相关
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref(null)
const form = ref({
  name: '',
  description: ''
})

// 表单验证规则
const rules = {
  name: [
    { required: true, message: '请输入分类名称', trigger: ['blur', 'change'] },
    { min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: ['blur', 'change'] }
  ],
  description: [
    { max: 200, message: '长度不能超过 200 个字符', trigger: ['blur', 'change'] }
  ]
}

// 获取分类列表
const getList = async () => {
  loading.value = true
  try {
    const res = await listCategories({
      page: currentPage.value,
      size: pageSize.value,
      name: searchName.value
    })
    categoryList.value = res.data
    total.value = res.total
  } catch (error) {
    ElMessage.error('获取分类列表失败')
  } finally {
    loading.value = false
  }
}

// 处理搜索
const handleSearch = () => {
  currentPage.value = 1
  getList()
}

// 处理新增
const handleAdd = () => {
  dialogType.value = 'add'
  form.value = {
    name: '',
    description: ''
  }
  dialogVisible.value = true
}

// 处理编辑
const handleEdit = (row) => {
  dialogType.value = 'edit'
  form.value = {
    id: row.id,
    name: row.name,
    description: row.description
  }
  dialogVisible.value = true
}

// 处理删除
const handleDelete = (row) => {
  ElMessageBox.confirm('确认删除该分类吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(async () => {
    try {
      await deleteCategory(row.id)
      ElMessage.success('删除成功')
      getList()
    } catch (error) {
      ElMessage.error('删除失败')
    }
  })
}

// 处理提交
const handleSubmit = async () => {
  if (!formRef.value) return
  await formRef.value.validate(async (valid) => {
    if (valid) {
      try {
        if (dialogType.value === 'add') {
          await createCategory(form.value)
          ElMessage.success('新增成功')
        } else {
          await updateCategory(form.value.id, form.value)
          ElMessage.success('更新成功')
        }
        dialogVisible.value = false
        getList()
      } catch (error) {
        ElMessage.error(dialogType.value === 'add' ? '新增失败' : '更新失败')
      }
    }
  })
}

// 处理分页
const handleSizeChange = (val) => {
  pageSize.value = val
  getList()
}

const handleCurrentChange = (val) => {
  currentPage.value = val
  getList()
}

// 初始化
onMounted(() => {
  getList()
})
</script>

<style scoped>
.category-container {
  padding: 20px;
}

.header {
  margin-bottom: 16px;
}

.header h2 {
  margin: 0;
  font-size: 20px;
  font-weight: 500;
}

.toolbar {
  margin-bottom: 16px;
}

.search-row {
  margin-bottom: 12px;
}

.search-input {
  width: 240px;
}

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

3. 跨域问题 🌐

3.1 配置cors中间件

// internal/api/api.go:NewServer

// 配置CORS
router.Use(func(c *gin.Context) {
	c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
	c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
	c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
	c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

	if c.Request.Method == "OPTIONS" {
		c.AbortWithStatus(204)
		return
	}

	c.Next()
})

4.页面效果 ✨

4.1 分类管理页面

image.png

4.2 书签管理页面

image.png

总结 📚

本文详细介绍了AI书签管理工具的前端页面实现和接口对接过程,主要完成了:

  1. ​书签管理页面​​:实现列表展示、搜索、分页、增删改查
  2. ​分类管理页面​​:实现分类的CRUD操作
  3. ​接口对接​​:封装API请求,处理跨域问题
  4. ​表单验证​​:实现表单验证逻辑

在下一篇文章中,我们将实现Ai创建书签功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值