This commit is contained in:
18796357645 2025-05-22 21:38:47 +08:00
parent 61fb85befa
commit 20aeb9596e
21 changed files with 729 additions and 0 deletions

46
db/tb_book.sql Normal file
View File

@ -0,0 +1,46 @@
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 50736 (5.7.36)
Source Host : localhost:3306
Source Schema : block-chaincopyright
Target Server Type : MySQL
Target Server Version : 50736 (5.7.36)
File Encoding : 65001
Date: 22/05/2025 20:27:46
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for tb_book
-- ----------------------------
DROP TABLE IF EXISTS `tb_book`;
CREATE TABLE `tb_book` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '封面',
`isbn` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'ISBN编号',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '图书标题',
`author` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '作者',
`publisher` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '出版社',
`publish_date` date NULL DEFAULT NULL COMMENT '出版日期',
`copyright_owner` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '版权持有人',
`copyright_start_year` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '版权起始年份',
`copyright_end_year` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '版权到期年份',
`edition` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '版次',
`language` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '中文' COMMENT '语言',
`price` decimal(10, 2) NULL DEFAULT NULL COMMENT '图书定价',
`hex` varchar(600) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '上链哈希值',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`file` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '电子数据文件地址',
`user_id` bigint(20) NULL DEFAULT NULL,
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '已通过',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1925518111079583746 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '基于区块链的图书版权结构表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;

683
ui/src/pages/admin/book.vue Normal file
View File

