本教程将以开发一个简单的 "文章管理 (Post)" 功能为例,带您完整体验 Tagtag Starter 的前后端开发流程。
我们将实现以下功能:
首先,在数据库中创建 sys_post 表。
CREATE TABLE `sys_post` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`title` varchar(100) NOT NULL COMMENT '文章标题',
`content` text COMMENT '文章内容',
`status` tinyint(4) DEFAULT 1 COMMENT '状态: 1-发布, 0-草稿',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
`deleted` tinyint(4) DEFAULT 0 COMMENT '删除标识: 0-未删除, 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_title` (`title`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表';
在 tagtag-contract 模块中定义 DTO (Data Transfer Object)。建议新建 PostDTO.java 和 PostQueryDTO.java。
PostDTO.java (用于新增/修改)
package dev.tagtag.contract.system.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class PostDTO {
private Long id;
@NotBlank(message = "标题不能为空")
private String title;
private String content;
@NotNull(message = "状态不能为空")
private Integer status;
}
PostQueryDTO.java (用于查询)
package dev.tagtag.contract.system.dto;
import lombok.Data;
@Data
public class PostQueryDTO {
private String title;
private Integer status;
}
PostOperationRequest.java (用于批量操作)
package dev.tagtag.contract.system.dto;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
@Data
public class PostOperationRequest {
@NotEmpty(message = "ID列表不能为空")
private java.util.List<Long> ids;
@NotNull(message = "状态不能为空")
private Integer status;
private String password;
}
在 tagtag-module-system 模块中创建实体类和 Mapper。
PostEntity.java
package dev.tagtag.module.system.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import dev.tagtag.framework.mybatis.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_post")
public class PostEntity extends BaseEntity {
private String title;
private String content;
private Integer status;
@TableLogic
private Integer deleted;
}
PostMapper.java
package dev.tagtag.module.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import dev.tagtag.contract.system.dto.PostQueryDTO;
import dev.tagtag.module.system.entity.Post;
import dev.tagtag.module.system.entity.vo.PostVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface PostMapper extends BaseMapper<Post> {
/**
* XML 分页查询(由 MyBatis XML 构建 WHERE/ORDER BY)
*
* @param page 分页对象(MyBatis Plus 拦截器识别)
* @param q 查询条件 DTO
* @return 分页结果
*/
IPage<PostVO> selectPage(IPage<PostVO> page, @Param("q") PostQueryDTO q);
}
PostVO.java (视图对象)
package dev.tagtag.module.system.entity.vo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class PostVO {
private Long id;
private String title;
private String content;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
PostService.java
package dev.tagtag.module.system.service;
import dev.tagtag.common.model.PageQuery;
import dev.tagtag.common.model.PageResult;
import dev.tagtag.contract.system.dto.PostDTO;
import dev.tagtag.contract.system.dto.PostQueryDTO;
import java.util.List;
public interface PostService {
/**
* 文章分页查询
* @param query 查询条件DTO
* @param pageQuery 分页参数
* @return 分页结果
*/
PageResult<PostDTO> page(PostQueryDTO query, PageQuery pageQuery);
/**
* 获取文章详情
* @param id 文章ID
* @return 文章DTO
*/
PostDTO getById(Long id);
/**
* 创建文章
* @param post 文章DTO
*/
void create(PostDTO post);
/**
* 更新文章(忽略源对象中的空值)
* @param post 文章DTO
*/
void update(PostDTO post);
/**
* 删除文章
* @param id 文章ID
*/
void delete(Long id);
/**
* 批量删除文章
* @param ids 文章ID列表
*/
void batchDelete(List<Long> ids);
/**
* 更新单个文章状态
* @param id 文章ID
* @param status 状态值
*/
void updateStatus(Long id, Integer status);
/**
* 批量更新文章状态
* @param ids 文章ID列表
* @param status 状态值
*/
void batchUpdateStatus(List<Long> ids, Integer status);
}
PostServiceImpl.java
package dev.tagtag.module.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import dev.tagtag.common.model.PageQuery;
import dev.tagtag.common.model.PageResult;
import dev.tagtag.common.util.BeanUtil;
import dev.tagtag.contract.system.dto.PostDTO;
import dev.tagtag.contract.system.dto.PostQueryDTO;
import dev.tagtag.module.system.entity.Post;
import dev.tagtag.module.system.entity.vo.PostVO;
import dev.tagtag.module.system.mapper.PostMapper;
import dev.tagtag.module.system.service.PostService;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class PostServiceImpl implements PostService {
private final PostMapper postMapper;
@Override
public PageResult<PostDTO> page(PostQueryDTO query, PageQuery pageQuery) {
IPage<PostVO> page = new Page<>(pageQuery.getPage(), pageQuery.getSize());
IPage<PostVO> result = postMapper.selectPage(page, query);
return PageResult.of(
result.getRecords().stream()
.map(vo -> BeanUtil.copyProperties(vo, PostDTO.class))
.toList(),
result.getTotal()
);
}
@Override
public PostDTO getById(Long id) {
Post post = postMapper.selectById(id);
if (post == null) {
throw new RuntimeException("文章不存在");
}
return BeanUtil.copyProperties(post, PostDTO.class);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void create(PostDTO post) {
Post entity = BeanUtil.copyProperties(post, Post.class);
postMapper.insert(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(PostDTO post) {
Post entity = BeanUtil.copyProperties(post, Post.class);
postMapper.updateById(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
postMapper.deleteById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void batchDelete(List<Long> ids) {
postMapper.deleteBatchIds(ids);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateStatus(Long id, Integer status) {
Post entity = new Post();
entity.setId(id);
entity.setStatus(status);
postMapper.updateById(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void batchUpdateStatus(List<Long> ids, Integer status) {
ids.forEach(id -> updateStatus(id, status));
}
}
PostController.java
package dev.tagtag.module.system.controller;
import dev.tagtag.common.model.PageQuery;
import dev.tagtag.common.model.PageResult;
import dev.tagtag.common.model.Result;
import dev.tagtag.contract.system.dto.PostDTO;
import dev.tagtag.contract.system.dto.PostQueryDTO;
import dev.tagtag.module.system.service.PostService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/posts")
@Tag(name = "文章管理")
@RequiredArgsConstructor
public class PostController {
private final PostService postService;
/**
* 分页查询文章
*/
@GetMapping
@Operation(summary = "分页查询文章")
@PreAuthorize("@ss.hasPermission('system:post:query')")
public Result<PageResult<PostDTO>> page(PostQueryDTO query, PageQuery pageQuery) {
return Result.ok(postService.page(query, pageQuery));
}
/**
* 获取文章详情
*/
@GetMapping("/{id}")
@Operation(summary = "获取文章详情")
@PreAuthorize("@ss.hasPermission('system:post:query')")
public Result<PostDTO> getById(@PathVariable Long id) {
return Result.ok(postService.getById(id));
}
/**
* 创建文章
*/
@PostMapping
@Operation(summary = "创建文章")
@PreAuthorize("@ss.hasPermission('system:post:create')")
public Result<Void> create(@Valid @RequestBody PostDTO dto) {
postService.create(dto);
return Result.okMsg("创建成功");
}
/**
* 更新文章
*/
@PutMapping
@Operation(summary = "更新文章")
@PreAuthorize("@ss.hasPermission('system:post:update')")
public Result<Void> update(@Valid @RequestBody PostDTO dto) {
postService.update(dto);
return Result.okMsg("更新成功");
}
/**
* 删除文章
*/
@DeleteMapping("/{id}")
@Operation(summary = "删除文章")
@PreAuthorize("@ss.hasPermission('system:post:delete')")
public Result<Void> delete(@PathVariable Long id) {
postService.delete(id);
return Result.okMsg("删除成功");
}
/**
* 批量删除文章
*/
@DeleteMapping("/batch")
@Operation(summary = "批量删除文章")
@PreAuthorize("@ss.hasPermission('system:post:delete')")
public Result<Void> batchDelete(@RequestBody List<Long> ids) {
postService.batchDelete(ids);
return Result.okMsg("批量删除成功");
}
/**
* 更新文章状态
*/
@PutMapping("/{id}/status")
@Operation(summary = "更新文章状态")
@PreAuthorize("@ss.hasPermission('system:post:update')")
public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
postService.updateStatus(id, status);
return Result.okMsg("状态更新成功");
}
/**
* 批量更新文章状态
*/
@PutMapping("/status/batch")
@Operation(summary = "批量更新文章状态")
@PreAuthorize("@ss.hasPermission('system:post:update')")
public Result<Void> batchUpdateStatus(@RequestBody List<Long> ids, @RequestParam Integer status) {
postService.batchUpdateStatus(ids, status);
return Result.okMsg("批量状态更新成功");
}
}
关键要点:
@PreAuthorize 注解进行权限控制@Operation 注解提供 API 文档说明在 frontend/apps/tagtag/src/api/modules/system/post.ts 中定义请求。
post.ts
import { request } from '@/api/request';
export interface PostDTO {
id?: number;
title: string;
content?: string;
author?: string;
status?: number;
categoryId?: number;
tags?: string[];
coverUrl?: string;
summary?: string;
}
export interface PostQueryDTO {
title?: string;
author?: string;
status?: number;
categoryId?: number;
startDate?: string;
endDate?: string;
}
export interface PageResult<T> {
records: T[];
total: number;
current: number;
size: number;
}
/**
* 分页查询文章
*/
export function getPostPage(params: PostQueryDTO & { page: number; size: number }) {
return request.get<PageResult<PostDTO>>({ url: '/posts', params });
}
/**
* 获取文章详情
*/
export function getPostById(id: number) {
return request.get<PostDTO>({ url: `/posts/${id}` });
}
/**
* 创建文章
*/
export function createPost(data: PostDTO) {
return request.post({ url: '/posts', data });
}
/**
* 更新文章
*/
export function updatePost(data: PostDTO) {
return request.put({ url: '/posts', data });
}
/**
* 删除文章
*/
export function deletePost(id: number) {
return request.delete({ url: `/posts/${id}` });
}
/**
* 批量删除文章
*/
export function batchDeletePost(ids: number[]) {
return request.delete({ url: '/posts/batch', data: ids });
}
/**
* 更新文章状态
*/
export function updatePostStatus(id: number, status: number) {
return request.put({ url: `/posts/${id}/status`, params: { status } });
}
/**
* 批量更新文章状态
*/
export function batchUpdatePostStatus(ids: number[], status: number) {
return request.put({ url: '/posts/status/batch', data: ids, params: { status } });
}
关键要点:
创建 frontend/apps/tagtag/src/views/modules/system/post/index.vue。
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { useTable } from '@vben/plugins/vxe-table';
import { getPostPage, deletePost, addPost, updatePost } from '@/api/modules/system/post';
import { Button, Tag, Modal } from 'ant-design-vue';
import FormModal from './FormModal.vue';
// 表格配置
const [Grid, gridApi] = useTable({
api: getPostPage,
columns: [
{ type: 'checkbox', width: 50 },
{ field: 'title', title: '标题' },
{ field: 'status', title: '状态', slots: { default: 'status' } },
{ field: 'createTime', title: '创建时间', width: 180 },
{ title: '操作', slots: { default: 'action' }, width: 150 },
],
formConfig: {
items: [
{ field: 'title', label: '标题', component: 'Input' },
{
field: 'status',
label: '状态',
component: 'Select',
componentProps: {
options: [
{ label: '已发布', value: 1 },
{ label: '草稿', value: 0 }
]
}
}
]
},
toolbarConfig: {
buttons: [
{ code: 'add', name: '新增', status: 'primary' },
{ code: 'delete', name: '删除', status: 'danger' }
]
}
});
// 弹窗配置
const modalVisible = ref(false);
const modalTitle = ref('');
const formData = reactive({});
const isEdit = ref(false);
// 打开新增弹窗
function handleAdd() {
isEdit.value = false;
modalTitle.value = '新增文章';
Object.assign(formData, {});
modalVisible.value = true;
}
// 打开编辑弹窗
function handleEdit(row) {
isEdit.value = true;
modalTitle.value = '编辑文章';
Object.assign(formData, row);
modalVisible.value = true;
}
// 删除单个文章
async function handleDelete(row) {
Modal.confirm({
title: '确认删除',
content: `确定要删除文章 "${row.title}" 吗?`,
onOk: async () => {
await deletePost([row.id]);
gridApi.refresh();
}
});
}
// 批量删除
async function handleBatchDelete(ids) {
Modal.confirm({
title: '确认删除',
content: `确定要删除选中的 ${ids.length} 篇文章吗?`,
onOk: async () => {
await deletePost(ids);
gridApi.refresh();
}
});
}
// 保存文章
async function handleSave(data) {
if (isEdit.value) {
await updatePost(data);
} else {
await addPost(data);
}
modalVisible.value = false;
gridApi.refresh();
}
// 监听工具栏按钮点击
function handleToolbarClick(button) {
if (button.code === 'add') {
handleAdd();
} else if (button.code === 'delete') {
const selectedRows = gridApi.getSelectedRows();
if (selectedRows.length === 0) {
Modal.warning({ title: '提示', content: '请选择要删除的文章' });
return;
}
handleBatchDelete(selectedRows.map(row => row.id));
}
}
</script>
<template>
<Grid @toolbar-click="handleToolbarClick">
<template #status="{ row }">
<Tag :color="row.status === 1 ? 'green' : 'red'">
{{ row.status === 1 ? '已发布' : '草稿' }}
</Tag>
</template>
<template #action="{ row }">
<Button type="link" @click="handleEdit(row)">编辑</Button>
<Button type="link" danger @click="handleDelete(row)">删除</Button>
</template>
</Grid>
<!-- 新增/编辑弹窗 -->
<FormModal
v-model:visible="modalVisible"
:title="modalTitle"
:form-data="formData"
@save="handleSave"
/>
</template>
创建 frontend/apps/tagtag/src/views/modules/system/post/FormModal.vue。
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import { Modal, Form, Input, Select, Switch } from 'ant-design-vue';
// Props
const props = defineProps({
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
formData: {
type: Object,
default: () => ({})
}
});
// Emits
const emit = defineEmits(['update:visible', 'save']);
// 表单实例
const formRef = ref();
// 表单模型
const formModel = reactive({
id: null,
title: '',
content: '',
status: 1
});
// 监听表单数据变化
watch(
() => props.formData,
(newData) => {
Object.assign(formModel, newData);
},
{ deep: true, immediate: true }
);
// 关闭弹窗
function handleCancel() {
emit('update:visible', false);
formRef.value?.resetFields();
}
// 提交表单
async function handleSubmit() {
if (!formRef.value) return;
try {
await formRef.value.validate();
emit('save', { ...formModel });
emit('update:visible', false);
} catch (error) {
console.error('表单验证失败:', error);
}
}
</script>
<template>
<Modal
v-model:open="visible"
:title="title"
ok-text="保存"
cancel-text="取消"
@ok="handleSubmit"
@cancel="handleCancel"
width="800px"
>
<Form
ref="formRef"
:model="formModel"
layout="vertical"
:rules="{
title: [{ required: true, message: '请输入文章标题', trigger: 'blur' }],
content: [{ required: true, message: '请输入文章内容', trigger: 'blur' }]
}"
>
<Form.Item name="id" hidden>
<Input v-model:value="formModel.id" />
</Form.Item>
<Form.Item label="文章标题" name="title">
<Input v-model:value="formModel.title" placeholder="请输入文章标题" />
</Form.Item>
<Form.Item label="文章内容" name="content">
<Input.TextArea
v-model:value="formModel.content"
placeholder="请输入文章内容"
rows="6"
/>
</Form.Item>
<Form.Item label="发布状态" name="status">
<Switch
v-model:checked="formModel.status"
checked-children="已发布"
un-checked-children="草稿"
/>
</Form.Item>
</Form>
</Modal>
</template>
/system/post,组件路径填 modules/system/post/index.vue。为了确保代码质量和功能正确性,我们需要为后端服务编写单元测试。Tagtag Starter 使用 JUnit 5 和 Mockito 进行单元测试。
首先,确保在 pom.xml 中添加了必要的测试依赖:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
创建 PostServiceImplTest.java 文件,位于 src/test/java 目录下:
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class PostServiceImplTest {
@Mock
private PostMapper postMapper;
@InjectMocks
private PostServiceImpl postService;
@Test
void testPage() {
// 准备测试数据
PostQuery query = new PostQuery();
query.setPage(1);
query.setSize(10);
query.setTitle("测试");
// 模拟分页结果
Page<PostEntity> page = new Page<>(1, 10);
List<PostEntity> records = new ArrayList<>();
records.add(new PostEntity(1L, "测试文章", "内容", 1));
page.setRecords(records);
page.setTotal(1);
// 设置 Mock 行为
when(postMapper.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(page);
// 执行测试
PageResult<PostEntity> result = postService.page(query);
// 验证结果
assertThat(result).isNotNull();
assertThat(result.getTotal()).isEqualTo(1);
assertThat(result.getRecords()).hasSize(1);
assertThat(result.getRecords().get(0).getTitle()).isEqualTo("测试文章");
}
@Test
void testSave() {
// 准备测试数据
PostDTO dto = new PostDTO();
dto.setTitle("测试文章");
dto.setContent("内容");
dto.setStatus(1);
// 设置 Mock 行为
when(postMapper.insert(any(PostEntity.class))).thenReturn(1);
// 执行测试
postService.save(dto);
// 验证结果(可以添加更多验证)
Mockito.verify(postMapper, times(1)).insert(any(PostEntity.class));
}
@Test
void testUpdate() {
// 准备测试数据
PostDTO dto = new PostDTO();
dto.setId(1L);
dto.setTitle("更新测试文章");
dto.setContent("更新内容");
dto.setStatus(0);
// 设置 Mock 行为
when(postMapper.updateById(any(PostEntity.class))).thenReturn(1);
// 执行测试
postService.update(dto);
// 验证结果
Mockito.verify(postMapper, times(1)).updateById(any(PostEntity.class));
}
@Test
void testDelete() {
// 准备测试数据
List<Long> ids = Arrays.asList(1L, 2L, 3L);
// 设置 Mock 行为
when(postMapper.deleteBatchIds(ids)).thenReturn(3);
// 执行测试
postService.delete(ids);
// 验证结果
Mockito.verify(postMapper, times(1)).deleteBatchIds(ids);
}
}
前端测试包括单元测试和集成测试,Tagtag Starter 使用 Vitest 进行单元测试,使用 Cypress 进行 E2E 测试。
创建 post.test.ts 文件,位于 frontend/apps/tagtag/src/api/modules/system/__tests__ 目录下:
import { describe, it, expect, vi } from 'vitest';
import { getPostPage, addPost, updatePost, deletePost } from '../post';
import { request } from '@/api/request';
// 模拟 request 模块
vi.mock('@/api/request', () => ({
request: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn()
}
}));
describe('Post API', () => {
it('getPostPage should call request.get with correct params', async () => {
// 准备测试数据
const params = { page: 1, size: 10, title: 'test' };
const mockResponse = { data: { records: [], total: 0 } };
// 设置模拟返回值
(request.get as vi.Mock).mockResolvedValue(mockResponse);
// 执行测试
const result = await getPostPage(params);
// 验证结果
expect(request.get).toHaveBeenCalledWith({ url: '/posts', params });
expect(result).toEqual(mockResponse);
});
it('addPost should call request.post with correct data', async () => {
// 准备测试数据
const data = { title: 'test', content: 'test content', status: 1 };
const mockResponse = { data: { success: true } };
// 设置模拟返回值
(request.post as vi.Mock).mockResolvedValue(mockResponse);
// 执行测试
const result = await addPost(data);
// 验证结果
expect(request.post).toHaveBeenCalledWith({ url: '/posts', data });
expect(result).toEqual(mockResponse);
});
it('updatePost should call request.put with correct data', async () => {
// 准备测试数据
const data = { id: 1, title: 'updated', content: 'updated content', status: 0 };
const mockResponse = { data: { success: true } };
// 设置模拟返回值
(request.put as vi.Mock).mockResolvedValue(mockResponse);
// 执行测试
const result = await updatePost(data);
// 验证结果
expect(request.put).toHaveBeenCalledWith({ url: '/posts', data });
expect(result).toEqual(mockResponse);
});
it('deletePost should call request.delete with correct ids', async () => {
// 准备测试数据
const ids = [1, 2, 3];
const mockResponse = { data: { success: true } };
// 设置模拟返回值
(request.delete as vi.Mock).mockResolvedValue(mockResponse);
// 执行测试
const result = await deletePost(ids);
// 验证结果
expect(request.delete).toHaveBeenCalledWith({ url: '/posts', data: ids });
expect(result).toEqual(mockResponse);
});
});
创建 index.test.ts 文件,位于 frontend/apps/tagtag/src/views/modules/system/post/__tests__ 目录下:
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import PostIndex from '../index.vue';
import { useTable } from '@vben/plugins/vxe-table';
// 模拟依赖
vi.mock('@vben/plugins/vxe-table', () => ({
useTable: vi.fn(() => [
// 模拟 Grid 组件
{ template: '<div></div>' },
// 模拟 gridApi
{
refresh: vi.fn(),
getSelectedRows: vi.fn(() => [])
}
])
}));
describe('PostIndex Component', () => {
it('should render correctly', () => {
const wrapper = mount(PostIndex);
expect(wrapper.exists()).toBe(true);
});
it('should call handleAdd when add button is clicked', async () => {
// 模拟 handleAdd 方法
const handleAdd = vi.fn();
const wrapper = mount(PostIndex, {
global: {
stubs: {
Grid: { template: '<div @toolbar-click="$emit(\'toolbar-click\', $event)"></div>' },
FormModal: { template: '<div></div>' }
}
}
});
// 触发工具栏点击事件
await wrapper.findComponent({ name: 'Grid' }).vm.$emit('toolbar-click', { code: 'add' });
// 验证 handleAdd 被调用
// 这里可以根据实际实现添加更详细的验证
expect(wrapper.vm.modalVisible).toBe(true);
});
});
通过以上步骤,您已经成功开发了一个包含前后端交互的完整 CRUD 模块,并编写了相应的测试用例。Tagtag Starter 的模块化设计让这一切变得清晰且规范。
通过遵循这个流程,您可以快速、高效地开发出高质量的 CRUD 功能模块。