From 7f51b2a0a8ba209ba2fa90a1f5002a38cac98d5f Mon Sep 17 00:00:00 2001 From: Blizzard Date: Thu, 5 Mar 2026 17:04:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 5 + pages/channel-detail/index.js | 74 +++++-- pages/channel-detail/index.wxml | 71 +++--- pages/channel-detail/index.wxss | 57 ++++- pages/discover/index.js | 37 ++-- pages/discover/index.wxml | 18 +- pages/history/index.js | 210 ++++++++++++++---- pages/history/index.wxml | 139 +++++++++--- pages/history/index.wxss | 367 ++++++++++++++++++++++++++------ pages/index/index.js | 114 ++++------ pages/index/index.wxml | 19 +- pages/index/index.wxss | 33 +++ pages/onboarding/index.js | 64 ------ pages/onboarding/index.json | 6 - pages/onboarding/index.wxml | 50 ----- pages/onboarding/index.wxss | 170 --------------- pages/player/index.js | 130 +++++++++-- pages/player/index.wxml | 79 +++++-- pages/player/index.wxss | 147 +++++++++++++ pages/profile/index.js | 37 ---- pages/profile/index.wxml | 104 ++++----- pages/profile/index.wxss | 357 +++++++++++++++++-------------- pages/vip/index.js | 127 +++++++++-- pages/vip/index.wxml | 80 ++++--- pages/vip/index.wxss | 155 ++++++++++++-- utils/api.js | 64 +++++- utils/audioManager.js | 19 +- utils/request.js | 4 +- 28 files changed, 1773 insertions(+), 964 deletions(-) delete mode 100644 pages/onboarding/index.js delete mode 100644 pages/onboarding/index.json delete mode 100644 pages/onboarding/index.wxml delete mode 100644 pages/onboarding/index.wxss diff --git a/app.js b/app.js index 38a23fd..8400645 100644 --- a/app.js +++ b/app.js @@ -191,8 +191,13 @@ App({ self.globalData.isLoggedIn = true self.globalData.token = token self.globalData.userInfo = user + self.globalData.isVip = user.isVip === 1 + self.globalData.vipExpireAt = user.vipExpireAt || null wx.setStorageSync('token', token) self.emit('loginStateChange', { isLoggedIn: true }) + if (user.isVip === 1) { + self.emit('vipChange', { isVip: true }) + } resolve(res.data) } else { reject(new Error(res.msg || '登录失败')) diff --git a/pages/channel-detail/index.js b/pages/channel-detail/index.js index f1aaa96..bac8a68 100644 --- a/pages/channel-detail/index.js +++ b/pages/channel-detail/index.js @@ -13,10 +13,12 @@ Page({ data: { domain: {}, isSubscribed: false, - isExpired: false, // 订阅是否已过期 - expiredAt: '', // 到期时间(格式化) - isFree: false, // 快捷字段,避免模板 domain.isFree + isExpired: false, + expiredAt: '', + isFree: false, isVipOnly: false, + isVip: false, + canPlay: false, domainContents: [], isPlaying: false, loading: true @@ -52,7 +54,6 @@ Page({ const ch = res.data const isFree = ch.isFree === 1 - // 免费频道:不关心订阅状态和到期时间 var expiredAt = '' var isExpired = false var isSubscribed = false @@ -64,13 +65,18 @@ Page({ isSubscribed = ch.hasSubscribed === 1 && !isExpired } + // 可播放:VIP 或免费频道或已订阅 + const canPlay = app.globalData.isVip || isFree || isSubscribed + self.setData({ domain: ch, isSubscribed, isExpired, expiredAt, isFree, - isVipOnly: ch.isVipOnly === 1 + isVipOnly: ch.isVipOnly === 1, + isVip: app.globalData.isVip, + canPlay }) } }).catch(function (err) { @@ -92,8 +98,7 @@ Page({ } var gd = app.globalData - var isSubscribed = self.data.isSubscribed - var isFree = self.data.domain.isFree === 1 + var canPlay = self.data.canPlay var total = contents.length contents = contents.map(function (item, idx) { @@ -102,7 +107,7 @@ Page({ _dateDot: item.createdAt ? item.createdAt.substring(0, 10).replace(/-/g, '.') : '', durationText: util.formatTime(item.duration || 0), _isThisPlaying: gd.activeContent && gd.activeContent.id === item.id, - _isLocked: !isSubscribed && !isFree && idx > 0 + _isLocked: !canPlay // 所有集全部锁定,没有试听 }) }) @@ -133,11 +138,12 @@ Page({ onPlayItem(e) { const id = e.currentTarget.dataset.id - const idx = parseInt(e.currentTarget.dataset.idx) const gd = app.globalData - if (!this.data.isSubscribed && !(this.data.domain.isFree === 1) && idx > 0) { - wx.showToast({ title: '请先订阅该频道以解锁往期内容', icon: 'none' }) + // 核心权限判断:VIP || 免费频道 || 已订阅 + if (!this.data.canPlay) { + // 引导到支付页 + this.onSubscribe() return } @@ -161,25 +167,51 @@ Page({ const id = this._domainId const domain = this.data.domain - // 已订阅 → 已在订阅中,无需操作 - if (this.data.isSubscribed) { - wx.showToast({ title: '您已订阅该频道', icon: 'none' }) + // 已可播放(VIP / 免费 / 已订阅)——正常情况下不会触发此方法 + if (this.data.canPlay) { + wx.showToast({ title: '您已可收听该频道', icon: 'none' }) return } - // 免费频道 → 直接收听 + // VIP专享频道:不支持单独订阅,必须开通 VIP + if (domain.isVipOnly === 1) { + wx.navigateTo({ url: '/pages/vip/index' }) + return + } + + // 免费频道(理论上不会到这里,安全先) if (domain.isFree === 1) { wx.showToast({ title: '免费频道,直接收听!', icon: 'none' }) return } - // VIP专享且未开通 → VIP页 - if (domain.isVipOnly === 1 && !app.globalData.isVip) { - wx.navigateTo({ url: '/pages/vip/index' }) - return - } + // 付费频道(未订阅 / 已过期)——跳转订阅/支付页 + var params = 'channelId=' + id + + '&channelName=' + encodeURIComponent(domain.name || '') + + '&monthlyPrice=' + (domain.monthlyPrice || 0) + + '&quarterlyPrice=' + (domain.quarterlyPrice || 0) + + '&annualPrice=' + (domain.annualPrice || 0) + wx.navigateTo({ url: '/pages/vip/index?' + params }) + }, - // 付费频道(含已过期续费)→ 跳转订阅/支付页 + goFirstProgram() { + var list = this.data.domainContents + if (list && list.length > 0) { + var first = list[0] + if (app.globalData.activeContent && app.globalData.activeContent.id === first.id) { + app.togglePlay() + } else { + app.playContent(first) + } + } else { + wx.showToast({ title: '暂无节目', icon: 'none' }) + } + }, + + /** 续订:直接跳支付页,不走 canPlay 检查 */ + onRenew() { + const id = this._domainId + const domain = this.data.domain var params = 'channelId=' + id + '&channelName=' + encodeURIComponent(domain.name || '') + '&monthlyPrice=' + (domain.monthlyPrice || 0) diff --git a/pages/channel-detail/index.wxml b/pages/channel-detail/index.wxml index ba0af09..2df7e84 100644 --- a/pages/channel-detail/index.wxml +++ b/pages/channel-detail/index.wxml @@ -9,10 +9,27 @@ {{domain.name}} {{domain.tag || domain.description || ''}} - + - - + + + + + 👑 VIP 会员畅享 + 🎁 永久免费频道 + + 有效至 {{expiredAt || '长期有效'}} + + + 续订 + + + + + + 🎁 永久免费 - - - - 有效至 {{expiredAt}} - - - + ⏰ 订阅已到期 - 已于 {{expiredAt}} 到期 + 已于 {{expiredAt}} 到期 - + - - - - diff --git a/pages/onboarding/index.wxss b/pages/onboarding/index.wxss deleted file mode 100644 index 486d639..0000000 --- a/pages/onboarding/index.wxss +++ /dev/null @@ -1,170 +0,0 @@ -/* 首次引导页样式 */ - -.onboarding-page { - height: 100vh; - display: flex; - flex-direction: column; - background: #FFFFFF; -} - -/* 顶部 */ -.header { - padding: 24rpx 40rpx 24rpx; - position: sticky; - top: 0; - z-index: 10; - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(20px); - border-bottom: 1rpx solid #F5F5F5; -} -.header-title { - font-size: 44rpx; - font-weight: 700; - color: #333; - display: block; -} -.header-sub { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 16rpx; -} -.sub-text { - font-size: 26rpx; - color: #999; -} -.sub-warn { - font-size: 22rpx; - color: var(--color-primary); -} - -/* 网格滚动 */ -.grid-scroll { - flex: 1; - overflow-y: auto; - scrollbar-width: none; - -ms-overflow-style: none; -} -.grid-scroll::-webkit-scrollbar { - display: none; - width: 0 !important; - height: 0 !important; -} -.grid { - display: flex; - flex-wrap: wrap; - padding: 24rpx 20rpx 200rpx; - gap: 16rpx; -} - -/* 网格项 */ -.grid-item { - width: calc(33.33% - 12rpx); - box-sizing: border-box; - background: #F8F8F8; - border-radius: 32rpx; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 28rpx 16rpx; - position: relative; - border: 4rpx solid transparent; - transition: all 0.3s ease; -} -.grid-item.selected { - border-color: var(--color-primary); - background: var(--color-primary-light); - box-shadow: 0 8rpx 24rpx rgba(255, 157, 66, 0.15); - transform: scale(1.02); -} - -/* 图标 */ -.item-icon { - width: 80rpx; - height: 80rpx; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 16rpx; -} -.icon-emoji { - font-size: 40rpx; -} - -/* 文字 */ -.item-name { - font-size: 24rpx; - font-weight: 700; - color: #333; - text-align: center; - line-height: 1.3; - margin-bottom: 6rpx; -} -.item-tag { - font-size: 18rpx; - color: #999; - text-align: center; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 100%; -} - -/* 选中对勾 */ -.check-mark { - position: absolute; - top: 12rpx; - right: 12rpx; - width: 40rpx; - height: 40rpx; - border-radius: 50%; - background: var(--color-primary); - display: flex; - align-items: center; - justify-content: center; -} -.check-icon { - font-size: 22rpx; - color: #FFF; - font-weight: 700; -} - -/* 底部 */ -.footer { - position: fixed; - bottom: 0; - left: 0; - right: 0; - padding: 24rpx 40rpx; - padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); - background: linear-gradient(to top, #FFFFFF, #FFFFFF, rgba(255,255,255,0)); -} -.confirm-btn { - width: 100%; - height: 100rpx; - border-radius: 999rpx; - font-size: 32rpx; - font-weight: 600; - border: none; - display: flex; - align-items: center; - justify-content: center; - line-height: 1; -} -.confirm-btn.active { - background: var(--color-primary); - color: #FFF; - box-shadow: 0 12rpx 32rpx rgba(255, 157, 66, 0.3); -} -.confirm-btn.active:active { - transform: scale(0.97); -} -.confirm-btn.disabled { - background: #E5E5E5; - color: #BBB; -} -.confirm-btn::after { - border: none; -} diff --git a/pages/player/index.js b/pages/player/index.js index fca6c70..48c249f 100644 --- a/pages/player/index.js +++ b/pages/player/index.js @@ -13,6 +13,7 @@ Page({ activeContent: null, isPlaying: false, isVip: false, + isLiked: false, currentTime: 0, duration: 0, currentTimeText: '00:00', @@ -20,16 +21,13 @@ Page({ displayDate: '', playbackRate: 1.0, statusBarHeight: 0, - showTranscript: false, // 封面 ⇔ 文案切换 - showSpeedSheet: false, - speedItems: [ - { label: '0.5x' }, - { label: '0.75x' }, - { label: '1.0x' }, - { label: '1.25x' }, - { label: '1.5x' }, - { label: '2.0x' } - ] + showTranscript: false, + // 评论弹层 + showComments: false, + commentList: [], + commentText: '', + commentLoading: false, + submitting: false }, _isSeeking: false, @@ -65,6 +63,9 @@ Page({ app.on('playerStateChange', this._onPlayerChange) app.on('timeUpdate', this._onTimeUpdate) + + // 查询当前节目点赞状态 + this._loadLikeStatus() }, onHide() { @@ -108,7 +109,7 @@ Page({ }, /** - * 获取频道信息 — 从后端 API 获取 + * 获取频道信息 */ _updateDomain() { const content = this.data.activeContent @@ -116,14 +117,12 @@ Page({ var channelId = content.channelId || (content.channel && content.channel.id) if (!channelId) { - // 如果节目数据中直接包含 channel 信息 if (content.channel) { this.setData({ domain: content.channel }) } return } - // 如果已经加载过且 channelId 没变,跳过 if (this.data.domain && this.data.domain.id === channelId) return var self = this @@ -136,6 +135,20 @@ Page({ }) }, + /** + * 查询当前节目点赞状态 + */ + _loadLikeStatus() { + const content = this.data.activeContent + if (!content) return + var self = this + api.getProgramDetail(content.id).then(function (res) { + if (res.code === 200 && res.data) { + self.setData({ isLiked: !!res.data.isLiked }) + } + }).catch(function () { }) + }, + /** * 播放/暂停 */ @@ -251,13 +264,100 @@ Page({ onLike() { const content = this.data.activeContent if (!content) return - api.toggleLike({ contentId: content.id }).then(function (res) { - wx.showToast({ title: res.code === 200 ? '已收藏 ♥' : '操作失败', icon: 'none' }) + const self = this + const wasLiked = this.data.isLiked + // 乐观更新 + this.setData({ isLiked: !wasLiked }) + api.toggleLike(content.id).then(function (res) { + if (res.code !== 200) { + // 回滚 + self.setData({ isLiked: wasLiked }) + wx.showToast({ title: res.msg || '操作失败', icon: 'none' }) + } else { + wx.showToast({ title: wasLiked ? '已取消喜欢' : '已喜欢 ♥', icon: 'none' }) + } }).catch(function () { + self.setData({ isLiked: wasLiked }) wx.showToast({ title: '网络异常', icon: 'none' }) }) }, + // ===== 评论弹层 ===== + + onOpenComments() { + this.setData({ showComments: true }) + this._loadComments() + }, + + onCloseComments() { + this.setData({ showComments: false, commentText: '' }) + }, + + _loadComments() { + const content = this.data.activeContent + if (!content) return + const self = this + this.setData({ commentLoading: true }) + api.getCommentList(content.id, { current: 1, pageSize: 30 }).then(function (res) { + if (res.code === 200 && res.data) { + var list = (res.data.list || res.data || []).map(function (c) { + return Object.assign({}, c, { + _isOwn: c.userId === (getApp().globalData.userInfo && getApp().globalData.userInfo.id) + }) + }) + self.setData({ commentList: list, commentLoading: false }) + } else { + self.setData({ commentLoading: false }) + } + }).catch(function () { + self.setData({ commentLoading: false }) + }) + }, + + onCommentInput(e) { + this.setData({ commentText: e.detail.value }) + }, + + onSubmitComment() { + const text = (this.data.commentText || '').trim() + if (!text) return + const content = this.data.activeContent + if (!content) return + const self = this + this.setData({ submitting: true }) + api.addComment(content.id, text).then(function (res) { + self.setData({ submitting: false, commentText: '' }) + if (res.code === 200) { + wx.showToast({ title: '发布成功', icon: 'none' }) + self._loadComments() + } else { + wx.showToast({ title: res.msg || '发布失败', icon: 'none' }) + } + }).catch(function () { + self.setData({ submitting: false }) + wx.showToast({ title: '网络异常', icon: 'none' }) + }) + }, + + onDeleteComment(e) { + const id = e.currentTarget.dataset.id + const self = this + wx.showModal({ + title: '提示', + content: '确认删除该评论吗?', + success(res) { + if (!res.confirm) return + api.deleteComment(id).then(function (r) { + if (r.code === 200) { + self._loadComments() + } else { + wx.showToast({ title: '删除失败', icon: 'none' }) + } + }) + } + }) + }, + onShare() { wx.showToast({ title: '分享功能开发中', icon: 'none' }) }, diff --git a/pages/player/index.wxml b/pages/player/index.wxml index 6b3005f..7a7c18b 100644 --- a/pages/player/index.wxml +++ b/pages/player/index.wxml @@ -2,7 +2,7 @@ - + @@ -10,7 +10,7 @@ style="background: radial-gradient(ellipse at 50% -10%, {{domain.bgColor || '#FF9D42'}}44 0%, transparent 65%);"> - + @@ -19,8 +19,8 @@ 正在播放 {{activeContent.title || '加载中...'}} - - + + 💬 @@ -36,7 +36,7 @@ - + 📻 @@ -58,11 +58,11 @@ - + {{displayDate}} @@ -107,11 +107,62 @@ - - + + + + + + 评论 ({{commentList.length}}) + + + + + + + + + + 发送 + + + + + + + 加载中... + + + 💬 + 还没有评论,来说第一句话 + + + + {{item.userName ? item.userName[0] : '?'}} + + + {{item.userName || '匿名'}} + {{item.content}} + {{item.createdAtStr || item.createdAt}} + + + 🗑 + + + + + + diff --git a/pages/player/index.wxss b/pages/player/index.wxss index 7053971..d73a764 100644 --- a/pages/player/index.wxss +++ b/pages/player/index.wxss @@ -320,3 +320,150 @@ background: #FFF8EE; border-radius: 5rpx; } + +/* ── like 状态 ── */ +.like-icon.liked { + color: #FF4D6D; +} +.top-comment-icon { + font-size: 38rpx; + line-height: 1; +} + +/* ── 评论弹层 ── */ +.comment-mask { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.5); + z-index: 200; +} +.comment-sheet { + position: fixed; + bottom: -100%; + left: 0; + right: 0; + height: 70vh; + background: #231808; + border-radius: 40rpx 40rpx 0 0; + z-index: 201; + display: flex; + flex-direction: column; + padding: 0 0 env(safe-area-inset-bottom); + transition: bottom 0.3s cubic-bezier(0.32,0.72,0,1); +} +.comment-sheet.sheet-up { + bottom: 0; +} +.sheet-handle { + width: 72rpx; + height: 6rpx; + background: rgba(255,200,120,0.2); + border-radius: 3rpx; + margin: 20rpx auto 0; +} +.sheet-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 24rpx 40rpx; + border-bottom: 1rpx solid rgba(255,200,120,0.08); +} +.sheet-title { + font-size: 30rpx; + font-weight: 700; + color: rgba(255,240,210,0.9); +} +.sheet-close-icon { + font-size: 32rpx; + color: rgba(255,200,120,0.4); +} +.comment-input-row { + display: flex; + align-items: center; + gap: 16rpx; + padding: 20rpx 32rpx; + border-bottom: 1rpx solid rgba(255,200,120,0.08); +} +.comment-input { + flex: 1; + background: rgba(255,200,120,0.07); + border-radius: 40rpx; + padding: 16rpx 28rpx; + font-size: 28rpx; + color: rgba(255,240,210,0.9); + min-height: 0; +} +.send-btn { + background: #FF9D42; + border-radius: 32rpx; + padding: 14rpx 28rpx; +} +.send-btn.sending { opacity: 0.5; } +.send-text { + font-size: 26rpx; + font-weight: 700; + color: #FFF; +} +.comment-list-scroll { + flex: 1; + padding: 8rpx 0; +} +.comment-loading, .comment-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80rpx 0; + gap: 16rpx; +} +.comment-loading-text, .comment-empty-text { + font-size: 26rpx; + color: rgba(255,200,120,0.3); +} +.comment-empty-icon { font-size: 60rpx; } +.comment-item { + display: flex; + align-items: flex-start; + gap: 20rpx; + padding: 24rpx 32rpx; + border-bottom: 1rpx solid rgba(255,200,120,0.05); +} +.comment-avatar { + width: 64rpx; + height: 64rpx; + border-radius: 50%; + background: rgba(255,157,66,0.25); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.comment-avatar-text { + font-size: 28rpx; + color: #FF9D42; + font-weight: 700; +} +.comment-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 6rpx; +} +.comment-author { + font-size: 24rpx; + font-weight: 600; + color: rgba(255,200,120,0.6); +} +.comment-text { + font-size: 28rpx; + color: rgba(255,240,210,0.85); + line-height: 1.6; +} +.comment-time { + font-size: 22rpx; + color: rgba(255,200,120,0.3); +} +.comment-del { + padding: 8rpx; +} +.comment-del-icon { font-size: 32rpx; } diff --git a/pages/profile/index.js b/pages/profile/index.js index 578a295..e8550a4 100644 --- a/pages/profile/index.js +++ b/pages/profile/index.js @@ -9,7 +9,6 @@ Page({ data: { isVip: false, userInfo: null, - subscribedData: [], menuItems: [ { id: 'vip', label: '会员中心', icon: '👑', desc: '未开通', highlight: true }, { id: 'help', label: '帮助与反馈', icon: '❓', desc: '', highlight: false }, @@ -39,24 +38,6 @@ Page({ userInfo: gd.userInfo }) - // 从后端获取已订阅频道 - api.getSubscriptionList({ current: 1, pageSize: 50 }).then(function (res) { - if (res.code === 200 && res.data) { - var subList = res.data.list || res.data || [] - - // 适配封面 URL - subList = subList.map(function (ch) { - return Object.assign({}, ch, { - _coverUrl: (ch.cover && ch.cover.url) || ch.coverUrl || '' - }) - }) - - self.setData({ subscribedData: subList }) - } - }).catch(function (err) { - console.error('[Profile] 加载订阅失败:', err) - }) - // 更新菜单VIP状态 var menuItems = this.data.menuItems.slice() menuItems[0].desc = gd.isVip ? '已开通' : '未开通' @@ -64,24 +45,6 @@ Page({ this.setData({ menuItems: menuItems }) }, - onUnsubscribe(e) { - const id = e.currentTarget.dataset.id - const name = e.currentTarget.dataset.name - const self = this - - wx.showModal({ - title: '提示', - content: '确定要取消订阅【' + name + '】吗?', - success(res) { - if (res.confirm) { - app.unsubscribeFromDomain(id).then(function () { - self._refresh() - }) - } - } - }) - }, - onMenuTap(e) { const id = e.currentTarget.dataset.id if (id === 'vip') { diff --git a/pages/profile/index.wxml b/pages/profile/index.wxml index 8f4a138..02a0604 100644 --- a/pages/profile/index.wxml +++ b/pages/profile/index.wxml @@ -1,85 +1,73 @@ - + - - - -