block-chaincopyright/ui/src/pages/admin/book.vue
18796357645 20aeb9596e add
2025-05-22 21:38:47 +08:00

684 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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">
支持PDF、EPUB等格式大小不超过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>