Compare commits

...

2 Commits

Author SHA1 Message Date
0591d0575d feat: 撤回意外的修改 2025-04-28 04:09:26 +08:00
8b908f5ac9 feat: 添加歌曲列表分页功能和键盘快捷键支持
- 在 SongList 组件中实现分页功能,支持上一页和下一页操作
- 添加键盘快捷键,允许用户通过方向键进行翻页
- 优化组件结构,增强可读性和用户体验
2025-04-28 04:04:21 +08:00
12 changed files with 1698 additions and 903 deletions

BIN
message_render_content.txt Normal file

Binary file not shown.

View File

@@ -87,6 +87,33 @@ const batchUpdate_Option = ref<SongRequestOption | undefined>() // 批量编辑
const columns = ref<DataTableColumns<SongsInfo>>() // 表格列定义 const columns = ref<DataTableColumns<SongsInfo>>() // 表格列定义
const selectedColumn = ref<DataTableRowKey[]>([]) // 表格选中行的 Key 数组 const selectedColumn = ref<DataTableRowKey[]>([]) // 表格选中行的 Key 数组
// 分页相关
const currentPage = ref(1) // 当前页码
const handlePageChange = (page: number) => {
currentPage.value = page
}
// 暴露分页方法
const nextPage = () => {
const pagination = songsComputed.value.length > 0 ? Math.ceil(songsComputed.value.length / pageSize.value) : 1
if (currentPage.value < pagination) {
currentPage.value++
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
// 暴露给父组件
defineExpose({
nextPage,
prevPage,
currentPage
})
// --- 计算属性 --- // --- 计算属性 ---
// 筛选后的歌曲列表 // 筛选后的歌曲列表
@@ -163,8 +190,6 @@ const authorsOptions = computed(() => {
})) }))
}) })
// --- 表格列定义 ---
// 作者列定义 (包含筛选逻辑) // 作者列定义 (包含筛选逻辑)
const authorColumn = ref<DataTableBaseColumn<SongsInfo>>({ const authorColumn = ref<DataTableBaseColumn<SongsInfo>>({
title: '作者', title: '作者',
@@ -751,7 +776,8 @@ onMounted(() => {
pageSizes: [10, 25, 50, 100, 200], pageSizes: [10, 25, 50, 100, 200],
showSizePicker: true, showSizePicker: true,
showQuickJumper: true, showQuickJumper: true,
page: currentPage,
onUpdatePage: handlePageChange
}" }"
:loading="isLoading && songsComputed.length === 0" :loading="isLoading && songsComputed.length === 0"
striped striped

File diff suppressed because it is too large Load Diff

View File

@@ -21,4 +21,5 @@ export interface ScheduleConfigType {
userInfo: UserInfo | undefined userInfo: UserInfo | undefined
biliInfo: any | undefined biliInfo: any | undefined
data: ScheduleWeekInfo[] | undefined data: ScheduleWeekInfo[] | undefined
config?: any
} }

View File

@@ -1,5 +1,5 @@
import DefaultIndexTemplateVue from '@/views/view/indexTemplate/DefaultIndexTemplate.vue'; import DefaultIndexTemplateVue from '@/views/view/indexTemplate/DefaultIndexTemplate.vue';
import { defineAsyncComponent, ref } from 'vue'; import { defineAsyncComponent, ref, markRaw } from 'vue';
const debugAPI = const debugAPI =
import.meta.env.VITE_API == 'dev' import.meta.env.VITE_API == 'dev'
@@ -74,40 +74,40 @@ export const ScheduleTemplateMap: TemplateMapType = {
'': { '': {
name: '默认', name: '默认',
//settingName: 'Template.Schedule.Default', //settingName: 'Template.Schedule.Default',
component: defineAsyncComponent( component: markRaw(defineAsyncComponent(
() => import('@/views/view/scheduleTemplate/DefaultScheduleTemplate.vue') () => import('@/views/view/scheduleTemplate/DefaultScheduleTemplate.vue')
) ))
}, },
pinky: { pinky: {
name: '粉粉', name: '粉粉',
//settingName: 'Template.Schedule.Pinky', //settingName: 'Template.Schedule.Pinky',
component: defineAsyncComponent( component: markRaw(defineAsyncComponent(
() => import('@/views/view/scheduleTemplate/PinkySchedule.vue') () => import('@/views/view/scheduleTemplate/PinkySchedule.vue')
) ))
} }
}; };
export const SongListTemplateMap: TemplateMapType = { export const SongListTemplateMap: TemplateMapType = {
'': { '': {
name: '默认', name: '默认',
//settingName: 'Template.SongList.Default', //settingName: 'Template.SongList.Default',
component: defineAsyncComponent( component: markRaw(defineAsyncComponent(
() => import('@/views/view/songListTemplate/DefaultSongListTemplate.vue') () => import('@/views/view/songListTemplate/DefaultSongListTemplate.vue')
) ))
}, },
simple: { simple: {
name: '简单', name: '简单',
//settingName: 'Template.SongList.Simple', //settingName: 'Template.SongList.Simple',
component: defineAsyncComponent( component: markRaw(defineAsyncComponent(
() => import('@/views/view/songListTemplate/SimpleSongListTemplate.vue') () => import('@/views/view/songListTemplate/SimpleSongListTemplate.vue')
) ))
}, },
traditional: { traditional: {
name: '列表', name: '列表',
settingName: 'Template.SongList.Traditional', settingName: 'Template.SongList.Traditional',
component: defineAsyncComponent( component: markRaw(defineAsyncComponent(
() => () =>
import('@/views/view/songListTemplate/TraditionalSongListTemplate.vue') import('@/views/view/songListTemplate/TraditionalSongListTemplate.vue')
) ))
} }
}; };
@@ -115,7 +115,7 @@ export const IndexTemplateMap: TemplateMapType = {
'': { '': {
name: '默认', name: '默认',
//settingName: 'Template.Index.Default', //settingName: 'Template.Index.Default',
component: DefaultIndexTemplateVue component: markRaw(DefaultIndexTemplateVue)
} }
}; };

View File

@@ -57,7 +57,7 @@
SelectOption, SelectOption,
useMessage, useMessage,
} from 'naive-ui'; } from 'naive-ui';
import { computed, h, nextTick, onActivated, onMounted, ref, shallowRef } from 'vue'; import { computed, h, nextTick, onActivated, onMounted, ref, shallowRef, markRaw } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
// 模板定义类型接口 // 模板定义类型接口

