├── php-api/ # 改造后的PHP接口层 ├── java-ad-service/ # 若依框架微服务(广告+VIP+分账) ├── uniapp-reader/ # UniApp前端项目 │ ├── pages/ # 各端页面 │ └──
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

reader.vue 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  1. <template>
  2. <view class="reader-container" :style="readerStyles">
  3. <!-- 登录提示弹窗 -->
  4. <uni-popup ref="loginPopup" type="dialog">
  5. <uni-popup-dialog
  6. type="info"
  7. title="登录提示"
  8. content="您已阅读完免费章节,登录后可继续阅读"
  9. :duration="2000"
  10. :before-close="true"
  11. @close="closeLoginDialog"
  12. @confirm="goToLogin"
  13. ></uni-popup-dialog>
  14. </uni-popup>
  15. <!-- 顶部导航 -->
  16. <view class="reader-header">
  17. <uni-icons type="arrowleft" size="28" color="#fff" @click="goBack"></uni-icons>
  18. <text class="novel-title">{{ novelTitle }}</text>
  19. <view class="header-actions">
  20. <uni-icons type="more" size="28" color="#fff" @click="showMoreActions"></uni-icons>
  21. </view>
  22. </view>
  23. <!-- 阅读区域 -->
  24. <scroll-view scroll-y class="reader-content" :scroll-top="scrollTop" @scroll="onScroll">
  25. <view class="chapter-title">{{ chapterDetail.title }}</view>
  26. <rich-text :nodes="chapterContent" class="content-text"></rich-text>
  27. <!-- 章节结束提示 -->
  28. <view v-if="isChapterEnd" class="chapter-end">
  29. <text>本章结束</text>
  30. <button v-if="!isLastChapter" @click="nextChapter">下一章</button>
  31. <text v-else>已是最新章节</text>
  32. </view>
  33. </scroll-view>
  34. <!-- 底部操作栏 -->
  35. <view class="reader-footer">
  36. <view class="progress">
  37. <text>{{ currentChapterIndex + 1 }}/{{ chapters.length }}</text>
  38. <text>{{ readingProgress }}%</text>
  39. </view>
  40. <view class="actions">
  41. <button class="action-btn" @click="prevChapter" :disabled="isFirstChapter">
  42. <uni-icons type="arrow-up" size="24"></uni-icons>
  43. <text>上一章</text>
  44. </button>
  45. <button class="action-btn" @click="toggleMenu">
  46. <uni-icons type="list" size="24"></uni-icons>
  47. <text>目录</text>
  48. </button>
  49. <button class="action-btn" @click="toggleSettings">
  50. <uni-icons type="gear" size="24"></uni-icons>
  51. <text>设置</text>
  52. </button>
  53. <button class="action-btn" @click="nextChapter" :disabled="isLastChapter">
  54. <uni-icons type="arrow-down" size="24"></uni-icons>
  55. <text>下一章</text>
  56. </button>
  57. </view>
  58. </view>
  59. <!-- 目录抽屉 -->
  60. <uni-drawer ref="drawer" mode="right" :width="300">
  61. <view class="drawer-content">
  62. <text class="drawer-title">目录</text>
  63. <view class="chapter-stats">
  64. <text>共{{ chapters.length }}章</text>
  65. <text>已读{{ readChaptersCount }}章</text>
  66. </view>
  67. <scroll-view scroll-y class="chapter-list">
  68. <view
  69. v-for="(chapter, index) in chapters"
  70. :key="chapter.id"
  71. class="chapter-item"
  72. :class="{
  73. active: chapter.id === chapterDetail.id,
  74. read: isChapterRead(chapter.id),
  75. locked: isChapterLocked(index)
  76. }"
  77. @click="selectChapter(chapter, index)"
  78. >
  79. <text>{{ index + 1 }}. {{ chapter.title }}</text>
  80. <uni-icons v-if="isChapterLocked(index)" type="locked" size="16" color="#999"></uni-icons>
  81. </view>
  82. </scroll-view>
  83. </view>
  84. </uni-drawer>
  85. <!-- 设置面板 -->
  86. <uni-popup ref="settingsPopup" type="bottom">
  87. <view class="settings-panel">
  88. <text class="panel-title">阅读设置</text>
  89. <view class="setting-item">
  90. <text>字体大小</text>
  91. <slider :value="fontSize" min="24" max="40" @change="updateFontSize" />
  92. </view>
  93. <view class="setting-item">
  94. <text>背景颜色</text>
  95. <view class="color-options">
  96. <view
  97. v-for="color in backgroundColors"
  98. :key="color.value"
  99. class="color-option"
  100. :class="{ active: readerStyles.backgroundColor === color.value }"
  101. :style="{ backgroundColor: color.value }"
  102. @click="changeBackgroundColor(color.value)"
  103. ></view>
  104. </view>
  105. </view>
  106. <button class="close-btn" @click="$refs.settingsPopup.close()">关闭</button>
  107. </view>
  108. </uni-popup>
  109. </view>
  110. </template>
  111. <script>
  112. export default {
  113. data() {
  114. return {
  115. novelId: null,
  116. chapterId: null,
  117. novelTitle: '',
  118. chapterDetail: {},
  119. chapterContent: '',
  120. chapters: [],
  121. currentChapterIndex: 0,
  122. // 阅读统计
  123. readChapters: [],
  124. readingProgress: 0,
  125. scrollTop: 0,
  126. isChapterEnd: false,
  127. // 阅读设置
  128. fontSize: 32,
  129. backgroundColors: [
  130. { name: '护眼模式', value: '#f8f2e0' },
  131. { name: '白色', value: '#ffffff' },
  132. { name: '夜间', value: '#2c2c2c' },
  133. { name: '淡蓝', value: '#e8f4f8' }
  134. ],
  135. readerStyles: {
  136. fontSize: '32rpx',
  137. lineHeight: '1.8',
  138. backgroundColor: '#f8f2e0',
  139. color: '#333'
  140. }
  141. }
  142. },
  143. computed: {
  144. // 计算属性
  145. isFirstChapter() {
  146. return this.currentChapterIndex === 0
  147. },
  148. isLastChapter() {
  149. return this.currentChapterIndex === this.chapters.length - 1
  150. },
  151. readChaptersCount() {
  152. return this.readChapters.length
  153. },
  154. // 检查是否达到免费章节限制
  155. reachedFreeLimit() {
  156. const isLoggedIn = this.$store.getters.token
  157. return !isLoggedIn && this.readChaptersCount >= 5 // 默认5章免费
  158. }
  159. },
  160. async onLoad(options) {
  161. console.log('阅读器页面参数:', options)
  162. // 确保参数正确解析
  163. this.novelId = options.novelId || null
  164. this.chapterId = options.chapterId || null
  165. if (!this.novelId) {
  166. uni.showToast({
  167. title: '小说ID参数缺失',
  168. icon: 'none'
  169. })
  170. setTimeout(() => {
  171. uni.navigateBack()
  172. }, 1500)
  173. return
  174. }
  175. // 加载阅读进度
  176. this.loadReadingProgress()
  177. try {
  178. await this.loadNovelData()
  179. // 如果有指定章节ID,加载该章节,否则加载第一章
  180. if (this.chapterId) {
  181. await this.loadChapter(this.chapterId)
  182. } else {
  183. // 尝试从阅读记录中恢复
  184. const lastReadChapter = this.getLastReadChapter()
  185. if (lastReadChapter) {
  186. await this.loadChapter(lastReadChapter.id)
  187. } else {
  188. await this.loadFirstChapter()
  189. }
  190. }
  191. } catch (error) {
  192. console.error('页面初始化失败:', error)
  193. uni.showToast({
  194. title: '页面初始化失败',
  195. icon: 'none'
  196. })
  197. }
  198. // 监听网络状态
  199. uni.onNetworkStatusChange(this.handleNetworkChange)
  200. },
  201. onUnload() {
  202. // 保存阅读进度
  203. this.saveReadingProgress()
  204. // 移除网络状态监听
  205. uni.offNetworkStatusChange(this.handleNetworkChange)
  206. },
  207. methods: {
  208. // 修复响应解析方法
  209. parseResponse(res) {
  210. console.log('原始响应对象:', res)
  211. // 处理不同的响应格式
  212. let responseData = {}
  213. // 如果res有arg属性,使用arg作为响应数据
  214. if (res && res.arg) {
  215. responseData = res.arg
  216. console.log('使用res.arg作为响应数据:', responseData)
  217. }
  218. // 如果res有data属性,使用data作为响应数据
  219. else if (res && res.data) {
  220. responseData = res.data
  221. console.log('使用res.data作为响应数据:', responseData)
  222. }
  223. // 如果res本身就是响应数据
  224. else {
  225. responseData = res
  226. console.log('使用res本身作为响应数据:', responseData)
  227. }
  228. return responseData
  229. },
  230. // 加载小说数据
  231. async loadNovelData() {
  232. try {
  233. uni.showLoading({ title: '加载中...' })
  234. // 加载小说基本信息
  235. const novelRes = await this.$http.get(`/novel/detail/${this.novelId}`)
  236. // 使用统一的响应解析方法
  237. const novelData = this.parseResponse(novelRes)
  238. console.log('小说详情响应:', novelData)
  239. if (novelData.code === 200 && novelData.data) {
  240. this.novelTitle = novelData.data.title
  241. uni.setNavigationBarTitle({ title: this.novelTitle })
  242. }
  243. // 加载章节列表
  244. const chapterRes = await this.$http.get(`/chapter/list/${this.novelId}`)
  245. // 使用统一的响应解析方法
  246. const chapterData = this.parseResponse(chapterRes)
  247. console.log('章节列表响应:', chapterData)
  248. if (chapterData.code === 200) {
  249. // 处理不同的响应格式
  250. if (Array.isArray(chapterData.rows)) {
  251. this.chapters = chapterData.rows
  252. } else if (Array.isArray(chapterData.data)) {
  253. this.chapters = chapterData.data
  254. }
  255. // 初始化当前章节索引
  256. if (this.chapterId) {
  257. this.currentChapterIndex = this.chapters.findIndex(ch => ch.id === this.chapterId)
  258. }
  259. }
  260. } catch (error) {
  261. console.error('加载小说数据失败:', error)
  262. uni.showToast({
  263. title: '加载失败',
  264. icon: 'none'
  265. })
  266. } finally {
  267. uni.hideLoading()
  268. }
  269. },
  270. // 加载章节内容
  271. async loadChapter(chapterId) {
  272. // 检查章节是否锁定
  273. const chapterIndex = this.chapters.findIndex(ch => ch.id === chapterId)
  274. if (this.isChapterLocked(chapterIndex)) {
  275. this.showLoginPrompt()
  276. return
  277. }
  278. try {
  279. uni.showLoading({ title: '加载中...' })
  280. const res = await this.$http.get(`/chapter/content/${chapterId}`)
  281. // 使用统一的响应解析方法
  282. const responseData = this.parseResponse(res)
  283. console.log('章节内容响应:', responseData)
  284. if (responseData.code === 200 && responseData.data) {
  285. this.chapterDetail = responseData.data
  286. this.chapterContent = this.formatContent(responseData.data.content)
  287. this.chapterId = chapterId
  288. this.currentChapterIndex = chapterIndex
  289. // 标记为已读
  290. this.markChapterAsRead(chapterId)
  291. // 计算阅读进度
  292. this.calculateReadingProgress()
  293. }
  294. } catch (error) {
  295. console.error('加载章节内容失败:', error)
  296. uni.showToast({
  297. title: '加载失败',
  298. icon: 'none'
  299. })
  300. } finally {
  301. uni.hideLoading()
  302. }
  303. },
  304. // 加载第一章
  305. async loadFirstChapter() {
  306. if (this.chapters.length > 0) {
  307. await this.loadChapter(this.chapters[0].id)
  308. }
  309. },
  310. // 格式化内容
  311. formatContent(content) {
  312. if (!content) return ''
  313. // 处理HTML标签和特殊字符
  314. return content
  315. .replace(/<br\s*\/?>/gi, '\n')
  316. .replace(/&nbsp;/g, ' ')
  317. .replace(/<p>/g, '\n\n')
  318. .replace(/<\/p>/g, '')
  319. .replace(/<[^>]+>/g, '')
  320. .replace(/\n{3,}/g, '\n\n')
  321. .trim()
  322. },
  323. // 标记章节为已读
  324. markChapterAsRead(chapterId) {
  325. if (!this.readChapters.includes(chapterId)) {
  326. this.readChapters.push(chapterId)
  327. this.saveReadingProgress()
  328. }
  329. },
  330. // 检查章节是否已读
  331. isChapterRead(chapterId) {
  332. return this.readChapters.includes(chapterId)
  333. },
  334. // 检查章节是否锁定
  335. isChapterLocked(chapterIndex) {
  336. // 已登录用户可以阅读所有章节
  337. if (this.$store.getters.token) return false
  338. // 未登录用户只能阅读前5章
  339. return chapterIndex >= 5
  340. },
  341. // 加载阅读进度
  342. loadReadingProgress() {
  343. const progress = uni.getStorageSync(`readingProgress_${this.novelId}`)
  344. if (progress) {
  345. this.readChapters = progress.readChapters || []
  346. this.readerStyles = progress.readerStyles || this.readerStyles
  347. this.fontSize = progress.fontSize || 32
  348. }
  349. },
  350. // 保存阅读进度
  351. saveReadingProgress() {
  352. const progress = {
  353. novelId: this.novelId,
  354. chapterId: this.chapterId,
  355. readChapters: this.readChapters,
  356. readerStyles: this.readerStyles,
  357. fontSize: this.fontSize,
  358. timestamp: Date.now()
  359. }
  360. uni.setStorageSync(`readingProgress_${this.novelId}`, progress)
  361. // 同步到服务器(如果已登录)
  362. if (this.$store.getters.token) {
  363. this.$http.post('/reading/progress', progress)
  364. }
  365. },
  366. // 获取最后阅读的章节
  367. getLastReadChapter() {
  368. if (this.chapterId) {
  369. return this.chapters.find(ch => ch.id === this.chapterId)
  370. }
  371. const progress = uni.getStorageSync(`readingProgress_${this.novelId}`)
  372. if (progress && progress.chapterId) {
  373. return this.chapters.find(ch => ch.id === progress.chapterId)
  374. }
  375. return null
  376. },
  377. // 计算阅读进度
  378. calculateReadingProgress() {
  379. if (this.chapters.length === 0) return 0
  380. this.readingProgress = Math.round((this.readChaptersCount / this.chapters.length) * 100)
  381. },
  382. // 显示登录提示
  383. showLoginPrompt() {
  384. this.$refs.loginPopup.open()
  385. },
  386. // 关闭登录对话框
  387. closeLoginDialog() {
  388. this.$refs.loginPopup.close()
  389. },
  390. // 跳转到登录页面
  391. goToLogin() {
  392. uni.navigateTo({
  393. url: '/pages/login?redirect=' + encodeURIComponent(this.$route.fullPath)
  394. })
  395. },
  396. // 上一章
  397. prevChapter() {
  398. if (this.currentChapterIndex > 0) {
  399. const prevChapter = this.chapters[this.currentChapterIndex - 1]
  400. this.loadChapter(prevChapter.id)
  401. }
  402. },
  403. // 下一章
  404. nextChapter() {
  405. if (this.currentChapterIndex < this.chapters.length - 1) {
  406. const nextChapter = this.chapters[this.currentChapterIndex + 1]
  407. // 检查下一章是否锁定
  408. if (this.isChapterLocked(this.currentChapterIndex + 1)) {
  409. this.showLoginPrompt()
  410. return
  411. }
  412. this.loadChapter(nextChapter.id)
  413. }
  414. },
  415. // 选择章节
  416. selectChapter(chapter, index) {
  417. // 检查章节是否锁定
  418. if (this.isChapterLocked(index)) {
  419. this.showLoginPrompt()
  420. return
  421. }
  422. this.loadChapter(chapter.id)
  423. this.$refs.drawer.close()
  424. },
  425. // 显示更多操作
  426. showMoreActions() {
  427. uni.showActionSheet({
  428. itemList: ['添加到书架', '分享', '举报', '设置'],
  429. success: (res) => {
  430. switch (res.tapIndex) {
  431. case 0:
  432. this.addToBookshelf()
  433. break
  434. case 1:
  435. this.shareNovel()
  436. break
  437. case 2:
  438. this.reportNovel()
  439. break
  440. case 3:
  441. this.toggleSettings()
  442. break
  443. }
  444. }
  445. })
  446. },
  447. // 添加到书架
  448. addToBookshelf() {
  449. if (!this.$store.getters.token) {
  450. uni.showToast({
  451. title: '请先登录',
  452. icon: 'none'
  453. })
  454. return
  455. }
  456. this.$http.post('/bookshelf/add', {
  457. novelId: this.novelId
  458. }).then(res => {
  459. uni.showToast({
  460. title: '已添加到书架',
  461. icon: 'success'
  462. })
  463. }).catch(error => {
  464. uni.showToast({
  465. title: '添加失败',
  466. icon: 'none'
  467. })
  468. })
  469. },
  470. // 分享小说
  471. shareNovel() {
  472. uni.share({
  473. title: this.novelTitle,
  474. path: `/pages/novel/reader?novelId=${this.novelId}`,
  475. success: () => {
  476. uni.showToast({
  477. title: '分享成功',
  478. icon: 'success'
  479. })
  480. }
  481. })
  482. },
  483. // 举报小说
  484. reportNovel() {
  485. uni.navigateTo({
  486. url: `/pages/report?novelId=${this.novelId}`
  487. })
  488. },
  489. // 打开目录
  490. toggleMenu() {
  491. this.$refs.drawer.open()
  492. },
  493. // 打开设置
  494. toggleSettings() {
  495. this.$refs.settingsPopup.open()
  496. },
  497. // 更新字体大小
  498. updateFontSize(e) {
  499. this.fontSize = e.detail.value
  500. this.readerStyles.fontSize = `${this.fontSize}rpx`
  501. this.saveReadingProgress()
  502. },
  503. // 更改背景颜色
  504. changeBackgroundColor(color) {
  505. this.readerStyles.backgroundColor = color
  506. // 根据背景色调整文字颜色
  507. this.readerStyles.color = color === '#2c2c2c' ? '#f0f0f0' : '#333'
  508. this.saveReadingProgress()
  509. },
  510. // 返回上一页
  511. goBack() {
  512. uni.navigateBack()
  513. },
  514. // 滚动事件
  515. onScroll(e) {
  516. this.scrollTop = e.detail.scrollTop
  517. // 检测是否滚动到章节末尾
  518. const scrollHeight = e.detail.scrollHeight
  519. const scrollTop = e.detail.scrollTop
  520. const clientHeight = e.detail.clientHeight
  521. this.isChapterEnd = scrollHeight - scrollTop - clientHeight < 50
  522. },
  523. // 处理网络状态变化
  524. handleNetworkChange(res) {
  525. if (!res.isConnected) {
  526. uni.showToast({
  527. title: '网络已断开',
  528. icon: 'none'
  529. })
  530. }
  531. }
  532. }
  533. }
  534. </script>
  535. <style scoped>
  536. .reader-container {
  537. position: relative;
  538. height: 100vh;
  539. padding: 20rpx;
  540. box-sizing: border-box;
  541. transition: all 0.3s ease;
  542. }
  543. .reader-header {
  544. position: absolute;
  545. top: 0;
  546. left: 0;
  547. right: 0;
  548. display: flex;
  549. justify-content: space-between;
  550. align-items: center;
  551. padding: 20rpx 30rpx;
  552. background: rgba(0, 0, 0, 0.7);
  553. color: white;
  554. z-index: 100;
  555. }
  556. .novel-title {
  557. font-size: 32rpx;
  558. max-width: 60%;
  559. overflow: hidden;
  560. text-overflow: ellipsis;
  561. white-space: nowrap;
  562. }
  563. .reader-content {
  564. height: calc(100vh - 200rpx);
  565. padding-top: 80rpx;
  566. padding-bottom: 120rpx;
  567. }
  568. .chapter-title {
  569. font-size: 40rpx;
  570. font-weight: bold;
  571. text-align: center;
  572. margin-bottom: 40rpx;
  573. color: #2a5caa;
  574. }
  575. .content-text {
  576. font-size: 32rpx;
  577. line-height: 1.8;
  578. }
  579. .chapter-end {
  580. text-align: center;
  581. margin-top: 40rpx;
  582. padding: 20rpx;
  583. border-top: 1rpx solid #eee;
  584. }
  585. .reader-footer {
  586. position: absolute;
  587. bottom: 0;
  588. left: 0;
  589. right: 0;
  590. background: rgba(255, 255, 255, 0.9);
  591. padding: 20rpx;
  592. border-top: 1rpx solid #eee;
  593. }
  594. .progress {
  595. display: flex;
  596. justify-content: space-between;
  597. font-size: 28rpx;
  598. color: #666;
  599. margin-bottom: 20rpx;
  600. }
  601. .actions {
  602. display: flex;
  603. justify-content: space-between;
  604. }
  605. .action-btn {
  606. display: flex;
  607. flex-direction: column;
  608. align-items: center;
  609. background: none;
  610. border: none;
  611. font-size: 24rpx;
  612. padding: 10rpx;
  613. }
  614. .action-btn:disabled {
  615. opacity: 0.5;
  616. }
  617. .drawer-content {
  618. padding: 30rpx;
  619. }
  620. .drawer-title {
  621. font-size: 36rpx;
  622. font-weight: bold;
  623. display: block;
  624. margin-bottom: 20rpx;
  625. border-bottom: 1rpx solid #eee;
  626. padding-bottom: 20rpx;
  627. }
  628. .chapter-stats {
  629. display: flex;
  630. justify-content: space-between;
  631. margin-bottom: 20rpx;
  632. font-size: 24rpx;
  633. color: #666;
  634. }
  635. .chapter-list {
  636. height: calc(100vh - 200rpx);
  637. }
  638. .chapter-item {
  639. display: flex;
  640. justify-content: space-between;
  641. align-items: center;
  642. padding: 20rpx 10rpx;
  643. border-bottom: 1rpx solid #f0f0f0;
  644. font-size: 28rpx;
  645. }
  646. .chapter-item.active {
  647. color: #2a5caa;
  648. font-weight: bold;
  649. }
  650. .chapter-item.read {
  651. color: #666;
  652. }
  653. .chapter-item.locked {
  654. color: #999;
  655. }
  656. .settings-panel {
  657. padding: 30rpx;
  658. background: white;
  659. border-radius: 20rpx 20rpx 0 0;
  660. }
  661. .panel-title {
  662. font-size: 36rpx;
  663. font-weight: bold;
  664. display: block;
  665. margin-bottom: 30rpx;
  666. text-align: center;
  667. }
  668. .setting-item {
  669. margin-bottom: 30rpx;
  670. }
  671. .setting-item text {
  672. display: block;
  673. margin-bottom: 15rpx;
  674. font-size: 28rpx;
  675. }
  676. .color-options {
  677. display: flex;
  678. justify-content: space-between;
  679. }
  680. .color-option {
  681. width: 60rpx;
  682. height: 60rpx;
  683. border-radius: 50%;
  684. border: 2rpx solid #eee;
  685. }
  686. .color-option.active {
  687. border-color: #2a5caa;
  688. }
  689. .close-btn {
  690. margin-top: 30rpx;
  691. background: #f0f0f0;
  692. color: #333;
  693. }
  694. </style>