Browse Source

feat: 新增作家中心功能及相关页面

新增作家中心功能,包括作品管理、读者管理、作品创建、作品设置等页面。优化路由守卫逻辑,确保作家权限验证。添加调试工具函数,便于开发过程中查看用户状态。调整部分组件样式和逻辑,提升用户体验。
master
前端-胡立永 2 months ago
parent
commit
7ff419b8eb
20 changed files with 3114 additions and 110 deletions
  1. +19
    -6
      src/App.vue
  2. +1
    -1
      src/api/index.js
  3. +12
    -1
      src/components/auth/AuthorApplicationModal.vue
  4. +47
    -25
      src/components/auth/AuthorApplicationProvider.vue
  5. +226
    -0
      src/components/author/WorkItem.vue
  6. +143
    -5
      src/components/book/BookComments.vue
  7. +381
    -0
      src/components/book/InteractiveReward.vue
  8. +31
    -46
      src/layout/index.vue
  9. +37
    -3
      src/layout/layout/Header.vue
  10. +13
    -2
      src/main.js
  11. +126
    -15
      src/router/index.js
  12. +38
    -1
      src/store/index.js
  13. +40
    -0
      src/utils/debug.js
  14. +124
    -0
      src/views/author/AuthorCenter.vue
  15. +303
    -0
      src/views/author/CreateWork.vue
  16. +549
    -0
      src/views/author/WorkEdit.vue
  17. +502
    -0
      src/views/author/WorkSetup.vue
  18. +161
    -0
      src/views/author/components/ReadersManagement.vue
  19. +306
    -0
      src/views/author/components/WorksManagement.vue
  20. +55
    -5
      src/views/book/index.vue

+ 19
- 6
src/App.vue View File