File diff suppressed because it is too large Load Diff

View File

@@ -89,9 +89,10 @@ const filteredUsers = computed(() => {
// 根据关键词搜索 // 根据关键词搜索
if (settings.value.searchKeyword) { if (settings.value.searchKeyword) {
const keyword = settings.value.searchKeyword.toLowerCase()
return ( return (
user.info.name?.toLowerCase().includes(settings.value.searchKeyword.toLowerCase()) == true || user.info.name?.toLowerCase().includes(keyword) == true ||
user.info.userId?.toString() == settings.value.searchKeyword user.info.userId?.toString() == keyword
) )
} }
@@ -103,6 +104,76 @@ const filteredUsers = computed(() => {
// 当前查看的用户详情 // 当前查看的用户详情
const currentUser = ref<ResponsePointUserModel>() const currentUser = ref<ResponsePointUserModel>()
// 渲染用户名或用户ID
const renderUsername = (user: ResponsePointUserModel) => {
if (user.info?.name) {
return user.info.name
}
return h(NFlex, null, () => [
'未知',
h(NText, { depth: 3 }, { default: () => `(${user.info.userId ?? user.info.openId})` }),
])
}
// 渲染订单数量,更友好的显示方式
const renderOrderCount = (user: ResponsePointUserModel) => {
if (!user.isAuthed) return h(NText, { depth: 3 }, { default: () => '未认证' })
return user.orderCount > 0 ? h(NText, {}, { default: () => formatNumber(user.orderCount) }) : h(NText, { depth: 3 }, { default: () => '无订单' })
}
// 渲染时间戳为相对时间和绝对时间
const renderTime = (timestamp: number) => {
return h(NTooltip, null, {
trigger: () => h(NTime, { time: timestamp, type: 'relative' }),
default: () => h(NTime, { time: timestamp }),
})
}
// 渲染操作按钮
const renderActions = (user: ResponsePointUserModel) => {
return h(NFlex, { justify: 'center', gap: 8 }, () => [
h(
NButton,
{
onClick: () => {
currentUser.value = user
showModal.value = true
},
type: 'info',
size: 'small',
},
{ default: () => '详情' },
),
h(
NPopconfirm,
{ onPositiveClick: () => deleteUser(user) },
{
default: () => '确定要删除这个用户吗?记录将无法恢复',
trigger: () =>
h(
NButton,
{
type: 'error',
size: 'small',
},
{ default: () => '删除' },
),
},
),
])
}
// 格式化数字,添加千位符
const formatNumber = (num: number) => {
return num.toLocaleString('zh-CN')
}
// 渲染积分,添加千位符并加粗
const renderPoint = (num: number) => {
return h(NText, { strong: true }, { default: () => formatNumber(num) })
}
// 数据表格列定义 // 数据表格列定义
const column: DataTableColumns<ResponsePointUserModel> = [ const column: DataTableColumns<ResponsePointUserModel> = [
{ {
@@ -115,77 +186,29 @@ const column: DataTableColumns<ResponsePointUserModel> = [
{ {
title: '用户名', title: '用户名',
key: 'username', key: 'username',
render: (row: ResponsePointUserModel) => { render: (row: ResponsePointUserModel) => renderUsername(row),
return (
row.info?.name ??
h(NFlex, null, () => [
'未知',
h(NText, { depth: 3 }, { default: () => `(${row.info.userId ?? row.info.openId})` }),
])
)
},
}, },
{ {
title: '积分', title: '积分',
key: 'point', key: 'point',
sorter: 'default', sorter: 'default',
render: (row: ResponsePointUserModel) => { render: (row: ResponsePointUserModel) => renderPoint(row.point),
return row.point
},
}, },
{ {
title: '订单数量', title: '订单数量',
key: 'orders', key: 'orderCount',
render: (row: ResponsePointUserModel) => { render: (row: ResponsePointUserModel) => renderOrderCount(row),
return row.isAuthed ? row.orderCount : '无'
},
}, },
{ {
title: '最后更新于', title: '最后更新于',
key: 'updateAt', key: 'updateAt',
sorter: 'default', sorter: 'default',
render: (row: ResponsePointUserModel) => { render: (row: ResponsePointUserModel) => renderTime(row.updateAt),
return h(NTooltip, null, {
trigger: () => h(NTime, { time: row.updateAt, type: 'relative' }),
default: () => h(NTime, { time: row.updateAt }),
})
},
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
render: (row: ResponsePointUserModel) => { render: (row: ResponsePointUserModel) => renderActions(row),
return h(NFlex, { justify: 'center', gap: 8 }, () => [
h(
NButton,
{
onClick: () => {
currentUser.value = row
showModal.value = true
},
type: 'info',
size: 'small',
},
{ default: () => '详情' },
),
h(
NPopconfirm,
{ onPositiveClick: () => deleteUser(row) },
{
default: () => '确定要删除这个用户吗?记录将无法恢复',
trigger: () =>
h(
NButton,
{
type: 'error',
size: 'small',
},
{ default: () => '删除' },
),
},
),
])
},
}, },
] ]
@@ -445,10 +468,10 @@ onMounted(async () => {
<NDivider /> <NDivider />
<!-- 无数据提示 --> <!-- 无数据提示 -->
<template v-if="filteredUsers.length == 0"> <NEmpty
<NDivider /> v-if="filteredUsers.length == 0"
<NEmpty :description="settings.onlyAuthed ? '没有已认证的用户' : '没有用户'" /> :description="isLoading ? '加载中...' : (settings.onlyAuthed ? '没有已认证的用户' : '没有用户')"
</template> />
<!-- 用户数据表格 --> <!-- 用户数据表格 -->
<NDataTable <NDataTable
@@ -465,6 +488,7 @@ onMounted(async () => {
onUpdatePage: (page) => (pn = page), onUpdatePage: (page) => (pn = page),
onUpdatePageSize: (pageSize) => (ps = pageSize) onUpdatePageSize: (pageSize) => (ps = pageSize)
}" }"
:loading="isLoading"
/> />
</NSpin> </NSpin>
@@ -592,8 +616,8 @@ onMounted(async () => {
<NButton <NButton
type="error" type="error"
:loading="isLoading" :loading="isLoading"
@click="resetAllPoints"
:disabled="resetConfirmText !== RESET_CONFIRM_TEXT" :disabled="resetConfirmText !== RESET_CONFIRM_TEXT"
@click="resetAllPoints"
> >
确认重置所有用户积分 确认重置所有用户积分
</NButton> </NButton>

View File

@@ -44,7 +44,7 @@
:author-name="message.authorName" :author-name="message.authorName"
:author-type="message.authorType" :author-type="message.authorType"
:privilege-type="message.privilegeType" :privilege-type="message.privilegeType"
:rich-content="getShowRichContent(message)" :content-parts="getShowContentParts(message)"
:repeated="message.repeated" :repeated="message.repeated"
/> />
<paid-message <paid-message
@@ -205,7 +205,7 @@ export default defineComponent({
}, },
getGiftShowNameAndNum: constants.getGiftShowNameAndNum, getGiftShowNameAndNum: constants.getGiftShowNameAndNum,
getShowContent: constants.getShowContent, getShowContent: constants.getShowContent,
getShowRichContent: constants.getShowRichContent, getShowContentParts: constants.getShowContentParts,
getShowAuthorName: constants.getShowAuthorName, getShowAuthorName: constants.getShowAuthorName,
addMessage(message) { addMessage(message) {

View File

@@ -29,7 +29,7 @@
id="message" id="message"
class="style-scope yt-live-chat-text-message-renderer" class="style-scope yt-live-chat-text-message-renderer"
> >
<template v-for="(content, index) in richContent"> <template v-for="(content, index) in contentParts">
<span <span
v-if="content.type === CONTENT_TYPE_TEXT" v-if="content.type === CONTENT_TYPE_TEXT"
:key="index" :key="index"
@@ -81,7 +81,7 @@ const props = defineProps({
time: Date, time: Date,
authorName: String, authorName: String,
authorType: Number, authorType: Number,
richContent: Array, contentParts: Array,
privilegeType: Number, privilegeType: Number,
repeated: Number repeated: Number
}) })

View File

@@ -16,6 +16,7 @@ const props = defineProps<{
userInfo: UserInfo | undefined userInfo: UserInfo | undefined
biliInfo: any | undefined biliInfo: any | undefined
currentData?: any currentData?: any
config?: any
}>() }>()
const isLoading = ref(true) const isLoading = ref(true)
const message = useMessage() const message = useMessage()

View File

@@ -4,9 +4,9 @@ import { SongsInfo } from '@/api/api-models'
import SongList from '@/components/SongList.vue' import SongList from '@/components/SongList.vue'
import { SongListConfigType } from '@/data/TemplateTypes' import { SongListConfigType } from '@/data/TemplateTypes'
import LiveRequestOBS from '@/views/obs/LiveRequestOBS.vue' import LiveRequestOBS from '@/views/obs/LiveRequestOBS.vue'
import { CloudAdd20Filled } from '@vicons/fluent' import { CloudAdd20Filled, ChevronLeft24Filled, ChevronRight24Filled } from '@vicons/fluent'
import { NButton, NCard, NCollapse, NCollapseItem, NDivider, NIcon, NTooltip, useMessage } from 'naive-ui' import { NButton, NCard, NCollapse, NCollapseItem, NDivider, NIcon, NTooltip, useMessage } from 'naive-ui'
import { h, ref } from 'vue' import { h, ref, onMounted, onUnmounted } from 'vue'
const accountInfo = useAccount() const accountInfo = useAccount()
@@ -16,6 +16,50 @@ const emits = defineEmits(['requestSong'])
const isLoading = ref('') const isLoading = ref('')
const message = useMessage() const message = useMessage()
const songListRef = ref<InstanceType<typeof SongList> | null>(null)
// 处理翻页逻辑
const handlePrevPage = () => {
if (songListRef.value) {
songListRef.value.prevPage()
}
}
const handleNextPage = () => {
if (songListRef.value) {
songListRef.value.nextPage()
}
}
// 键盘快捷键处理函数
const handleKeyDown = (event: KeyboardEvent) => {
// 忽略在输入框内的按键
if (event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
event.target instanceof HTMLSelectElement) {
return
}
// 左方向键 - 上一页
if (event.key === 'ArrowLeft') {
handlePrevPage()
event.preventDefault()
}
// 右方向键 - 下一页
else if (event.key === 'ArrowRight') {
handleNextPage()
event.preventDefault()
}
}
// 添加和移除事件监听器
onMounted(() => {
window.addEventListener('keydown', handleKeyDown)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown)
})
const buttons = (song: SongsInfo) => [ const buttons = (song: SongsInfo) => [
accountInfo.value?.id != props.userInfo?.id accountInfo.value?.id != props.userInfo?.id
@@ -55,25 +99,89 @@ const buttons = (song: SongsInfo) => [
</script> </script>
<template> <template>
<NDivider style="margin-top: 10px" /> <div class="song-list-container">
<SongList <NDivider style="margin-top: 10px" />
v-if="data" <!-- 左侧翻页按钮 -->
:songs="data ?? []" <div class="page-button page-button-left">
:is-self="accountInfo?.id == userInfo?.id" <NButton
:extra-button="buttons" circle
v-bind="$attrs" secondary
/> size="large"
<NCollapse v-if="userInfo?.canRequestSong"> title="上一页 (←)"
<NCollapseItem title="点歌列表"> @click="handlePrevPage"
<NCard
size="small"
embedded
> >
<div style="height: 400px; width: 700px; max-width: 100%; position: relative; margin: 0 auto"> <template #icon>
<LiveRequestOBS :id="userInfo?.id" /> <NIcon :component="ChevronLeft24Filled" />
</div> </template>
</NCard> </NButton>
</NCollapseItem> </div>
</NCollapse>
<NDivider /> <!-- 右侧翻页按钮 -->
<div class="page-button page-button-right">
<NButton
circle
secondary
size="large"
title="下一页 (→)"
@click="handleNextPage"
>
<template #icon>
<NIcon :component="ChevronRight24Filled" />
</template>
</NButton>
</div>
<SongList
v-if="data"
ref="songListRef"
:songs="data ?? []"
:is-self="accountInfo?.id == userInfo?.id"
:extra-button="buttons"
v-bind="$attrs"
/>
<NCollapse v-if="userInfo?.canRequestSong">
<NCollapseItem title="点歌列表">
<NCard
size="small"
embedded
>
<div style="height: 400px; width: 700px; max-width: 100%; position: relative; margin: 0 auto">
<LiveRequestOBS :id="userInfo?.id" />
</div>
</NCard>
</NCollapseItem>
</NCollapse>
<NDivider />
</div>
</template> </template>
<style scoped>
.song-list-container {
position: relative;
}
.page-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10;
}
.page-button-left {
left: -20px;
}
.page-button-right {
right: -20px;
}
@media (max-width: 768px) {
.page-button-left {
left: 0;
}
.page-button-right {
right: 0;
}
}
</style>