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';
// 模板定义类型接口 // 模板定义类型接口

View File

@@ -9,6 +9,7 @@ import { CN_HOST, CURRENT_HOST, FILE_BASE_URL, POINT_API_URL } from '@/data/cons
import { useAuthStore } from '@/store/useAuthStore' import { useAuthStore } from '@/store/useAuthStore'
import { Info24Filled } from '@vicons/fluent' import { Info24Filled } from '@vicons/fluent'
import { useRouteHash } from '@vueuse/router' import { useRouteHash } from '@vueuse/router'
import { useStorage } from '@vueuse/core'
import { import {
FormItemRule, FormItemRule,
NAlert, NAlert,
@@ -46,16 +47,19 @@ import { computed, onMounted, ref } from 'vue'
import PointOrderManage from './PointOrderManage.vue' import PointOrderManage from './PointOrderManage.vue'
import PointSettings from './PointSettings.vue' import PointSettings from './PointSettings.vue'
import PointUserManage from './PointUserManage.vue' import PointUserManage from './PointUserManage.vue'
import { useStorage } from '@vueuse/core'
const message = useMessage() const message = useMessage()
const accountInfo = useAccount() const accountInfo = useAccount()
const dialog = useDialog() const dialog = useDialog()
const useBiliAuth = useAuthStore() const useBiliAuth = useAuthStore()
const formRef = ref()
const isUpdating = ref(false)
const isAllowedPrivacyPolicy = ref(false)
const useCNUrl = useStorage('Settings.UseCNUrl', false)
const showAddGoodsModal = ref(false)
const realHash = useRouteHash('goods', { // 路由哈希处理
mode: 'replace', const realHash = useRouteHash('goods', { mode: 'replace' })
})
const hash = computed({ const hash = computed({
get() { get() {
return realHash.value?.startsWith('#') ? realHash.value.slice(1) : realHash.value || 'goods' return realHash.value?.startsWith('#') ? realHash.value.slice(1) : realHash.value || 'goods'
@@ -65,6 +69,7 @@ const hash = computed({
}, },
}) })
// 商品数据及模型
const goods = ref<ResponsePointGoodModel[]>(await useBiliAuth.GetGoods(accountInfo.value?.id, message)) const goods = ref<ResponsePointGoodModel[]>(await useBiliAuth.GetGoods(accountInfo.value?.id, message))
const defaultGoodsModel = { const defaultGoodsModel = {
goods: { goods: {
@@ -72,39 +77,60 @@ const defaultGoodsModel = {
status: GoodsStatus.Normal, status: GoodsStatus.Normal,
maxBuyCount: 1, maxBuyCount: 1,
isAllowRebuy: false, isAllowRebuy: false,
setting: {}, setting: {
allowGuardLevel: 0
},
} as PointGoodsModel, } as PointGoodsModel,
fileList: [], fileList: [],
} as { goods: PointGoodsModel; fileList: UploadFileInfo[] } } as { goods: PointGoodsModel; fileList: UploadFileInfo[] }
const currentGoodsModel = ref<{ goods: PointGoodsModel; fileList: UploadFileInfo[] }>( const currentGoodsModel = ref<{ goods: PointGoodsModel; fileList: UploadFileInfo[] }>(
JSON.parse(JSON.stringify(defaultGoodsModel)), JSON.parse(JSON.stringify(defaultGoodsModel))
) )
const showAddGoodsModal = ref(false) // 计算属性
const isAllowedPrivacyPolicy = ref(false)
const isUpdating = ref(false)
const useCNUrl = useStorage('Settings.UseCNUrl', false)
const allowedYearOptions = computed(() => { const allowedYearOptions = computed(() => {
//从2024到现在的年份 return Array.from({ length: new Date().getFullYear() - 2024 + 1 }, (_, i) => 2024 + i).map((item) => ({
return Array.from({ length: new Date().getFullYear() - 2024 + 1 }, (_, i) => 2024 + i).map((item) => {
return {
label: item.toString() + '年', label: item.toString() + '年',
value: item, value: item,
} }))
})
})
const allowedMonthOptions = computed(() => {
return Array.from({ length: 12 }, (_, i) => i + 1).map((item) => {
return {
label: item.toString() + '月',
value: item + 1,
}
})
}) })
const allowedMonthOptions = computed(() => {
return Array.from({ length: 12 }, (_, i) => i + 1).map((item) => ({
label: item.toString() + '月',
value: item,
}))
})
const existTags = computed(() => {
if (goods.value.length === 0) return []
const tempSet = new Set<string>()
for (const good of goods.value) {
if (!good.tags || good.tags.length === 0) continue
good.tags.forEach(tag => tempSet.add(tag))
}
return Array.from(tempSet).map(tag => ({ label: tag, value: tag }))
})
// 下拉菜单选项
const dropDownActions = {
update: {
label: '修改',
key: 'update',
action: (item: ResponsePointGoodModel) => onUpdateClick(item),
},
delete: {
label: '删除',
key: 'delete',
action: (item: ResponsePointGoodModel) => onDeleteClick(item),
},
} as { [key: string]: { label: string; key: string; action: (item: ResponsePointGoodModel) => void } }
const dropDownOptions = computed(() => Object.values(dropDownActions))
// 表单验证规则
const rules = { const rules = {
name: { name: {
required: true, required: true,
@@ -121,30 +147,23 @@ const rules = {
content: { content: {
required: true, required: true,
message: '请输入虚拟礼物的具体内容', message: '请输入虚拟礼物的具体内容',
validator: (rule: FormItemRule, value: string) => { validator: (rule: FormItemRule, value: string) =>
return currentGoodsModel.value.goods.type != GoodsTypes.Virtual || (value?.length ?? 0) > 0 currentGoodsModel.value.goods.type != GoodsTypes.Virtual || (value?.length ?? 0) > 0
},
}, },
privacy: { privacy: {
required: true, required: true,
message: '需要阅读并同意本站隐私协议', message: '需要阅读并同意本站隐私协议',
validator: (rule: FormItemRule, value: boolean) => { validator: (rule: FormItemRule, value: boolean) =>
return (
(currentGoodsModel.value.goods.type != GoodsTypes.Physical && (currentGoodsModel.value.goods.type != GoodsTypes.Physical &&
currentGoodsModel.value.goods.collectUrl != undefined) || currentGoodsModel.value.goods.collectUrl != undefined) ||
isAllowedPrivacyPolicy.value isAllowedPrivacyPolicy.value
)
},
}, },
maxBuyCount: { maxBuyCount: {
required: true, required: true,
message: '需要输入最大购买数量', message: '需要输入最大购买数量',
validator: (rule: FormItemRule, value: number) => { validator: (rule: FormItemRule, value: number) =>
return (
currentGoodsModel.value.goods.type != GoodsTypes.Physical || currentGoodsModel.value.goods.type != GoodsTypes.Physical ||
(currentGoodsModel.value.goods.maxBuyCount ?? 0) > 0 (currentGoodsModel.value.goods.maxBuyCount ?? 0) > 0
)
},
}, },
'goods.url': { 'goods.url': {
required: true, required: true,
@@ -159,104 +178,64 @@ const rules = {
}, },
}, },
} }
const formRef = ref()
const existTags = computed(() => {
if (goods.value.length == 0) {
return []
}
//获取所有已存在商品的tags并去重
const tempSet = new Set<string>()
for (let i = 0; i < goods.value.length; i++) {
const goodsTags = goods.value[i].tags
if (!goodsTags || goodsTags.length == 0) {
continue
}
for (let j = 0; j < goods.value[i].tags.length; j++) {
tempSet.add(goods.value[i].tags[j])
}
}
return Array.from(tempSet).map((item) => {
return { label: item, value: item }
})
})
const dropDownActions = {
update: {
label: '修改',
key: 'update',
action: (item: ResponsePointGoodModel) => onUpdateClick(item),
},
delete: {
label: '删除',
key: 'delete',
action: (item: ResponsePointGoodModel) => onDeleteClick(item),
},
} as { [key: string]: { label: string; key: string; action: (item: ResponsePointGoodModel) => void } }
const dropDownOptions = computed(() => {
return Object.values(dropDownActions)
})
// 方法
async function setFunctionEnable(enable: boolean) { async function setFunctionEnable(enable: boolean) {
let success = false const success = enable ? await EnableFunction(FunctionTypes.Point) : await DisableFunction(FunctionTypes.Point)
if (enable) {
success = await EnableFunction(FunctionTypes.Point)
} else {
success = await DisableFunction(FunctionTypes.Point)
}
if (success) { if (success) {
message.success('已' + (enable ? '启用' : '禁用') + '积分系统') message.success('已' + (enable ? '启用' : '禁用') + '积分系统')
} else { } else {
message.error('无法' + (enable ? '启用' : '禁用') + '积分系统') message.error('无法' + (enable ? '启用' : '禁用') + '积分系统')
} }
} }
async function updateGoods(e: MouseEvent) { async function updateGoods(e: MouseEvent) {
if (isUpdating.value || !formRef.value) return if (isUpdating.value || !formRef.value) return
e.preventDefault() e.preventDefault()
isUpdating.value = true isUpdating.value = true
await formRef.value
.validate() try {
.then(async () => { await formRef.value.validate()
if (currentGoodsModel.value.fileList.length > 0) { if (currentGoodsModel.value.fileList.length > 0) {
currentGoodsModel.value.goods.cover = await getImageUploadModel(currentGoodsModel.value.fileList) currentGoodsModel.value.goods.cover = await getImageUploadModel(currentGoodsModel.value.fileList)
} }
await QueryPostAPI<ResponsePointGoodModel>(POINT_API_URL + 'update-goods', currentGoodsModel.value.goods)
.then((data) => { const { code, data, message: errMsg } = await QueryPostAPI<ResponsePointGoodModel>(
if (data.code == 200) { POINT_API_URL + 'update-goods',
currentGoodsModel.value.goods
)
if (code === 200) {
message.success('成功') message.success('成功')
showAddGoodsModal.value = false showAddGoodsModal.value = false
currentGoodsModel.value = JSON.parse(JSON.stringify(defaultGoodsModel)) currentGoodsModel.value = JSON.parse(JSON.stringify(defaultGoodsModel))
if (goods.value.find((g) => g.id == data.data.id)) {
goods.value[goods.value.findIndex((g) => g.id == data.data.id)] = data.data const index = goods.value.findIndex(g => g.id === data.id)
if (index >= 0) {
goods.value[index] = data
} else { } else {
goods.value.push(data.data) goods.value.push(data)
} }
} else { } else {
message.error('失败: ' + data.message) message.error('失败: ' + errMsg)
} }
}) } catch (err) {
.catch((err) => {
message.error('失败: ' + err)
console.error(err) console.error(err)
}) message.error(typeof err === 'string' ? `失败: ${err}` : '表单验证失败')
}) } finally {
.catch((err: unknown) => {
console.log(err)
message.error('表单验证失败')
})
.finally(() => {
isUpdating.value = false isUpdating.value = false
}) }
} }
function OnFileListChange(files: UploadFileInfo[]) { function OnFileListChange(files: UploadFileInfo[]) {
if (files.length == 1) { if (files.length === 1 && (files[0].file?.size ?? 0) > 10 * 1024 * 1024) {
const file = files[0]
if ((file.file?.size ?? 0) > 10 * 1024 * 1024) {
message.error('文件大小不能超过10MB') message.error('文件大小不能超过10MB')
currentGoodsModel.value.fileList = [] currentGoodsModel.value.fileList = []
} }
}
} }
function onUpdateClick(item: ResponsePointGoodModel) { function onUpdateClick(item: ResponsePointGoodModel) {
currentGoodsModel.value = { currentGoodsModel.value = {
goods: { goods: {
@@ -278,8 +257,8 @@ function onUpdateClick(item: ResponsePointGoodModel) {
isAllowedPrivacyPolicy.value = true isAllowedPrivacyPolicy.value = true
showAddGoodsModal.value = true showAddGoodsModal.value = true
} }
//下架
function onSetShelfClick(item: ResponsePointGoodModel, status: GoodsStatus) { async function onSetShelfClick(item: ResponsePointGoodModel, status: GoodsStatus) {
const d = dialog.warning({ const d = dialog.warning({
title: '警告', title: '警告',
content: `你确定要${status == GoodsStatus.Normal ? '重新上架' : '下架'}这个礼物吗?`, content: `你确定要${status == GoodsStatus.Normal ? '重新上架' : '下架'}这个礼物吗?`,
@@ -288,22 +267,23 @@ function onSetShelfClick(item: ResponsePointGoodModel, status: GoodsStatus) {
onPositiveClick: async () => { onPositiveClick: async () => {
d.loading = true d.loading = true
const originStatus = item.status const originStatus = item.status
//item.status = status
try { try {
const data = await QueryPostAPI(POINT_API_URL + 'update-goods-status', { const { code, message: errMsg } = await QueryPostAPI(POINT_API_URL + 'update-goods-status', {
ids: [item.id], ids: [item.id],
status: status, status: status,
}) })
if (data.code == 200) {
if (code === 200) {
message.success('成功') message.success('成功')
const index = goods.value.findIndex((g) => g.id == item.id) const index = goods.value.findIndex(g => g.id === item.id)
if (index > -1) { if (index > -1) {
goods.value[index].status = status goods.value[index].status = status
} }
} else { } else {
message.error('失败: ' + data.message) message.error('失败: ' + errMsg)
item.status = originStatus item.status = originStatus
console.error(data.message) console.error(errMsg)
} }
} catch (err) { } catch (err) {
message.error('失败: ' + err) message.error('失败: ' + err)
@@ -315,6 +295,7 @@ function onSetShelfClick(item: ResponsePointGoodModel, status: GoodsStatus) {
}, },
}) })
} }
function onDeleteClick(item: ResponsePointGoodModel) { function onDeleteClick(item: ResponsePointGoodModel) {
const d = dialog.warning({ const d = dialog.warning({
title: '警告', title: '警告',
@@ -323,16 +304,18 @@ function onDeleteClick(item: ResponsePointGoodModel) {
negativeText: '取消', negativeText: '取消',
onPositiveClick: async () => { onPositiveClick: async () => {
d.loading = true d.loading = true
try { try {
const data = await QueryGetAPI(POINT_API_URL + 'delete-goods', { const { code, message: errMsg } = await QueryGetAPI(POINT_API_URL + 'delete-goods', {
id: item.id, id: item.id,
}) })
if (data.code == 200) {
if (code === 200) {
message.success('成功') message.success('成功')
goods.value = goods.value.filter((g) => g.id != item.id) goods.value = goods.value.filter(g => g.id !== item.id)
} else { } else {
message.error('失败: ' + data.message) message.error('失败: ' + errMsg)
console.error(data.message) console.error(errMsg)
} }
} catch (err) { } catch (err) {
message.error('失败: ' + err) message.error('失败: ' + err)
@@ -343,30 +326,44 @@ function onDeleteClick(item: ResponsePointGoodModel) {
}, },
}) })
} }
function onModalOpen() { function onModalOpen() {
if (currentGoodsModel.value.goods.id) { if (currentGoodsModel.value.goods.id) {
resetGoods() resetGoods()
} }
showAddGoodsModal.value = true showAddGoodsModal.value = true
} }
function resetGoods() { function resetGoods() {
currentGoodsModel.value = JSON.parse(JSON.stringify(defaultGoodsModel)) currentGoodsModel.value = JSON.parse(JSON.stringify(defaultGoodsModel))
} }
function responseGoodsToModel(goods: ResponsePointGoodModel) { }
onMounted(() => { }) onMounted(() => { })
</script> </script>
<template> <template>
<NFlex> <!-- 头部状态卡片 -->
<NFlex
vertical
:size="16"
>
<NFlex
justify="space-between"
align="center"
:gap="16"
>
<NAlert <NAlert
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Point) && accountInfo.eventFetcherState.online :type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Point) && accountInfo.eventFetcherState.online
? 'success' ? 'success'
: 'warning' : 'warning'
" "
style="min-width: 400px" style="flex: 1; min-width: 300px"
> >
启用 <NFlex
align="center"
:gap="8"
>
<span>启用</span>
<NButton <NButton
text text
type="primary" type="primary"
@@ -381,8 +378,11 @@ onMounted(() => { })
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.Point)" :value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.Point)"
@update:value="setFunctionEnable" @update:value="setFunctionEnable"
/> />
<br> </NFlex>
<NText depth="3"> <NText
depth="3"
style="margin-top: 8px; display: block"
>
此功能需要部署 此功能需要部署
<NButton <NButton
text text
@@ -398,13 +398,19 @@ onMounted(() => { })
</NAlert> </NAlert>
<EventFetcherStatusCard /> <EventFetcherStatusCard />
</NFlex> </NFlex>
<!-- 礼物展示页链接 -->
<NDivider <NDivider
style="margin: 16px 0 16px 0" style="margin: 16px 0"
title-placement="left" title-placement="left"
> >
礼物展示页链接 礼物展示页链接
</NDivider> </NDivider>
<NFlex align="center">
<NFlex
align="center"
:gap="12"
>
<NInputGroup style="max-width: 400px;"> <NInputGroup style="max-width: 400px;">
<NInput <NInput
:value="`${useCNUrl ? CN_HOST : CURRENT_HOST}@${accountInfo.name}/goods`" :value="`${useCNUrl ? CN_HOST : CURRENT_HOST}@${accountInfo.name}/goods`"
@@ -421,16 +427,26 @@ onMounted(() => { })
使用国内镜像(访问更快) 使用国内镜像(访问更快)
</NCheckbox> </NCheckbox>
</NFlex> </NFlex>
<NDivider /> </NFlex>
<NDivider style="margin: 16px 0" />
<!-- 主要内容标签页 -->
<NTabs <NTabs
v-model:value="hash" v-model:value="hash"
animated animated
style="margin-top: 8px"
> >
<!-- 礼物管理标签页 -->
<NTabPane <NTabPane
name="goods" name="goods"
tab="礼物" tab="礼物"
> >
<NFlex> <NFlex
justify="start"
:gap="12"
style="margin-bottom: 16px"
>
<NButton <NButton
type="primary" type="primary"
@click="onModalOpen" @click="onModalOpen"
@@ -444,7 +460,8 @@ onMounted(() => { })
前往展示页 前往展示页
</NButton> </NButton>
</NFlex> </NFlex>
<NDivider />
<!-- 上架礼物列表 -->
<NEmpty <NEmpty
v-if="goods.filter((g) => g.status != GoodsStatus.Discontinued).length == 0" v-if="goods.filter((g) => g.status != GoodsStatus.Discontinued).length == 0"
description="暂无礼物" description="暂无礼物"
@@ -452,17 +469,28 @@ onMounted(() => { })
<NGrid <NGrid
v-else v-else
cols="1 500:2 700:3 1000:4 1200:5" cols="1 500:2 700:3 1000:4 1200:5"
:x-gap="12" :x-gap="16"
:y-gap="8" :y-gap="16"
> >
<NGridItem <NGridItem
v-for="item in goods.filter((g) => g.status != GoodsStatus.Discontinued)" v-for="item in goods.filter((g) => g.status != GoodsStatus.Discontinued)"
:key="item.id" :key="item.id"
> >
<PointGoodsItem :goods="item"> <PointGoodsItem
:goods="item"
class="point-goods-card"
>
<template #footer> <template #footer>
<span> 价格: {{ item.price }} </span> <NFlex
<NFlex> vertical
:gap="8"
style="width: 100%"
>
<span>价格: {{ item.price }}</span>
<NFlex
justify="space-between"
:gap="8"
>
<NButton <NButton
type="info" type="info"
size="small" size="small"
@@ -485,11 +513,16 @@ onMounted(() => { })
删除 删除
</NButton> </NButton>
</NFlex> </NFlex>
</NFlex>
</template> </template>
</PointGoodsItem> </PointGoodsItem>
</NGridItem> </NGridItem>
</NGrid> </NGrid>
<NDivider>已下架</NDivider>
<!-- 下架礼物列表 -->
<NDivider style="margin: 24px 0 16px">
已下架
</NDivider>
<NEmpty <NEmpty
v-if="goods.filter((g) => g.status == GoodsStatus.Discontinued).length == 0" v-if="goods.filter((g) => g.status == GoodsStatus.Discontinued).length == 0"
description="暂无已下架的礼物" description="暂无已下架的礼物"
@@ -497,17 +530,28 @@ onMounted(() => { })
<NGrid <NGrid
v-else v-else
cols="1 500:2 700:3 1000:4 1200:5" cols="1 500:2 700:3 1000:4 1200:5"
:x-gap="12" :x-gap="16"
:y-gap="8" :y-gap="16"
> >
<NGridItem <NGridItem
v-for="item in goods.filter((g) => g.status == GoodsStatus.Discontinued)" v-for="item in goods.filter((g) => g.status == GoodsStatus.Discontinued)"
:key="item.id" :key="item.id"
> >
<PointGoodsItem :goods="item"> <PointGoodsItem
:goods="item"
class="point-goods-card"
>
<template #footer> <template #footer>
<span> 价格: {{ item.price }} </span> <NFlex
<NFlex> vertical
:gap="8"
style="width: 100%"
>
<span>价格: {{ item.price }}</span>
<NFlex
justify="space-between"
:gap="8"
>
<NButton <NButton
type="info" type="info"
size="small" size="small"
@@ -530,11 +574,14 @@ onMounted(() => { })
删除 删除
</NButton> </NButton>
</NFlex> </NFlex>
</NFlex>
</template> </template>
</PointGoodsItem> </PointGoodsItem>
</NGridItem> </NGridItem>
</NGrid> </NGrid>
</NTabPane> </NTabPane>
<!-- 订单管理标签页 -->
<NTabPane <NTabPane
name="orders" name="orders"
tab="订单" tab="订单"
@@ -542,6 +589,8 @@ onMounted(() => { })
> >
<PointOrderManage :goods="goods" /> <PointOrderManage :goods="goods" />
</NTabPane> </NTabPane>
<!-- 用户管理标签页 -->
<NTabPane <NTabPane
name="users" name="users"
tab="用户" tab="用户"
@@ -549,6 +598,8 @@ onMounted(() => { })
> >
<PointUserManage :goods="goods" /> <PointUserManage :goods="goods" />
</NTabPane> </NTabPane>
<!-- 设置标签页 -->
<NTabPane <NTabPane
name="settings" name="settings"
tab="设置" tab="设置"
@@ -557,12 +608,14 @@ onMounted(() => { })
<PointSettings /> <PointSettings />
</NTabPane> </NTabPane>
</NTabs> </NTabs>
<NDivider />
<!-- 添加/修改礼物模态框 -->
<NModal <NModal
v-model:show="showAddGoodsModal" v-model:show="showAddGoodsModal"
preset="card" preset="card"
style="width: 600px; max-width: 90%" style="width: 600px; max-width: 90%"
title="添加/修改礼物信息" title="添加/修改礼物信息"
class="goods-modal"
> >
<template #header-extra> <template #header-extra>
<NPopconfirm <NPopconfirm
@@ -585,7 +638,19 @@ onMounted(() => { })
ref="formRef" ref="formRef"
:model="currentGoodsModel" :model="currentGoodsModel"
:rules="rules" :rules="rules"
style="width: 95%" style="width: 100%"
>
<!-- 基本信息分组 -->
<NDivider
title-placement="left"
style="margin: 8px 0 16px"
>
基本信息
</NDivider>
<NFlex
vertical
:gap="12"
style="margin-bottom: 16px"
> >
<NFormItem <NFormItem
path="goods.name" path="goods.name"
@@ -597,6 +662,7 @@ onMounted(() => { })
placeholder="必填, 礼物名称" placeholder="必填, 礼物名称"
/> />
</NFormItem> </NFormItem>
<NFormItem <NFormItem
path="goods.price" path="goods.price"
label="所需积分" label="所需积分"
@@ -608,9 +674,14 @@ onMounted(() => { })
min="0" min="0"
/> />
</NFormItem> </NFormItem>
<NFormItem <NFormItem
path="goods.count" path="goods.count"
label="库存" label="库存"
>
<NFlex
:gap="12"
align="center"
> >
<NCheckbox <NCheckbox
:checked="!currentGoodsModel.goods.count" :checked="!currentGoodsModel.goods.count"
@@ -621,10 +692,25 @@ onMounted(() => { })
<NInputNumber <NInputNumber
v-if="currentGoodsModel.goods.count" v-if="currentGoodsModel.goods.count"
v-model:value="currentGoodsModel.goods.count" v-model:value="currentGoodsModel.goods.count"
placeholder="可选, 礼物库存" placeholder="礼物库存"
style="max-width: 120px" style="max-width: 120px"
/> />
</NFlex>
</NFormItem> </NFormItem>
</NFlex>
<!-- 详细描述分组 -->
<NDivider
title-placement="left"
style="margin: 16px 0"
>
详细描述
</NDivider>
<NFlex
vertical
:gap="12"
style="margin-bottom: 16px"
>
<NFormItem <NFormItem
path="goods.description" path="goods.description"
label="描述" label="描述"
@@ -636,6 +722,7 @@ onMounted(() => { })
type="textarea" type="textarea"
/> />
</NFormItem> </NFormItem>
<NFormItem <NFormItem
path="goods.tags" path="goods.tags"
label="标签" label="标签"
@@ -650,11 +737,20 @@ onMounted(() => { })
:options="existTags" :options="existTags"
/> />
</NFormItem> </NFormItem>
<NFormItem <NFormItem
path="goods.cover" path="goods.cover"
label="封面" label="封面"
> >
<NFlex v-if="currentGoodsModel.goods.cover"> <NFlex
vertical
:gap="8"
>
<NFlex
v-if="currentGoodsModel.goods.cover"
:gap="8"
align="center"
>
<NText>当前封面: </NText> <NText>当前封面: </NText>
<NImage <NImage
:src="FILE_BASE_URL + currentGoodsModel.goods.cover" :src="FILE_BASE_URL + currentGoodsModel.goods.cover"
@@ -672,12 +768,53 @@ onMounted(() => { })
> >
+ {{ currentGoodsModel.goods.cover ? '更换' : '上传' }}封面 + {{ currentGoodsModel.goods.cover ? '更换' : '上传' }}封面
</NUpload> </NUpload>
</NFlex>
</NFormItem> </NFormItem>
</NFlex>
<!-- 兑换规则分组 -->
<NDivider
title-placement="left"
style="margin: 16px 0"
>
兑换规则
</NDivider>
<NFlex
vertical
:gap="12"
style="margin-bottom: 16px"
>
<NFormItem
path="goods.type"
label="礼物类型"
>
<NRadioGroup v-model:value="currentGoodsModel.goods.type">
<NRadioButton :value="GoodsTypes.Virtual">
虚拟礼物
</NRadioButton>
<NRadioButton :value="GoodsTypes.Physical">
实体礼物
</NRadioButton>
</NRadioGroup>
</NFormItem>
<NFormItem
path="settings"
label="选项"
>
<NCheckbox v-model:checked="currentGoodsModel.goods.isAllowRebuy">
允许重复兑换
</NCheckbox>
</NFormItem>
<NFormItem <NFormItem
path="goods.guardFree" path="goods.guardFree"
label="兑换规则" label="特殊权限"
>
<NFlex
vertical
:gap="8"
> >
<NFlex vertical>
<NCheckbox <NCheckbox
:checked="currentGoodsModel.goods.setting?.guardFree != undefined" :checked="currentGoodsModel.goods.setting?.guardFree != undefined"
@update:checked=" @update:checked="
@@ -705,7 +842,11 @@ onMounted(() => { })
中存在对应记录时才能生效 中存在对应记录时才能生效
</NTooltip> </NTooltip>
</NCheckbox> </NCheckbox>
<NFlex v-if="currentGoodsModel.goods.setting?.guardFree">
<NFlex
v-if="currentGoodsModel.goods.setting?.guardFree"
:gap="8"
>
<NSelect <NSelect
v-model:value="currentGoodsModel.goods.setting.guardFree.year" v-model:value="currentGoodsModel.goods.setting.guardFree.year"
:options="allowedYearOptions" :options="allowedYearOptions"
@@ -717,6 +858,7 @@ onMounted(() => { })
placeholder="请选择月份" placeholder="请选择月份"
/> />
</NFlex> </NFlex>
<NText> <NText>
最低兑换等级 最低兑换等级
<NTooltip> <NTooltip>
@@ -753,28 +895,21 @@ onMounted(() => { })
</NRadioGroup> </NRadioGroup>
</NFlex> </NFlex>
</NFormItem> </NFormItem>
<NFormItem </NFlex>
path="goods.type"
label="类型" <!-- 礼物类型特定配置 -->
>
<NRadioGroup v-model:value="currentGoodsModel.goods.type">
<NRadioButton :value="GoodsTypes.Virtual">
虚拟礼物
</NRadioButton>
<NRadioButton :value="GoodsTypes.Physical">
实体礼物
</NRadioButton>
</NRadioGroup>
</NFormItem>
<NFormItem
path="settings"
label="选项"
>
<NCheckbox v-model:checked="currentGoodsModel.goods.isAllowRebuy">
允许重复兑换
</NCheckbox>
</NFormItem>
<template v-if="currentGoodsModel.goods.type == GoodsTypes.Physical"> <template v-if="currentGoodsModel.goods.type == GoodsTypes.Physical">
<NDivider
title-placement="left"
style="margin: 16px 0"
>
实物礼物配置
</NDivider>
<NFlex
vertical
:gap="12"
style="margin-bottom: 16px"
>
<NFormItem <NFormItem
path="goods.maxBuyCount" path="goods.maxBuyCount"
label="最大兑换数量" label="最大兑换数量"
@@ -785,11 +920,15 @@ onMounted(() => { })
min="1" min="1"
/> />
</NFormItem> </NFormItem>
<NFormItem <NFormItem
path="address" path="address"
label="收货地址" label="收货地址"
> >
<NFlex vertical> <NFlex
vertical
:gap="8"
>
<NRadioGroup <NRadioGroup
:value="currentGoodsModel.goods.collectUrl == undefined ? 0 : 1" :value="currentGoodsModel.goods.collectUrl == undefined ? 0 : 1"
@update:value="(v) => (currentGoodsModel.goods.collectUrl = v == 1 ? '' : undefined)" @update:value="(v) => (currentGoodsModel.goods.collectUrl = v == 1 ? '' : undefined)"
@@ -809,6 +948,7 @@ onMounted(() => { })
</NRadioGroup> </NRadioGroup>
</NFlex> </NFlex>
</NFormItem> </NFormItem>
<template v-if="currentGoodsModel.goods.collectUrl != undefined"> <template v-if="currentGoodsModel.goods.collectUrl != undefined">
<NFormItem <NFormItem
path="goods.collectUrl" path="goods.collectUrl"
@@ -816,6 +956,7 @@ onMounted(() => { })
> >
<NFlex <NFlex
vertical vertical
:gap="8"
style="width: 100%" style="width: 100%"
> >
<NInput <NInput
@@ -840,8 +981,20 @@ onMounted(() => { })
</NCheckbox> </NCheckbox>
</NFormItem> </NFormItem>
</template> </template>
</NFlex>
</template> </template>
<template v-else> <template v-else>
<NDivider
title-placement="left"
style="margin: 16px 0"
>
虚拟礼物配置
</NDivider>
<NFlex
vertical
:gap="12"
style="margin-bottom: 16px"
>
<NFormItem <NFormItem
path="goods.content" path="goods.content"
required required
@@ -864,15 +1017,52 @@ onMounted(() => { })
clearable clearable
/> />
</NFormItem> </NFormItem>
</NFlex>
</template> </template>
<NFlex
justify="center"
style="margin-top: 24px"
>
<NButton <NButton
type="primary" type="primary"
size="large"
:loading="isUpdating" :loading="isUpdating"
@click="updateGoods" @click="updateGoods"
> >
{{ currentGoodsModel.goods.id ? '修改' : '创建' }} {{ currentGoodsModel.goods.id ? '修改' : '创建' }}
</NButton> </NButton>
</NFlex>
</NForm> </NForm>
</NScrollbar> </NScrollbar>
</NModal> </NModal>
</template> </template>
<style scoped>
.point-goods-card {
height: 100%;
display: flex;
flex-direction: column;
}
.point-goods-card :deep(.n-card-header) {
padding: 16px;
}
.point-goods-card :deep(.n-card-content) {
padding: 16px;
flex-grow: 1;
}
.point-goods-card :deep(.n-card-footer) {
padding: 16px;
}
.goods-modal :deep(.n-card-header) {
padding: 16px 20px;
}
.goods-modal :deep(.n-card-content) {
padding: 0 20px 20px;
}
</style>

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,64 +104,40 @@ const filteredUsers = computed(() => {
// 当前查看的用户详情 // 当前查看的用户详情
const currentUser = ref<ResponsePointUserModel>() const currentUser = ref<ResponsePointUserModel>()
// 数据表格列定义 // 渲染用户名或用户ID
const column: DataTableColumns<ResponsePointUserModel> = [ const renderUsername = (user: ResponsePointUserModel) => {
{ if (user.info?.name) {
title: '认证', return user.info.name
key: 'auth', }
render: (row: ResponsePointUserModel) => {
return h(NTag, { type: row.isAuthed ? 'success' : 'error' }, () => (row.isAuthed ? '已认证' : '未认证')) return h(NFlex, null, () => [
},
},
{
title: '用户名',
key: 'username',
render: (row: ResponsePointUserModel) => {
return (
row.info?.name ??
h(NFlex, null, () => [
'未知', '未知',
h(NText, { depth: 3 }, { default: () => `(${row.info.userId ?? row.info.openId})` }), h(NText, { depth: 3 }, { default: () => `(${user.info.userId ?? user.info.openId})` }),
]) ])
) }
},
}, // 渲染订单数量,更友好的显示方式
{ const renderOrderCount = (user: ResponsePointUserModel) => {
title: '积分', if (!user.isAuthed) return h(NText, { depth: 3 }, { default: () => '未认证' })
key: 'point', return user.orderCount > 0 ? h(NText, {}, { default: () => formatNumber(user.orderCount) }) : h(NText, { depth: 3 }, { default: () => '无订单' })
sorter: 'default', }
render: (row: ResponsePointUserModel) => {
return row.point // 渲染时间戳为相对时间和绝对时间
}, const renderTime = (timestamp: number) => {
},
{
title: '订单数量',
key: 'orders',
render: (row: ResponsePointUserModel) => {
return row.isAuthed ? row.orderCount : '无'
},
},
{
title: '最后更新于',
key: 'updateAt',
sorter: 'default',
render: (row: ResponsePointUserModel) => {
return h(NTooltip, null, { return h(NTooltip, null, {
trigger: () => h(NTime, { time: row.updateAt, type: 'relative' }), trigger: () => h(NTime, { time: timestamp, type: 'relative' }),
default: () => h(NTime, { time: row.updateAt }), default: () => h(NTime, { time: timestamp }),
}) })
}, }
},
{ // 渲染操作按钮
title: '操作', const renderActions = (user: ResponsePointUserModel) => {
key: 'action',
render: (row: ResponsePointUserModel) => {
return h(NFlex, { justify: 'center', gap: 8 }, () => [ return h(NFlex, { justify: 'center', gap: 8 }, () => [
h( h(
NButton, NButton,
{ {
onClick: () => { onClick: () => {
currentUser.value = row currentUser.value = user
showModal.value = true showModal.value = true
}, },
type: 'info', type: 'info',
@@ -170,7 +147,7 @@ const column: DataTableColumns<ResponsePointUserModel> = [
), ),
h( h(
NPopconfirm, NPopconfirm,
{ onPositiveClick: () => deleteUser(row) }, { onPositiveClick: () => deleteUser(user) },
{ {
default: () => '确定要删除这个用户吗?记录将无法恢复', default: () => '确定要删除这个用户吗?记录将无法恢复',
trigger: () => trigger: () =>
@@ -185,8 +162,54 @@ const column: DataTableColumns<ResponsePointUserModel> = [
}, },
), ),
]) ])
}
// 格式化数字,添加千位符
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> = [
{
title: '认证',
key: 'auth',
render: (row: ResponsePointUserModel) => {
return h(NTag, { type: row.isAuthed ? 'success' : 'error' }, () => (row.isAuthed ? '已认证' : '未认证'))
}, },
}, },
{
title: '用户名',
key: 'username',
render: (row: ResponsePointUserModel) => renderUsername(row),
},
{
title: '积分',
key: 'point',
sorter: 'default',
render: (row: ResponsePointUserModel) => renderPoint(row.point),
},
{
title: '订单数量',
key: 'orderCount',
render: (row: ResponsePointUserModel) => renderOrderCount(row),
},
{
title: '最后更新于',
key: 'updateAt',
sorter: 'default',
render: (row: ResponsePointUserModel) => renderTime(row.updateAt),
},
{
title: '操作',
key: 'action',
render: (row: ResponsePointUserModel) => renderActions(row),
},
] ]
// 获取所有用户 // 获取所有用户
@@ -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,9 +99,41 @@ const buttons = (song: SongsInfo) => [
</script> </script>
<template> <template>
<div class="song-list-container">
<NDivider style="margin-top: 10px" /> <NDivider style="margin-top: 10px" />
<!-- 左侧翻页按钮 -->
<div class="page-button page-button-left">
<NButton
circle
secondary
size="large"
title="上一页 (←)"
@click="handlePrevPage"
>
<template #icon>
<NIcon :component="ChevronLeft24Filled" />
</template>
</NButton>
</div>
<!-- 右侧翻页按钮 -->
<div class="page-button page-button-right">
<NButton
circle
secondary
size="large"
title="下一页 (→)"
@click="handleNextPage"
>
<template #icon>
<NIcon :component="ChevronRight24Filled" />
</template>
</NButton>
</div>
<SongList <SongList
v-if="data" v-if="data"
ref="songListRef"
:songs="data ?? []" :songs="data ?? []"
:is-self="accountInfo?.id == userInfo?.id" :is-self="accountInfo?.id == userInfo?.id"
:extra-button="buttons" :extra-button="buttons"
@@ -76,4 +152,36 @@ const buttons = (song: SongsInfo) => [
</NCollapseItem> </NCollapseItem>
</NCollapse> </NCollapse>
<NDivider /> <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>