更新:增加“图书首页”功能
This commit is contained in:
2
App.vue
2
App.vue
@@ -13,7 +13,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
// @import "@/style/tailwind.css";
|
@import "@/style/tailwind.css";
|
||||||
@import "@/style/ui.scss";
|
@import "@/style/ui.scss";
|
||||||
/* 覆盖 Tailwind 的默认 block 样式 */
|
/* 覆盖 Tailwind 的默认 block 样式 */
|
||||||
img, svg, video, canvas, audio, iframe, embed, object {
|
img, svg, video, canvas, audio, iframe, embed, object {
|
||||||
|
|||||||
105
api/modules/home.ts
Normal file
105
api/modules/home.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// api/modules/home.ts
|
||||||
|
import { createRequestClient } from '../request'
|
||||||
|
import { SERVICE_MAP } from '../config'
|
||||||
|
import type {
|
||||||
|
IMyBooksResponse,
|
||||||
|
IRecommendBooksResponse,
|
||||||
|
ILabelListResponse,
|
||||||
|
IBookListResponse,
|
||||||
|
IVipInfoResponse,
|
||||||
|
ISearchResponse
|
||||||
|
} from '@/types/book'
|
||||||
|
|
||||||
|
const client = createRequestClient({ baseURL: SERVICE_MAP.MAIN })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页相关API
|
||||||
|
*/
|
||||||
|
export const homeApi = {
|
||||||
|
/**
|
||||||
|
* 获取VIP信息
|
||||||
|
*/
|
||||||
|
getVipInfo() {
|
||||||
|
return client.request<IVipInfoResponse>({
|
||||||
|
url: 'bookAbroad/home/getVipInfo',
|
||||||
|
method: 'POST',
|
||||||
|
data: {}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的书单
|
||||||
|
* @param current 当前页码
|
||||||
|
* @param limit 每页数量
|
||||||
|
*/
|
||||||
|
getMyBooks(current: number, limit: number) {
|
||||||
|
return client.request<IMyBooksResponse>({
|
||||||
|
url: 'bookAbroad/home/getMyBooks',
|
||||||
|
method: 'POST',
|
||||||
|
data: { current, limit }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推荐图书
|
||||||
|
*/
|
||||||
|
getRecommendBooks() {
|
||||||
|
return client.request<IRecommendBooksResponse>({
|
||||||
|
url: 'bookAbroad/home/getRecommendBooks',
|
||||||
|
method: 'POST',
|
||||||
|
data: {}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取标签列表
|
||||||
|
* @param type 0: 分类标签, 1: 活动标签
|
||||||
|
*/
|
||||||
|
getBookLabelList(type: number) {
|
||||||
|
return client.request<ILabelListResponse>({
|
||||||
|
url: 'bookAbroad/home/getBookAbroadLableList',
|
||||||
|
method: 'POST',
|
||||||
|
data: { type }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据父级ID获取子标签列表
|
||||||
|
* @param pid 父级标签ID
|
||||||
|
*/
|
||||||
|
getSubLabelList(pid: number) {
|
||||||
|
return client.request<ILabelListResponse>({
|
||||||
|
url: 'bookAbroad/home/getBookAbroadLableListByPid',
|
||||||
|
method: 'POST',
|
||||||
|
data: { pid }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据标签ID获取图书列表
|
||||||
|
* @param lableId 标签ID(注意:原接口参数名为 lableId)
|
||||||
|
*/
|
||||||
|
getBooksByLabel(lableId: number) {
|
||||||
|
return client.request<IBookListResponse>({
|
||||||
|
url: 'bookAbroad/home/getAbroadBookListByLable',
|
||||||
|
method: 'POST',
|
||||||
|
data: { lableId }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索图书
|
||||||
|
* @param keyword 搜索关键字
|
||||||
|
*/
|
||||||
|
searchBooks(data: {
|
||||||
|
key: string,
|
||||||
|
page: number,
|
||||||
|
limit: number,
|
||||||
|
}) {
|
||||||
|
return client.request<ISearchResponse>({
|
||||||
|
url: 'book/shopproduct/selectList',
|
||||||
|
method: 'POST',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
178
components/book/BookCardIndex.vue
Normal file
178
components/book/BookCardIndex.vue
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
:class="['book-card', `book-card-${layout}`]"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
:src="book.images"
|
||||||
|
:class="['book-cover', `book-cover-${layout}`]"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view :class="['book-info', `book-info-${layout}`]">
|
||||||
|
<text :class="['book-title', `book-title-${layout}`]">{{ book.name }}</text>
|
||||||
|
<view v-if="layout === 'vertical'" class="book-footer">
|
||||||
|
<text v-if="displayPrice" class="book-price">{{ displayPrice }}</text>
|
||||||
|
<text v-if="displayStats" class="book-stats">{{ displayStats }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import type { IBookWithStats, IVipInfo } from '@/types/home'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
book: IBookWithStats
|
||||||
|
layout?: 'horizontal' | 'vertical'
|
||||||
|
showPrice?: boolean
|
||||||
|
showStats?: boolean
|
||||||
|
vipInfo?: IVipInfo | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
layout: 'vertical',
|
||||||
|
showPrice: true,
|
||||||
|
showStats: true,
|
||||||
|
vipInfo: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
click: [bookId: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算显示的价格
|
||||||
|
*/
|
||||||
|
const displayPrice = computed(() => {
|
||||||
|
if (!props.showPrice) return ''
|
||||||
|
|
||||||
|
// 已购买不显示价格
|
||||||
|
if (props.book.isBuy) return ''
|
||||||
|
|
||||||
|
// VIP用户且图书为VIP专享
|
||||||
|
if (props.vipInfo?.id && props.book.isVip === '2') {
|
||||||
|
const price = props.book.sysDictData?.dictValue
|
||||||
|
return price ? `$ ${price} NZD` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通用户
|
||||||
|
if (!props.vipInfo?.id) {
|
||||||
|
const price = props.book.sysDictData?.dictValue
|
||||||
|
return price ? `$ ${price} NZD` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算显示的统计信息
|
||||||
|
*/
|
||||||
|
const displayStats = computed(() => {
|
||||||
|
if (!props.showStats) return ''
|
||||||
|
|
||||||
|
if (props.book.readCount && props.book.readCount > 0) {
|
||||||
|
return `${props.book.readCount}${t('home.readingCount')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.book.buyCount && props.book.buyCount > 0) {
|
||||||
|
return `${props.book.buyCount}${t('home.purchased')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理点击事件
|
||||||
|
*/
|
||||||
|
const handleClick = () => {
|
||||||
|
emit('click', props.book.bookId || props.book.id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.book-card {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 横向布局
|
||||||
|
.book-card-horizontal {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 25rpx;
|
||||||
|
|
||||||
|
.book-cover-horizontal {
|
||||||
|
width: 110rpx;
|
||||||
|
height: 135rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-info-horizontal {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-title-horizontal {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 32rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 纵向布局
|
||||||
|
.book-card-vertical {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.book-cover-vertical {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-info-vertical {
|
||||||
|
width: 160rpx;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-title-vertical {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 40rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 5rpx;
|
||||||
|
|
||||||
|
.book-price {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #ff4703;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-stats {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
<view class="navbar">
|
<view class="navbar">
|
||||||
<view class="statusBar" :style="{height:getStatusBarHeight()+'px'}"></view>
|
<view class="statusBar" :style="{height:getStatusBarHeight()+'px'}"></view>
|
||||||
<view class="titleBar" :style="{height:getTitleBarHeight()+'px'}">
|
<view class="titleBar" :style="{height:getTitleBarHeight()+'px'}">
|
||||||
<wd-navbar :title="title" :left-arrow="leftArrow" @click="handleClickLeft"></wd-navbar>
|
<wd-navbar :title="title" :left-arrow="leftArrow" @click="handleClickLeft">
|
||||||
|
<template #title>
|
||||||
|
<slot name="title"></slot>
|
||||||
|
</template>
|
||||||
|
</wd-navbar>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view :style="{height:getNavBarHeight() +'px'}"></view>
|
<view :style="{height:getNavBarHeight() +'px'}"></view>
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
"close": "Close",
|
"close": "Close",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"tips": "Tips"
|
"tips": "Tips",
|
||||||
|
"searchNoResult": "No search results found",
|
||||||
|
"more": "More",
|
||||||
|
"dataNull": "No data available"
|
||||||
},
|
},
|
||||||
"tabar.course": "COURSE",
|
"tabar.course": "COURSE",
|
||||||
"tabar.book": "EBOOK",
|
"tabar.book": "EBOOK",
|
||||||
@@ -19,7 +22,7 @@
|
|||||||
"title": "Taimed International",
|
"title": "Taimed International",
|
||||||
"schema": "Schema",
|
"schema": "Schema",
|
||||||
"demo": "uni-app globalization",
|
"demo": "uni-app globalization",
|
||||||
"demo-description": "Include uni-framework, manifest.json, pages.json, tabbar, Page, Component, API, Schema",
|
"demoDescription": "Include uni-framework, manifest.json, pages.json, tabbar, Page, Component, API, Schema",
|
||||||
"detail": "Detail",
|
"detail": "Detail",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"language-info": "Settings",
|
"language-info": "Settings",
|
||||||
@@ -250,9 +253,14 @@
|
|||||||
"deleteSuccess": "Deleted"
|
"deleteSuccess": "Deleted"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"block1": "My Books",
|
||||||
|
"block2": "Recommended",
|
||||||
|
"more": "More",
|
||||||
|
"activityTitle": "Featured Books",
|
||||||
"readingCount": " reads",
|
"readingCount": " reads",
|
||||||
"listenCount": " listens",
|
"listenCount": " listens",
|
||||||
"purchased": " purchased"
|
"purchased": " purchased",
|
||||||
|
"searchPlaceholder": "Search books..."
|
||||||
},
|
},
|
||||||
"listen": {
|
"listen": {
|
||||||
"title": "Audio Book",
|
"title": "Audio Book",
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"confirm": "确认",
|
"confirm": "确认",
|
||||||
"loading": "加载中",
|
"loading": "加载中",
|
||||||
"tips": "提示"
|
"tips": "提示",
|
||||||
|
"searchNoResult": "暂无搜索结果",
|
||||||
|
"more": "更多",
|
||||||
|
"dataNull": "暂无数据"
|
||||||
},
|
},
|
||||||
"tabar.course": "课程",
|
"tabar.course": "课程",
|
||||||
"tabar.book": "图书",
|
"tabar.book": "图书",
|
||||||
@@ -19,7 +22,7 @@
|
|||||||
"title": "太湖国际",
|
"title": "太湖国际",
|
||||||
"schema": "Schema",
|
"schema": "Schema",
|
||||||
"demo": "uni-app 国际化演示",
|
"demo": "uni-app 国际化演示",
|
||||||
"demo-description": "包含 uni-framework、manifest.json、pages.json、tabbar、页面、组件、API、Schema",
|
"demoDescription": "包含 uni-framework、manifest.json、pages.json、tabbar、页面、组件、API、Schema",
|
||||||
"detail": "详情",
|
"detail": "详情",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"language-info": "语言信息",
|
"language-info": "语言信息",
|
||||||
@@ -251,9 +254,14 @@
|
|||||||
"deleteSuccess": "删除成功"
|
"deleteSuccess": "删除成功"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"block1": "我的书单",
|
||||||
|
"block2": "推荐图书",
|
||||||
|
"more": "更多",
|
||||||
|
"activityTitle": "活动图书",
|
||||||
"readingCount": "次阅读",
|
"readingCount": "次阅读",
|
||||||
"listenCount": "次听书",
|
"listenCount": "次听书",
|
||||||
"purchased": "人购买"
|
"purchased": "人购买",
|
||||||
|
"searchPlaceholder": "搜索图书..."
|
||||||
},
|
},
|
||||||
"listen": {
|
"listen": {
|
||||||
"title": "听书",
|
"title": "听书",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"appid" : "__UNI__1250B39",
|
"appid" : "__UNI__1250B39",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "1.0.1",
|
"versionName" : "1.0.1",
|
||||||
"versionCode" : "100",
|
"versionCode" : 101,
|
||||||
"transformPx" : false,
|
"transformPx" : false,
|
||||||
/* 5+App特有相关 */
|
/* 5+App特有相关 */
|
||||||
"app-plus" : {
|
"app-plus" : {
|
||||||
@@ -38,10 +38,14 @@
|
|||||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||||
]
|
],
|
||||||
|
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
|
||||||
|
"minSdkVersion" : 21
|
||||||
},
|
},
|
||||||
/* ios打包配置 */
|
/* ios打包配置 */
|
||||||
"ios" : {},
|
"ios" : {
|
||||||
|
"dSYMs" : false
|
||||||
|
},
|
||||||
/* SDK配置 */
|
/* SDK配置 */
|
||||||
"sdkConfigs" : {}
|
"sdkConfigs" : {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
"中医学 国学 心理学"
|
"中医学 国学 心理学"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tailwind-dev": "npx @tailwindcss/cli -i ./tailwind-input.css -o ./static/tailwind.css --watch",
|
"tailwind-dev": "npx @tailwindcss/cli -i ./tailwind-input.css -o ./style/tailwind.css --watch",
|
||||||
"tailwind-build": "npx @tailwindcss/cli -i ./tailwind-input.css -o ./static/tailwind.css"
|
"tailwind-build": "npx @tailwindcss/cli -i ./tailwind-input.css -o ./style/tailwind.css"
|
||||||
},
|
},
|
||||||
"dcloudext": {
|
"dcloudext": {
|
||||||
"sale": {
|
"sale": {
|
||||||
|
|||||||
@@ -103,6 +103,12 @@
|
|||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"navigationBarTitleText": "%book.read%"
|
"navigationBarTitleText": "%book.read%"
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
"path": "pages/book/search",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "%home.searchPlaceholder%"
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
"path": "pages/book/listen/index",
|
"path": "pages/book/listen/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
# 我的书单功能模块
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本模块是从nuttyreading项目迁移并升级到Vue3+TypeScript+Pinia+TailwindCSS+WotUI+i18n技术栈的"我的书单"功能。
|
|
||||||
|
|
||||||
## 功能列表
|
|
||||||
|
|
||||||
### 1. 书单列表 (`pages/book/index.vue`)
|
|
||||||
- ✅ 显示用户已购买的所有书籍
|
|
||||||
- ✅ 分页加载
|
|
||||||
- ✅ 空状态处理
|
|
||||||
- ✅ 支持跳转到详情、阅读器、听书、书评页面
|
|
||||||
- ✅ iOS平台自动隐藏书评按钮
|
|
||||||
|
|
||||||
### 2. 书籍详情 (`pages/book/detail.vue`)
|
|
||||||
- ✅ 显示书籍封面、标题、作者、简介
|
|
||||||
- ✅ 显示阅读数、听书数、购买数统计
|
|
||||||
- ✅ 显示前2条书评(非iOS)
|
|
||||||
- ✅ 显示相关推荐书籍
|
|
||||||
- ✅ 根据购买状态显示不同操作按钮
|
|
||||||
- ✅ 购买弹窗
|
|
||||||
|
|
||||||
### 3. 书评系统 (`pages/book/review.vue`)
|
|
||||||
- ✅ 评论列表展示
|
|
||||||
- ✅ 发表评论(富文本编辑器)
|
|
||||||
- ✅ 点赞/取消点赞
|
|
||||||
- ✅ 回复评论
|
|
||||||
- ✅ 删除评论
|
|
||||||
- ✅ 分页加载更多
|
|
||||||
- ✅ Emoji支持(待完善)
|
|
||||||
|
|
||||||
### 4. 阅读器 (`pages/book/reader.vue`)
|
|
||||||
- ✅ 上下滚动模式
|
|
||||||
- ✅ 左右翻页模式
|
|
||||||
- ✅ 字体大小调节(8个级别)
|
|
||||||
- ✅ 主题切换(5种主题)
|
|
||||||
- ✅ 章节目录
|
|
||||||
- ✅ 阅读进度保存和恢复
|
|
||||||
- ✅ 图片内容显示
|
|
||||||
- ✅ 试读限制提示
|
|
||||||
|
|
||||||
### 5. 听书功能
|
|
||||||
#### 章节列表 (`pages/book/listen/index.vue`)
|
|
||||||
- ✅ 显示书籍信息
|
|
||||||
- ✅ 章节列表
|
|
||||||
- ✅ 章节锁定状态
|
|
||||||
- ✅ 音频文件检查
|
|
||||||
|
|
||||||
#### 音频播放器 (`pages/book/listen/player.vue`)
|
|
||||||
- ✅ 音频播放/暂停
|
|
||||||
- ✅ 进度条控制
|
|
||||||
- ✅ 快进/快退(15秒)
|
|
||||||
- ✅ 上一章/下一章
|
|
||||||
- ✅ 播放速度调节(0.5x - 2x)
|
|
||||||
- ✅ 自动播放下一章
|
|
||||||
- ✅ 封面旋转动画
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
- **框架**: Vue3 Composition API
|
|
||||||
- **语言**: TypeScript
|
|
||||||
- **状态管理**: Pinia
|
|
||||||
- **UI组件**: WotUI
|
|
||||||
- **样式**: SCSS + TailwindCSS
|
|
||||||
- **国际化**: vue-i18n
|
|
||||||
|
|
||||||
## 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
pages/book/
|
|
||||||
├── index.vue # 书单列表
|
|
||||||
├── detail.vue # 书籍详情
|
|
||||||
├── review.vue # 书评页面
|
|
||||||
├── reader.vue # 阅读器
|
|
||||||
└── listen/
|
|
||||||
├── index.vue # 听书章节列表
|
|
||||||
└── player.vue # 音频播放器
|
|
||||||
|
|
||||||
components/book/
|
|
||||||
├── CustomNavbar.vue # 自定义导航栏
|
|
||||||
├── BookCard.vue # 书籍卡片
|
|
||||||
└── CommentList.vue # 评论列表
|
|
||||||
|
|
||||||
api/modules/
|
|
||||||
└── book.ts # 书籍API
|
|
||||||
|
|
||||||
stores/
|
|
||||||
└── book.ts # 书籍状态管理
|
|
||||||
|
|
||||||
types/
|
|
||||||
└── book.d.ts # 类型定义
|
|
||||||
```
|
|
||||||
|
|
||||||
## API接口
|
|
||||||
|
|
||||||
所有API接口保持与原项目完全一致:
|
|
||||||
|
|
||||||
- `bookAbroad/home/getbooks` - 获取我的书单
|
|
||||||
- `bookAbroad/home/getBookInfo` - 获取书籍详情
|
|
||||||
- `bookAbroad/home/getBookReadCount` - 获取统计数据
|
|
||||||
- `bookAbroad/home/getRecommendBook` - 获取推荐书籍
|
|
||||||
- `bookAbroad/getBookAbroadCommentTree` - 获取评论列表
|
|
||||||
- `bookAbroad/insertBookAbroadComment` - 发表评论
|
|
||||||
- `bookAbroad/insertBookAbroadCommentLike` - 点赞
|
|
||||||
- `bookAbroad/delBookAbroadCommentLike` - 取消点赞
|
|
||||||
- `bookAbroad/delBookAbroadComment` - 删除评论
|
|
||||||
- `bookAbroad/home/getBookChapter` - 获取章节列表
|
|
||||||
- `bookAbroad/home/getBookChapterContent` - 获取章节内容
|
|
||||||
- `bookAbroad/home/getBookReadRate` - 获取阅读进度
|
|
||||||
- `bookAbroad/home/insertBookReadRate` - 保存阅读进度
|
|
||||||
|
|
||||||
## 国际化
|
|
||||||
|
|
||||||
支持中文和英文两种语言,所有文本通过i18n配置管理。
|
|
||||||
|
|
||||||
### 翻译键
|
|
||||||
- `book.*` - 书单相关
|
|
||||||
- `details.*` - 详情相关
|
|
||||||
- `listen.*` - 听书相关
|
|
||||||
- `common.*` - 通用文本
|
|
||||||
|
|
||||||
## 平台适配
|
|
||||||
|
|
||||||
### iOS特殊处理
|
|
||||||
- 书评功能在iOS平台自动隐藏
|
|
||||||
- 使用条件编译 `#ifdef APP-PLUS` 判断平台
|
|
||||||
|
|
||||||
### 刘海屏适配
|
|
||||||
- 所有页面自动适配状态栏高度
|
|
||||||
- 使用 `uni.getSystemInfoSync().safeArea` 获取安全区域
|
|
||||||
|
|
||||||
## 使用说明
|
|
||||||
|
|
||||||
### 1. 从书单列表进入
|
|
||||||
```typescript
|
|
||||||
uni.navigateTo({
|
|
||||||
url: '/pages/book/index'
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 直接进入书籍详情
|
|
||||||
```typescript
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/book/detail?id=${bookId}`
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 进入阅读器
|
|
||||||
```typescript
|
|
||||||
// 已购买
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/book/reader?isBuy=0&bookId=${bookId}`
|
|
||||||
})
|
|
||||||
|
|
||||||
// 试读
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/book/reader?isBuy=1&bookId=${bookId}&count=${freeChapterCount}`
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 进入听书
|
|
||||||
```typescript
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/book/listen/index?bookId=${bookId}`
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **不要修改API接口**:所有接口地址和参数必须与原项目保持一致
|
|
||||||
2. **UI组件使用WotUI**:不要使用uView或uni-ui组件
|
|
||||||
3. **国际化文本**:所有文本必须通过i18n配置,不能硬编码
|
|
||||||
4. **iOS平台**:注意书评功能的隐藏处理
|
|
||||||
5. **类型安全**:充分利用TypeScript类型检查
|
|
||||||
|
|
||||||
## 待优化项
|
|
||||||
|
|
||||||
1. Emoji选择器组件需要集成完整的Emoji库
|
|
||||||
2. 阅读器可以添加更多主题
|
|
||||||
3. 音频播放器可以添加播放列表功能
|
|
||||||
4. 可以添加书签功能
|
|
||||||
5. 可以添加笔记功能
|
|
||||||
|
|
||||||
## 测试清单
|
|
||||||
|
|
||||||
- [ ] 书单列表加载和分页
|
|
||||||
- [ ] 书籍详情所有信息显示
|
|
||||||
- [ ] 书评发表、点赞、删除
|
|
||||||
- [ ] 阅读器两种模式切换
|
|
||||||
- [ ] 阅读器字体和主题设置
|
|
||||||
- [ ] 阅读进度保存和恢复
|
|
||||||
- [ ] 听书播放控制
|
|
||||||
- [ ] 听书速度调节
|
|
||||||
- [ ] iOS平台书评隐藏
|
|
||||||
- [ ] 试读/试听限制
|
|
||||||
- [ ] 国际化文本切换
|
|
||||||
|
|
||||||
## 更新日志
|
|
||||||
|
|
||||||
### v1.0.0 (2024-01-XX)
|
|
||||||
- ✅ 完成从Vue2到Vue3的迁移
|
|
||||||
- ✅ 完成TypeScript类型定义
|
|
||||||
- ✅ 完成Pinia状态管理
|
|
||||||
- ✅ 完成WotUI组件替换
|
|
||||||
- ✅ 完成国际化配置
|
|
||||||
- ✅ 完成所有功能页面
|
|
||||||
@@ -1,24 +1,844 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="home-page">
|
||||||
<view class="title bg-[red] text-center text-[#fff]">这是一个等待开发的图书首页</view>
|
<!-- 顶部背景区域 -->
|
||||||
<view class="description bg-[red]">内容是在线图书</view>
|
<view class="home-bg" :style="{ paddingTop: notchHeight + 'px' }">
|
||||||
|
<wd-search
|
||||||
|
hide-cancel
|
||||||
|
light
|
||||||
|
clearable
|
||||||
|
class="search-bar"
|
||||||
|
:placeholder="$t('home.searchPlaceholder')"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
<view class="icon-hua">
|
||||||
|
<image
|
||||||
|
src="../../static/home_icon.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="icon-hua-img"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<view class="content-wrapper">
|
||||||
|
<!-- 我的书单 & 推荐图书模块 -->
|
||||||
|
<view class="mine-block">
|
||||||
|
<!-- 我的书单 -->
|
||||||
|
<view class="mine-1">
|
||||||
|
<text class="mine-title">{{ $t('home.block1') }}</text>
|
||||||
|
<view
|
||||||
|
v-if="myBooksList.length > 0"
|
||||||
|
class="mine-more"
|
||||||
|
@click="handleMoreClick"
|
||||||
|
>
|
||||||
|
{{ $t('home.more') }}
|
||||||
|
<image src="@/static/icon/icon_right.png" />
|
||||||
|
</view>
|
||||||
|
<view v-if="myBooksList.length > 0" class="mine-1-list">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in myBooksList"
|
||||||
|
:key="index"
|
||||||
|
class="mine-item"
|
||||||
|
@click="handleMyBookClick(item.id)"
|
||||||
|
>
|
||||||
|
<image :src="item.images" />
|
||||||
|
<text>{{ item.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text v-else class="zanwu">{{ $t('common.data_null') }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 推荐图书 -->
|
||||||
|
<view class="mine-2">
|
||||||
|
<text class="mine-title">{{ $t('home.block2') }}</text>
|
||||||
|
<swiper
|
||||||
|
v-if="recommendBooksList.length > 0"
|
||||||
|
autoplay
|
||||||
|
:interval="3000"
|
||||||
|
:duration="500"
|
||||||
|
class="recommend-list"
|
||||||
|
>
|
||||||
|
<swiper-item
|
||||||
|
v-for="(item, index) in recommendBooksList"
|
||||||
|
:key="index"
|
||||||
|
class="recommend-item"
|
||||||
|
@click="handleBookClick(item.id)"
|
||||||
|
>
|
||||||
|
<image :src="item.images" width="100%" height="100%" />
|
||||||
|
<text>{{ item.name }}</text>
|
||||||
|
</swiper-item>
|
||||||
|
</swiper>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 活动图书模块 -->
|
||||||
|
<view v-if="showActivity" class="activity-block">
|
||||||
|
<text class="activity-title">{{ $t('home.activityTitle') }}</text>
|
||||||
|
<scroll-view class="scroll-view" scroll-x :show-scrollbar="false">
|
||||||
|
<view class="activity-label-list">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in activityLabelList"
|
||||||
|
:key="index"
|
||||||
|
:class="[
|
||||||
|
'activity-label-item',
|
||||||
|
currentActivityIndex === index ? 'active-label' : ''
|
||||||
|
]"
|
||||||
|
@click="handleActivityLabelClick(item.id, index)"
|
||||||
|
>
|
||||||
|
<text>{{ item.title }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<scroll-view
|
||||||
|
v-if="activityList.length > 0"
|
||||||
|
class="scroll-view"
|
||||||
|
scroll-x
|
||||||
|
:show-scrollbar="false"
|
||||||
|
>
|
||||||
|
<view class="activity-list">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in activityList"
|
||||||
|
:key="index"
|
||||||
|
class="activity-item"
|
||||||
|
@click="handleBookClick(item.bookId)"
|
||||||
|
>
|
||||||
|
<image :src="item.images" />
|
||||||
|
<text class="activity-text">{{ item.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<text v-else class="zanwu" style="padding: 80rpx 0">{{ $t('global.dataNull') }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 分类图书模块 -->
|
||||||
|
<view v-if="showCategory" class="book-block">
|
||||||
|
<!-- 一级分类标签 -->
|
||||||
|
<scroll-view class="scroll-view" scroll-x :show-scrollbar="false">
|
||||||
|
<view class="book-tab-one">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in categoryLevel1List"
|
||||||
|
:key="index"
|
||||||
|
:class="[
|
||||||
|
'tab-one-item',
|
||||||
|
currentLevel1Index === index ? 'tab-one-active' : ''
|
||||||
|
]"
|
||||||
|
@click="handleCategoryLevel1Click(item.id, index)"
|
||||||
|
>
|
||||||
|
<text>{{ item.title }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 二级分类标签 -->
|
||||||
|
<scroll-view
|
||||||
|
v-if="categoryLevel2List.length > 0"
|
||||||
|
class="scroll-view"
|
||||||
|
scroll-x
|
||||||
|
:show-scrollbar="false"
|
||||||
|
style="background: #fff; margin-top: 15rpx"
|
||||||
|
>
|
||||||
|
<view class="book-tab-two">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in categoryLevel2List"
|
||||||
|
:key="index"
|
||||||
|
:class="[
|
||||||
|
'tab-two-item',
|
||||||
|
currentLevel2Index === index ? 'tab-two-active' : ''
|
||||||
|
]"
|
||||||
|
@click="handleCategoryLevel2Click(item.id, index)"
|
||||||
|
>
|
||||||
|
<text>{{ item.title }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 分类图书列表 -->
|
||||||
|
<view v-if="categoryBookList.length > 0" class="book-list">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in categoryBookList"
|
||||||
|
:key="index"
|
||||||
|
class="book-item"
|
||||||
|
@click="handleBookClick(item.bookId)"
|
||||||
|
>
|
||||||
|
<image :src="item.images" />
|
||||||
|
<text class="book-text">{{ item.name }}</text>
|
||||||
|
<text v-if="formatPrice(item)" class="book-price">{{
|
||||||
|
formatPrice(item)
|
||||||
|
}}</text>
|
||||||
|
<text v-if="formatStats(item)" class="book-flag">{{
|
||||||
|
formatStats(item)
|
||||||
|
}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text v-else class="zanwu" style="padding: 100rpx 0">{{ $t('global.dataNull') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { homeApi } from '@/api/modules/home'
|
||||||
|
import type {
|
||||||
|
IBook,
|
||||||
|
IBookWithStats,
|
||||||
|
ILabel,
|
||||||
|
IVipInfo
|
||||||
|
} from '@/types/book'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 状态定义
|
||||||
|
const notchHeight = ref(0)
|
||||||
|
const showMyBooks = ref(false)
|
||||||
|
const showActivity = ref(false)
|
||||||
|
const showCategory = ref(false)
|
||||||
|
|
||||||
|
// 我的书单
|
||||||
|
const myBooksList = ref<IBook[]>([])
|
||||||
|
|
||||||
|
// 推荐图书
|
||||||
|
const recommendBooksList = ref<IBook[]>([])
|
||||||
|
|
||||||
|
// 活动图书
|
||||||
|
const activityLabelList = ref<ILabel[]>([])
|
||||||
|
const activityList = ref<IBookWithStats[]>([])
|
||||||
|
const currentActivityIndex = ref(0)
|
||||||
|
|
||||||
|
// 分类图书
|
||||||
|
const categoryLevel1List = ref<ILabel[]>([])
|
||||||
|
const categoryLevel2List = ref<ILabel[]>([])
|
||||||
|
const categoryBookList = ref<IBookWithStats[]>([])
|
||||||
|
const currentLevel1Index = ref(0)
|
||||||
|
const currentLevel2Index = ref(0)
|
||||||
|
|
||||||
|
// VIP信息
|
||||||
|
const vipInfo = ref<IVipInfo | null>(null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取VIP信息
|
||||||
|
*/
|
||||||
|
const getVipInfo = async () => {
|
||||||
|
try {
|
||||||
|
const res = await homeApi.getVipInfo()
|
||||||
|
if (res.vipInfo) {
|
||||||
|
vipInfo.value = res.vipInfo
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取VIP信息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的书单
|
||||||
|
*/
|
||||||
|
const getMyBooks = async () => {
|
||||||
|
try {
|
||||||
|
const res = await homeApi.getMyBooks(1, 10)
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
showMyBooks.value = true
|
||||||
|
if (res.page.records && res.page.records.length > 0) {
|
||||||
|
myBooksList.value = res.page.records
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 未登录,跳转到登录页
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/login/login'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取我的书单失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推荐图书
|
||||||
|
*/
|
||||||
|
const getRecommendBooks = async () => {
|
||||||
|
try {
|
||||||
|
const res = await homeApi.getRecommendBooks()
|
||||||
|
if (res.books && res.books.length > 0) {
|
||||||
|
recommendBooksList.value = res.books
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取推荐图书失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取活动标签列表
|
||||||
|
*/
|
||||||
|
const getActivityLabels = async () => {
|
||||||
|
try {
|
||||||
|
const res = await homeApi.getBookLabelList(1)
|
||||||
|
showActivity.value = true
|
||||||
|
if (res.lableList && res.lableList.length > 0) {
|
||||||
|
activityLabelList.value = res.lableList
|
||||||
|
// 默认加载第一个标签的图书列表
|
||||||
|
await getBooksByLabel(res.lableList[0].id, 'activity')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取活动标签失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分类标签列表
|
||||||
|
*/
|
||||||
|
const getCategoryLabels = async () => {
|
||||||
|
try {
|
||||||
|
const res = await homeApi.getBookLabelList(0)
|
||||||
|
showCategory.value = true
|
||||||
|
if (res.lableList && res.lableList.length > 0) {
|
||||||
|
categoryLevel1List.value = res.lableList
|
||||||
|
// 默认加载第一个标签的二级标签
|
||||||
|
await getSubLabels(res.lableList[0].id, 0)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取分类标签失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取二级标签列表
|
||||||
|
*/
|
||||||
|
const getSubLabels = async (pid: number, index: number) => {
|
||||||
|
try {
|
||||||
|
const res = await homeApi.getSubLabelList(pid)
|
||||||
|
currentLevel1Index.value = index
|
||||||
|
if (res.lableList && res.lableList.length > 0) {
|
||||||
|
categoryLevel2List.value = res.lableList
|
||||||
|
currentLevel2Index.value = 0
|
||||||
|
// 加载第一个二级标签的图书列表
|
||||||
|
await getBooksByLabel(res.lableList[0].id, 'category')
|
||||||
|
} else {
|
||||||
|
// 没有二级标签,直接加载一级标签的图书列表
|
||||||
|
categoryLevel2List.value = []
|
||||||
|
await getBooksByLabel(pid, 'category')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取二级标签失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据标签获取图书列表
|
||||||
|
*/
|
||||||
|
const getBooksByLabel = async (
|
||||||
|
labelId: number,
|
||||||
|
type: 'activity' | 'category'
|
||||||
|
) => {
|
||||||
|
uni.showLoading({ title: t('common.loading') })
|
||||||
|
try {
|
||||||
|
const res = await homeApi.getBooksByLabel(labelId)
|
||||||
|
if (type === 'activity') {
|
||||||
|
if (res.bookList && res.bookList.length > 0) {
|
||||||
|
activityList.value = res.bookList
|
||||||
|
} else {
|
||||||
|
activityList.value = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (res.bookList && res.bookList.length > 0) {
|
||||||
|
categoryBookList.value = res.bookList
|
||||||
|
} else {
|
||||||
|
categoryBookList.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取图书列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化价格
|
||||||
|
*/
|
||||||
|
const formatPrice = (book: IBookWithStats): string => {
|
||||||
|
// 已购买不显示价格
|
||||||
|
if (book.isBuy) return ''
|
||||||
|
|
||||||
|
// VIP用户且图书为VIP专享
|
||||||
|
if (vipInfo.value?.id && book.isVip === '2') {
|
||||||
|
const price = book.sysDictData?.dictValue
|
||||||
|
return price ? `$ ${price} NZD` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通用户
|
||||||
|
if (!vipInfo.value?.id) {
|
||||||
|
const price = book.sysDictData?.dictValue
|
||||||
|
return price ? `$ ${price} NZD` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化统计信息
|
||||||
|
*/
|
||||||
|
const formatStats = (book: IBookWithStats): string => {
|
||||||
|
if (book.readCount && book.readCount > 0) {
|
||||||
|
return `${book.readCount}${t('home.readingCount')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (book.buyCount && book.buyCount > 0) {
|
||||||
|
return `${book.buyCount}${t('home.purchased')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理搜索点击
|
||||||
|
*/
|
||||||
|
const handleSearch = ({ value }: { value: string }) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/book/search?keyword=${value}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理我的书单图书点击
|
||||||
|
*/
|
||||||
|
const handleMyBookClick = (bookId: number) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/book/reader?isBuy=0&bookId=${bookId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理图书点击
|
||||||
|
*/
|
||||||
|
const handleBookClick = (bookId: number) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/book/detail?id=${bookId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理更多按钮点击
|
||||||
|
*/
|
||||||
|
const handleMoreClick = () => {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/book/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理活动标签点击
|
||||||
|
*/
|
||||||
|
const handleActivityLabelClick = async (labelId: number, index: number) => {
|
||||||
|
currentActivityIndex.value = index
|
||||||
|
await getBooksByLabel(labelId, 'activity')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理一级分类标签点击
|
||||||
|
*/
|
||||||
|
const handleCategoryLevel1Click = async (labelId: number, index: number) => {
|
||||||
|
await getSubLabels(labelId, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理二级分类标签点击
|
||||||
|
*/
|
||||||
|
const handleCategoryLevel2Click = async (labelId: number, index: number) => {
|
||||||
|
currentLevel2Index.value = index
|
||||||
|
await getBooksByLabel(labelId, 'category')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面加载
|
||||||
|
*/
|
||||||
|
onMounted(() => {
|
||||||
|
// 获取刘海高度
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
notchHeight.value = systemInfo.safeArea.top
|
||||||
|
|
||||||
|
// 重置活动标签选中状态
|
||||||
|
currentActivityIndex.value = 0
|
||||||
|
showActivity.value = false
|
||||||
|
|
||||||
|
getVipInfo()
|
||||||
|
getMyBooks()
|
||||||
|
getRecommendBooks()
|
||||||
|
getActivityLabels()
|
||||||
|
getCategoryLabels()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss" scoped>
|
||||||
.title {
|
.home-page {
|
||||||
font-size: 16px;
|
min-height: 100vh;
|
||||||
font-weight: bold;
|
background: #f7faf9;
|
||||||
margin-bottom: 15px;
|
}
|
||||||
|
|
||||||
|
.home-bg {
|
||||||
|
background-image: url('@/static/icon/home_bg.jpg');
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
padding: 30rpx;
|
||||||
|
position: relative;
|
||||||
|
height: 340rpx;
|
||||||
|
|
||||||
|
.icon-hua {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
|
||||||
|
.icon-hua-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 160rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-block {
|
||||||
|
padding: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.mine-1,
|
||||||
|
.mine-2 {
|
||||||
|
width: 49%;
|
||||||
|
height: 290rpx;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.mine-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-more {
|
||||||
|
position: absolute;
|
||||||
|
top: 30rpx;
|
||||||
|
right: 5rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 34rpx;
|
||||||
|
color: #999;
|
||||||
|
|
||||||
|
image {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
width: 34rpx;
|
||||||
|
height: 34rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-1-list {
|
||||||
|
width: 260rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.mine-item {
|
||||||
|
width: 110rpx;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20rpx 25rpx 0 0;
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 110rpx;
|
||||||
|
height: 135rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
display: block;
|
||||||
|
width: 120rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 32rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding-top: 10rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-list {
|
||||||
|
width: 310rpx;
|
||||||
|
height: 164rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
|
||||||
|
.recommend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 164rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
display: block;
|
||||||
|
width: 190rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 40rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding-left: 15rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.mine-1 {
|
||||||
font-size: 14px;
|
background-image: linear-gradient(60deg, #fff9e9 20%, #fffbf2 100%);
|
||||||
opacity: 0.6;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mine-2 {
|
||||||
|
background-image: linear-gradient(60deg, #fef0f0 20%, #fdf9f8 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-block {
|
||||||
|
margin: 0 20rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
min-height: 445rpx;
|
||||||
|
|
||||||
|
.activity-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 38rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 40rpx;
|
||||||
|
padding: 15rpx 0 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-label-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
|
||||||
|
.activity-label-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #d7ece8;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
text {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 110rpx;
|
||||||
|
height: 58rpx;
|
||||||
|
line-height: 58rpx;
|
||||||
|
color: #55aa7f;
|
||||||
|
font-size: 30rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-label {
|
||||||
|
background-color: #55aa7f;
|
||||||
|
|
||||||
|
text {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-list {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.activity-item {
|
||||||
|
margin-right: 25rpx;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-text {
|
||||||
|
display: block;
|
||||||
|
width: 160rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
padding-top: 10rpx;
|
||||||
|
line-height: 40rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-block {
|
||||||
|
padding: 20rpx 20rpx 0;
|
||||||
|
|
||||||
|
.book-tab-one {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.tab-one-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #d7ece8;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
margin-right: 15rpx;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 25%;
|
||||||
|
height: 160rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
text {
|
||||||
|
display: flex;
|
||||||
|
width: 98%;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #55aa7f;
|
||||||
|
line-height: 50rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-one-item:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-one-active {
|
||||||
|
background-color: #55aa7f;
|
||||||
|
|
||||||
|
text {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-tab-two {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 70rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
|
||||||
|
.tab-two-item {
|
||||||
|
min-width: 25%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-right: 1rpx solid #acacac33;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
text {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 165rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 48rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-two-item:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-two-active {
|
||||||
|
text {
|
||||||
|
color: #55aa7f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
|
||||||
|
.book-item {
|
||||||
|
width: 49%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
height: 575rpx;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 85%;
|
||||||
|
margin: 25rpx auto 0;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
height: 380rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 36rpx;
|
||||||
|
width: 80%;
|
||||||
|
margin: 15rpx auto 0;
|
||||||
|
max-height: 72rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-price {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #ff4703;
|
||||||
|
left: 30rpx;
|
||||||
|
bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-flag {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
position: absolute;
|
||||||
|
right: 6%;
|
||||||
|
bottom: 20rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zanwu {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
padding: 40rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-view {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -99,6 +99,8 @@
|
|||||||
<wd-icon v-if="isLocked(index)" name="lock" size="20px" />
|
<wd-icon v-if="isLocked(index)" name="lock" size="20px" />
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
<!-- 底部占位 -->
|
||||||
|
<view class="setting-ooter-placeholder"></view>
|
||||||
</view>
|
</view>
|
||||||
</wd-popup>
|
</wd-popup>
|
||||||
|
|
||||||
@@ -108,7 +110,7 @@
|
|||||||
<!-- 切换语言 -->
|
<!-- 切换语言 -->
|
||||||
<view class="setting-item">
|
<view class="setting-item">
|
||||||
<text class="setting-label">{{ $t('book.language') }}</text>
|
<text class="setting-label">{{ $t('book.language') }}</text>
|
||||||
<wd-radio-group v-model="currentLanguage" shape="button" custom-class="bg-[transparent]" @change="changeBookLanguage">
|
<wd-radio-group v-model="currentLanguage" shape="button" style="background-color: transparent;" @change="changeBookLanguage">
|
||||||
<wd-radio v-for="lang in bookLanguages" :key="lang.language" :value="lang.language">{{ lang.language }}</wd-radio>
|
<wd-radio v-for="lang in bookLanguages" :key="lang.language" :value="lang.language">{{ lang.language }}</wd-radio>
|
||||||
</wd-radio-group>
|
</wd-radio-group>
|
||||||
</view>
|
</view>
|
||||||
@@ -162,10 +164,9 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部占位 -->
|
|
||||||
<view class="setting-ooter-placeholder"></view>
|
|
||||||
</view>
|
</view>
|
||||||
|
<!-- 底部占位 -->
|
||||||
|
<view class="setting-ooter-placeholder"></view>
|
||||||
</wd-popup>
|
</wd-popup>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
@@ -407,7 +408,7 @@ async function loadChapterContent(chapterId: number, index: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 切换章节
|
// 切换章节
|
||||||
function switchChapter(chapter: IChapter, index: number) {
|
async function switchChapter(chapter: IChapter, index: number) {
|
||||||
if (isLocked(index)) {
|
if (isLocked(index)) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: t('book.afterPurchase'),
|
title: t('book.afterPurchase'),
|
||||||
@@ -416,7 +417,8 @@ function switchChapter(chapter: IChapter, index: number) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
loadChapterContent(chapter.id, index)
|
await loadChapterContent(chapter.id, index)
|
||||||
|
showControls.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断章节是否锁定
|
// 判断章节是否锁定
|
||||||
@@ -797,6 +799,10 @@ function goBack() {
|
|||||||
.setting-item {
|
.setting-item {
|
||||||
margin-bottom: 40rpx;
|
margin-bottom: 40rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.setting-label {
|
.setting-label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
@@ -866,11 +872,6 @@ function goBack() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部占位 */
|
|
||||||
.setting-ooter-placeholder {
|
|
||||||
height: 80rpx;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
@@ -884,5 +885,10 @@ function goBack() {
|
|||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 底部占位 */
|
||||||
|
.setting-ooter-placeholder {
|
||||||
|
height: 55px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
293
pages/book/search.vue
Normal file
293
pages/book/search.vue
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
<template>
|
||||||
|
<view class="search-page">
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<nav-bar>
|
||||||
|
<template #title>
|
||||||
|
<view class="search-box">
|
||||||
|
<wd-search
|
||||||
|
v-model="keyword"
|
||||||
|
hide-cancel
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</nav-bar>
|
||||||
|
|
||||||
|
<!-- 搜索结果 -->
|
||||||
|
<view class="search-results">
|
||||||
|
<view v-if="loading" class="loading-wrapper">
|
||||||
|
<wd-loading />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="searchResults.length > 0" class="book-list">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in searchResults"
|
||||||
|
:key="index"
|
||||||
|
class="book-item"
|
||||||
|
@click="handleBookClick(item.bookId)"
|
||||||
|
>
|
||||||
|
<image :src="item.images" />
|
||||||
|
<text class="book-text">{{ item.name }}</text>
|
||||||
|
<text v-if="formatPrice(item)" class="book-price">{{
|
||||||
|
formatPrice(item)
|
||||||
|
}}</text>
|
||||||
|
<text v-if="formatStats(item)" class="book-flag">{{
|
||||||
|
formatStats(item)
|
||||||
|
}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="isEmpty" class="empty-wrapper">
|
||||||
|
<wd-status-tip image="content" :tip="$t('global.searchNoResult')" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { homeApi } from '@/api/modules/home'
|
||||||
|
import type { IBookWithStats, IVipInfo } from '@/types/home'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 状态定义
|
||||||
|
const notchHeight = ref(0)
|
||||||
|
const keyword = ref('')
|
||||||
|
const searchResults = ref<IBookWithStats[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const isEmpty = ref(false)
|
||||||
|
const vipInfo = ref<IVipInfo | null>(null)
|
||||||
|
|
||||||
|
// 获取URL参数
|
||||||
|
onLoad((options: any) => {
|
||||||
|
keyword.value = options.keyword
|
||||||
|
|
||||||
|
handleSearch()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取VIP信息
|
||||||
|
*/
|
||||||
|
const getVipInfo = async () => {
|
||||||
|
try {
|
||||||
|
const res = await homeApi.getVipInfo()
|
||||||
|
if (res.vipInfo) {
|
||||||
|
vipInfo.value = res.vipInfo
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取VIP信息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理搜索
|
||||||
|
*/
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!keyword.value.trim()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
isEmpty.value = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await homeApi.searchBooks({
|
||||||
|
key: keyword.value.trim(),
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
})
|
||||||
|
if (res.bookList && res.bookList.length > 0) {
|
||||||
|
searchResults.value = res.bookList
|
||||||
|
isEmpty.value = false
|
||||||
|
} else {
|
||||||
|
searchResults.value = []
|
||||||
|
isEmpty.value = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('搜索失败:', error)
|
||||||
|
searchResults.value = []
|
||||||
|
isEmpty.value = true
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理清空
|
||||||
|
*/
|
||||||
|
const handleClear = () => {
|
||||||
|
keyword.value = ''
|
||||||
|
searchResults.value = []
|
||||||
|
isEmpty.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理图书点击
|
||||||
|
*/
|
||||||
|
const handleBookClick = (bookId: number) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/book/detail?id=${bookId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化价格
|
||||||
|
*/
|
||||||
|
const formatPrice = (book: IBookWithStats): string => {
|
||||||
|
// 已购买不显示价格
|
||||||
|
if (book.isBuy) return ''
|
||||||
|
|
||||||
|
// VIP用户且图书为VIP专享
|
||||||
|
if (vipInfo.value?.id && book.isVip === '2') {
|
||||||
|
const price = book.sysDictData?.dictValue
|
||||||
|
return price ? `$ ${price} NZD` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通用户
|
||||||
|
if (!vipInfo.value?.id) {
|
||||||
|
const price = book.sysDictData?.dictValue
|
||||||
|
return price ? `$ ${price} NZD` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化统计信息
|
||||||
|
*/
|
||||||
|
const formatStats = (book: IBookWithStats): string => {
|
||||||
|
if (book.readCount && book.readCount > 0) {
|
||||||
|
return `${book.readCount}${t('home.readingCount')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (book.buyCount && book.buyCount > 0) {
|
||||||
|
return `${book.buyCount}${t('home.purchased')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面加载
|
||||||
|
*/
|
||||||
|
onMounted(async () => {
|
||||||
|
// 获取刘海高度
|
||||||
|
const systemInfo = uni.getSystemInfoSync()
|
||||||
|
notchHeight.value = systemInfo.safeArea.top
|
||||||
|
|
||||||
|
// 获取VIP信息
|
||||||
|
await getVipInfo()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7faf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
--wot-search-padding: 0;
|
||||||
|
--wot-search-side-padding: 0;
|
||||||
|
:deep() {
|
||||||
|
.wd-search {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20rpx;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.search-bar-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.back-icon {
|
||||||
|
margin-right: 20rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 100rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.book-item {
|
||||||
|
width: 49%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
height: 575rpx;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 85%;
|
||||||
|
margin: 25rpx auto 0;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
height: 380rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 36rpx;
|
||||||
|
width: 80%;
|
||||||
|
margin: 15rpx auto 0;
|
||||||
|
max-height: 72rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-price {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #ff4703;
|
||||||
|
left: 30rpx;
|
||||||
|
bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-flag {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
position: absolute;
|
||||||
|
right: 6%;
|
||||||
|
bottom: 20rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="container">
|
<view class="container">
|
||||||
<view class="title bg-[blue] text-center text-[#000]">这是一个等待开发的首页</view>
|
<view class="title bg-[transparent] text-center text-[#000]">这是一个等待开发的首页</view>
|
||||||
<view class="title bg-[blue] text-center text-[#fff]">这是一个等待开发的首页</view>
|
<view class="title bg-[blue] text-center text-[#fff]">这是一个等待开发的首页</view>
|
||||||
<view class="description bg-[red]">首页的内容是在线课程</view>
|
<view class="description bg-[red]">首页的内容是在线课程</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
# 登录功能说明
|
|
||||||
|
|
||||||
## 功能概述
|
|
||||||
|
|
||||||
本模块实现了从 nuttyreading-hw2 项目迁移的完整登录功能,包括:
|
|
||||||
|
|
||||||
- ✅ 验证码登录/注册
|
|
||||||
- ✅ 密码登录
|
|
||||||
- ✅ 忘记密码
|
|
||||||
- ✅ 用户协议和隐私政策
|
|
||||||
- ✅ 游客体验入口
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
- Vue3 Composition API
|
|
||||||
- TypeScript
|
|
||||||
- Pinia (状态管理)
|
|
||||||
- WotUI (UI 组件库)
|
|
||||||
- Tailwind CSS + SCSS
|
|
||||||
- UniApp
|
|
||||||
|
|
||||||
## 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
pages/user/
|
|
||||||
├── login.vue # 登录页面
|
|
||||||
└── forget.vue # 忘记密码页面
|
|
||||||
|
|
||||||
api/modules/
|
|
||||||
├── auth.ts # 认证相关 API
|
|
||||||
└── common.ts # 通用 API
|
|
||||||
|
|
||||||
stores/
|
|
||||||
└── user.ts # 用户状态管理
|
|
||||||
|
|
||||||
types/
|
|
||||||
└── user.ts # 用户相关类型定义
|
|
||||||
|
|
||||||
utils/
|
|
||||||
└── validator.ts # 表单验证工具
|
|
||||||
```
|
|
||||||
|
|
||||||
## API 接口
|
|
||||||
|
|
||||||
### 登录相关
|
|
||||||
|
|
||||||
1. **验证码登录/注册**
|
|
||||||
- 接口:`GET book/user/registerOrLogin`
|
|
||||||
- 参数:`{ tel: string, code: string }`
|
|
||||||
|
|
||||||
2. **密码登录**
|
|
||||||
- 接口:`POST book/user/login`
|
|
||||||
- 参数:`{ phone: string, password: string }`
|
|
||||||
|
|
||||||
3. **重置密码**
|
|
||||||
- 接口:`POST book/user/setPassword`
|
|
||||||
- 参数:`{ phone: string, code: string, password: string }`
|
|
||||||
|
|
||||||
### 通用接口
|
|
||||||
|
|
||||||
1. **发送邮箱验证码**
|
|
||||||
- 接口:`GET common/user/getMailCaptcha`
|
|
||||||
- 参数:`{ email: string }`
|
|
||||||
|
|
||||||
2. **获取协议内容**
|
|
||||||
- 接口:`GET common/agreement/detail`
|
|
||||||
- 参数:`{ id: number }` (111: 用户协议, 112: 隐私政策)
|
|
||||||
|
|
||||||
## 使用说明
|
|
||||||
|
|
||||||
### 1. 登录页面
|
|
||||||
|
|
||||||
访问路径:`/pages/user/login`
|
|
||||||
|
|
||||||
**验证码登录**:
|
|
||||||
1. 输入邮箱地址
|
|
||||||
2. 点击"Get Code"获取验证码
|
|
||||||
3. 输入收到的验证码
|
|
||||||
4. 勾选用户协议
|
|
||||||
5. 点击"Go Login"登录
|
|
||||||
|
|
||||||
**密码登录**:
|
|
||||||
1. 点击"Password Login"切换到密码登录
|
|
||||||
2. 输入邮箱地址和密码
|
|
||||||
3. 勾选用户协议
|
|
||||||
4. 点击"Go Login"登录
|
|
||||||
|
|
||||||
### 2. 忘记密码
|
|
||||||
|
|
||||||
访问路径:`/pages/user/forget`
|
|
||||||
|
|
||||||
1. 输入邮箱地址
|
|
||||||
2. 点击"Get Code"获取验证码
|
|
||||||
3. 输入验证码
|
|
||||||
4. 输入新密码(需满足强度要求)
|
|
||||||
5. 再次输入新密码确认
|
|
||||||
6. 点击"Submit"提交
|
|
||||||
|
|
||||||
### 3. 密码强度要求
|
|
||||||
|
|
||||||
- **强密码**:8位以上,包含大小写字母、数字和特殊字符
|
|
||||||
- **中等密码**:8位以上,包含大小写字母、数字、特殊字符中的两项
|
|
||||||
- **弱密码**:8位以上
|
|
||||||
- **最低要求**:6-20位,必须包含字母和数字
|
|
||||||
|
|
||||||
## 状态管理
|
|
||||||
|
|
||||||
使用 Pinia 管理用户状态:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useUserStore } from '@/stores/user'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
// 登录成功后设置用户信息
|
|
||||||
userStore.setUserInfo(userInfo)
|
|
||||||
|
|
||||||
// 检查登录状态
|
|
||||||
if (userStore.isLoggedIn) {
|
|
||||||
// 已登录
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登出
|
|
||||||
userStore.logout()
|
|
||||||
```
|
|
||||||
|
|
||||||
## 国际化
|
|
||||||
|
|
||||||
支持中英文切换,翻译文件位于:
|
|
||||||
- `locale/en.json` - 英文
|
|
||||||
- `locale/zh-Hans.json` - 简体中文
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **API 地址**:已配置为 `https://global.nuttyreading.com/`
|
|
||||||
2. **请求头**:自动添加 `token`、`appType: 'abroad'`、`version_code`
|
|
||||||
3. **Token 失效**:自动处理 401 错误,清除用户信息并跳转登录页
|
|
||||||
4. **验证码倒计时**:60秒,防止重复发送
|
|
||||||
5. **协议同意**:登录和获取验证码前必须同意用户协议和隐私政策
|
|
||||||
|
|
||||||
## 测试建议
|
|
||||||
|
|
||||||
1. 测试验证码登录流程
|
|
||||||
2. 测试密码登录流程
|
|
||||||
3. 测试忘记密码流程
|
|
||||||
4. 测试登录方式切换
|
|
||||||
5. 测试表单验证
|
|
||||||
6. 测试协议弹窗
|
|
||||||
7. 测试多平台兼容性(H5、小程序、APP)
|
|
||||||
|
|
||||||
## 已知问题
|
|
||||||
|
|
||||||
- 游客页面 `/pages/visitor/visitor` 需要单独实现
|
|
||||||
- 部分图标可能需要根据实际设计调整
|
|
||||||
|
|
||||||
## 更新日志
|
|
||||||
|
|
||||||
### v1.0.0 (2025-11-02)
|
|
||||||
- ✅ 完成登录功能迁移
|
|
||||||
- ✅ 实现验证码登录和密码登录
|
|
||||||
- ✅ 实现忘记密码功能
|
|
||||||
- ✅ 添加用户协议和隐私政策
|
|
||||||
- ✅ 支持中英文国际化
|
|
||||||
@@ -8,7 +8,10 @@
|
|||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
"Courier New", monospace;
|
"Courier New", monospace;
|
||||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||||
--color-emerald-600: oklch(59.6% 0.145 163.225);
|
--color-blue-500: oklch(62.3% 0.214 259.815);
|
||||||
|
--color-white: #fff;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
--radius-lg: 0.5rem;
|
||||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
--default-transition-duration: 150ms;
|
--default-transition-duration: 150ms;
|
||||||
@@ -187,9 +190,6 @@
|
|||||||
.sticky {
|
.sticky {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
}
|
}
|
||||||
.isolate {
|
|
||||||
isolation: isolate;
|
|
||||||
}
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@media (width >= 40rem) {
|
@media (width >= 40rem) {
|
||||||
@@ -229,36 +229,63 @@
|
|||||||
.inline-block {
|
.inline-block {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.list-item {
|
|
||||||
display: list-item;
|
|
||||||
}
|
|
||||||
.table {
|
.table {
|
||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
|
.w-\[100px\] {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
.flex-shrink {
|
.flex-shrink {
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
.border-collapse {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
.transform {
|
.transform {
|
||||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||||
}
|
}
|
||||||
.resize {
|
.resize {
|
||||||
resize: both;
|
resize: both;
|
||||||
}
|
}
|
||||||
|
.flex-wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.rounded-lg {
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
.border {
|
.border {
|
||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
|
.bg-\[blue\] {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
.bg-\[red\] {
|
.bg-\[red\] {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
}
|
}
|
||||||
.text-left {
|
.bg-\[transparent\] {
|
||||||
text-align: left;
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.bg-blue-500 {
|
||||||
|
background-color: var(--color-blue-500);
|
||||||
|
}
|
||||||
|
.p-4 {
|
||||||
|
padding: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.text-\[\#000\] {
|
||||||
|
color: #000;
|
||||||
}
|
}
|
||||||
.text-\[\#fff\] {
|
.text-\[\#fff\] {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.capitalize {
|
.text-white {
|
||||||
text-transform: capitalize;
|
color: var(--color-white);
|
||||||
}
|
}
|
||||||
.lowercase {
|
.lowercase {
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
@@ -266,9 +293,6 @@
|
|||||||
.uppercase {
|
.uppercase {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
.ordinal {
|
.ordinal {
|
||||||
--tw-ordinal: ordinal;
|
--tw-ordinal: ordinal;
|
||||||
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
||||||
@@ -276,10 +300,6 @@
|
|||||||
.underline {
|
.underline {
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
}
|
}
|
||||||
.shadow {
|
|
||||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
||||||
}
|
|
||||||
.ring {
|
.ring {
|
||||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
@@ -304,10 +324,6 @@
|
|||||||
--tw-ease: var(--ease-in-out);
|
--tw-ease: var(--ease-in-out);
|
||||||
transition-timing-function: var(--ease-in-out);
|
transition-timing-function: var(--ease-in-out);
|
||||||
}
|
}
|
||||||
.ease-out {
|
|
||||||
--tw-ease: var(--ease-out);
|
|
||||||
transition-timing-function: var(--ease-out);
|
|
||||||
}
|
|
||||||
.hover\:bg-red-500 {
|
.hover\:bg-red-500 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
|
|||||||
@@ -1 +1,54 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
separator: '__', // 如果是小程序项目需要设置这一项,将 : 选择器替换成 __,之后 hover:bg-red-500 将改为 hover__bg-red-500
|
||||||
|
corePlugins: {
|
||||||
|
// 预设样式
|
||||||
|
preflight: false, // 一般uniapp都有预设样式,所以不需要tailwindcss的预设
|
||||||
|
|
||||||
|
// 以下功能小程序不支持
|
||||||
|
space: false, // > 子节点选择器
|
||||||
|
divideWidth: false,
|
||||||
|
divideColor: false,
|
||||||
|
divideStyle: false,
|
||||||
|
divideOpacity: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 指定要处理的文件
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{vue,js}',
|
||||||
|
'./components/**/*.{vue,js}',
|
||||||
|
'./main.js',
|
||||||
|
'./App.vue',
|
||||||
|
'./index.html'
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
// 字号,使用 App.vue 中的 --x-font-size 样式变量配置
|
||||||
|
fontSize(config){
|
||||||
|
const list = ['2xs','xs','sm','base','md','lg','xl','2xl','3xl'];
|
||||||
|
let result = {}
|
||||||
|
list.forEach(it=>{
|
||||||
|
result[it] = `var(--x-font-size-${it})`
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
// 间距,tailwindcss中默认间距是rem单位,可以统一设置为uniapp的rpx单位。
|
||||||
|
// 类似的设置根据项目需求自己调整一下就好了,没必要去安装别人的预设,其实主要是小程序不兼容的css比较多,H5和App基本都直接兼容tailwindcss默认的预设
|
||||||
|
spacing(config) {
|
||||||
|
let result = { 0: '0' }
|
||||||
|
// 允许的数值大一些也无所谓,最后打包tailwindcss会摇树优化,未使用的样式并不会打包
|
||||||
|
for (let i = 1; i <= 300; i++) {
|
||||||
|
result[i] = `${i}rpx`
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
// 增加颜色板,现在主流UI组件库大都是采用css变量实现定制主题,所以这里引用了全局的css变量,这个css变量的定义位置可以在 App.vue 中 page{} 选择器下
|
||||||
|
// 其实tailwindcss只是一个css工具,不必局限于它内部提供的东西,灵活运用css变量这些特性完全可以整合出自己的生产力工具
|
||||||
|
colors:{
|
||||||
|
'primary': 'var(--x-color-primary)',
|
||||||
|
'tips' : 'var(--x-color-tips)'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
73
types/book.d.ts
vendored
73
types/book.d.ts
vendored
@@ -89,3 +89,76 @@ export interface IReaderSettings {
|
|||||||
theme: 'default' | 'blue' | 'green' | 'purple' | 'night'
|
theme: 'default' | 'blue' | 'green' | 'purple' | 'night'
|
||||||
readMode: 'scroll' | 'page'
|
readMode: 'scroll' | 'page'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 标签数据 */
|
||||||
|
export interface ILabel {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
type: number // 0: 分类标签, 1: 活动标签
|
||||||
|
pid?: number // 父级ID(用于二级分类)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 带统计的图书数据 */
|
||||||
|
export interface IBookWithStats extends IBook {
|
||||||
|
bookId: number // 图书ID(部分接口返回的字段名)
|
||||||
|
readCount?: number // 阅读次数
|
||||||
|
listenCount?: number // 听书次数
|
||||||
|
buyCount?: number // 购买人数
|
||||||
|
isVip?: string // VIP专享标识 '2'表示VIP专享
|
||||||
|
sysDictData?: {
|
||||||
|
dictValue: string // 价格数值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** VIP信息 */
|
||||||
|
export interface IVipInfo {
|
||||||
|
id: number
|
||||||
|
endTime: string
|
||||||
|
vipType: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页数据 */
|
||||||
|
export interface IPageData<T> {
|
||||||
|
records: T[]
|
||||||
|
total: number
|
||||||
|
current: number
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API响应基础结构 */
|
||||||
|
export interface IApiResponse<T = any> {
|
||||||
|
code: number
|
||||||
|
msg?: string
|
||||||
|
info?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 我的书单响应 */
|
||||||
|
export interface IMyBooksResponse extends IApiResponse {
|
||||||
|
page: IPageData<IBook>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 推荐图书响应 */
|
||||||
|
export interface IRecommendBooksResponse extends IApiResponse {
|
||||||
|
books: IBook[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标签列表响应 */
|
||||||
|
export interface ILabelListResponse extends IApiResponse {
|
||||||
|
lableList: ILabel[] // 注意:原接口拼写为 lableList
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 图书列表响应 */
|
||||||
|
export interface IBookListResponse extends IApiResponse {
|
||||||
|
bookList: IBookWithStats[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** VIP信息响应 */
|
||||||
|
export interface IVipInfoResponse extends IApiResponse {
|
||||||
|
vipInfo: IVipInfo | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索结果响应 */
|
||||||
|
export interface ISearchResponse extends IApiResponse {
|
||||||
|
bookList: IBookWithStats[]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user