diff --git a/dist.zip b/dist.zip new file mode 100644 index 0000000..25cb71f Binary files /dev/null and b/dist.zip differ diff --git a/src/api/home.js b/src/api/home.js index 9420039..8590056 100644 --- a/src/api/home.js +++ b/src/api/home.js @@ -135,4 +135,24 @@ export const homeApi = { getBookAreaList() { return api.get('/all_index/getBookAreaList'); }, + + /** + * 检查章节是否需要付费 + * @param {Object} params 参数 + * @param {string} params.bookId 书本ID + * @param {string} params.novelId 章节ID + */ + getMyShopNovel(params) { + return api.get('/my_book/getMyShopNovel', { params }); + }, + + /** + * 购买章节 + * @param {Object} params 参数 + * @param {string} params.bookId 书本ID + * @param {string} params.novelId 章节ID(可以是单个或多个,多个用逗号分隔) + */ + buyNovel(params) { + return api.post('/my_order/buyNovel', params); + }, } \ No newline at end of file diff --git a/src/assets/images/推荐票.png b/src/assets/images/Recommendationvote.png similarity index 100% rename from src/assets/images/推荐票.png rename to src/assets/images/Recommendationvote.png diff --git a/src/assets/images/crown.png b/src/assets/images/crown.png new file mode 100644 index 0000000..8c91ffb Binary files /dev/null and b/src/assets/images/crown.png differ diff --git a/src/components/book/SubscriptionPopup.vue b/src/components/book/SubscriptionPopup.vue new file mode 100644 index 0000000..b7b1bfe --- /dev/null +++ b/src/components/book/SubscriptionPopup.vue @@ -0,0 +1,457 @@ + + + + + \ No newline at end of file diff --git a/src/components/ranking/IntimacyRanking.vue b/src/components/ranking/IntimacyRanking.vue index 6ffcbf1..70e3b83 100644 --- a/src/components/ranking/IntimacyRanking.vue +++ b/src/components/ranking/IntimacyRanking.vue @@ -2,7 +2,7 @@
- 皇冠 + 皇冠
读者亲密值榜单 diff --git a/src/utils/theme.js b/src/utils/theme.js new file mode 100644 index 0000000..48a1afe --- /dev/null +++ b/src/utils/theme.js @@ -0,0 +1,36 @@ +import { ref } from 'vue' + +// 主题状态 +const isDarkMode = ref(false) + +// 切换主题 +export const toggleTheme = () => { + isDarkMode.value = !isDarkMode.value + if (isDarkMode.value) { + document.documentElement.classList.add('dark-theme') + } else { + document.documentElement.classList.remove('dark-theme') + } +} + +// 获取主题状态 +export const getThemeMode = () => { + return isDarkMode.value +} + +// 设置主题状态 +export const setThemeMode = (dark) => { + isDarkMode.value = dark + if (dark) { + document.documentElement.classList.add('dark-theme') + } else { + document.documentElement.classList.remove('dark-theme') + } +} + +export default { + isDarkMode, + toggleTheme, + getThemeMode, + setThemeMode +} \ No newline at end of file diff --git a/src/views/author/WorkEdit.vue b/src/views/author/WorkEdit.vue index c81a02b..b2c2c16 100644 --- a/src/views/author/WorkEdit.vue +++ b/src/views/author/WorkEdit.vue @@ -64,7 +64,7 @@
- {{ getStatusText(chapter._status) }} + {{ getStatusText(chapter._status, chapter.state) }} {{ chapter.num }}字
@@ -76,28 +76,45 @@
-
- -
-
- -
@@ -298,15 +315,51 @@ export default defineComponent({ }; // 获取状态文本 - const getStatusText = (status) => { + const getStatusText = (status, state) => { + // 如果是已发布状态,需要根据审核状态显示 + if (status == '1' || status == 1) { + const stateMap = { + 0: '审核中', + 1: '已发布', + 2: '已驳回' + }; + // 确保 state 是数字类型 + const stateValue = parseInt(state) || 0; + return stateMap[stateValue] || '审核中'; + } + + // 其他状态 const statusMap = { 'new': '未保存', '0': '草稿', - '1': '已发布' + 0: '草稿' }; return statusMap[status] || status; }; + // 获取状态样式类 + const getStatusClass = (status, state) => { + // 如果是已发布状态,根据审核状态返回不同的样式类 + if (status == '1' || status == 1) { + const stateClassMap = { + 0: 'reviewing', // 审核中 + 1: 'published', // 已发布 + 2: 'rejected' // 已驳回 + }; + // 确保 state 是数字类型 + const stateValue = parseInt(state) || 0; + return stateClassMap[stateValue] || 'reviewing'; + } + + // 其他状态 + const statusClassMap = { + 'new': 'new', + '0': 'draft', + 0: 'draft' + }; + return statusClassMap[status] || 'default'; + }; + // 返回作品列表 const goToWorksList = async () => { if (await checkSaveStatus()) { @@ -357,6 +410,7 @@ export default defineComponent({ chapters.value = chaptersResponse.result.records.map(chapter => ({ ...chapter, _status: chapter.status, + state: chapter.state || 0, // 确保 state 字段存在 details: chapter.details || '', title: chapter.title || '', num: chapter.num || 0, @@ -372,11 +426,15 @@ export default defineComponent({ await selectChapter(savedChapters[savedChapters.length - 1]); } } else { - throw new Error(chaptersResponse.message || '获取章节列表失败'); + // 如果没有章节数据,保持空状态 + chapters.value = []; } } catch (error) { console.error('加载作品信息失败:', error); ElMessage.error('加载作品信息失败'); + + // 如果加载失败,保持空状态 + chapters.value = []; } }; @@ -579,6 +637,7 @@ export default defineComponent({ saveChapter, publishChapter, getStatusText, + getStatusClass, goToWorksList, moveChapterUp, moveChapterDown, @@ -795,14 +854,37 @@ export default defineComponent({ border-color: #ffd591; } - // 草稿状态 (status = 0 或 "0") + &.draft { + background-color: #f6ffed; + color: #52c41a; + border-color: #b7eb8f; + } + + &.published { + background-color: #e6f7ff; + color: #1890ff; + border-color: #91d5ff; + } + + &.reviewing { + background-color: #fff7e6; + color: #fa8c16; + border-color: #ffd591; + } + + &.rejected { + background-color: #fff2f0; + color: #ff4d4f; + border-color: #ffccc7; + } + + // 兼容旧的状态样式 &[class*="0"] { background-color: #f6ffed; color: #52c41a; border-color: #b7eb8f; } - // 已发布状态 (status = 1 或 "1") &[class*="1"] { background-color: #e6f7ff; color: #1890ff; @@ -810,7 +892,7 @@ export default defineComponent({ } // 默认样式 - &:not(.new):not([class*="0"]):not([class*="1"]) { + &:not(.new):not(.draft):not(.published):not(.reviewing):not(.rejected):not([class*="0"]):not([class*="1"]) { background-color: #fafafa; color: #666; border-color: #d9d9d9; @@ -875,6 +957,31 @@ export default defineComponent({ font-size: 14px; } } + + .empty-editor { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + + :deep(.el-empty) { + .el-empty__description { + color: #666; + font-size: 16px; + margin-bottom: 20px; + } + + .el-button--primary { + background-color: #0A2463; + border-color: #0A2463; + + &:hover, &:focus { + background-color: #1a3473; + border-color: #1a3473; + } + } + } + } } } } diff --git a/src/views/author/components/ReadersManagement.vue b/src/views/author/components/ReadersManagement.vue index 46fab81..0cdd501 100644 --- a/src/views/author/components/ReadersManagement.vue +++ b/src/views/author/components/ReadersManagement.vue @@ -42,7 +42,7 @@ -
+
{ - return form.status === undefined || form.status === 2; // 未设置或驳回状态可编辑 + return form.status == undefined || form.status == 2; // 未设置或驳回状态可编辑 }); // 计算属性:是否可以提交 @@ -83,22 +83,18 @@ export default defineComponent({ // 获取状态类型 const getStatusType = (status) => { - switch (status) { - case 0: return 'warning'; // 待审核 - case 1: return 'success'; // 已通过 - case 2: return 'danger'; // 驳回 - default: return 'info'; - } + if (status == 0) return 'warning'; // 待审核 + if (status == 1) return 'success'; // 已通过 + if (status == 2) return 'danger'; // 驳回 + return 'info'; }; // 获取状态文本 const getStatusText = (status) => { - switch (status) { - case 0: return '待审核'; - case 1: return '已通过'; - case 2: return '已驳回'; - default: return '未设置'; - } + if (status == 0) return '待审核'; + if (status == 1) return '已通过'; + if (status == 2) return '已驳回'; + return '未设置'; }; // 获取提交按钮文本 @@ -106,13 +102,13 @@ export default defineComponent({ if (isSubmitting.value) { return '提交中...'; } - if (form.status === 0) { + if (form.status == 0) { return '审核中'; } - if (form.status === 1) { + if (form.status == 1) { return '已通过'; } - if (form.status === 2) { + if (form.status == 2) { return '重新提交'; } return '提交申请'; @@ -136,8 +132,12 @@ export default defineComponent({ try { const response = await achievementApi.getAchievement(); if (response.success && response.result) { - response.result.status = parseInt(response.result.status) - Object.assign(form, response.result); + // 确保状态是数字类型 + const result = { + ...response.result, + status: parseInt(response.result.status) || 0 + }; + Object.assign(form, result); } } catch (error) { console.error('获取成就设置失败:', error); @@ -155,7 +155,7 @@ export default defineComponent({ } // 如果状态是已通过,不允许重复提交 - if (form.status === 1) { + if (form.status == 1) { ElMessage.warning('成就设置已通过,无需重复提交'); return; } @@ -176,8 +176,10 @@ export default defineComponent({ const response = await achievementApi.setAchievementName(data); if (response.success) { ElMessage.success('提交成功,请等待审核'); - // 重新获取详情以更新状态 - await getAchievementDetail(); + // 等待一小段时间后重新获取详情以更新状态 + setTimeout(async () => { + await getAchievementDetail(); + }, 500); } else { ElMessage.error(response.message || '提交失败'); } diff --git a/src/views/book/chapter.vue b/src/views/book/chapter.vue index fa4e407..b11ae59 100644 --- a/src/views/book/chapter.vue +++ b/src/views/book/chapter.vue @@ -42,8 +42,27 @@ @selectstart.prevent @dragstart.prevent > - -
+ +
+

+ {{ paragraph }} +

+
+
+
+ + + +
+
+

这是付费章节

+

订阅后可阅读完整内容

+
+
+
+
+ +

{{ paragraph }}

@@ -87,7 +106,7 @@ 目录
-
+
@@ -107,6 +126,19 @@ :current-chapter-id="chapterId" @chapter-select="handleChapterSelect" /> + + +
@@ -114,10 +146,12 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { ElMessage } from 'element-plus'; -import { ArrowLeft, ArrowRight, ZoomIn, ZoomOut, MoonNight, ArrowUp, ArrowDown, Reading, Grid } from '@element-plus/icons-vue'; +import { ArrowLeft, ArrowRight, ZoomIn, ZoomOut, MoonNight, ArrowUp, ArrowDown, Reading, Grid, Lock } from '@element-plus/icons-vue'; import { homeApi } from '@/api/home.js'; import { bookshelfApi } from '@/api/bookshelf.js'; import CatalogDialog from '@/components/book/CatalogDialog.vue'; +import SubscriptionPopup from '@/components/book/SubscriptionPopup.vue'; +import { toggleTheme, getThemeMode, setThemeMode } from '@/utils/theme.js'; export default { name: 'ChapterDetail', @@ -131,7 +165,9 @@ export default { ArrowDown, Reading, Grid, - CatalogDialog + Lock, + CatalogDialog, + SubscriptionPopup }, setup() { const route = useRoute(); @@ -147,8 +183,10 @@ export default { const bookDetail = ref(null); const isInBookshelf = ref(false); const fontSize = ref(18); - const isDarkMode = ref(false); + const isDarkMode = ref(getThemeMode()); const catalogDialogVisible = ref(false); + const subscriptionVisible = ref(false); + const isPay = ref(false); // 计算上一章和下一章 const currentChapterIndex = computed(() => { @@ -166,6 +204,11 @@ export default { return index >= 0 && index < chapterList.value.length - 1 ? chapterList.value[index + 1] : null; }); + // 当前章节信息 + const currentChapterInfo = computed(() => { + return chapterList.value.find(ch => ch.id === chapterId.value) || {}; + }); + // 获取书籍详情 const getBookDetail = async () => { try { @@ -188,6 +231,22 @@ export default { throw new Error('章节ID不存在'); } + // 只有登录后才检查是否已购买 + const token = localStorage.getItem('token'); + if (token) { + try { + const payResponse = await homeApi.getMyShopNovel({ + bookId: bookId.value, + novelId: chapterId.value, + }); + // payResponse为true表示已购买,为false表示未购买 + isPay.value = !payResponse.result; + } catch (err) { + console.error('检查付费状态失败:', err); + isPay.value = true; // 默认需要付费 + } + } + // 获取章节详情 const response = await homeApi.getBookCatalogDetail(chapterId.value); if (response && response.success) { @@ -198,6 +257,11 @@ export default { // 按换行符分割成段落数组 chapter.value.paragraphs = chapter.value.details.split('\n') } + + // 如果章节标记为付费章节,设置付费状态 + if (chapter.value.isPay != 'Y') { + isPay.value = false; + } } else { throw new Error(response?.message || '获取章节详情失败'); } @@ -205,6 +269,9 @@ export default { // 更新阅读进度到书架 await updateReadProgress(); + // 更新订阅弹窗状态 + updateSubscriptionStatus(); + } catch (err) { console.error('获取章节详情失败:', err); throw err; @@ -346,12 +413,95 @@ export default { }; // 切换暗黑模式 - const toggleTheme = () => { + const toggleThemeMode = () => { isDarkMode.value = !isDarkMode.value; - if (isDarkMode.value) { - document.documentElement.classList.add('dark-theme'); + setThemeMode(isDarkMode.value); + }; + + // 更新订阅弹窗状态 + const updateSubscriptionStatus = () => { + if (isPay.value) { + subscriptionVisible.value = true; } else { - document.documentElement.classList.remove('dark-theme'); + subscriptionVisible.value = false; + } + }; + + // 处理订阅 + const handleSubscribe = async () => { + try { + await homeApi.buyNovel({ + bookId: bookId.value, + novelId: chapterId.value, + }); + + isPay.value = false; + updateSubscriptionStatus(); + ElMessage.success('订阅成功'); + + // 重新获取章节详情 + await getBookCatalogDetail(); + } catch (err) { + console.error('订阅失败:', err); + ElMessage.error('订阅失败,请重试'); + } + }; + + // 处理批量订阅 + const handleBatchSubscribe = async (batchCount) => { + try { + // 获取从当前章节开始的连续章节ID + const chapterIds = []; + const startIndex = currentChapterIndex.value; + + for (let i = 0; i < batchCount && (startIndex + i) < chapterList.value.length; i++) { + const chapter = chapterList.value[startIndex + i]; + // 使用pay字段判断是否已购买,isPay为'Y'且pay为false表示需要购买 + if (chapter.isPay === 'Y' && !chapter.pay) { + chapterIds.push(chapter.id); + } + } + + if (chapterIds.length === 0) { + ElMessage.warning('没有需要购买的章节'); + return; + } + + // 调用批量订阅接口 + await homeApi.buyNovel({ + bookId: bookId.value, + novelId: chapterIds.join(',') + }); + + ElMessage.success(`成功订阅${chapterIds.length}章`); + + // 刷新章节列表状态 + await getBookCatalogList(); + isPay.value = false; + updateSubscriptionStatus(); + + } catch (error) { + console.error('批量订阅失败:', error); + ElMessage.error('订阅失败,请重试'); + } + }; + + // 处理视频解锁 + const handleVideoUnlock = async () => { + try { + ElMessage.info('暂未开放'); + // 这里可以添加视频解锁的逻辑 + // await homeApi.openBookCatalog({ + // bookId: bookId.value, + // catalogId: chapterId.value + // }); + + // isPay.value = false; + // updateSubscriptionStatus(); + + } catch (error) { + console.error('视频解锁失败:', error); + ElMessage.error('解锁失败,请重试'); } }; @@ -380,6 +530,10 @@ export default { nextChapter, isInBookshelf, isDarkMode, + subscriptionVisible, + isPay, + currentChapterInfo, + currentChapterIndex, fetchChapterDetail, goToPrevChapter, goToNextChapter, @@ -387,9 +541,12 @@ export default { goToChapterList, toggleBookshelf, toggleFontSize, - toggleTheme, + toggleThemeMode, catalogDialogVisible, handleChapterSelect, + handleSubscribe, + handleBatchSubscribe, + handleVideoUnlock, chapterList, chapterId, }; @@ -432,6 +589,20 @@ export default { .paragraph { color: var(--reading-text-color) !important; } + + .pay-content-mask { + background: linear-gradient(180deg, rgba(26, 26, 26, 0) 0%, rgba(26, 26, 26, 0.9) 50%, rgba(26, 26, 26, 1) 100%) !important; + + .mask-text { + .mask-title { + color: #ff9800 !important; + } + + .mask-desc { + color: #999 !important; + } + } + } } .el-button { @@ -512,23 +683,59 @@ export default { .paragraph-content { width: 100%; - .paragraph { - text-indent: 2em; - margin-bottom: 30px; - line-height: 1.8; - font-size: var(--reading-font-size); - color: #333; - word-wrap: break-word; - word-break: normal; - white-space: normal; - transition: color 0.3s ease; - /* 禁止选中文字 */ - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; + .paragraph { + text-indent: 2em; + margin-bottom: 30px; + line-height: 1.8; + font-size: var(--reading-font-size); + color: #333; + word-wrap: break-word; + word-break: normal; + white-space: normal; + transition: color 0.3s ease; + /* 禁止选中文字 */ + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + } + + .pay-content-mask { + position: relative; + margin-top: 40px; + padding: 60px 20px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 50%, rgba(255, 255, 255, 1) 100%); + border-radius: 8px; + text-align: center; + + .mask-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + + .mask-icon { + margin-bottom: 10px; + } + + .mask-text { + .mask-title { + font-size: 18px; + font-weight: bold; + color: #ff9800; + margin: 0 0 8px 0; + } + + .mask-desc { + font-size: 14px; + color: #666; + margin: 0; + } + } } } + } .paragraph { margin-bottom: 20px; diff --git a/src/views/home/Home.vue b/src/views/home/Home.vue index 0a68efa..ad4d146 100644 --- a/src/views/home/Home.vue +++ b/src/views/home/Home.vue @@ -15,10 +15,10 @@
- 推荐票 + 推荐票 推荐票 +{{ day.num }}
@@ -60,7 +60,7 @@

{{ task.title }}

推荐票 @@ -143,7 +143,7 @@ export default { try { const res = await taskApi.getSignTaskToday(); // res.result == 0 表示可以签到,否则表示已签到 - canSignToday.value = res.result === 0; + canSignToday.value = res.result; } catch (error) { console.error('获取今日签到状态失败:', error); // 默认设置为不可签到(已签到状态) @@ -397,24 +397,25 @@ export default { } } - .sign-button-container { - text-align: center; - margin-top: 16px; - - .sign-button { - width: 280px; - height: 44px; - border-radius: 22px; - font-weight: 600; - background-color: #0A2463; - border-color: #0A2463; - - &.checked-btn { - background: linear-gradient(90deg, #e0e0e0, #bdbdbd) !important; - border-color: #bdbdbd !important; - color: #666 !important; - cursor: not-allowed; - } + } + + .sign-button-container { + margin-top: 16px; + display: flex; + justify-content: center; + .sign-button { + width: 280px; + height: 44px; + border-radius: 22px; + font-weight: 600; + background-color: #0A2463; + border-color: #0A2463; + + &.checked-btn { + background: linear-gradient(90deg, #e0e0e0, #bdbdbd) !important; + border-color: #bdbdbd !important; + color: #666 !important; + cursor: not-allowed; } } }