新增作家中心功能,包括作品管理、读者管理、作品创建、作品设置等页面。优化路由守卫逻辑,确保作家权限验证。添加调试工具函数,便于开发过程中查看用户状态。调整部分组件样式和逻辑,提升用户体验。master
@ -0,0 +1,226 @@ | |||
<template> | |||
<div class="work-item"> | |||
<div class="work-image"> | |||
<img :src="workCover || defaultCover" :alt="workTitle" /> | |||
<!-- <div v-if="hasCover" class="work-status-tag" :class="statusTagClass">{{ statusText }}</div> --> | |||
</div> | |||
<div class="work-info"> | |||
<h3 class="work-title">{{ workTitle }}</h3> | |||
<div class="work-status"> | |||
<span v-if="isNew" class="status-tag new-tag">新建</span> | |||
<span v-if="statusTag" class="status-tag" :class="statusTagClass">{{ statusText }}</span> | |||
<span class="status-tag audit-tag">作品设置审核中</span> | |||
</div> | |||
<div class="work-status"> | |||
<span class="work-status-tag" :class="statusTagClass">连载中</span> | |||
</div> | |||
<div class="work-actions"> | |||
<el-button class="action-btn" type="text" @click="handleSetup">作品设置</el-button> | |||
<el-button class="action-btn" type="text" @click="handleEdit">去写作</el-button> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import { defineComponent, computed } from 'vue'; | |||
export default defineComponent({ | |||
name: 'WorkItem', | |||
props: { | |||
workId: { | |||
type: [String, Number], | |||
required: true | |||
}, | |||
workTitle: { | |||
type: String, | |||
required: true | |||
}, | |||
workCover: { | |||
type: String, | |||
default: '' | |||
}, | |||
status: { | |||
type: String, | |||
default: 'draft' // draft, publishing, published | |||
}, | |||
auditStatus: { | |||
type: String, | |||
default: '' // 通过 pending rejected passed | |||
}, | |||
isNew: { | |||
type: Boolean, | |||
default: false | |||
} | |||
}, | |||
emits: ['setup', 'edit'], | |||
setup(props, { emit }) { | |||
const defaultCover = computed(() => { | |||
// 使用网络图片作为默认封面,避免静态资源路径问题 | |||
return 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'; | |||
}); | |||
const hasCover = computed(() => !!props.workCover); | |||
const statusTagClass = computed(() => { | |||
const map = { | |||
'draft': 'draft-tag', | |||
'publishing': 'publishing-tag', | |||
'published': 'published-tag' | |||
}; | |||
return map[props.status] || ''; | |||
}); | |||
const statusText = computed(() => { | |||
const map = { | |||
'draft': '连载中', | |||
'publishing': '发布审核中', | |||
'published': '已完结' | |||
}; | |||
return map[props.status] || ''; | |||
}); | |||
const auditText = computed(() => { | |||
const map = { | |||
'pending': '作品设置审核中', | |||
'rejected': '发布审核被拒', | |||
'passed': '作品设置审核通过' | |||
}; | |||
return map[props.auditStatus] || ''; | |||
}); | |||
const handleSetup = () => { | |||
emit('setup', props.workId); | |||
}; | |||
const handleEdit = () => { | |||
emit('edit', props.workId); | |||
}; | |||
return { | |||
defaultCover, | |||
hasCover, | |||
statusTagClass, | |||
statusText, | |||
auditText, | |||
auditTag: computed(() => !!props.auditStatus), | |||
handleSetup, | |||
handleEdit | |||
}; | |||
} | |||
}); | |||
</script> | |||
<style lang="scss" scoped> | |||
.work-item { | |||
display: flex; | |||
padding: 20px 0; | |||
border-bottom: 1px solid #eee; | |||
&:last-child { | |||
border-bottom: none; | |||
} | |||
.work-image { | |||
position: relative; | |||
width: 100px; | |||
height: 133px; | |||
margin-right: 20px; | |||
overflow: hidden; | |||
border-radius: 4px; | |||
background-color: #f5f5f5; | |||
img { | |||
width: 100%; | |||
height: 100%; | |||
object-fit: cover; | |||
} | |||
} | |||
.work-info { | |||
flex: 1; | |||
display: flex; | |||
flex-direction: column; | |||
.work-title { | |||
font-size: 16px; | |||
font-weight: bold; | |||
margin: 0 0 10px; | |||
color: #333; | |||
} | |||
.work-status { | |||
margin-bottom: 15px; | |||
.status-tag { | |||
display: inline-block; | |||
padding: 2px 8px; | |||
margin-right: 8px; | |||
font-size: 12px; | |||
border-radius: 2px; | |||
&.new-tag { | |||
color: #0A2463; | |||
background-color: #e0e7f5; | |||
} | |||
&.draft-tag { | |||
color: #606266; | |||
background-color: #ebeef5; | |||
} | |||
&.publishing-tag { | |||
color: #E6A23C; | |||
background-color: #fdf6ec; | |||
} | |||
&.published-tag { | |||
color: #67C23A; | |||
background-color: #f0f9eb; | |||
} | |||
&.audit-tag { | |||
color: #409EFF; | |||
background-color: #ecf5ff; | |||
} | |||
} | |||
.work-status-tag { | |||
padding: 2px 8px; | |||
font-size: 12px; | |||
color: #fff; | |||
border-radius: 4px; | |||
&.draft-tag { | |||
background-color: #909399; | |||
} | |||
&.publishing-tag { | |||
background-color: #E6A23C; | |||
} | |||
&.published-tag { | |||
background-color: #67C23A; | |||
} | |||
} | |||
} | |||
.work-actions { | |||
display: flex; | |||
gap: 15px; | |||
.action-btn { | |||
padding: 0; | |||
font-size: 14px; | |||
font-weight: normal; | |||
color: #0A2463; | |||
&:hover { | |||
opacity: 0.8; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
</style> |
@ -0,0 +1,381 @@ | |||
<template> | |||
<el-dialog v-model="dialogVisible" width="800px" :show-close="true" :close-on-click-modal="false" | |||
class="interactive-reward-dialog"> | |||
<div class="interactive-title">互动打赏</div> | |||
<div class="interactive-items-container"> | |||
<div class="interactive-items"> | |||
<div v-for="(item, index) in rewardItems" :key="index" class="reward-item" :class="{ active: item.active }" | |||
@click="toggleItemSelection(index)"> | |||
<div class="item-header"> | |||
<img :src="item.icon" :alt="item.name" class="item-icon" /> | |||
<div class="check-mark" v-if="item.active"> | |||
<el-icon> | |||
<Check /> | |||
</el-icon> | |||
</div> | |||
</div> | |||
<div class="item-body"> | |||
<div class="item-name">{{ item.name }} <span v-if="item.hot" class="hot-tag">HOT</span></div> | |||
<div class="item-price">{{ item.price }}币/{{ item.unit }}</div> | |||
<div class="item-counter"> | |||
<el-button class="counter-btn minus" @click.stop="decreaseCount(index)" | |||
:disabled="item.count <= 0">-</el-button> | |||
<span class="count-value">{{ item.count }}</span> | |||
<el-button class="counter-btn plus" @click.stop="increaseCount(index)">+</el-button> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="reward-footer"> | |||
<el-button type="primary" class="submit-btn" @click="submitReward">打赏</el-button> | |||
</div> | |||
</el-dialog> | |||
</template> | |||
<script setup> | |||
import { ref, reactive, computed } from 'vue'; | |||
import { Check } from '@element-plus/icons-vue'; | |||
import { ElMessage } from 'element-plus'; | |||
// 导入图片资源 | |||
import image1 from '@/assets/images/image-1.png'; | |||
import image2 from '@/assets/images/image-2.png'; | |||
import image3 from '@/assets/images/image-3.png'; | |||
import image4 from '@/assets/images/image-4.png'; | |||
import image5 from '@/assets/images/image-5.png'; | |||
import image6 from '@/assets/images/image-6.png'; | |||
const props = defineProps({ | |||
visible: { | |||
type: Boolean, | |||
default: false | |||
}, | |||
bookId: { | |||
type: [String, Number], | |||
required: true | |||
} | |||
}); | |||
const emit = defineEmits(['update:visible', 'reward-success']); | |||
const dialogVisible = computed({ | |||
get: () => props.visible, | |||
set: (val) => emit('update:visible', val) | |||
}); | |||
// 使用现有图像资源 | |||
const rewardItems = reactive([ | |||
{ | |||
id: 1, | |||
name: '小星星', | |||
icon: image1, | |||
price: 10, | |||
unit: '个', | |||
count: 0, | |||
active: false, | |||
hot: true | |||
}, | |||
{ | |||
id: 2, | |||
name: '爱你哦', | |||
icon: image2, | |||
price: 50, | |||
unit: '个', | |||
count: 0, | |||
active: false, | |||
hot: true | |||
}, | |||
{ | |||
id: 3, | |||
name: '加油鸭', | |||
icon: image3, | |||
price: 100, | |||
unit: '个', | |||
count: 0, | |||
active: false, | |||
hot: true | |||
}, | |||
{ | |||
id: 4, | |||
name: '花儿朵朵', | |||
icon: image4, | |||
price: 200, | |||
unit: '个', | |||
count: 0, | |||
active: false, | |||
hot: false | |||
}, | |||
{ | |||
id: 5, | |||
name: '西瓜', | |||
icon: image5, | |||
price: 500, | |||
unit: '个', | |||
count: 0, | |||
active: false, | |||
hot: true | |||
}, | |||
{ | |||
id: 6, | |||
name: '冰棍', | |||
icon: image6, | |||
price: 1000, | |||
unit: '个', | |||
count: 0, | |||
active: false, | |||
hot: false | |||
} | |||
]); | |||
// 切换选中状态(支持多选) | |||
const toggleItemSelection = (index) => { | |||
rewardItems[index].active = !rewardItems[index].active; | |||
// 如果取消选中,数量清零 | |||
if (!rewardItems[index].active) { | |||
rewardItems[index].count = 0; | |||
} | |||
// 如果选中且数量为0,设为1 | |||
else if (rewardItems[index].count === 0) { | |||
rewardItems[index].count = 1; | |||
} | |||
}; | |||
// 增加数量 | |||
const increaseCount = (index) => { | |||
rewardItems[index].count++; | |||
// 如果增加数量,自动选中该项 | |||
if (rewardItems[index].count > 0 && !rewardItems[index].active) { | |||
rewardItems[index].active = true; | |||
} | |||
}; | |||
// 减少数量 | |||
const decreaseCount = (index) => { | |||
if (rewardItems[index].count > 0) { | |||
rewardItems[index].count--; | |||
// 如果数量变为0,取消选中 | |||
if (rewardItems[index].count === 0) { | |||
rewardItems[index].active = false; | |||
} | |||
} | |||
}; | |||
// 提交打赏 | |||
const submitReward = () => { | |||
const selectedItems = rewardItems.filter(item => item.active && item.count > 0); | |||
if (selectedItems.length === 0) { | |||
ElMessage.warning('请选择至少一项打赏内容'); | |||
return; | |||
} | |||
// 构建发送到后端的数据 | |||
const rewardData = { | |||
bookId: props.bookId, | |||
items: selectedItems.map(item => ({ | |||
id: item.id, | |||
count: item.count | |||
})) | |||
}; | |||
// 这里应该添加实际的打赏逻辑,调用API等 | |||
console.log('打赏数据:', rewardData); | |||
// 模拟打赏成功 | |||
ElMessage.success('打赏成功!感谢您的支持!'); | |||
emit('reward-success', rewardData); | |||
// 重置并关闭弹窗 | |||
rewardItems.forEach(item => { | |||
item.count = 0; | |||
item.active = false; | |||
}); | |||
dialogVisible.value = false; | |||
}; | |||
</script> | |||
<style lang="scss" scoped> | |||
.interactive-reward-dialog { | |||
:deep(.el-dialog) { | |||
background: linear-gradient(to bottom, #F1E4FE, #F3F9FF) !important; | |||
} | |||
:deep(.el-dialog__header) { | |||
text-align: center; | |||
margin-bottom: 10px; | |||
.el-dialog__title { | |||
font-size: 18px; | |||
font-weight: bold; | |||
} | |||
} | |||
.interactive-title{ | |||
font-size: 22px; | |||
font-weight: bold; | |||
margin-bottom: 25px; | |||
text-align: center; | |||
} | |||
.interactive-items-container { | |||
max-height: 600px; | |||
overflow-y: auto; | |||
padding: 5px; | |||
margin-bottom: 20px; | |||
&::-webkit-scrollbar { | |||
width: 6px; | |||
} | |||
&::-webkit-scrollbar-track { | |||
background: #f1f1f1; | |||
border-radius: 3px; | |||
} | |||
&::-webkit-scrollbar-thumb { | |||
background: #0A2463; | |||
border-radius: 3px; | |||
} | |||
} | |||
.interactive-items { | |||
display: grid; | |||
grid-template-columns: repeat(3, 1fr); | |||
gap: 15px; | |||
.reward-item { | |||
aspect-ratio: 1 / 1; | |||
border: 1.5px solid #ebebeb; | |||
border-radius: 12px; | |||
padding: 15px; | |||
cursor: pointer; | |||
transition: all 0.3s; | |||
display: flex; | |||
flex-direction: column; | |||
position: relative; | |||
background-color: white; | |||
&.active { | |||
border-color: #0A2463; | |||
background-color: rgba(10, 36, 99, 0.05); | |||
box-shadow: 0 2px 8px rgba(10, 36, 99, 0.1); | |||
} | |||
.item-header { | |||
flex: 1; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
position: relative; | |||
margin-bottom: 12px; | |||
.item-icon { | |||
width: 60px; | |||
height: 60px; | |||
object-fit: contain; | |||
} | |||
.check-mark { | |||
position: absolute; | |||
top: -8px; | |||
right: -8px; | |||
width: 24px; | |||
height: 24px; | |||
background-color: #0A2463; | |||
border-radius: 50%; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
color: white; | |||
font-size: 14px; | |||
} | |||
} | |||
.item-body { | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
margin-top: 10px; | |||
.item-name { | |||
font-size: 16px; | |||
font-weight: 500; | |||
margin-bottom: 6px; | |||
display: flex; | |||
align-items: center; | |||
.hot-tag { | |||
background-color: #FF7C6A; | |||
color: white; | |||
font-size: 12px; | |||
padding: 1px 5px; | |||
border-radius: 4px; | |||
margin-left: 6px; | |||
} | |||
} | |||
.item-price { | |||
font-size: 14px; | |||
color: #999; | |||
margin-bottom: 12px; | |||
} | |||
.item-counter { | |||
display: flex; | |||
align-items: center; | |||
margin-top: 5px; | |||
.counter-btn { | |||
width: 28px; | |||
height: 28px; | |||
padding: 0; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
border: 1px solid #ddd; | |||
font-size: 16px; | |||
&.minus { | |||
border-radius: 4px 0 0 4px; | |||
} | |||
&.plus { | |||
border-radius: 0 4px 4px 0; | |||
} | |||
} | |||
.count-value { | |||
width: 40px; | |||
height: 28px; | |||
line-height: 28px; | |||
text-align: center; | |||
border-top: 1px solid #ddd; | |||
border-bottom: 1px solid #ddd; | |||
font-size: 16px; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
.reward-footer { | |||
text-align: center; | |||
margin-top: 15px; | |||
.submit-btn { | |||
width: 80%; | |||
height: 40px; | |||
background-color: #0A2463; | |||
border: none; | |||
border-radius: 20px; | |||
font-size: 16px; | |||
} | |||
} | |||
} | |||
</style> |
@ -0,0 +1,40 @@ | |||
/** | |||
* 调试工具函数 | |||
*/ | |||
// 打印当前用户状态 | |||
export const logUserState = () => { | |||
console.group('用户状态'); | |||
// 从localStorage获取状态 | |||
const userData = localStorage.getItem('user'); | |||
const token = localStorage.getItem('token'); | |||
const isAuthor = localStorage.getItem('isAuthor'); | |||
console.log('localStorage状态:'); | |||
console.log('- token:', token ? '已设置' : '未设置'); | |||
console.log('- isAuthor:', isAuthor); | |||
console.log('- user:', userData ? JSON.parse(userData) : null); | |||
// 从全局状态获取 | |||
if (window.$pinia?.state?.value?.main) { | |||
const storeState = window.$pinia.state.value.main; | |||
console.log('Store状态:'); | |||
console.log('- isLoggedIn:', storeState.isLoggedIn); | |||
console.log('- isAuthor:', storeState.isAuthor); | |||
console.log('- user:', storeState.user); | |||
} else { | |||
console.log('Store未初始化'); | |||
} | |||
console.groupEnd(); | |||
}; | |||
// 导出全局调试对象 | |||
window.$debug = { | |||
logUserState | |||
}; | |||
export default { | |||
logUserState | |||
}; |
@ -0,0 +1,124 @@ | |||
<template> | |||
<div class="author-center-container"> | |||
<div class="author-center-wrapper"> | |||
<!-- 左侧菜单 --> | |||
<div class="author-sidebar"> | |||
<ul class="author-nav"> | |||
<li | |||
v-for="(item, index) in menuItems" | |||
:key="index" | |||
:class="{ active: isMenuActive(item.path) }" | |||
@click="switchMenu(item.path)" | |||
> | |||
{{ item.name }} | |||
</li> | |||
</ul> | |||
</div> | |||
<!-- 右侧内容区 - 通过路由渲染 --> | |||
<router-view /> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import { defineComponent, ref, computed } from 'vue'; | |||
import { useRouter, useRoute } from 'vue-router'; | |||
import { ElMessage } from 'element-plus'; | |||
export default defineComponent({ | |||
name: 'AuthorCenter', | |||
setup() { | |||
const router = useRouter(); | |||
const route = useRoute(); | |||
// 菜单项定义 | |||
const menuItems = [ | |||
{ name: '作品管理', path: 'works' }, | |||
{ name: '读者管理', path: 'readers' } | |||
]; | |||
// 判断菜单是否激活 | |||
const isMenuActive = (path) => { | |||
return route.path.includes(`/author/${path}`); | |||
}; | |||
// 切换菜单 | |||
const switchMenu = (path) => { | |||
router.push({ name: `author${path.charAt(0).toUpperCase() + path.slice(1)}` }); | |||
}; | |||
return { | |||
menuItems, | |||
isMenuActive, | |||
switchMenu | |||
}; | |||
} | |||
}); | |||
</script> | |||
<style lang="scss" scoped> | |||
.author-center-container { | |||
background-color: #f5f5f9; | |||
min-height: calc(100vh - 60px); | |||
padding: 20px 0; | |||
.author-center-wrapper { | |||
max-width: 1200px; | |||
margin: 0 auto; | |||
padding: 0 20px; | |||
display: flex; | |||
gap: 20px; | |||
// 左侧菜单区 | |||
.author-sidebar { | |||
width: 160px; | |||
flex-shrink: 0; | |||
background-color: #fff; | |||
border-radius: 8px; | |||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); | |||
overflow: hidden; | |||
.author-nav { | |||
list-style: none; | |||
padding: 0; | |||
margin: 0; | |||
li { | |||
height: 50px; | |||
line-height: 50px; | |||
padding: 0 20px; | |||
font-size: 15px; | |||
color: #333; | |||
cursor: pointer; | |||
transition: all 0.3s; | |||
position: relative; | |||
&:hover { | |||
color: #0A2463; | |||
background-color: #f6f8ff; | |||
} | |||
&.active { | |||
color: #0A2463; | |||
font-weight: bold; | |||
background-color: #f0f3ff; | |||
&::before { | |||
content: ''; | |||
position: absolute; | |||
left: 0; | |||
top: 50%; | |||
transform: translateY(-50%); | |||
width: 4px; | |||
height: 20px; | |||
background-color: #0A2463; | |||
border-radius: 0 2px 2px 0; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
</style> |
@ -0,0 +1,303 @@ | |||
<template> | |||
<div class="create-work-container"> | |||
<div class="create-work-content"> | |||
<h2 class="page-title">创建新作品</h2> | |||
<div class="form-container"> | |||
<el-form :model="workForm" :rules="rules" ref="workFormRef" label-position="top"> | |||
<el-form-item label="作品名称" prop="title"> | |||
<el-input v-model="workForm.title" placeholder="请输入作品名称"></el-input> | |||
</el-form-item> | |||
<el-form-item label="作品分类" prop="category"> | |||
<el-select v-model="workForm.category" placeholder="请选择分类"> | |||
<el-option v-for="item in categories" :key="item.id" :label="item.name" :value="item.id"></el-option> | |||
</el-select> | |||
</el-form-item> | |||
<el-form-item label="作品标签" prop="tags"> | |||
<el-select | |||
v-model="workForm.tags" | |||
multiple | |||
placeholder="请选择标签(最多选择3个)" | |||
:multiple-limit="3" | |||
> | |||
<el-option v-for="item in tags" :key="item.id" :label="item.name" :value="item.id"></el-option> | |||
</el-select> | |||
</el-form-item> | |||
<el-form-item label="简介" prop="description"> | |||
<el-input | |||
v-model="workForm.description" | |||
type="textarea" | |||
:rows="5" | |||
placeholder="请输入作品简介" | |||
maxlength="500" | |||
show-word-limit | |||
></el-input> | |||
</el-form-item> | |||
<el-form-item label="封面图片"> | |||
<el-upload | |||
class="cover-uploader" | |||
action="#" | |||
:http-request="uploadCover" | |||
:show-file-list="false" | |||
:before-upload="beforeUpload" | |||
> | |||
<img v-if="workForm.cover" :src="workForm.cover" class="cover-image" /> | |||
<div v-else class="cover-placeholder"> | |||
<el-icon><Plus /></el-icon> | |||
<div class="upload-text">点击上传封面</div> | |||
</div> | |||
</el-upload> | |||
<div class="upload-tip">建议尺寸:300x400像素,JPG/PNG格式</div> | |||
</el-form-item> | |||
<div class="form-actions"> | |||
<el-button @click="goBack">取消</el-button> | |||
<el-button type="primary" @click="submitForm" :loading="loading">创建作品</el-button> | |||
</div> | |||
</el-form> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import { defineComponent, ref, reactive } from 'vue'; | |||
import { useRouter } from 'vue-router'; | |||
import { ElMessage } from 'element-plus'; | |||
import { Plus } from '@element-plus/icons-vue'; | |||
export default defineComponent({ | |||
name: 'CreateWork', | |||
components: { | |||
Plus | |||
}, | |||
setup() { | |||
const router = useRouter(); | |||
const workFormRef = ref(null); | |||
const loading = ref(false); | |||
// 表单数据 | |||
const workForm = reactive({ | |||
title: '', | |||
category: '', | |||
tags: [], | |||
description: '', | |||
cover: '' | |||
}); | |||
// 表单验证规则 | |||
const rules = { | |||
title: [ | |||
{ required: true, message: '请输入作品名称', trigger: 'blur' }, | |||
{ min: 2, max: 30, message: '长度应为2-30个字符', trigger: 'blur' } | |||
], | |||
category: [ | |||
{ required: true, message: '请选择作品分类', trigger: 'change' } | |||
], | |||
description: [ | |||
{ required: true, message: '请输入作品简介', trigger: 'blur' }, | |||
{ min: 10, max: 500, message: '简介长度应为10-500个字符', trigger: 'blur' } | |||
] | |||
}; | |||
// 分类和标签数据,实际应用中应该从API获取 | |||
const categories = ref([ | |||
{ id: 1, name: '玄幻' }, | |||
{ id: 2, name: '奇幻' }, | |||
{ id: 3, name: '武侠' }, | |||
{ id: 4, name: '仙侠' }, | |||
{ id: 5, name: '都市' }, | |||
{ id: 6, name: '历史' }, | |||
{ id: 7, name: '军事' }, | |||
{ id: 8, name: '科幻' } | |||
]); | |||
const tags = ref([ | |||
{ id: 1, name: '热血' }, | |||
{ id: 2, name: '爽文' }, | |||
{ id: 3, name: '冒险' }, | |||
{ id: 4, name: '复仇' }, | |||
{ id: 5, name: '种田' }, | |||
{ id: 6, name: '经商' }, | |||
{ id: 7, name: '争霸' }, | |||
{ id: 8, name: '升级' }, | |||
{ id: 9, name: '穿越' }, | |||
{ id: 10, name: '重生' } | |||
]); | |||
// 上传前校验 | |||
const beforeUpload = (file) => { | |||
const isImage = file.type === 'image/jpeg' || file.type === 'image/png'; | |||
const isLt2M = file.size / 1024 / 1024 < 2; | |||
if (!isImage) { | |||
ElMessage.error('封面图片只能是JPG或PNG格式!'); | |||
return false; | |||
} | |||
if (!isLt2M) { | |||
ElMessage.error('封面图片大小不能超过2MB!'); | |||
return false; | |||
} | |||
return true; | |||
}; | |||
// 上传封面 | |||
const uploadCover = (options) => { | |||
const { file } = options; | |||
// 实际应用中应该上传到服务器,这里模拟上传成功 | |||
const reader = new FileReader(); | |||
reader.readAsDataURL(file); | |||
reader.onload = (e) => { | |||
workForm.cover = e.target.result; | |||
}; | |||
}; | |||
// 提交表单 | |||
const submitForm = () => { | |||
workFormRef.value.validate(async (valid) => { | |||
if (valid) { | |||
try { | |||
loading.value = true; | |||
// 模拟API调用,实际项目中应调用真实的后端API | |||
await new Promise(resolve => setTimeout(resolve, 1000)); | |||
ElMessage.success('作品创建成功!'); | |||
router.push({ name: 'authorCenter' }); | |||
} catch (error) { | |||
ElMessage.error('创建失败,请稍后重试'); | |||
} finally { | |||
loading.value = false; | |||
} | |||
} else { | |||
ElMessage.warning('请正确填写表单信息'); | |||
return false; | |||
} | |||
}); | |||
}; | |||
// 返回上一页 | |||
const goBack = () => { | |||
router.back(); | |||
}; | |||
return { | |||
workFormRef, | |||
workForm, | |||
rules, | |||
categories, | |||
tags, | |||
loading, | |||
beforeUpload, | |||
uploadCover, | |||
submitForm, | |||
goBack | |||
}; | |||
} | |||
}); | |||
</script> | |||
<style lang="scss" scoped> | |||
.create-work-container { | |||
background-color: #f5f5f9; | |||
min-height: calc(100vh - 60px); | |||
padding: 30px 0; | |||
.create-work-content { | |||
max-width: 800px; | |||
margin: 0 auto; | |||
padding: 30px; | |||
background-color: #fff; | |||
border-radius: 8px; | |||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); | |||
.page-title { | |||
font-size: 24px; | |||
color: #333; | |||
margin: 0 0 30px; | |||
padding-bottom: 15px; | |||
border-bottom: 1px solid #eee; | |||
} | |||
.form-container { | |||
:deep(.el-form-item__label) { | |||
font-weight: 500; | |||
color: #333; | |||
} | |||
:deep(.el-input__wrapper) { | |||
padding: 0 15px; | |||
height: 44px; | |||
} | |||
.cover-uploader { | |||
.cover-image { | |||
width: 150px; | |||
height: 200px; | |||
object-fit: cover; | |||
border-radius: 4px; | |||
} | |||
.cover-placeholder { | |||
width: 150px; | |||
height: 200px; | |||
background-color: #f7f9fc; | |||
border: 1px dashed #d9d9d9; | |||
border-radius: 4px; | |||
display: flex; | |||
flex-direction: column; | |||
justify-content: center; | |||
align-items: center; | |||
cursor: pointer; | |||
&:hover { | |||
border-color: #0A2463; | |||
color: #0A2463; | |||
} | |||
.el-icon { | |||
font-size: 28px; | |||
color: #8c939d; | |||
margin-bottom: 8px; | |||
} | |||
.upload-text { | |||
font-size: 14px; | |||
color: #8c939d; | |||
} | |||
} | |||
} | |||
.upload-tip { | |||
font-size: 12px; | |||
color: #aaa; | |||
margin-top: 8px; | |||
} | |||
.form-actions { | |||
margin-top: 40px; | |||
display: flex; | |||
justify-content: center; | |||
gap: 20px; | |||
.el-button { | |||
width: 120px; | |||
} | |||
.el-button--primary { | |||
background-color: #0A2463; | |||
border-color: #0A2463; | |||
&:hover, &:focus { | |||
background-color: #1a3473; | |||
border-color: #1a3473; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
</style> |
@ -0,0 +1,549 @@ | |||
<template> | |||
<div class="work-edit-container"> | |||
<div class="work-edit-header"> | |||
<div class="header-left"> | |||
<el-button @click="goToWorksList" plain> | |||
<el-icon><Back /></el-icon> 返回作品列表 | |||
</el-button> | |||
<h3 class="work-title">{{ workTitle }}</h3> | |||
</div> | |||
<div class="header-right"> | |||
<el-button type="primary" @click="publishChapter" :disabled="!chapterTitle || !chapterContent">发布</el-button> | |||
<el-button @click="saveChapter" :disabled="!chapterTitle || !chapterContent">保存</el-button> | |||
</div> | |||
</div> | |||
<div class="work-edit-main"> | |||
<div class="chapters-sidebar"> | |||
<div class="sidebar-header"> | |||
<h4 class="sidebar-title">章节目录</h4> | |||
<el-button type="primary" size="small" @click="createNewChapter">新建章节</el-button> | |||
</div> | |||
<div class="chapters-list" v-if="chapters.length > 0"> | |||
<div | |||
v-for="chapter in chapters" | |||
:key="chapter.id" | |||
class="chapter-item" | |||
:class="{ active: currentChapter && currentChapter.id === chapter.id }" | |||
@click="selectChapter(chapter)" | |||
> | |||
<div class="chapter-info"> | |||
<span class="chapter-index">第{{ chapter.index }}章</span> | |||
<span class="chapter-name">{{ chapter.title }}</span> | |||
</div> | |||
<div class="chapter-status"> | |||
<span class="status-tag" :class="chapter.status">{{ getStatusText(chapter.status) }}</span> | |||
<span class="word-count">{{ chapter.wordCount }}字</span> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="no-chapters" v-else> | |||
<el-empty description='暂无章节,点击"新建章节"开始创作' :image-size="80"></el-empty> | |||
</div> | |||
</div> | |||
<div class="editor-container"> | |||
<div class="chapter-header"> | |||
<el-input | |||
v-model="chapterTitle" | |||
placeholder="请输入章节标题" | |||
clearable | |||
maxlength="50" | |||
show-word-limit | |||
></el-input> | |||
</div> | |||
<div class="chapter-editor"> | |||
<el-input | |||
v-model="chapterContent" | |||
type="textarea" | |||
:rows="20" | |||
placeholder="请输入章节正文内容..." | |||
resize="none" | |||
></el-input> | |||
<div class="editor-footer"> | |||
<span>字数统计:{{ wordCount }}</span> | |||
<span>上次保存:{{ lastSaved }}</span> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import { defineComponent, ref, computed, watch, onBeforeUnmount } from 'vue'; | |||
import { useRouter, useRoute } from 'vue-router'; | |||
import { ElMessage, ElMessageBox } from 'element-plus'; | |||
import { Back } from '@element-plus/icons-vue'; | |||
export default defineComponent({ | |||
name: 'WorkEdit', | |||
components: { | |||
Back | |||
}, | |||
setup() { | |||
const router = useRouter(); | |||
const route = useRoute(); | |||
const workId = route.params.id; | |||
const workTitle = ref(''); | |||
const chapters = ref([]); | |||
const currentChapter = ref(null); | |||
const chapterTitle = ref(''); | |||
const chapterContent = ref(''); | |||
const lastSaved = ref('未保存'); | |||
let autoSaveInterval = null; | |||
// 字数统计 | |||
const wordCount = computed(() => { | |||
if (!chapterContent.value) return 0; | |||
// 去除空格、换行等字符后计算字数 | |||
return chapterContent.value.replace(/\s+/g, '').length; | |||
}); | |||
// 初始化数据 | |||
const initData = async () => { | |||
try { | |||
// 实际应用中应该从API获取 | |||
await new Promise(resolve => setTimeout(resolve, 500)); | |||
// 模拟小说数据 | |||
workTitle.value = '大宋好原夫'; | |||
chapters.value = [ | |||
{ id: 1, index: 1, title: '楔子', content: '这是第一章的内容...', status: 'published', wordCount: 2500 }, | |||
{ id: 2, index: 2, title: '初入汴京', content: '这是第二章的内容...', status: 'published', wordCount: 3200 }, | |||
{ id: 3, index: 3, title: '府试之始', content: '这是第三章的内容...', status: 'draft', wordCount: 2800 } | |||
]; | |||
// 默认选中最后一章 | |||
if (chapters.value.length > 0) { | |||
selectChapter(chapters.value[chapters.value.length - 1]); | |||
} | |||
} catch (error) { | |||
ElMessage.error('加载作品信息失败'); | |||
} | |||
}; | |||
// 选择章节 | |||
const selectChapter = (chapter) => { | |||
// 如果当前有未保存的内容,提示用户保存 | |||
if (currentChapter.value && | |||
(chapterTitle.value !== currentChapter.value.title || | |||
chapterContent.value !== currentChapter.value.content)) { | |||
ElMessageBox.confirm('当前章节有未保存的内容,是否保存?', '提示', { | |||
confirmButtonText: '保存', | |||
cancelButtonText: '不保存', | |||
type: 'warning' | |||
}).then(() => { | |||
saveChapter(); | |||
loadChapter(chapter); | |||
}).catch(() => { | |||
loadChapter(chapter); | |||
}); | |||
} else { | |||
loadChapter(chapter); | |||
} | |||
}; | |||
// 加载章节内容 | |||
const loadChapter = (chapter) => { | |||
currentChapter.value = chapter; | |||
chapterTitle.value = chapter.title; | |||
chapterContent.value = chapter.content; | |||
lastSaved.value = chapter.status === 'draft' ? '草稿' : '已发布'; | |||
}; | |||
// 创建新章节 | |||
const createNewChapter = () => { | |||
// 如果当前有未保存的内容,提示用户保存 | |||
if (currentChapter.value && | |||
(chapterTitle.value !== currentChapter.value.title || | |||
chapterContent.value !== currentChapter.value.content)) { | |||
ElMessageBox.confirm('当前章节有未保存的内容,是否保存?', '提示', { | |||
confirmButtonText: '保存', | |||
cancelButtonText: '不保存', | |||
type: 'warning' | |||
}).then(() => { | |||
saveChapter(); | |||
createEmptyChapter(); | |||
}).catch(() => { | |||
createEmptyChapter(); | |||
}); | |||
} else { | |||
createEmptyChapter(); | |||
} | |||
}; | |||
// 创建空白章节 | |||
const createEmptyChapter = () => { | |||
const newIndex = chapters.value.length > 0 ? chapters.value[chapters.value.length - 1].index + 1 : 1; | |||
const newChapter = { | |||
id: Date.now(), // 临时ID,实际应从后端获取 | |||
index: newIndex, | |||
title: '', | |||
content: '', | |||
status: 'new', | |||
wordCount: 0 | |||
}; | |||
chapters.value.push(newChapter); | |||
currentChapter.value = newChapter; | |||
chapterTitle.value = ''; | |||
chapterContent.value = ''; | |||
lastSaved.value = '未保存'; | |||
}; | |||
// 保存章节 | |||
const saveChapter = async () => { | |||
if (!chapterTitle.value) { | |||
ElMessage.warning('请输入章节标题'); | |||
return; | |||
} | |||
try { | |||
// 实际应用中应该调用API保存 | |||
await new Promise(resolve => setTimeout(resolve, 500)); | |||
// 更新本地数据 | |||
if (currentChapter.value) { | |||
const index = chapters.value.findIndex(c => c.id === currentChapter.value.id); | |||
if (index !== -1) { | |||
chapters.value[index].title = chapterTitle.value; | |||
chapters.value[index].content = chapterContent.value; | |||
chapters.value[index].wordCount = wordCount.value; | |||
if (chapters.value[index].status === 'new') { | |||
chapters.value[index].status = 'draft'; | |||
} | |||
} | |||
} | |||
lastSaved.value = new Date().toLocaleTimeString(); | |||
ElMessage.success('保存成功'); | |||
} catch (error) { | |||
ElMessage.error('保存失败,请重试'); | |||
} | |||
}; | |||
// 发布章节 | |||
const publishChapter = async () => { | |||
if (!chapterTitle.value) { | |||
ElMessage.warning('请输入章节标题'); | |||
return; | |||
} | |||
if (!chapterContent.value) { | |||
ElMessage.warning('请输入章节内容'); | |||
return; | |||
} | |||
try { | |||
// 实际应用中应该调用API发布 | |||
await new Promise(resolve => setTimeout(resolve, 800)); | |||
// 更新本地数据 | |||
if (currentChapter.value) { | |||
const index = chapters.value.findIndex(c => c.id === currentChapter.value.id); | |||
if (index !== -1) { | |||
chapters.value[index].title = chapterTitle.value; | |||
chapters.value[index].content = chapterContent.value; | |||
chapters.value[index].wordCount = wordCount.value; | |||
chapters.value[index].status = 'published'; | |||
} | |||
} | |||
lastSaved.value = '已发布'; | |||
ElMessage.success('发布成功'); | |||
} catch (error) { | |||
ElMessage.error('发布失败,请重试'); | |||
} | |||
}; | |||
// 获取状态文本 | |||
const getStatusText = (status) => { | |||
const statusMap = { | |||
'new': '未保存', | |||
'draft': '草稿', | |||
'published': '已发布' | |||
}; | |||
return statusMap[status] || status; | |||
}; | |||
// 返回作品列表 | |||
const goToWorksList = () => { | |||
// 如果有未保存的内容,提示用户保存 | |||
if (currentChapter.value && | |||
(chapterTitle.value !== currentChapter.value.title || | |||
chapterContent.value !== currentChapter.value.content)) { | |||
ElMessageBox.confirm('当前章节有未保存的内容,离开将丢失这些修改,是否继续?', '警告', { | |||
confirmButtonText: '离开', | |||
cancelButtonText: '取消', | |||
type: 'warning' | |||
}).then(() => { | |||
router.push({ name: 'authorCenter' }); | |||
}).catch(() => { | |||
// 取消离开 | |||
}); | |||
} else { | |||
router.push({ name: 'authorCenter' }); | |||
} | |||
}; | |||
// 自动保存功能 | |||
const setupAutoSave = () => { | |||
autoSaveInterval = setInterval(() => { | |||
if (currentChapter.value && | |||
(chapterTitle.value !== currentChapter.value.title || | |||
chapterContent.value !== currentChapter.value.content) && | |||
chapterTitle.value) { | |||
saveChapter(); | |||
} | |||
}, 60000); // 1分钟自动保存一次 | |||
}; | |||
// 监听章节内容变化 | |||
watch(chapterContent, (newVal) => { | |||
if (currentChapter.value && currentChapter.value.status === 'published') { | |||
// 如果已发布的章节被编辑,状态变为草稿 | |||
currentChapter.value.status = 'draft'; | |||
} | |||
}); | |||
// 初始化 | |||
initData(); | |||
setupAutoSave(); | |||
// 组件销毁前清除定时器 | |||
onBeforeUnmount(() => { | |||
if (autoSaveInterval) { | |||
clearInterval(autoSaveInterval); | |||
} | |||
}); | |||
return { | |||
workTitle, | |||
chapters, | |||
currentChapter, | |||
chapterTitle, | |||
chapterContent, | |||
lastSaved, | |||
wordCount, | |||
selectChapter, | |||
createNewChapter, | |||
saveChapter, | |||
publishChapter, | |||
getStatusText, | |||
goToWorksList | |||
}; | |||
} | |||
}); | |||
</script> | |||
<style lang="scss" scoped> | |||
.work-edit-container { | |||
min-height: 100vh; | |||
display: flex; | |||
flex-direction: column; | |||
background-color: #f9f9f9; | |||
.work-edit-header { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
height: 60px; | |||
padding: 0 20px; | |||
background-color: #fff; | |||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); | |||
z-index: 10; | |||
.header-left { | |||
display: flex; | |||
align-items: center; | |||
.work-title { | |||
margin: 0 0 0 20px; | |||
font-size: 18px; | |||
color: #333; | |||
} | |||
} | |||
.header-right { | |||
display: flex; | |||
gap: 10px; | |||
.el-button--primary { | |||
background-color: #0A2463; | |||
border-color: #0A2463; | |||
&:hover, &:focus { | |||
background-color: #1a3473; | |||
border-color: #1a3473; | |||
} | |||
} | |||
} | |||
} | |||
.work-edit-main { | |||
display: flex; | |||
flex: 1; | |||
height: calc(100vh - 60px); | |||
.chapters-sidebar { | |||
width: 280px; | |||
background-color: #fff; | |||
border-right: 1px solid #eee; | |||
display: flex; | |||
flex-direction: column; | |||
.sidebar-header { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
padding: 16px; | |||
border-bottom: 1px solid #eee; | |||
.sidebar-title { | |||
margin: 0; | |||
font-size: 16px; | |||
color: #333; | |||
} | |||
.el-button--primary { | |||
background-color: #0A2463; | |||
border-color: #0A2463; | |||
&:hover, &:focus { | |||
background-color: #1a3473; | |||
border-color: #1a3473; | |||
} | |||
} | |||
} | |||
.chapters-list { | |||
flex: 1; | |||
overflow-y: auto; | |||
.chapter-item { | |||
padding: 12px 16px; | |||
border-bottom: 1px solid #f5f5f5; | |||
cursor: pointer; | |||
&:hover { | |||
background-color: #f5f5f9; | |||
} | |||
&.active { | |||
background-color: #e0e7f5; | |||
} | |||
.chapter-info { | |||
display: flex; | |||
margin-bottom: 6px; | |||
.chapter-index { | |||
color: #888; | |||
margin-right: 8px; | |||
font-size: 14px; | |||
} | |||
.chapter-name { | |||
font-size: 14px; | |||
color: #333; | |||
flex: 1; | |||
white-space: nowrap; | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
} | |||
} | |||
.chapter-status { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
.status-tag { | |||
padding: 2px 6px; | |||
border-radius: 2px; | |||
font-size: 12px; | |||
&.new { | |||
background-color: #e0e7f5; | |||
color: #0A2463; | |||
} | |||
&.draft { | |||
background-color: #f4f4f5; | |||
color: #909399; | |||
} | |||
&.published { | |||
background-color: #f0f9eb; | |||
color: #67c23a; | |||
} | |||
} | |||
.word-count { | |||
font-size: 12px; | |||
color: #999; | |||
} | |||
} | |||
} | |||
} | |||
.no-chapters { | |||
flex: 1; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
padding: 20px; | |||
} | |||
} | |||
.editor-container { | |||
flex: 1; | |||
padding: 20px; | |||
display: flex; | |||
flex-direction: column; | |||
.chapter-header { | |||
margin-bottom: 20px; | |||
:deep(.el-input__wrapper) { | |||
padding: 0 15px; | |||
height: 48px; | |||
} | |||
:deep(.el-input__inner) { | |||
font-size: 18px; | |||
font-weight: 500; | |||
} | |||
} | |||
.chapter-editor { | |||
flex: 1; | |||
display: flex; | |||
flex-direction: column; | |||
:deep(.el-textarea__inner) { | |||
font-size: 16px; | |||
line-height: 1.8; | |||
padding: 16px; | |||
height: calc(100vh - 200px); | |||
} | |||
.editor-footer { | |||
display: flex; | |||
justify-content: space-between; | |||
margin-top: 12px; | |||
color: #999; | |||
font-size: 14px; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
</style> |
@ -0,0 +1,502 @@ | |||
<template> | |||
<div class="work-setup-container"> | |||
<div class="work-setup-content"> | |||
<div class="page-header"> | |||
<h2 class="page-title">作品设置</h2> | |||
<el-button @click="goBack">返回作品列表</el-button> | |||
</div> | |||
<div class="setup-form-container"> | |||
<el-tabs v-model="activeTab"> | |||
<el-tab-pane label="基本信息" name="basic"> | |||
<el-form :model="workForm" :rules="rules" ref="workFormRef" label-position="top"> | |||
<el-form-item label="作品名称" prop="title"> | |||
<el-input v-model="workForm.title" placeholder="请输入作品名称"></el-input> | |||
</el-form-item> | |||
<el-form-item label="作品分类" prop="category"> | |||
<el-select v-model="workForm.category" placeholder="请选择分类"> | |||
<el-option v-for="item in categories" :key="item.id" :label="item.name" :value="item.id"></el-option> | |||
</el-select> | |||
</el-form-item> | |||
<el-form-item label="作品标签" prop="tags"> | |||
<el-select | |||
v-model="workForm.tags" | |||
multiple | |||
placeholder="请选择标签(最多选择3个)" | |||
:multiple-limit="3" | |||
> | |||
<el-option v-for="item in tags" :key="item.id" :label="item.name" :value="item.id"></el-option> | |||
</el-select> | |||
</el-form-item> | |||
<el-form-item label="作品状态" prop="status"> | |||
<el-radio-group v-model="workForm.status"> | |||
<el-radio label="draft">连载中</el-radio> | |||
<el-radio label="published">已完结</el-radio> | |||
</el-radio-group> | |||
</el-form-item> | |||
<el-form-item label="简介" prop="description"> | |||
<el-input | |||
v-model="workForm.description" | |||
type="textarea" | |||
:rows="5" | |||
placeholder="请输入作品简介" | |||
maxlength="500" | |||
show-word-limit | |||
></el-input> | |||
</el-form-item> | |||
<el-form-item label="作品封面"> | |||
<el-upload | |||
class="cover-uploader" | |||
action="#" | |||
:http-request="uploadCover" | |||
:show-file-list="false" | |||
:before-upload="beforeUpload" | |||
> | |||
<img v-if="workForm.cover" :src="workForm.cover" class="cover-image" /> | |||
<div v-else class="cover-placeholder"> | |||
<el-icon><Plus /></el-icon> | |||
<div class="upload-text">点击上传封面</div> | |||
</div> | |||
</el-upload> | |||
<div class="upload-tip">建议尺寸:300x400像素,JPG/PNG格式</div> | |||
</el-form-item> | |||
<div class="form-actions"> | |||
<el-button @click="goBack">取消</el-button> | |||
<el-button type="primary" @click="submitForm" :loading="loading">保存设置</el-button> | |||
</div> | |||
</el-form> | |||
</el-tab-pane> | |||
<el-tab-pane label="发布设置" name="publish"> | |||
<div class="publish-settings"> | |||
<div class="setting-item"> | |||
<h3 class="setting-title">付费设置</h3> | |||
<div class="setting-content"> | |||
<el-form :model="publishForm" label-position="top"> | |||
<el-form-item label="收费模式"> | |||
<el-radio-group v-model="publishForm.paymentModel"> | |||
<el-radio label="free">免费作品</el-radio> | |||
<el-radio label="paid">付费作品</el-radio> | |||
</el-radio-group> | |||
</el-form-item> | |||
<el-form-item label="定价" v-if="publishForm.paymentModel === 'paid'"> | |||
<div class="price-setting"> | |||
<el-input-number | |||
v-model="publishForm.price" | |||
:min="1" | |||
:max="100" | |||
:precision="0" | |||
></el-input-number> | |||
<span class="unit">阅点/1000字</span> | |||
</div> | |||
</el-form-item> | |||
<el-form-item label="免费章节数量" v-if="publishForm.paymentModel === 'paid'"> | |||
<el-input-number | |||
v-model="publishForm.freeChapters" | |||
:min="0" | |||
:max="20" | |||
:precision="0" | |||
></el-input-number> | |||
</el-form-item> | |||
</el-form> | |||
</div> | |||
</div> | |||
<div class="setting-item"> | |||
<h3 class="setting-title">授权设置</h3> | |||
<div class="setting-content"> | |||
<el-form :model="publishForm" label-position="top"> | |||
<el-form-item> | |||
<el-checkbox v-model="publishForm.authorizeAudio">授权产生有声作品</el-checkbox> | |||
</el-form-item> | |||
<el-form-item> | |||
<el-checkbox v-model="publishForm.authorizeFilm">授权改编影视作品</el-checkbox> | |||
</el-form-item> | |||
</el-form> | |||
</div> | |||
</div> | |||
<div class="form-actions"> | |||
<el-button @click="goBack">取消</el-button> | |||
<el-button type="primary" @click="submitPublishForm" :loading="publishLoading">保存发布设置</el-button> | |||
</div> | |||
</div> | |||
</el-tab-pane> | |||
</el-tabs> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import { defineComponent, ref, reactive, onMounted } from 'vue'; | |||
import { useRouter, useRoute } from 'vue-router'; | |||
import { ElMessage } from 'element-plus'; | |||
import { Plus } from '@element-plus/icons-vue'; | |||
export default defineComponent({ | |||
name: 'WorkSetup', | |||
components: { | |||
Plus | |||
}, | |||
setup() { | |||
const router = useRouter(); | |||
const route = useRoute(); | |||
const workId = route.params.id; | |||
const workFormRef = ref(null); | |||
const loading = ref(false); | |||
const publishLoading = ref(false); | |||
const activeTab = ref('basic'); | |||
// 表单数据 | |||
const workForm = reactive({ | |||
id: workId, | |||
title: '', | |||
category: '', | |||
tags: [], | |||
status: 'draft', | |||
description: '', | |||
cover: '' | |||
}); | |||
// 发布设置表单 | |||
const publishForm = reactive({ | |||
paymentModel: 'free', | |||
price: 5, | |||
freeChapters: 3, | |||
authorizeAudio: false, | |||
authorizeFilm: false | |||
}); | |||
// 表单验证规则 | |||
const rules = { | |||
title: [ | |||
{ required: true, message: '请输入作品名称', trigger: 'blur' }, | |||
{ min: 2, max: 30, message: '长度应为2-30个字符', trigger: 'blur' } | |||
], | |||
category: [ | |||
{ required: true, message: '请选择作品分类', trigger: 'change' } | |||
], | |||
description: [ | |||
{ required: true, message: '请输入作品简介', trigger: 'blur' }, | |||
{ min: 10, max: 500, message: '简介长度应为10-500个字符', trigger: 'blur' } | |||
], | |||
status: [ | |||
{ required: true, message: '请选择作品状态', trigger: 'change' } | |||
] | |||
}; | |||
// 分类和标签数据,实际应用中应该从API获取 | |||
const categories = ref([ | |||
{ id: 1, name: '玄幻' }, | |||
{ id: 2, name: '奇幻' }, | |||
{ id: 3, name: '武侠' }, | |||
{ id: 4, name: '仙侠' }, | |||
{ id: 5, name: '都市' }, | |||
{ id: 6, name: '历史' }, | |||
{ id: 7, name: '军事' }, | |||
{ id: 8, name: '科幻' } | |||
]); | |||
const tags = ref([ | |||
{ id: 1, name: '热血' }, | |||
{ id: 2, name: '爽文' }, | |||
{ id: 3, name: '冒险' }, | |||
{ id: 4, name: '复仇' }, | |||
{ id: 5, name: '种田' }, | |||
{ id: 6, name: '经商' }, | |||
{ id: 7, name: '争霸' }, | |||
{ id: 8, name: '升级' }, | |||
{ id: 9, name: '穿越' }, | |||
{ id: 10, name: '重生' } | |||
]); | |||
// 加载作品信息 | |||
const loadWorkData = async () => { | |||
try { | |||
// 这里模拟API调用,实际开发中应该调用真实API | |||
await new Promise(resolve => setTimeout(resolve, 500)); | |||
// 模拟数据,实际开发中应该从API获取 | |||
const workData = { | |||
id: workId, | |||
title: '大宋好原夫', | |||
category: 6, // 历史分类ID | |||
tags: [5, 6], // 标签ID | |||
status: 'draft', | |||
description: '这是一部历史小说,讲述了一个现代人穿越到宋朝,凭借现代知识在政治和商业上取得成功的故事。', | |||
cover: '@/assets/images/book-covers/cover1.jpg' | |||
}; | |||
// 更新表单数据 | |||
Object.assign(workForm, workData); | |||
// 加载发布设置数据 | |||
const publishData = { | |||
paymentModel: 'paid', | |||
price: 8, | |||
freeChapters: 5, | |||
authorizeAudio: true, | |||
authorizeFilm: false | |||
}; | |||
Object.assign(publishForm, publishData); | |||
} catch (error) { | |||
ElMessage.error('加载作品信息失败'); | |||
} | |||
}; | |||
// 上传前校验 | |||
const beforeUpload = (file) => { | |||
const isImage = file.type === 'image/jpeg' || file.type === 'image/png'; | |||
const isLt2M = file.size / 1024 / 1024 < 2; | |||
if (!isImage) { | |||
ElMessage.error('封面图片只能是JPG或PNG格式!'); | |||
return false; | |||
} | |||
if (!isLt2M) { | |||
ElMessage.error('封面图片大小不能超过2MB!'); | |||
return false; | |||
} | |||
return true; | |||
}; | |||
// 上传封面 | |||
const uploadCover = (options) => { | |||
const { file } = options; | |||
// 实际应用中应该上传到服务器,这里模拟上传成功 | |||
const reader = new FileReader(); | |||
reader.readAsDataURL(file); | |||
reader.onload = (e) => { | |||
workForm.cover = e.target.result; | |||
}; | |||
}; | |||
// 提交基本信息表单 | |||
const submitForm = () => { | |||
workFormRef.value.validate(async (valid) => { | |||
if (valid) { | |||
try { | |||
loading.value = true; | |||
// 模拟API调用,实际项目中应调用真实的后端API | |||
await new Promise(resolve => setTimeout(resolve, 1000)); | |||
ElMessage.success('作品信息保存成功!'); | |||
} catch (error) { | |||
ElMessage.error('保存失败,请稍后重试'); | |||
} finally { | |||
loading.value = false; | |||
} | |||
} else { | |||
ElMessage.warning('请正确填写表单信息'); | |||
return false; | |||
} | |||
}); | |||
}; | |||
// 提交发布设置表单 | |||
const submitPublishForm = async () => { | |||
try { | |||
publishLoading.value = true; | |||
// 模拟API调用 | |||
await new Promise(resolve => setTimeout(resolve, 1000)); | |||
ElMessage.success('发布设置保存成功!'); | |||
} catch (error) { | |||
ElMessage.error('保存失败,请稍后重试'); | |||
} finally { | |||
publishLoading.value = false; | |||
} | |||
}; | |||
// 返回作品列表 | |||
const goBack = () => { | |||
router.push({ name: 'authorCenter' }); | |||
}; | |||
onMounted(() => { | |||
loadWorkData(); | |||
}); | |||
return { | |||
workFormRef, | |||
workForm, | |||
publishForm, | |||
rules, | |||
categories, | |||
tags, | |||
loading, | |||
publishLoading, | |||
activeTab, | |||
beforeUpload, | |||
uploadCover, | |||
submitForm, | |||
submitPublishForm, | |||
goBack | |||
}; | |||
} | |||
}); | |||
</script> | |||
<style lang="scss" scoped> | |||
.work-setup-container { | |||
background-color: #f5f5f9; | |||
min-height: calc(100vh - 60px); | |||
padding: 30px 0; | |||
.work-setup-content { | |||
max-width: 800px; | |||
margin: 0 auto; | |||
padding: 30px; | |||
background-color: #fff; | |||
border-radius: 8px; | |||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); | |||
.page-header { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
margin-bottom: 30px; | |||
.page-title { | |||
font-size: 24px; | |||
color: #333; | |||
margin: 0; | |||
} | |||
} | |||
.setup-form-container { | |||
:deep(.el-tabs__header) { | |||
margin-bottom: 30px; | |||
} | |||
:deep(.el-tabs__item) { | |||
font-size: 16px; | |||
&.is-active { | |||
color: #0A2463; | |||
} | |||
} | |||
:deep(.el-tabs__active-bar) { | |||
background-color: #0A2463; | |||
} | |||
:deep(.el-form-item__label) { | |||
font-weight: 500; | |||
color: #333; | |||
} | |||
:deep(.el-input__wrapper) { | |||
padding: 0 15px; | |||
height: 44px; | |||
} | |||
.cover-uploader { | |||
.cover-image { | |||
width: 150px; | |||
height: 200px; | |||
object-fit: cover; | |||
border-radius: 4px; | |||
} | |||
.cover-placeholder { | |||
width: 150px; | |||
height: 200px; | |||
background-color: #f7f9fc; | |||
border: 1px dashed #d9d9d9; | |||
border-radius: 4px; | |||
display: flex; | |||
flex-direction: column; | |||
justify-content: center; | |||
align-items: center; | |||
cursor: pointer; | |||
&:hover { | |||
border-color: #0A2463; | |||
color: #0A2463; | |||
} | |||
.el-icon { | |||
font-size: 28px; | |||
color: #8c939d; | |||
margin-bottom: 8px; | |||
} | |||
.upload-text { | |||
font-size: 14px; | |||
color: #8c939d; | |||
} | |||
} | |||
} | |||
.upload-tip { | |||
font-size: 12px; | |||
color: #aaa; | |||
margin-top: 8px; | |||
} | |||
.publish-settings { | |||
.setting-item { | |||
margin-bottom: 30px; | |||
.setting-title { | |||
font-size: 18px; | |||
font-weight: 500; | |||
color: #333; | |||
margin: 0 0 16px; | |||
padding-bottom: 10px; | |||
border-bottom: 1px solid #eee; | |||
} | |||
.setting-content { | |||
padding: 0 16px; | |||
.price-setting { | |||
display: flex; | |||
align-items: center; | |||
.unit { | |||
margin-left: 10px; | |||
color: #666; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
.form-actions { | |||
margin-top: 40px; | |||
display: flex; | |||
justify-content: center; | |||
gap: 20px; | |||
.el-button { | |||
width: 120px; | |||
} | |||
.el-button--primary { | |||
background-color: #0A2463; | |||
border-color: #0A2463; | |||
&:hover, &:focus { | |||
background-color: #1a3473; | |||
border-color: #1a3473; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
</style> |
@ -0,0 +1,161 @@ | |||
<template> | |||
<div class="author-content"> | |||
<div class="page-header"> | |||
<div class="header-title">读者成就设置</div> | |||
<el-button type="primary" class="submit-btn" @click="handleSubmit">提交申请</el-button> | |||
</div> | |||
<div class="readers-container"> | |||
<div class="achievement-grid"> | |||
<div v-for="(achievement, index) in achievements" :key="index" class="achievement-item"> | |||
<div class="achievement-icon"> | |||
<img :src="achievement.icon" :alt="achievement.name" /> | |||
</div> | |||
<div class="achievement-name">{{ achievement.name }}</div> | |||
<div class="achievement-input"> | |||
<el-input v-model="achievement.value" placeholder="请输入..." /> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import { defineComponent, ref } from 'vue'; | |||
import { ElMessage } from 'element-plus'; | |||
export default defineComponent({ | |||
name: 'ReadersManagement', | |||
setup() { | |||
// 成就数据 | |||
const achievements = ref([ | |||
{ | |||
name: '一级成就名称', | |||
icon: 'https://img.alicdn.com/imgextra/i1/O1CN01S3eWbc24CUQrfZ2LE_!!6000000007358-2-tps-128-128.png', | |||
value: '' | |||
}, | |||
{ | |||
name: '二级成就名称', | |||
icon: 'https://img.alicdn.com/imgextra/i2/O1CN018v4k9v28wLLd3zV1A_!!6000000007998-2-tps-128-128.png', | |||
value: '' | |||
}, | |||
{ | |||
name: '三级成就名称', | |||
icon: 'https://img.alicdn.com/imgextra/i3/O1CN01FQW4kk1Mg0fKbMjkS_!!6000000001467-2-tps-128-128.png', | |||
value: '' | |||
}, | |||
{ | |||
name: '四级成就名称', | |||
icon: 'https://img.alicdn.com/imgextra/i4/O1CN01AXhj8T1LxJwo3AxXp_!!6000000001365-2-tps-128-128.png', | |||
value: '' | |||
}, | |||
{ | |||
name: '五级成就名称', | |||
icon: 'https://img.alicdn.com/imgextra/i2/O1CN01JX4lZA24CUIk8xMAx_!!6000000007358-2-tps-128-128.png', | |||
value: '' | |||
} | |||
]); | |||
// 提交设置 | |||
const handleSubmit = () => { | |||
// 验证所有输入是否填写 | |||
const emptyFields = achievements.value.filter(item => !item.value.trim()); | |||
if (emptyFields.length > 0) { | |||
ElMessage.warning('请填写所有成就名称'); | |||
return; | |||
} | |||
// 提交数据 | |||
ElMessage.success('设置已提交,等待审核'); | |||
console.log('提交的成就设置:', achievements.value); | |||
}; | |||
return { | |||
achievements, | |||
handleSubmit | |||
}; | |||
} | |||
}); | |||
</script> | |||
<style lang="scss" scoped> | |||
.author-content { | |||
flex: 1; | |||
.page-header { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
margin-bottom: 20px; | |||
background-color: #fff; | |||
padding: 16px 20px; | |||
border-radius: 8px; | |||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); | |||
.header-title { | |||
font-size: 18px; | |||
font-weight: bold; | |||
color: #333; | |||
} | |||
.submit-btn { | |||
background-color: #0A2463; | |||
border-color: #0A2463; | |||
&:hover, &:focus { | |||
background-color: #1a3473; | |||
border-color: #1a3473; | |||
} | |||
} | |||
} | |||
.readers-container { | |||
background-color: #fff; | |||
border-radius: 8px; | |||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); | |||
padding: 40px; | |||
.achievement-grid { | |||
display: flex; | |||
flex-wrap: wrap; | |||
justify-content: space-around; | |||
gap: 40px; | |||
.achievement-item { | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
width: 180px; | |||
.achievement-icon { | |||
width: 100px; | |||
height: 100px; | |||
margin-bottom: 16px; | |||
img { | |||
width: 100%; | |||
height: 100%; | |||
object-fit: contain; | |||
} | |||
} | |||
.achievement-name { | |||
font-size: 14px; | |||
color: #333; | |||
margin-bottom: 12px; | |||
text-align: center; | |||
} | |||
.achievement-input { | |||
width: 100%; | |||
:deep(.el-input__wrapper) { | |||
text-align: center; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
</style> |
@ -0,0 +1,306 @@ | |||
<template> | |||
<div class="author-content"> | |||
<div class="page-header"> | |||
<div class="header-tabs"> | |||
<router-link :to="{ name: 'authorWorks' }" class="tab-item active">我的作品</router-link> | |||
<!-- 可以根据需要添加其他标签页 --> | |||
</div> | |||
<el-button type="primary" class="new-work-btn" @click="handleCreateNewWork">新建作品</el-button> | |||
</div> | |||
<div class="works-container"> | |||
<!-- <div class="works-filter"> | |||
<el-radio-group v-model="statusFilter" size="default" @change="handleFilterChange"> | |||
<el-radio-button label="all">全部</el-radio-button> | |||
<el-radio-button label="draft">连载中</el-radio-button> | |||
<el-radio-button label="published">已完结</el-radio-button> | |||
<el-radio-button label="publishing">审核中</el-radio-button> | |||
</el-radio-group> | |||
</div> --> | |||
<div v-if="loading" class="loading-container"> | |||
<el-skeleton :rows="5" animated /> | |||
</div> | |||
<div v-else-if="error" class="error-container"> | |||
<el-result | |||
icon="error" | |||
title="加载失败" | |||
sub-title="无法加载作品数据,请稍后再试" | |||
> | |||
<template #extra> | |||
<el-button type="primary" @click="loadWorks">重试</el-button> | |||
</template> | |||
</el-result> | |||
</div> | |||
<div v-else-if="filteredWorks.length > 0" class="works-list"> | |||
<work-item | |||
v-for="work in filteredWorks" | |||
:key="work.id" | |||
:work-id="work.id" | |||
:work-title="work.title" | |||
:work-cover="work.cover" | |||
:status="work.status" | |||
:audit-status="work.auditStatus" | |||
:is-new="work.isNew" | |||
@setup="handleWorkSetup" | |||
@edit="handleWorkEdit" | |||
/> | |||
</div> | |||
<div v-else class="no-works"> | |||
<el-empty | |||
:description="works.length === 0 ? '暂无作品,快来创作您的第一部作品吧!' : '暂无符合条件的作品'" | |||
:image-size="200" | |||
> | |||
<template #extra v-if="works.length === 0"> | |||
<el-button type="primary" @click="handleCreateNewWork">立即创建</el-button> | |||
</template> | |||
</el-empty> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import { defineComponent, ref, onMounted, computed } from 'vue'; | |||
import { useRouter } from 'vue-router'; | |||
import { ElMessage } from 'element-plus'; | |||
import WorkItem from '@/components/author/WorkItem.vue'; | |||
export default defineComponent({ | |||
name: 'WorksManagement', | |||
components: { | |||
WorkItem | |||
}, | |||
setup() { | |||
const router = useRouter(); | |||
const works = ref([]); | |||
const loading = ref(true); | |||
const error = ref(false); | |||
const statusFilter = ref('all'); | |||
const filteredWorks = computed(() => { | |||
if (statusFilter.value === 'all') { | |||
return works.value; | |||
} | |||
return works.value.filter(work => work.status === statusFilter.value); | |||
}); | |||
// 加载作品列表 | |||
const loadWorks = async () => { | |||
loading.value = true; | |||
error.value = false; | |||
try { | |||
// 模拟API调用 | |||
await new Promise(resolve => setTimeout(resolve, 1000)); | |||
// 设置测试数据 | |||
works.value = [ | |||
{ | |||
id: 1, | |||
title: '大宋好原夫', | |||
cover: 'https://bookcover.yuewen.com/qdbimg/349573/1041637443/150.webp', | |||
status: 'draft', | |||
auditStatus: '', | |||
isNew: true | |||
}, | |||
{ | |||
id: 2, | |||
title: '苏莫是什么小说', | |||
cover: '', | |||
status: 'draft', | |||
auditStatus: 'passed', | |||
isNew: false | |||
}, | |||
{ | |||
id: 3, | |||
title: '罗修炎月武道大帝', | |||
cover: 'https://bookcover.yuewen.com/qdbimg/349573/1031532393/150.webp', | |||
status: 'published', | |||
auditStatus: '', | |||
isNew: false | |||
}, | |||
{ | |||
id: 4, | |||
title: '顾道长生', | |||
cover: 'https://bookcover.yuewen.com/qdbimg/349573/1031565332/150.webp', | |||
status: 'publishing', | |||
auditStatus: 'rejected', | |||
isNew: false | |||
}, | |||
{ | |||
id: 5, | |||
title: '神象无极限', | |||
cover: 'https://bookcover.yuewen.com/qdbimg/349573/1030293649/150.webp', | |||
status: 'published', | |||
auditStatus: '', | |||
isNew: false | |||
} | |||
]; | |||
} catch (err) { | |||
console.error('加载作品失败', err); | |||
error.value = true; | |||
} finally { | |||
loading.value = false; | |||
} | |||
}; | |||
// 处理筛选变化 | |||
const handleFilterChange = (value) => { | |||
console.log('筛选状态变为:', value); | |||
}; | |||
// 创建新作品 | |||
const handleCreateNewWork = () => { | |||
// 实际应用中应该跳转到创建作品的页面或打开创建作品的弹窗 | |||
ElMessage.success('创建新作品'); | |||
router.push({ name: 'createWork' }); | |||
}; | |||
// 作品设置 | |||
const handleWorkSetup = (workId) => { | |||
ElMessage.info(`设置作品ID: ${workId}`); | |||
router.push({ name: 'workSetup', params: { id: workId } }); | |||
}; | |||
// 跳转到写作页面 | |||
const handleWorkEdit = (workId) => { | |||
ElMessage.info(`编辑作品ID: ${workId}`); | |||
router.push({ name: 'workEdit', params: { id: workId } }); | |||
}; | |||
onMounted(() => { | |||
// 加载作品列表 | |||
loadWorks(); | |||
// 输出调试信息 | |||
console.log('[WorksManagement] 组件已挂载'); | |||
}); | |||
return { | |||
works, | |||
loading, | |||
error, | |||
loadWorks, | |||
statusFilter, | |||
filteredWorks, | |||
handleFilterChange, | |||
handleCreateNewWork, | |||
handleWorkSetup, | |||
handleWorkEdit | |||
}; | |||
} | |||
}); | |||
</script> | |||
<style lang="scss" scoped> | |||
.author-content { | |||
flex: 1; | |||
.page-header { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
margin-bottom: 20px; | |||
background-color: #fff; | |||
padding: 16px 20px; | |||
border-radius: 8px; | |||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); | |||
.header-tabs { | |||
display: flex; | |||
position: relative; | |||
.tab-item { | |||
padding: 8px 16px; | |||
margin-right: 24px; | |||
font-size: 16px; | |||
color: #666; | |||
text-decoration: none; | |||
position: relative; | |||
&.active { | |||
color: #0A2463; | |||
font-weight: bold; | |||
&::after { | |||
content: ''; | |||
position: absolute; | |||
bottom: -8px; | |||
left: 0; | |||
width: 100%; | |||
height: 2px; | |||
background-color: #0A2463; | |||
border-radius: 1px; | |||
} | |||
} | |||
&:hover:not(.active) { | |||
color: #0A2463; | |||
} | |||
} | |||
} | |||
.new-work-btn { | |||
background-color: #0A2463; | |||
border-color: #0A2463; | |||
&:hover, &:focus { | |||
background-color: #1a3473; | |||
border-color: #1a3473; | |||
} | |||
} | |||
} | |||
.works-container { | |||
background-color: #fff; | |||
border-radius: 8px; | |||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); | |||
overflow: hidden; | |||
.works-filter { | |||
padding: 16px 20px; | |||
border-bottom: 1px solid #eee; | |||
:deep(.el-radio-button__inner) { | |||
border-color: #dcdfe6; | |||
color: #606266; | |||
} | |||
:deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) { | |||
background-color: #0A2463; | |||
border-color: #0A2463; | |||
box-shadow: -1px 0 0 0 #0A2463; | |||
color: #fff; | |||
} | |||
} | |||
.loading-container, | |||
.error-container { | |||
padding: 20px; | |||
} | |||
.works-list { | |||
padding: 0; | |||
// 为每个作品项添加边框 | |||
:deep(.work-item) { | |||
border-bottom: 1px solid #f0f0f0; | |||
padding: 20px; | |||
&:last-child { | |||
border-bottom: none; | |||
} | |||
} | |||
} | |||
.no-works { | |||
padding: 60px 0; | |||
text-align: center; | |||
} | |||
} | |||
} | |||
</style> |