@ -1,15 +1,18 @@
<template>
<auth-provider>
<author-application-provider>
<router-view />
</author-application-provider>
</auth-provider>
<div id="app">
<AuthProvider>
<AuthorApplicationProvider>
<router-view />
</AuthorApplicationProvider>
</AuthProvider>
</div>
</template>
<script>
import { onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { routerEvents } from '@/router';
import { useMainStore } from './store';
import AuthProvider from '@/components/auth/AuthProvider.vue';
import AuthorApplicationProvider from '@/components/auth/AuthorApplicationProvider.vue';
@ -21,17 +24,27 @@ export default {
},
setup() {
const route = useRoute();
const store = useMainStore();
// 便访
// 使
window.$store = store;
window.routerEvents = routerEvents;
//
onMounted(() => {
//
store.initializeAuth();
//
if (typeof routerEvents.triggerLogin === 'function') {
routerEvents.triggerLogin();
routerEvents.triggerLogin = null;
}
//
if (window.$debug?.logUserState) {
window.$debug.logUserState();
}
});
//


+ 1
- 1
src/api/index.js View File

@ -36,4 +36,4 @@ api.interceptors.response.use(
}
);
export default api;
export default api;

+ 12
- 1
src/components/auth/AuthorApplicationModal.vue View File

@ -41,6 +41,7 @@
<script>
import { ref, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { useMainStore } from '@/store';
export default {
name: 'AuthorApplicationModal',
@ -52,6 +53,8 @@ export default {
},
emits: ['update:visible', 'application-success'],
setup(props, { emit }) {
const store = useMainStore();
//
const penName = ref('');
const introduction = ref('');
@ -75,8 +78,16 @@ export default {
// APIAPI
await new Promise(resolve => setTimeout(resolve, 1000));
// localStorage
localStorage.setItem('isAuthor', 'true');
// store
store.setAsAuthor();
//
ElMessage.success('申请已提交,我们将尽快审核');
ElMessage.success('申请已通过,您已成为创作者!');
//
emit('application-success', {
penName: penName.value,
introduction: introduction.value


+ 47
- 25
src/components/auth/AuthorApplicationProvider.vue View File

@ -1,10 +1,10 @@
<template>
<div>
<slot></slot>
<author-application-modal
v-model:visible="showApplicationModal"
<div class="author-application-provider">
<AuthorApplicationModal
v-model:visible="modalVisible"
@application-success="handleApplicationSuccess"
/>
<slot></slot>
</div>
</template>
@ -12,8 +12,8 @@
import { ref, provide } from 'vue';
import AuthorApplicationModal from './AuthorApplicationModal.vue';
//
export const AUTHOR_APPLICATION_INJECTION_KEY = Symbol('author-application-context');
//
export const AUTHOR_APPLICATION_INJECTION_KEY = Symbol('author-application');
export default {
name: 'AuthorApplicationProvider',
@ -21,41 +21,63 @@ export default {
AuthorApplicationModal
},
setup() {
const showApplicationModal = ref(false);
const applicationSuccessCallback = ref(null);
const modalVisible = ref(false);
const successCallback = ref(null);
//
const openApplicationModal = (callback) => {
showApplicationModal.value = true;
applicationSuccessCallback.value = callback;
//
const openApplicationModal = (callback = null) => {
modalVisible.value = true;
successCallback.value = callback;
};
//
const handleApplicationSuccess = (applicationData) => {
if (typeof applicationSuccessCallback.value === 'function') {
applicationSuccessCallback.value(applicationData);
applicationSuccessCallback.value = null;
// localStoragestore
localStorage.setItem('isAuthor', 'true');
// store
if (window.$pinia?.state?.value?.main) {
const store = window.$pinia._s.get('main');
if (store && typeof store.setAsAuthor === 'function') {
store.setAsAuthor();
}
}
//
if (successCallback.value && typeof successCallback.value === 'function') {
successCallback.value(applicationData);
}
//
showApplicationModal.value = false;
modalVisible.value = false;
successCallback.value = null;
//
if (window.$debug?.logUserState) {
window.$debug.logUserState();
}
};
//
const authorApplicationContext = {
//
const context = {
openApplicationModal
};
provide(AUTHOR_APPLICATION_INJECTION_KEY, context);
//
provide(AUTHOR_APPLICATION_INJECTION_KEY, authorApplicationContext);
// 访
window.$authorApplicationContext = authorApplicationContext;
// 便使
window.$authorApplicationContext = context;
return {
showApplicationModal,
modalVisible,
handleApplicationSuccess
};
}
};
</script>
</script>
<style scoped>
.author-application-provider {
display: contents;
}
</style>

+ 226
- 0
src/components/author/WorkItem.vue View File

@ -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>

+ 143
- 5
src/components/book/BookComments.vue View File

@ -31,7 +31,7 @@
<div class="comment-text">{{ comment.content }}</div>
<div class="comment-footer">
<div class="comment-actions">
<span class="action-item">
<span class="action-item" @click="toggleReplyForm(index)">
<el-icon>
<Message />
</el-icon>
@ -43,6 +43,31 @@
</span>
</div>
</div>
<!-- 回复表单 -->
<div class="reply-form" v-if="activeReplyIndex === index">
<el-input v-model="replyText" type="textarea" :rows="2" placeholder="请输入回复内容..." maxlength="300" show-word-limit />
<div class="reply-actions">
<el-button size="small" @click="cancelReply">取消</el-button>
<el-button size="small" type="primary" @click="submitReply(index)" :disabled="!replyText.trim()">回复</el-button>
</div>
</div>
<!-- 回复列表 -->
<div class="replies-list" v-if="comment.replies && comment.replies.length > 0">
<div v-for="(reply, replyIndex) in comment.replies" :key="replyIndex" class="reply-item">
<div class="user-avatar">
<img src="@/assets/images/center/headImage.png" :alt="reply.username">
</div>
<div class="reply-content">
<div class="reply-header">
<span class="username">{{ reply.username }}</span>
<span class="time">{{ reply.time }}</span>
</div>
<div class="reply-text">{{ reply.content }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -82,12 +107,14 @@ export default defineComponent({
username: '万年捧',
avatar: 'https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKlR5PibUEEsVjXGfH4c1eR5hXDicoH0EJUTHYwDO3EvZLXXgON8GrNTbRg8DnzaddicibYnGcfq28tYg/132',
time: '2022-07-03',
content: '这是本年内看的唯一一部完结的!看的人真幸运,发家文和风险防控写的都是一流'
content: '这是本年内看的唯一一部完结的!看的人真幸运,发家文和风险防控写的都是一流',
replies: []
},
{
username: '残生往事',
avatar: 'https://thirdwx.qlogo.cn/mmopen/vi_32/3F4feeHnMyoGjqKfP8vGKCHwyvovMHiaO0Q1QkQMRTGibLcyJbUcUJ4LmdkkDqC5ZcqP1rvqKMviaYAyehqYb6ciaA/132',
content: '我很喜欢男主的性格,不小心眼,有格局,做事情多考虑下一步,商业和情感都处理得不错,就是那个林涵有点没必要吧?'
content: '我很喜欢男主的性格,不小心眼,有格局,做事情多考虑下一步,商业和情感都处理得不错,就是那个林涵有点没必要吧?',
replies: []
}
]);
@ -96,6 +123,10 @@ export default defineComponent({
const currentPage = ref(1);
const pageSize = ref(10);
const totalComments = ref(comments.value.length);
//
const activeReplyIndex = ref(null);
const replyText = ref('');
const submitComment = () => {
if (!commentText.value.trim()) {
@ -108,7 +139,8 @@ export default defineComponent({
username: '当前用户',
avatar: defaultAvatar.value,
time: new Date().toLocaleDateString(),
content: commentText.value
content: commentText.value,
replies: []
});
totalComments.value = comments.value.length;
@ -121,6 +153,47 @@ export default defineComponent({
currentPage.value = page;
//
};
//
const toggleReplyForm = (index) => {
if (activeReplyIndex.value === index) {
activeReplyIndex.value = null;
} else {
activeReplyIndex.value = index;
replyText.value = '';
}
};
//
const cancelReply = () => {
activeReplyIndex.value = null;
replyText.value = '';
};
//
const submitReply = (index) => {
if (!replyText.value.trim()) {
ElMessage.warning('请输入回复内容');
return;
}
if (!comments.value[index].replies) {
comments.value[index].replies = [];
}
//
comments.value[index].replies.push({
username: '当前用户',
avatar: defaultAvatar.value,
time: new Date().toLocaleDateString(),
content: replyText.value
});
//
replyText.value = '';
activeReplyIndex.value = null;
ElMessage.success('回复成功!');
};
return {
comments,
@ -131,7 +204,12 @@ export default defineComponent({
pageSize,
totalComments,
submitComment,
handlePageChange
handlePageChange,
activeReplyIndex,
replyText,
toggleReplyForm,
cancelReply,
submitReply
};
}
});
@ -258,6 +336,66 @@ export default defineComponent({
}
}
}
//
.reply-form {
margin-top: 12px;
padding: 12px;
background-color: #f9f9f9;
border-radius: 4px;
.reply-actions {
display: flex;
justify-content: flex-end;
margin-top: 8px;
gap: 8px;
}
}
//
.replies-list {
margin-top: 16px;
padding-left: 12px;
border-left: 2px solid #eee;
.reply-item {
display: flex;
padding: 12px 0;
.user-avatar {
width: 36px;
height: 36px;
margin-right: 12px;
}
.reply-content {
flex: 1;
.reply-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
.username {
font-size: 14px;
font-weight: 500;
}
.time {
font-size: 12px;
color: #999;
}
}
.reply-text {
font-size: 14px;
line-height: 1.5;
color: #333;
}
}
}
}
}
}
}


+ 381
- 0
src/components/book/InteractiveReward.vue View File

@ -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;
}
// 01
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>

+ 31
- 46
src/layout/index.vue View File

@ -1,25 +1,21 @@
<template>
<div class="layout-container">
<!-- 头部导航 -->
<app-header />
<!-- 主内容区 -->
<main class="layout-main">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</main>
<!-- 页脚 -->
<app-footer />
<!-- 回到顶部 -->
<back-to-top />
</div>
<div class="layout-container">
<!-- 头部导航 -->
<app-header />
<!-- 主内容区 -->
<main class="layout-main">
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</main>
<!-- 页脚 -->
<app-footer />
<!-- 回到顶部 -->
<back-to-top />
</div>
</template>
<script>
@ -30,13 +26,13 @@ import Breadcrumb from './components/Breadcrumb.vue';
import BackToTop from './components/BackToTop.vue';
export default defineComponent({
name: 'Layout',
components: {
AppHeader,
AppFooter,
Breadcrumb,
BackToTop
}
name: 'Layout',
components: {
AppHeader,
AppFooter,
Breadcrumb,
BackToTop
}
});
</script>
@ -44,26 +40,15 @@ export default defineComponent({
@use '@/assets/styles/variables.scss' as vars;
.layout-container {
display: flex;
flex-direction: column;
min-height: 100vh;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.layout-main {
flex: 1;
max-width: 1240px;
margin: 0 auto;
width: 100%;
}
//
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
flex: 1;
max-width: 1240px;
margin: 0 auto;
width: 100%;
}
</style>

+ 37
- 3
src/layout/layout/Header.vue View File

@ -23,8 +23,8 @@
<el-menu-item index="/category/3">玄幻</el-menu-item>
<el-menu-item index="/ranking">排行榜</el-menu-item>
<el-menu-item index="/category">其他</el-menu-item>
<el-menu-item index="/bookshelf">书架</el-menu-item>
<el-menu-item index="/author">作家专区</el-menu-item>
<el-menu-item index="/bookshelf" @click="goToBookshelf">书架</el-menu-item>
<el-menu-item index="/author" @click="goToAuthorCenter">作家专区</el-menu-item>
</el-menu>
</div>
@ -214,6 +214,38 @@ export default {
//
authorApplicationContext.openApplicationModal();
};
//
const goToAuthorCenter = () => {
//
if (!isLoggedIn.value) {
//
authContext.openLogin(() => {
//
if (!store.isAuthor) {
//
authorApplicationContext.openApplicationModal(() => {
router.push('/author');
});
} else {
//
router.push('/author');
}
});
return;
}
//
if (!store.isAuthor) {
//
authorApplicationContext.openApplicationModal(() => {
router.push('/author');
});
} else {
//
router.push('/author');
}
};
return {
isLoggedIn,
@ -228,7 +260,9 @@ export default {
goToBookshelf,
goToSettings,
logout,
applyForAuthor
applyForAuthor,
goToAuthorCenter,
router,
};
}
};


+ 13
- 2
src/main.js View File

@ -5,6 +5,8 @@ import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import { useMainStore } from './store';
// 导入调试工具
import './utils/debug';
// 导入全局样式
import './assets/styles/global.scss';
@ -14,13 +16,22 @@ const pinia = createPinia();
app.use(ElementPlus);
app.use(pinia);
app.use(router);
// 初始化身份验证状态
// 初始化身份验证状态
const store = useMainStore();
store.initializeAuth();
// 全局挂载pinia,用于路由守卫
window.$pinia = pinia;
// 确保在状态恢复后再挂载路由
app.use(router);
// 打印初始状态
if (import.meta.env.DEV) {
setTimeout(() => {
window.$debug.logUserState();
}, 0);
}
app.mount('#app');

+ 126
- 15
src/router/index.js View File

@ -17,22 +17,26 @@ const routes = [
{
path: 'book/:id',
name: 'BookDetail',
component: () => import('../views/book/index.vue')
component: () => import('../views/book/index.vue'),
props: true
},
{
path: 'book/:id/chapter/:chapterId',
name: 'ChapterDetail',
component: () => import('../views/book/chapter.vue')
component: () => import('../views/book/chapter.vue'),
props: true
},
{
path: 'category',
name: 'Category',
component: () => import('../views/home/category.vue')
component: () => import('../views/home/category.vue'),
props: true
},
{
path: 'category/:id',
name: 'CategoryDetail',
component: () => import('../views/home/category.vue')
component: () => import('../views/home/category.vue'),
props: true
},
{
path: 'ranking',
@ -44,6 +48,45 @@ const routes = [
name: 'Bookshelf',
component: () => import('../views/home/Bookshelf.vue'),
meta: { requiresAuth: true }
},
{
path: 'author',
name: 'authorCenter',
component: () => import('../views/author/AuthorCenter.vue'),
meta: { requiresAuth: true, requiresAuthor: true },
redirect: { name: 'authorWorks' },
children: [
{
path: 'works',
name: 'authorWorks',
component: () => import('../views/author/components/WorksManagement.vue'),
meta: { requiresAuth: true, requiresAuthor: true }
},
{
path: 'readers',
name: 'authorReaders',
component: () => import('../views/author/components/ReadersManagement.vue'),
meta: { requiresAuth: true, requiresAuthor: true }
}
]
},
{
path: 'author/work/create',
name: 'createWork',
component: () => import('../views/author/CreateWork.vue'),
meta: { requiresAuth: true, requiresAuthor: true }
},
{
path: 'author/work/:id/setup',
name: 'workSetup',
component: () => import('../views/author/WorkSetup.vue'),
meta: { requiresAuth: true, requiresAuthor: true }
},
{
path: 'author/work/:id/edit',
name: 'workEdit',
component: () => import('../views/author/WorkEdit.vue'),
meta: { requiresAuth: true, requiresAuthor: true }
}
]
},
@ -66,14 +109,37 @@ export const routerEvents = {
// 全局路由守卫
router.beforeEach((to, from, next) => {
const store = window.$pinia?.state.value?.main;
// 调试信息
console.log('[Router] 路由切换:', {
from: from.path,
to: to.path,
meta: to.meta
});
// 首先尝试从localStorage获取登录和作家状态
const token = localStorage.getItem('token');
const isLoggedIn = !!token;
const isAuthor = localStorage.getItem('isAuthor') === 'true';
// 输出当前状态
console.log('[Router] 当前状态:', { isLoggedIn, isAuthor });
// 获取路由需要的权限
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
const requiresAuthor = to.matched.some(record => record.meta.requiresAuthor);
if (requiresAuth && (!store || !store.isLoggedIn)) {
// 保存当前要访问的路由信息
// 如果路由不需要任何权限,直接放行
if (!requiresAuth && !requiresAuthor) {
next();
return;
}
// 处理需要登录的路由
if (requiresAuth && !isLoggedIn) {
const targetRoute = to.fullPath;
console.log('[Router] 需要登录权限,未登录,跳转到首页');
// 立即尝试调用登录弹窗
// 尝试调用登录弹窗
setTimeout(() => {
const authContext = window.$authContext;
if (authContext && typeof authContext.openLogin === 'function') {
@ -94,16 +160,61 @@ router.beforeEach((to, from, next) => {
}
}, 0);
// 如果不是首页,则跳转到首页
if (to.name !== 'Home') {
next({ path: '/' });
} else {
// 如果已经在首页则直接渲染
// 跳转到首页
next({ path: '/' });
return;
}
// 处理需要作家权限的路由
if (requiresAuthor) {
// 如果已经是作家,直接放行
if (isAuthor) {
console.log('[Router] 需要作家权限,已是作家,直接放行');
next();
return;
}
} else {
next();
console.log('[Router] 需要作家权限,非作家,跳转到首页');
// 未登录或不是作家,需要先登录再申请成为作家
if (!isLoggedIn) {
const targetRoute = to.fullPath;
setTimeout(() => {
const authContext = window.$authContext;
if (authContext && typeof authContext.openLogin === 'function') {
authContext.openLogin(() => {
// 登录成功后显示作家申请
const authorContext = window.$authorApplicationContext;
if (authorContext && typeof authorContext.openApplicationModal === 'function') {
authorContext.openApplicationModal(() => {
// 申请成功后导航到作家专区
router.push(targetRoute);
});
}
});
}
}, 0);
} else {
// 已登录但不是作家,直接显示作家申请
setTimeout(() => {
const authorContext = window.$authorApplicationContext;
if (authorContext && typeof authorContext.openApplicationModal === 'function') {
authorContext.openApplicationModal(() => {
// 申请成功后导航到作家专区
router.push(to.fullPath);
});
}
}, 0);
}
// 跳转到首页
next({ path: '/' });
return;
}
// 通过所有检查
console.log('[Router] 通过所有权限检查');
next();
});
export default router;

+ 38
- 1
src/store/index.js View File

@ -4,6 +4,7 @@ export const useMainStore = defineStore('main', {
state: () => ({
user: null,
isLoggedIn: false,
isAuthor: false,
token: null,
bookshelf: []
}),
@ -25,16 +26,19 @@ export const useMainStore = defineStore('main', {
id: 'user_' + Date.now(),
name: '用户' + loginData.phone.substring(loginData.phone.length - 4),
phone: loginData.phone,
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
isAuthor: false
};
this.user = userData;
this.isLoggedIn = true;
this.isAuthor = userData.isAuthor;
this.token = 'mock_token_' + Date.now();
// 保存到本地存储,使登录状态持久化
localStorage.setItem('user', JSON.stringify(userData));
localStorage.setItem('token', this.token);
localStorage.setItem('isAuthor', userData.isAuthor);
return userData;
} catch (error) {
@ -59,39 +63,72 @@ export const useMainStore = defineStore('main', {
logout() {
this.user = null;
this.isLoggedIn = false;
this.isAuthor = false;
this.token = null;
// 清除本地存储
localStorage.removeItem('user');
localStorage.removeItem('token');
localStorage.removeItem('isAuthor');
},
// 初始化,从本地存储恢复登录状态
initializeAuth() {
// 先直接从localStorage中获取各个状态
const userData = localStorage.getItem('user');
const token = localStorage.getItem('token');
const isAuthor = localStorage.getItem('isAuthor') === 'true';
const bookshelf = localStorage.getItem('bookshelf');
// 恢复用户数据和登录状态
if (userData && token) {
try {
this.user = JSON.parse(userData);
this.token = token;
this.isLoggedIn = true;
// 明确设置作家状态,以localStorage为准
this.isAuthor = isAuthor;
// 确保用户对象中的isAuthor与状态一致
if (this.user) {
this.user.isAuthor = isAuthor;
}
// 输出调试信息
console.log('[Store] Auth initialized:', {
isLoggedIn: this.isLoggedIn,
isAuthor: this.isAuthor,
user: this.user
});
} catch (e) {
console.error('[Store] Error parsing user data:', e);
this.logout();
}
}
// 恢复书架数据
if (bookshelf) {
try {
this.bookshelf = JSON.parse(bookshelf);
} catch (e) {
console.error('[Store] Error parsing bookshelf:', e);
this.bookshelf = [];
localStorage.removeItem('bookshelf');
}
}
},
// 设置为作家身份
setAsAuthor() {
if (this.user) {
this.user.isAuthor = true;
this.isAuthor = true;
localStorage.setItem('user', JSON.stringify(this.user));
localStorage.setItem('isAuthor', 'true');
}
},
// 添加书籍到书架
addToBookshelf(book) {
// 检查是否已在书架中,避免重复添加


+ 40
- 0
src/utils/debug.js View File

@ -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
};

+ 124
- 0
src/views/author/AuthorCenter.vue View File

@ -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>

+ 303
- 0
src/views/author/CreateWork.vue View File

@ -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;
// APIAPI
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>

+ 549
- 0
src/views/author/WorkEdit.vue View File

@ -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>

+ 502
- 0
src/views/author/WorkSetup.vue View File

@ -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 {
// APIAPI
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;
// APIAPI
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>

+ 161
- 0
src/views/author/components/ReadersManagement.vue View File

@ -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>

+ 306
- 0
src/views/author/components/WorksManagement.vue View File

@ -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>

+ 55
- 5
src/views/book/index.vue View File

@ -38,7 +38,7 @@
<div class="action-buttons">
<div class="action-btn-group">
<el-button class="reward-btn" plain>互动打赏</el-button>
<el-button class="reward-btn" plain @click="showRewardDialog">互动打赏</el-button>
</div>
<div class="action-btn-group">
<el-button
@ -72,10 +72,16 @@
</div>
</div>
</div>
<!-- 互动打赏弹窗 -->
<InteractiveReward
v-model:visible="rewardDialogVisible"
:book-id="book.id"
@reward-success="handleRewardSuccess"
/>
</template>
<script>
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import BookCard from '@/components/common/BookCard.vue';
import BookStats from '@/components/book/BookStats.vue';
@ -83,6 +89,7 @@ import BookIntro from '@/components/book/BookIntro.vue';
import BookCatalog from '@/components/book/BookCatalog.vue';
import BookComments from '@/components/book/BookComments.vue';
import IntimacyRanking from '@/components/ranking/IntimacyRanking.vue';
import InteractiveReward from '@/components/book/InteractiveReward.vue';
export default {
name: 'BookDetail',
@ -92,7 +99,8 @@ export default {
BookIntro,
BookCatalog,
BookComments,
IntimacyRanking
IntimacyRanking,
InteractiveReward
},
setup() {
const route = useRoute();
@ -176,14 +184,43 @@ export default {
const currentPage = ref(1);
const commentText = ref('');
const isInShelf = ref(false);
const rewardDialogVisible = ref(false);
const handlePageChange = (page) => {
currentPage.value = page;
//
};
//
const cleanup = () => {
//
Object.keys(book).forEach(key => {
if (typeof book[key] === 'object') {
book[key] = null;
}
});
activeTab.value = 'intro';
currentPage.value = 1;
commentText.value = '';
isInShelf.value = false;
rewardDialogVisible.value = false;
};
//
onBeforeUnmount(() => {
cleanup();
});
//
const goToChapter = (chapterId) => {
router.push(`/book/${bookId}/chapter/${chapterId}`);
cleanup();
router.push({
name: 'ChapterDetail',
params: {
id: bookId,
chapterId: chapterId
}
});
};
const submitComment = () => {
@ -207,6 +244,15 @@ export default {
isInShelf.value = !isInShelf.value;
};
const showRewardDialog = () => {
rewardDialogVisible.value = true;
};
const handleRewardSuccess = (items) => {
console.log('打赏成功,打赏项目:', items);
//
};
onMounted(() => {
// bookIdAPI
console.log('加载书籍详情,ID:', bookId);
@ -222,7 +268,11 @@ export default {
goToChapter,
submitComment,
isInShelf,
toggleShelf
toggleShelf,
rewardDialogVisible,
showRewardDialog,
handleRewardSuccess,
cleanup
};
}
};


Loading…
Cancel
Save