文章目录
AI书签管理工具开发全记录(七):页面编写与接口对接
前言 📝
在上一篇博客中,我们完成了前端基础框架的搭建。现在,我们将实现书签和分类管理的具体页面,并与后端API进行对接,完成整个前后端交互流程。
1. 页面功能规划 📌
我们需要实现以下核心功能页面:
- 书签管理页面
- 书签列表展示
- 书签增删改查
- 分类管理页面
- 分类列表展示
- 分类增删改查
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 分类管理页面
4.2 书签管理页面
总结 📚
本文详细介绍了AI书签管理工具的前端页面实现和接口对接过程,主要完成了:
- 书签管理页面:实现列表展示、搜索、分页、增删改查
- 分类管理页面:实现分类的CRUD操作
- 接口对接:封装API请求,处理跨域问题
- 表单验证:实现表单验证逻辑
在下一篇文章中,我们将实现Ai创建书签功能。