新增作家中心功能,包括作品管理、读者管理、作品创建、作品设置等页面。优化路由守卫逻辑,确保作家权限验证。添加调试工具函数,便于开发过程中查看用户状态。调整部分组件样式和逻辑,提升用户体验。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> | |||