后端代码实现
文档表设计与代码生成
文档表设计
drop table if exists `doc`;
create table `doc` (
`id` bigint not null comment 'id',
`ebook_id` bigint not null default 0 comment '电子书id',
`parent` bigint not null default 0 comment '父id',
`name` varchar(50) not null comment '名称',
`sort` int comment '顺序',
`view_count` int default 0 comment '阅读数',
`vote_count` int default 0 comment '点赞数',
primary key (`id`)
)engine=innodb default charset=utf8mb4 comment='文档';
insert into doc values
(1,1,0,'文档1',1,0,0),
(2,1,1,'文档1.1',1,0,0),
(3,1,0,'文档2',2,0,0),
(4,1,3,'文档2.1',1,0,0),
(5,1,3,'文档2.2',2,0,0),
(6,1,5,'文档2.2.1',1,0,0);
Mybatis生成Model实体类、Mapper接口类和Mapper XML文件
generatorConfig.xml内容如下,
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="Mysql" targetRuntime="MyBatis3">
<commentGenerator>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/test-database"
userId="test-user"
password="test@123">
</jdbcConnection>
<javaModelGenerator targetPackage="com.jepcc.test.model" targetProject="src\main\java">
</javaModelGenerator>
<sqlMapGenerator targetPackage="mapper" targetProject="src\main\resources">
</sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER" targetPackage="com.jepcc.test.mapper" targetProject="src\main\java">
</javaClientGenerator>
<!-- <table tableName="ebook" domainObjectName="Ebook" ></table>-->
<!-- <table tableName="category" domainObjectName="Category" ></table>-->
<table tableName="doc" domainObjectName="Doc" ></table>
</context>
</generatorConfiguration>
执行命令:mybatis-generator:generate -e
,生成如下文件:
- Model实体类:
com.jepcc.test.model.Doc
和com.jepcc.test.model.DocExample
- Mapper接口类:
com.jepcc.test.mapper.DocMapper
- Mapper XML文件:
src/main/resources/mapper/DocMapper.xml
完成文档表的增删改查功能
- com.jepcc.test.controller.DocController
package com.jepcc.test.controller;
import com.jepcc.test.req.DocReq;
import com.jepcc.test.req.DocSaveReq;
import com.jepcc.test.resp.DocResp;
import com.jepcc.test.resp.CommonResp;
import com.jepcc.test.resp.PageResp;
import com.jepcc.test.service.DocService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/doc")
public class DocController {
private final static Logger LOG = LoggerFactory.getLogger(DocController.class);
@Resource
private DocService docService;
@GetMapping("/all")
public CommonResp all(){
CommonResp resp = new CommonResp();
List<DocResp> list = docService.all();
resp.setContent(list);
return resp;
}
@GetMapping("/list")
public CommonResp list(DocReq req){
CommonResp resp = new CommonResp();
PageResp<DocResp> list = docService.list(req);
resp.setContent(list);
return resp;
}
@PostMapping("/save")
public CommonResp save(@RequestBody DocSaveReq req){
CommonResp resp = new CommonResp();
docService.save(req);
return resp;
}
@DeleteMapping("/delete/{id}")
public CommonResp delete(@PathVariable long id){
CommonResp resp = new CommonResp();
docService.delete(id);
return resp;
}
}
- com.jepcc.test.service.DocService
package com.jepcc.test.service;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.jepcc.test.mapper.DocMapper;
import com.jepcc.test.model.Doc;
import com.jepcc.test.model.DocExample;
import com.jepcc.test.req.DocReq;
import com.jepcc.test.req.DocSaveReq;
import com.jepcc.test.resp.DocResp;
import com.jepcc.test.resp.PageResp;
import com.jepcc.test.util.CopyUtil;
import com.jepcc.test.util.SnowFlake;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
import java.util.List;
@Service
public class DocService {
@Resource
private DocMapper docMapper;
@Resource
private SnowFlake snowFlake;
private static final Logger LOG = LoggerFactory.getLogger(DocService.class);
public List<DocResp> all(){
DocExample docExample = new DocExample();
//按照sort字段asc排序
docExample.setOrderByClause("sort asc");
List<Doc> list = docMapper.selectByExample(docExample);
List<DocResp> result = CopyUtil.copyList(list,DocResp.class);
return result;
}
public PageResp<DocResp> list(DocReq req){
DocExample docExample = new DocExample();
DocExample.Criteria criteria = docExample.or();
if(!ObjectUtils.isEmpty(req.getName())){
criteria.andNameLike("%"+req.getName()+"%");
}
PageHelper.startPage(req.getPage(),req.getSize());
List<Doc> list = docMapper.selectByExample(docExample);
PageInfo<Doc> pageInfo = new PageInfo<>(list);
List<DocResp> result = CopyUtil.copyList(list,DocResp.class);
PageResp<DocResp> pageResp = new PageResp<>();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(result);
return pageResp;
}
public void save(DocSaveReq req){
Doc doc = CopyUtil.copy(req,Doc.class);
if(!ObjectUtils.isEmpty(doc.getId())){
// 更新
docMapper.updateByPrimaryKey(doc);
}else{
// 插入
doc.setId(snowFlake.nextId());
docMapper.insert(doc);
}
}
public void delete(long id){
docMapper.deleteByPrimaryKey(id);
}
}
- com.jepcc.test.req.DocReq
package com.jepcc.test.req;
public class DocReq extends PageReq{
private Long id;
private Long ebookId;
private Long parent;
private String name;
private Integer sort;
private Integer viewCount;
private Integer voteCount;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getEbookId() {
return ebookId;
}
public void setEbookId(Long ebookId) {
this.ebookId = ebookId;
}
public Long getParent() {
return parent;
}
public void setParent(Long parent) {
this.parent = parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
public Integer getViewCount() {
return viewCount;
}
public void setViewCount(Integer viewCount) {
this.viewCount = viewCount;
}
public Integer getVoteCount() {
return voteCount;
}
public void setVoteCount(Integer voteCount) {
this.voteCount = voteCount;
}
}
- com.jepcc.test.req.DocSaveReq
package com.jepcc.test.req;
public class DocSaveReq {
private Long id;
private Long ebookId;
private Long parent;
private String name;
private Integer sort;
private Integer viewCount;
private Integer voteCount;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getEbookId() {
return ebookId;
}
public void setEbookId(Long ebookId) {
this.ebookId = ebookId;
}
public Long getParent() {
return parent;
}
public void setParent(Long parent) {
this.parent = parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
public Integer getViewCount() {
return viewCount;
}
public void setViewCount(Integer viewCount) {
this.viewCount = viewCount;
}
public Integer getVoteCount() {
return voteCount;
}
public void setVoteCount(Integer voteCount) {
this.voteCount = voteCount;
}
}
- com.jepcc.test.resp.DocResp
package com.jepcc.test.resp;
public class DocResp {
private Long id;
private Long ebookId;
private Long parent;
private String name;
private Integer sort;
private Integer viewCount;
private Integer voteCount;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getEbookId() {
return ebookId;
}
public void setEbookId(Long ebookId) {
this.ebookId = ebookId;
}
public Long getParent() {
return parent;
}
public void setParent(Long parent) {
this.parent = parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
public Integer getViewCount() {
return viewCount;
}
public void setViewCount(Integer viewCount) {
this.viewCount = viewCount;
}
public Integer getVoteCount() {
return voteCount;
}
public void setVoteCount(Integer voteCount) {
this.voteCount = voteCount;
}
}
前端代码实现
- web/src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'
import About from "../views/About.vue";
import AdminEbook from "../views/admin/admin-ebook.vue";
import AdminCategory from "../views/admin/admin-category.vue";
import AdminDoc from "../views/admin/admin-doc.vue";
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component:About
},
{
path:"/admin/ebook",
name:"AdminEbook",
component:AdminEbook
},
{
path:"/admin/category",
name:"AdminCategory",
component:AdminCategory
},
{
path:"/admin/doc",
name:"AdminDoc",
component:AdminDoc
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
- web/src/views/admin/admin-ebook.vue
<template>
<a-layout>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<p>
<a-form :model="param" layout="inline">
<a-form-item>
<a-input v-model:value="param.name" placeholder="请输入名称"></a-input>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleQuery({page:1,size:pagination.pageSize})">查询</a-button>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="add">新增</a-button>
</a-form-item>
</a-form>
</p>
<a-table :columns="columns"
:dataSource="ebooks"
:row-key="record=>record.id"
:pagination="pagination"
:loading="loading"
@change="handleTableChange">
<template #cover="{text:cover}">
<img v-if="cover" :src="cover" alt="avatar">
</template>
<template #category="{text,record}">
<text>{{getCategoryName(record.category1Id)}}/{{getCategoryName(record.category2Id)}}</text>
</template>
<template #action="{text,record}">
<a-space>
<router-link to="/admin/doc">
<a-button type="primary">文档管理</a-button>
</router-link>
<a-button type="primary" @click="edit(record)">编辑</a-button>
<a-popconfirm
title="删除后不可恢复,确认删除?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDelete(record)"
>
<a-button type="danger">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
</a-layout-content>
</a-layout>
<a-modal
title="电子书表单"
v-model:visible="visible"
:confirm-loading="confirmLoading"
@ok="handleOk"
>
<a-form :model="ebook" :label-col="{span:4}" :wrapper-col="{span:16}">
<a-form-item label="封面">
<a-input v-model:value="ebook.cover" />
</a-form-item>
<a-form-item label="名称">
<a-input v-model:value="ebook.name" />
</a-form-item>
<a-form-item label="选择分类">
<a-cascader v-model:value="categoryIds" :options="options" @change="handleCascaderChange"
:field-names="{label:'name',value:'id',children:'children'}"/>
</a-form-item>
<a-form-item label="描述">
<a-input v-model:value="ebook.description" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts">
import {defineComponent, ref, onMounted, reactive} from 'vue';
import axios from "axios";
import {message} from "ant-design-vue";
import {arrayToTree} from "@/tools/tool";
export default defineComponent({
name:"AdminEbook",
setup: function () {
const ebooks = ref();
const pagination = ref({
current: 1,
pageSize: 4,
total: 0
});
const loading = ref(false);
const columns = [
{
title: "封面",
dataIndex: "cover",
key: "cover",
slots: {customRender: 'cover'}
},
{
title: "名称",
key: "name",
dataIndex: "name"
},
{
title: "分类",
key: "categoryId",
slots:{customRender: "category"}
},
{
title: "文档数",
key: "docCount",
dataIndex: "docCount"
},
{
title: "阅读数",
key: "viewCount",
dataIndex: "viewCount"
},
{
title: "点赞数",
key: "vouteCount",
dataIndex: "voteCount"
},
{
title: "操作",
key: "action",
dataIndex: "action",
slots: {customRender: 'action'}
}
];
let categorys:any = [];
const categoryIds = ref();
const options = ref();
const handleCategoryQuery = () => {
axios.get("/category/all").then(response => {
const data = response.data;
if(data.success){
categorys = data.content;
const result = arrayToTree(data.content,0);
console.log("get options successfully,",result);
options.value = result;
}else{
console.log("get options failed");
}
})
}
const getCategoryName = (categoryId:number) => {
let name = "";
for(let i=0;i<categorys.length;i++){
let item = categorys[i]
if(Number(item.id) === Number(categoryId)){
name = item.name;
break;
}
}
return name;
}
const handleCascaderChange = (value:any) => {
}
const visible = ref(false);
const modalText = ref('test');
const confirmLoading = ref(false);
const ebook = ref();
const param = reactive({
name:""
});
const handleQuery = (params: any) => {
loading.value = true;
axios.get("/ebook/list", {params: {
page:params.page,
size:params.size,
name:param.name
}}).then(response => {
loading.value = false;
const data = response.data;
const content = data.content;
if (data.success) {
console.log(content.list);
ebooks.value = content.list;
pagination.value.total = content.total;
pagination.value.current = params.page;
} else {
message.warn(data.message);
}
})
};
const handleTableChange = (pagination: any) => {
handleQuery({
page: pagination.current,
size: pagination.pageSize
});
}
const edit = (record: any) => {
visible.value = true;
ebook.value = JSON.parse(JSON.stringify(record));
categoryIds.value = [ebook.value.category1Id,ebook.value.category2Id];
}
const add = () => {
visible.value = true;
ebook.value = {};
}
const handleDelete = (record: any) => {
axios.delete("ebook/delete/" + record.id).then(response => {
const data = response.data;
if (data.success) {
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize
})
}
})
}
const handleOk = () => {
ebook.value.category1Id = categoryIds.value[0];
ebook.value.category2Id = categoryIds.value[1];
axios.post("/ebook/save", ebook.value).then(response => {
const data = response.data;
if (data.success) {
visible.value = false;
confirmLoading.value = false;
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize
})
} else {
message.warn(data.message);
}
})
};
onMounted(() => {
handleCategoryQuery();
handleQuery({
page: 1,
size: pagination.value.pageSize
});
})
return {
columns,
ebooks,
loading,
pagination,
visible,
modalText,
confirmLoading,
ebook,
param,
categoryIds,
options,
handleTableChange,
edit,
add,
handleDelete,
handleOk,
handleQuery,
handleCascaderChange,
getCategoryName
}
}
})
</script>
<style scoped>
img{
width: 48px;
height: 48px;
}
</style>
- web/src/views/admin/admin-doc.vue
<template>
<a-layout>
<a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">
<p>
<a-button type="primary" @click="handleAdd">新增</a-button>
</p>
<a-table :columns="columns" :data-source="docs" :row-key="record=>record.id"
:loading="loading" :pagination="false" @change="handleChange">
<template #action="{text,record}">
<a-space>
<a-button type="primary" @click="edit(record)">编辑</a-button>
<a-popconfirm
title="删除后不可恢复,确认删除?"
ok-text="确定"
cancel-text="取消"
@confirm="confirm(record)"
>
<a-button type="danger">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
</a-layout-content>
</a-layout>
<a-modal :visible="visible" title="文档管理" @ok="handleOk" @cancel="handleCancel">
<a-form :model="doc" :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }">
<a-form-item label="名称">
<a-input v-model:value="doc.name" />
</a-form-item>
<a-form-item label="文档">
<a-input v-model:value="doc.parent" />
<a-tree-select
v-model:value="doc.parent"
:replace-fields="{children:'children', title:'name', key:'id', value: 'id' }"
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="treeData"
placeholder="请选择父文档"
tree-default-expand-all
>
</a-tree-select>
</a-form-item>
<a-form-item label="排序">
<a-input v-model:value="doc.sort" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts">
import {defineComponent, onMounted, ref} from 'vue';
import axios from "axios";
import {arrayToTree} from "@/tools/tool";
interface docItem {
id:number,
name:string,
parent?:number,
sort?:number,
children?:docItem[],
ebookId?:number,
viewCount?: number,
voteCount?: number,
disabled?:boolean,
}
// const setChildrenDisable = (children:docItem[]) => {
// for(let i=0;i<children.length;i++){
// let child = children[i];
// child.disabled = true;
// if(child.children && child.children.length){
// setChildrenDisable(child.children);
// }
// }
// }
// const setDisable = (arr:docItem[],targetId:number) => {
// const len = arr.length;
// if(len === 0) return;
// for(let i=0;i<len;i++){
// let item = arr[i];
// if(Number(item["id"]) === Number(targetId)){
// item.disabled = true;
// if(item.children && item.children.length){
// setChildrenDisable(item.children);
// }
// }
// if(item.children && item.children.length){
// setDisable(item.children,targetId);
// }
// }
// }
const setDisable = (arr:docItem[],targetId:number|string) => {
if(arr.length === 0) return ;
for(let i=0;i<arr.length;i++){
let node = arr[i];
console.log("typeof node.id",typeof node.id);
console.log("typeof targetId",typeof targetId);
if(Number(node.id)=== Number(targetId)){
node.disabled = true;
let children = node.children;
if(children && children.length){
for(let j=0;j<children.length;j++){
setDisable(children,children[j].id);
}
}
}else{
let children = node.children;
if(children && children.length){
setDisable(children,targetId);
}
}
}
}
export default defineComponent({
name:"AdminDoc",
setup() {
const columns = [
{
dataIndex: 'name',
key: 'name',
title:"名称"
},
{
dataIndex: 'parent',
key: 'parent',
title:"父文档"
},
{
dataIndex: 'sort',
key: 'sort',
title:"排序"
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' },
},
];
const loading = ref(false);
const docs = ref();
const visible = ref(false);
const doc = ref({
id:"",
parent:"",
name:"",
sort:""
});
const treeData = ref();
const handleQuery = () => {
loading.value = true;
axios.get("/doc/all").then(response => {
loading.value = false;
const data = response.data;
if(data.success){
let result = arrayToTree(data.content,0);
console.log("result of arrayToTree:",result);
docs.value = result;
}
})
}
const handleSave = (params:any) => {
axios.post("/doc/save",params).then(response => {
visible.value = false;
const data = response.data;
if(data.success){
handleQuery();
}else{
console.log("save failed");
}
})
}
const handleDelete = (record:any) => {
axios.delete("/doc/delete/"+record.id).then(response => {
const data = response.data;
if(data.success){
handleQuery();
}else{
console.log("delete failed");
}
})
}
const handleChange = (p:any) => {
handleQuery();
}
const edit = (record:any) => {
visible.value = true;
doc.value = JSON.parse(JSON.stringify(record));
treeData.value = JSON.parse(JSON.stringify(docs.value));
treeData.value.unshift({id:0,name:"无"});
setDisable(treeData.value,record.id);
}
const handleOk = () => {
handleSave(doc.value);
}
const handleCancel = () => {
visible.value = false;
}
const confirm = (record:any) => {
handleDelete(record)
}
const handleSearch = () => {
handleQuery();
}
const handleAdd = () => {
doc.value = {
id:"",
parent:"",
name:"",
sort:""
}
visible.value = true;
treeData.value = JSON.parse(JSON.stringify(docs.value));
treeData.value.unshift({id:0,name:"无"});
}
onMounted(() => {
handleQuery();
})
return {
docs,
columns,
loading,
visible,
doc,
treeData,
handleQuery,
handleChange,
edit,
handleOk,
handleCancel,
confirm,
handleSearch,
handleAdd
};
}
});
</script>
<style scoped>
</style>