@ -0,0 +1,683 @@
<template>
<div class="house-management-container">
<!-- 搜索区域 -->
<div class="search-area">
<el-form :inline="true" :model="state.query" class="search-form">
<el-form-item label="版权名称:" class="search-item">
<el-input
v-model="state.query.title"
placeholder="请输入版权名称"
clearable
@input="handleSearch"
class="search-input"
/>
</el-form-item>
<el-form-item label="状态:" class="search-item">
<el-select clearable
v-model="state.query.status"
placeholder="请选择状态"
@change="handleSearch"
style="width: 120px">
<el-option
v-for="item in state.statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item class="action-buttons">
<el-button type="primary" @click="init" class="query-button">
<el-icon>
<Search />
</el-icon>
<span>查询</span>
</el-button>
<el-button type="primary" @click="openAddDialog" class="add-button">
<el-icon>
<Plus />
</el-icon>
<span>添加</span>
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 表格区域 -->
<div class="table-area">
<el-table
v-loading="state.loading"
:data="state.list"
stripe
border
class="data-table"
empty-text="暂无数据"
>
<el-table-column label="封面图" width="100" align="center">
<template #default="{ row }">
<el-image :src="row.image" fit="cover" class="book-image" />
</template>
</el-table-column>
<el-table-column prop="isbn" label="ISBN" align="center" />
<el-table-column prop="title" label="书名" align="center" />
<el-table-column prop="author" label="作者" align="center" />
<el-table-column prop="publisher" label="出版社" align="center" />
<el-table-column prop="publishDate" label="出版日期" align="center" />
<el-table-column prop="copyrightOwner" label="版权持有人" align="center" />
<el-table-column prop="copyrightStartYear" label="版权开始" align="center" />
<el-table-column prop="copyrightEndYear" label="版权到期" align="center" />
<el-table-column prop="edition" label="版次" align="center" />
<el-table-column prop="price" label="定价(元)" align="center" />
<el-table-column label="状态" align="center" width="100">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="220" align="center">
<template #default="{ row }">
<el-button size="small" type="info" plain @click="showDetail(row)">详情</el-button>
<el-button
size="small"
type="primary"
plain
@click="edit(row)"
v-if="row.status === '未审核'"
>
编辑
</el-button>
<el-button
size="small"
type="warning"
plain
@click="openAuditDialog(row)"
v-if="row.status === '未审核'"
>
审核
</el-button>
<el-popconfirm
title="确认删除该版权信息?"
@confirm="del(row.id)"
>
<template #reference>
<el-button size="small" type="danger" plain>删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container" v-if="state.query.total > 0">
<el-pagination
:current-page="state.query.page"
:page-size="state.query.limit"
:total="state.query.total"
:page-sizes="[5,10,20,50]"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
<!-- 在template中添加详情对话框 -->
<el-dialog
v-model="state.detailVisible"
title="版权详情"
width="50%"
class="detail-dialog"
>
<el-descriptions :column="2" border>
<el-descriptions-item label="ISBN">{{ state.detailData.isbn }}</el-descriptions-item>
<el-descriptions-item label="书名">{{ state.detailData.title }}</el-descriptions-item>
<el-descriptions-item label="作者">{{ state.detailData.author }}</el-descriptions-item>
<el-descriptions-item label="出版社">{{ state.detailData.publisher }}</el-descriptions-item>
<el-descriptions-item label="出版日期">{{ state.detailData.publishDate }}</el-descriptions-item>
<el-descriptions-item label="版权持有人">{{ state.detailData.copyrightOwner }}</el-descriptions-item>
<el-descriptions-item label="版权开始">{{ state.detailData.copyrightStartYear }}</el-descriptions-item>
<el-descriptions-item label="版权到期">{{ state.detailData.copyrightEndYear }}</el-descriptions-item>
<el-descriptions-item label="版次">{{ state.detailData.edition }}</el-descriptions-item>
<el-descriptions-item label="定价(元)">{{ state.detailData.price }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusTagType(state.detailData.status)">
{{ state.detailData.status }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<div class="detail-image-container">
<el-image
:src="state.detailData.image"
fit="contain"
class="detail-image"
:preview-src-list="[state.detailData.image]"
/>
</div>
<template #footer>
<el-button @click="state.detailVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 在template中添加审核对话框 -->
<el-dialog
v-model="state.auditVisible"
title="版权审核"
width="40%"
>
<el-form :model="state.auditForm" label-width="100px">
<el-form-item label="审核状态">
<el-radio-group v-model="state.auditForm.status">
<el-radio label="已同意">同意</el-radio>
<el-radio label="已拒绝">拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见">
<el-input
v-model="state.auditForm.remark"
type="textarea"
:rows="3"
placeholder="请输入审核意见"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="state.auditVisible = false">取消</el-button>
<el-button type="primary" @click="submitAudit">提交审核</el-button>
</template>
</el-dialog>
<!-- 在template中添加表单对话框 -->
<el-dialog
v-model="state.dialogVisible"
:title="state.formData.id ? '编辑版权' : '新增版权'"
width="50%"
class="form-dialog"
>
<el-form
ref="formRef"
:model="state.formData"
:rules="state.rules"
label-width="100px"
>
<el-form-item label="ISBN" prop="isbn">
<el-input v-model="state.formData.isbn" placeholder="请输入ISBN" />
</el-form-item>
<el-form-item label="书名" prop="title">
<el-input v-model="state.formData.title" placeholder="请输入书名" />
</el-form-item>
<el-form-item label="作者" prop="author">
<el-input v-model="state.formData.author" placeholder="请输入作者" />
</el-form-item>
<el-form-item label="出版社" prop="publisher">
<el-input v-model="state.formData.publisher" placeholder="请输入出版社" />
</el-form-item>
<el-form-item label="出版日期" prop="publishDate">
<el-date-picker
v-model="state.formData.publishDate"
type="date"
placeholder="选择出版日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="版权持有人" prop="copyrightOwner">
<el-input
v-model="state.formData.copyrightOwner"
placeholder="请输入版权持有人"
/>
</el-form-item>
<el-form-item label="开始年份" prop="copyrightStartYear">
<el-input
v-model="state.formData.copyrightStartYear"
placeholder="请输入版权开始年份"
/>
</el-form-item>
<el-form-item label="到期年份" prop="copyrightEndYear">
<el-input
v-model="state.formData.copyrightEndYear"
placeholder="请输入版权到期年份"
/>
</el-form-item>
<el-form-item label="版次" prop="edition">
<el-input v-model="state.formData.edition" placeholder="请输入版次" />
</el-form-item>
<el-form-item label="定价(元)" prop="price">
<el-input-number
v-model="state.formData.price"
:min="0"
:precision="2"
controls-position="right"
/>
</el-form-item>
<el-form-item label="封面图" prop="image">
<el-upload
:action="state.path"
list-type="picture-card"
:show-file-list="false"
:on-success="handleImageSuccess"
:before-upload="beforeImageUpload"
>
<img
v-if="state.formData.image"
:src="state.formData.image"
class="uploaded-image"
alt="封面图片"
/>
<el-icon v-else class="uploader-icon">
<Plus />
</el-icon>
</el-upload>
<div class="upload-tip">建议尺寸16:9大小不超过5MB</div>
</el-form-item>
<el-form-item label="电子文件" prop="file" class="file-upload-item">
<el-upload
drag
:action="state.path"
:limit="1"
:on-success="handleFileSuccess"
:on-error="handleUploadError"
:before-upload="beforeFileUpload"
:file-list="fileList"
>
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">
将图书电子文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
支持PDFEPUB等格式大小不超过50MB
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="saveTransaction">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import type { FormInstance, UploadProps } from 'element-plus'
import { Search, Plus, Picture, UploadFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getDecoration, getFace, getHouseType } from '~/utils/utils'
//
const fileList = ref([])
const formRef = ref<FormInstance>()
const state = reactive({
path:import.meta.env.VITE_API_FRONT_BASE_URL+"/api/upload",
route: 'sys/book',
loading: false,
list: [] as any[],
dialogVisible: false,
// /
query: {
total: 0,
page: 1,
limit: 5,
title: '',
status: null as number | null
},
//
statusOptions: [
{ label: '全部', value: null },
{ label: '未审核', value: '未审核' },
{ label: '已同意', value: '已同意' },
{ label: '已拒绝', value: '已拒绝' }
],
//
formData: {} as Record<string, any>,
userList: [],
detailVisible: false,
detailData: {} as any,
auditVisible: false,
auditForm: {
id: null as number | null,
status: '已同意',
remark: ''
},
//
rules: {
isbn: [
{ required: true, message: '请输入ISBN', trigger: 'blur' },
{ min: 10, max: 13, message: 'ISBN长度在10到13个字符', trigger: 'blur' }
],
title: [
{ required: true, message: '请输入书名', trigger: 'blur' },
{ max: 100, message: '书名不能超过100个字符', trigger: 'blur' }
],
author: [
{ required: true, message: '请输入作者', trigger: 'blur' },
{ max: 50, message: '作者名不能超过50个字符', trigger: 'blur' }
],
publisher: [
{ required: true, message: '请输入出版社', trigger: 'blur' },
{ max: 100, message: '出版社名称不能超过100个字符', trigger: 'blur' }
],
publishDate: [
{ required: true, message: '请选择出版日期', trigger: 'change' }
],
copyrightOwner: [
{ required: true, message: '请输入版权持有人', trigger: 'blur' },
{ max: 100, message: '版权持有人不能超过100个字符', trigger: 'blur' }
],
copyrightStartYear: [
{ required: true, message: '请输入版权开始年份', trigger: 'blur' },
{ pattern: /^\d{4}$/, message: '请输入4位年份', trigger: 'blur' }
],
copyrightEndYear: [
{ required: true, message: '请输入版权到期年份', trigger: 'blur' },
{ pattern: /^\d{4}$/, message: '请输入4位年份', trigger: 'blur' }
],
edition: [
{ required: true, message: '请输入版次', trigger: 'blur' },
{ max: 20, message: '版次不能超过20个字符', trigger: 'blur' }
],
price: [
{ required: true, message: '请输入定价', trigger: 'blur' },
{ type: 'number', min: 0, message: '定价必须大于0', trigger: 'blur' }
],
image: [
{ required: true, message: '请上传封面图', trigger: 'change' }
]
}
})
//
const beforeFileUpload: UploadProps['beforeUpload'] = (file) => {
const allowedTypes = ['application/pdf', 'application/epub+zip']
const isAllowedType = allowedTypes.includes(file.type)
const isLt50M = file.size / 1024 / 1024 < 50
if (!isAllowedType) {
ElMessage.error('只能上传PDF或EPUB文件!')
return false
}
if (!isLt50M) {
ElMessage.error('文件大小不能超过50MB!')
return false
}
return true
}
//
const handleUploadError: UploadProps['onError'] = () => {
ElMessage.error('文件上传失败,请重试')
}
//
const beforeImageUpload: UploadProps['beforeUpload'] = (file) => {
const isImage = file.type.startsWith('image/')
const isLt5M = file.size / 1024 / 1024 < 5
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt5M) {
ElMessage.error('图片大小不能超过5MB!')
return false
}
return true
}
//
const handleImageSuccess: UploadProps['onSuccess'] = (response) => {
state.formData.image = response.data.path
}
//
const handleFileSuccess: UploadProps['onSuccess'] = (response) => {
state.formData.file = response.data.path
}
//
const showDetail = (row: any) => {
state.detailData = { ...row }
state.detailVisible = true
}
const openAuditDialog = (row: any) => {
state.auditForm = {
id: row.id,
status: '已同意',
remark: ''
}
state.auditVisible = true
}
const submitAudit = async () => {
try {
await adminRequest.put(`${state.route}/audit`, state.auditForm)
ElMessage.success('审核提交成功')
state.auditVisible = false
init()
} catch {
ElMessage.error('审核提交失败')
}
}
//
const init = () => {
state.loading = true
adminRequest
.get(`${state.route}/page`, { params: state.query })
.then((res: any) => {
state.list = res.data.list
state.query.total = res.data.total
})
.finally(() => {
state.loading = false
})
adminRequest.get(`sys/user-front/page`, {
params: { limit: 999 }
}).then((res: any) => {
state.userList = res.data.list
})
}
//
const handleSearch = () => {
state.query.page = 1
init()
}
//
const openAddDialog = () => {
state.formData = {}
state.dialogVisible = true
nextTick(() => formRef.value?.clearValidate())
}
//
const closeDialog = () => {
state.dialogVisible = false
state.formData = {}
}
//
const edit = (row: any) => {
state.formData = { ...row }
state.dialogVisible = true
}
// /
const saveTransaction = () => {
formRef.value?.validate(async valid => {
if (!valid) return
try {
const req = state.formData.id
? adminRequest.put(`${state.route}`, state.formData)
: adminRequest.post(`${state.route}`, state.formData)
await req
ElMessage.success('操作成功')
closeDialog()
init()
} catch {
ElMessage.error('操作失败')
}
})
}
//
const del = async (id: number) => {
try {
await adminRequest.delete(`${state.route}/${id}`)
ElMessage.success('删除成功')
init()
} catch {
ElMessage.error('删除失败')
}
}
//
const handlePageChange = (page: number) => {
state.query.page = page
init()
}
const handleSizeChange = (size: number) => {
state.query.limit = size
init()
}
//
const handleImageUrl = (url: string) => {
state.formData.image = url
}
onMounted(() => {
init()
})
//
function getStatusTagType(status: string) {
switch (status) {
case '已同意':
return 'success'
case '已拒绝':
return 'danger'
case '未审核':
return 'warning'
default:
return 'info'
}
}
</script>
<style lang="scss" scoped>
.avatar-uploader {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 150px;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-uploader:hover {
border-color: #409eff;
}
.avatar {
max-width: 100%;
max-height: 100%;
display: block;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
}
.house-management-container {
padding: 20px;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.search-area {
margin-bottom: 20px;
.search-form {
display: flex;
align-items: center;
.search-item {
margin-right: 16px;
}
.search-input {
width: 220px;
}
.action-buttons {
margin-left: auto;
display: flex;
gap: 8px;
}
}
}
.table-area {
.data-table {
width: 100%;
.house-image {
width: 60px;
height: 60px;
border-radius: 4px;
&:hover {
transform: scale(1.05);
}
}
}
.pagination-container {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
}
.form-dialog {
:deep(.el-dialog__body) {
padding: 20px 30px;
}
.image-uploader {
width: 100%;
}
.dialog-footer {
text-align: center;
padding-top: 16px;
border-top: 1px solid #eee;
}
}
.detail-dialog {
.detail-image-container {
margin-top: 20px;
display: flex;
justify-content: center;
.detail-image {
max-width: 200px;
max-height: 300px;
}
}
}
</style>
<route lang="json">
{
"meta": {
"layout": "admin"
}
}
</route>

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

BIN
upload/ts017005.pdf Normal file

Binary file not shown.

BIN
upload/ts017019.pdf Normal file

Binary file not shown.