Skip to content

Commit a462ebd

Browse files
committed
feat: 实现全局音频播放器功能并优化播放体验
- 新增全局音频播放器组件,支持播放队列管理、播放控制、进度调整等功能 - 重构听书、文章列表等页面的音频播放逻辑,统一使用全局播放器 - 添加播放速率调节、播放列表展示等实用功能 - 为AI学习圈页面增加文章详情跳转和官网链接功能 - 修复路由跳转和错误处理中的若干问题
1 parent 07b5c2e commit a462ebd

19 files changed

Lines changed: 1353 additions & 308 deletions

File tree

backend/app/download.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ func extractCourseDownloadData(articles *services.ArticleList, aid int, flag int
287287
continue
288288
}
289289

290-
if article.VideoStatus == 0 && len(article.AudioAliasIds) > 0 {
290+
if article.Audio.Mp3PlayURL != "" && len(article.AudioAliasIds) > 0 {
291291
audioIds[article.ID] = article.Audio.AliasID
292292

293293
var urls []downloader.URL

backend/app/oob.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ func AudioDetail(id string) (detail *services.AudioInfoResp, err error) {
1313
return
1414
}
1515

16+
func AudioDetailAlias(aliasID string) (detail *services.Audio, err error) {
17+
detail, err = getService().AudioDetailAlias(aliasID)
18+
if err != nil {
19+
return
20+
}
21+
return
22+
}
23+
1624
// OdobShelfAdd 听书加入书架
1725
func OdobShelfAdd(enIDs []string) (resp *services.EbookShelfAddResp, err error) {
1826
resp, err = getService().OdobShelfAdd(enIDs)

backend/course.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ func (a *App) ArticleDetail(aType int, aEnid string) (markdown string, err error
6363
return
6464
}
6565

66+
func (a *App) GetArticleIntro(aType int, enid string) (intro *services.ArticleIntro, err error) {
67+
info, err := Instance.ArticleInfo(enid, aType)
68+
if err != nil {
69+
return
70+
}
71+
intro = &info.ArticleInfo
72+
return
73+
}
74+
6675
func (a *App) GetVolcPlayAuthToken(mediaID, securityToken string) (info *services.MediaVolc, err error) {
6776
info, err = Instance.GetVolcPlayAuthToken(mediaID, securityToken)
6877
// fmt.Println(info)

backend/odob.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ func (a *App) AudioDetail(id string) (detail *services.AudioInfoResp, err error)
1515
return
1616
}
1717

18+
// AudioDetailAlias 听书音频播放信息(包含 mp3_play_url)
19+
func (a *App) AudioDetailAlias(aliasID string) (detail *services.Audio, err error) {
20+
detail, err = app.AudioDetailAlias(aliasID)
21+
if err != nil {
22+
return
23+
}
24+
return
25+
}
26+
1827
func (a *App) OdobShelfAdd(enids []string) (resp *services.EbookShelfAddResp, err error) {
1928
resp, err = app.OdobShelfAdd(enids)
2029
if err != nil {

frontend/components.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ declare module 'vue' {
3030
ElDialog: typeof import('element-plus/es')['ElDialog']
3131
ElDivider: typeof import('element-plus/es')['ElDivider']
3232
ElDrawer: typeof import('element-plus/es')['ElDrawer']
33+
ElDropdown: typeof import('element-plus/es')['ElDropdown']
34+
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
35+
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
3336
ElEmpty: typeof import('element-plus/es')['ElEmpty']
3437
ElForm: typeof import('element-plus/es')['ElForm']
3538
ElFormItem: typeof import('element-plus/es')['ElFormItem']
@@ -51,14 +54,17 @@ declare module 'vue' {
5154
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
5255
ElSelect: typeof import('element-plus/es')['ElSelect']
5356
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
57+
ElSlider: typeof import('element-plus/es')['ElSlider']
5458
ElSpace: typeof import('element-plus/es')['ElSpace']
5559
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
5660
ElSwitch: typeof import('element-plus/es')['ElSwitch']
61+
ElTable: typeof import('element-plus/es')['ElTable']
5762
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
5863
ElTabPane: typeof import('element-plus/es')['ElTabPane']
5964
ElTabs: typeof import('element-plus/es')['ElTabs']
6065
ElTag: typeof import('element-plus/es')['ElTag']
6166
ElTooltip: typeof import('element-plus/es')['ElTooltip']
67+
GlobalAudioPlayer: typeof import('./src/components/GlobalAudioPlayer.vue')['default']
6268
Menu: typeof import('./src/components/Menu.vue')['default']
6369
MenuItem: typeof import('./src/components/MenuItem.vue')['default']
6470
NotesItem: typeof import('./src/components/NotesItem.vue')['default']

frontend/src/App.vue

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,81 @@
11
<script lang="ts" setup>
2-
import { onMounted, computed } from 'vue'
2+
import { onMounted, onUnmounted, computed } from 'vue'
33
import Menu from './components/Menu.vue'
4+
import GlobalAudioPlayer from './components/GlobalAudioPlayer.vue'
45
import { ElConfigProvider } from 'element-plus'
56
import 'element-plus/es/components/message/style/css'
67
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
78
import { themeStore } from './stores/theme'
9+
import { playerStore } from './stores/player'
10+
import { AudioDetailAlias } from '../wailsjs/go/backend/App'
811
912
// 初始化主题
1013
const store = themeStore()
1114
onMounted(() => {
1215
store.initTheme()
1316
})
1417
18+
const pStore = playerStore()
19+
const odobCache = new Map<string, { src: string; poster?: string }>()
20+
const odobPending = new Map<string, Promise<{ src: string; poster?: string }>>()
21+
22+
const globalPlayerHeight = computed(() => {
23+
if (!pStore.hasTrack) return 0
24+
const h = Number(pStore.barHeight) || 0
25+
if (h > 0) return h
26+
return pStore.collapsed ? 76 : 120
27+
})
28+
29+
const mainStyle = computed(() => {
30+
return { '--global-player-height': `${globalPlayerHeight.value}px` } as any
31+
})
32+
33+
const resolveOdobSrc = async (aliasId: string) => {
34+
const key = String(aliasId || '').trim()
35+
if (!key) return { src: '' }
36+
const cached = odobCache.get(key)
37+
if (cached) return cached
38+
const pending = odobPending.get(key)
39+
if (pending) return pending
40+
const p = AudioDetailAlias(key)
41+
.then((detail) => {
42+
const src = String(detail?.mp3_play_url ?? '').trim()
43+
const poster = String(detail?.icon ?? '').trim() || undefined
44+
const val = { src, poster }
45+
if (src) odobCache.set(key, val)
46+
return val
47+
})
48+
.finally(() => {
49+
odobPending.delete(key)
50+
})
51+
odobPending.set(key, p)
52+
return p
53+
}
54+
55+
const onResolveTrack = async (ev: any) => {
56+
const detail = ev?.detail || {}
57+
const contextKey = String(detail?.contextKey ?? '')
58+
if (contextKey !== 'odob:study') return
59+
const trackId = String(detail?.trackId ?? '')
60+
if (!trackId) return
61+
const aliasId = trackId.startsWith('odob:') ? trackId.slice(5) : trackId
62+
if (!aliasId) return
63+
try {
64+
const { src, poster } = await resolveOdobSrc(aliasId)
65+
if (!src) return
66+
pStore.updateTrackSource(trackId, src, poster)
67+
} catch {
68+
}
69+
}
70+
71+
onMounted(() => {
72+
window.addEventListener('player:resolveTrack', onResolveTrack as any)
73+
})
74+
75+
onUnmounted(() => {
76+
window.removeEventListener('player:resolveTrack', onResolveTrack as any)
77+
})
78+
1579
// Element Plus 主题配置
1680
const elementTheme = computed(() => store.isDark ? 'dark' : 'light')
1781
</script>
@@ -22,9 +86,10 @@ const elementTheme = computed(() => store.isDark ? 'dark' : 'light')
2286
<el-header>
2387
<Menu />
2488
</el-header>
25-
<el-main>
89+
<el-main :style="mainStyle">
2690
<router-view></router-view>
2791
</el-main>
92+
<GlobalAudioPlayer />
2893
<!-- <el-footer>Footer</el-footer> -->
2994
</el-container>
3095
</el-config-provider>
@@ -110,6 +175,7 @@ body {
110175
color: var(--text-color-secondary);
111176
width: 100%;
112177
height: 100%;
178+
padding-bottom: calc(var(--global-player-height, 0px) + 10px);
113179
background-color: var(--bg-color);
114180
transition: background-color 0.3s ease, color 0.3s ease;
115181
@@ -131,4 +197,4 @@ body {
131197
132198
133199
}
134-
</style>
200+
</style>

0 commit comments

Comments
 (0)