mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
Compare commits
2 Commits
00ce0fc7e1
...
0591d0575d
| Author | SHA1 | Date | |
|---|---|---|---|
| 0591d0575d | |||
| 8b908f5ac9 |
BIN
message_render_content.txt
Normal file
BIN
message_render_content.txt
Normal file
Binary file not shown.
@@ -87,6 +87,33 @@ const batchUpdate_Option = ref<SongRequestOption | undefined>() // 批量编辑
|
||||
const columns = ref<DataTableColumns<SongsInfo>>() // 表格列定义
|
||||
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>>({
|
||||
title: '作者',
|
||||
@@ -751,7 +776,8 @@ onMounted(() => {
|
||||
pageSizes: [10, 25, 50, 100, 200],
|
||||
showSizePicker: true,
|
||||
showQuickJumper: true,
|
||||
|
||||
page: currentPage,
|
||||
onUpdatePage: handlePageChange
|
||||
}"
|
||||
:loading="isLoading && songsComputed.length === 0"
|
||||
striped
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,4 +21,5 @@ export interface ScheduleConfigType {
|
||||
userInfo: UserInfo | undefined
|
||||
biliInfo: any | undefined
|
||||
data: ScheduleWeekInfo[] | undefined
|
||||
config?: any
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import DefaultIndexTemplateVue from '@/views/view/indexTemplate/DefaultIndexTemplate.vue';
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import { defineAsyncComponent, ref, markRaw } from 'vue';
|
||||
|
||||
const debugAPI =
|
||||
import.meta.env.VITE_API == 'dev'
|
||||
@@ -74,40 +74,40 @@ export const ScheduleTemplateMap: TemplateMapType = {
|
||||
'': {
|
||||
name: '默认',
|
||||
//settingName: 'Template.Schedule.Default',
|
||||
component: defineAsyncComponent(
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/scheduleTemplate/DefaultScheduleTemplate.vue')
|
||||
)
|
||||
))
|
||||
},
|
||||
pinky: {
|
||||
name: '粉粉',
|
||||
//settingName: 'Template.Schedule.Pinky',
|
||||
component: defineAsyncComponent(
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/scheduleTemplate/PinkySchedule.vue')
|
||||
)
|
||||
))
|
||||
}
|
||||
};
|
||||
export const SongListTemplateMap: TemplateMapType = {
|
||||
'': {
|
||||
name: '默认',
|
||||
//settingName: 'Template.SongList.Default',
|
||||
component: defineAsyncComponent(
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/songListTemplate/DefaultSongListTemplate.vue')
|
||||
)
|
||||
))
|
||||
},
|
||||
simple: {
|
||||
name: '简单',
|
||||
//settingName: 'Template.SongList.Simple',
|
||||
component: defineAsyncComponent(
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() => import('@/views/view/songListTemplate/SimpleSongListTemplate.vue')
|
||||
)
|
||||
))
|
||||
},
|
||||
traditional: {
|
||||
name: '列表',
|
||||
settingName: 'Template.SongList.Traditional',
|
||||
component: defineAsyncComponent(
|
||||
component: markRaw(defineAsyncComponent(
|
||||
() =>
|
||||
import('@/views/view/songListTemplate/TraditionalSongListTemplate.vue')
|
||||
)
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -115,7 +115,7 @@ export const IndexTemplateMap: TemplateMapType = {
|
||||
'': {
|
||||
name: '默认',
|
||||
//settingName: 'Template.Index.Default',
|
||||
component: DefaultIndexTemplateVue
|
||||
component: markRaw(DefaultIndexTemplateVue)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
SelectOption,
|
||||
useMessage,
|
||||
} 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';
|
||||
|
||||
// 模板定义类型接口
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CN_HOST, CURRENT_HOST, FILE_BASE_URL, POINT_API_URL } from '@/data/cons
|
||||
import { useAuthStore } from '@/store/useAuthStore'
|
||||
import { Info24Filled } from '@vicons/fluent'
|
||||
import { useRouteHash } from '@vueuse/router'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import {
|
||||
FormItemRule,
|
||||
NAlert,
|
||||
@@ -46,16 +47,19 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import PointOrderManage from './PointOrderManage.vue'
|
||||
import PointSettings from './PointSettings.vue'
|
||||
import PointUserManage from './PointUserManage.vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const message = useMessage()
|
||||
const accountInfo = useAccount()
|
||||
const dialog = useDialog()
|
||||
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({
|
||||
get() {
|
||||
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 defaultGoodsModel = {
|
||||
goods: {
|
||||
@@ -72,39 +77,60 @@ const defaultGoodsModel = {
|
||||
status: GoodsStatus.Normal,
|
||||
maxBuyCount: 1,
|
||||
isAllowRebuy: false,
|
||||
setting: {},
|
||||
setting: {
|
||||
allowGuardLevel: 0
|
||||
},
|
||||
} as PointGoodsModel,
|
||||
fileList: [],
|
||||
} as { 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(() => {
|
||||
//从2024到现在的年份
|
||||
return Array.from({ length: new Date().getFullYear() - 2024 + 1 }, (_, i) => 2024 + i).map((item) => {
|
||||
return {
|
||||
return Array.from({ length: new Date().getFullYear() - 2024 + 1 }, (_, i) => 2024 + i).map((item) => ({
|
||||
label: item.toString() + '年',
|
||||
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 = {
|
||||
name: {
|
||||
required: true,
|
||||
@@ -121,30 +147,23 @@ const rules = {
|
||||
content: {
|
||||
required: true,
|
||||
message: '请输入虚拟礼物的具体内容',
|
||||
validator: (rule: FormItemRule, value: string) => {
|
||||
return currentGoodsModel.value.goods.type != GoodsTypes.Virtual || (value?.length ?? 0) > 0
|
||||
},
|
||||
validator: (rule: FormItemRule, value: string) =>
|
||||
currentGoodsModel.value.goods.type != GoodsTypes.Virtual || (value?.length ?? 0) > 0
|
||||
},
|
||||
privacy: {
|
||||
required: true,
|
||||
message: '需要阅读并同意本站隐私协议',
|
||||
validator: (rule: FormItemRule, value: boolean) => {
|
||||
return (
|
||||
validator: (rule: FormItemRule, value: boolean) =>
|
||||
(currentGoodsModel.value.goods.type != GoodsTypes.Physical &&
|
||||
currentGoodsModel.value.goods.collectUrl != undefined) ||
|
||||
isAllowedPrivacyPolicy.value
|
||||
)
|
||||
},
|
||||
},
|
||||
maxBuyCount: {
|
||||
required: true,
|
||||
message: '需要输入最大购买数量',
|
||||
validator: (rule: FormItemRule, value: number) => {
|
||||
return (
|
||||
validator: (rule: FormItemRule, value: number) =>
|
||||
currentGoodsModel.value.goods.type != GoodsTypes.Physical ||
|
||||
(currentGoodsModel.value.goods.maxBuyCount ?? 0) > 0
|
||||
)
|
||||
},
|
||||
},
|
||||
'goods.url': {
|
||||
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) {
|
||||
let success = false
|
||||
if (enable) {
|
||||
success = await EnableFunction(FunctionTypes.Point)
|
||||
} else {
|
||||
success = await DisableFunction(FunctionTypes.Point)
|
||||
}
|
||||
const success = enable ? await EnableFunction(FunctionTypes.Point) : await DisableFunction(FunctionTypes.Point)
|
||||
|
||||
if (success) {
|
||||
message.success('已' + (enable ? '启用' : '禁用') + '积分系统')
|
||||
} else {
|
||||
message.error('无法' + (enable ? '启用' : '禁用') + '积分系统')
|
||||
}
|
||||
}
|
||||
|
||||
async function updateGoods(e: MouseEvent) {
|
||||
if (isUpdating.value || !formRef.value) return
|
||||
e.preventDefault()
|
||||
isUpdating.value = true
|
||||
await formRef.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (currentGoodsModel.value.fileList.length > 0) {
|
||||
currentGoodsModel.value.goods.cover = await getImageUploadModel(currentGoodsModel.value.fileList)
|
||||
}
|
||||
await QueryPostAPI<ResponsePointGoodModel>(POINT_API_URL + 'update-goods', currentGoodsModel.value.goods)
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
|
||||
const { code, data, message: errMsg } = await QueryPostAPI<ResponsePointGoodModel>(
|
||||
POINT_API_URL + 'update-goods',
|
||||
currentGoodsModel.value.goods
|
||||
)
|
||||
|
||||
if (code === 200) {
|
||||
message.success('成功')
|
||||
showAddGoodsModal.value = false
|
||||
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 {
|
||||
goods.value.push(data.data)
|
||||
goods.value.push(data)
|
||||
}
|
||||
} else {
|
||||
message.error('失败: ' + data.message)
|
||||
message.error('失败: ' + errMsg)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error('失败: ' + err)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
})
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.log(err)
|
||||
message.error('表单验证失败')
|
||||
})
|
||||
.finally(() => {
|
||||
message.error(typeof err === 'string' ? `失败: ${err}` : '表单验证失败')
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function OnFileListChange(files: UploadFileInfo[]) {
|
||||
if (files.length == 1) {
|
||||
const file = files[0]
|
||||
if ((file.file?.size ?? 0) > 10 * 1024 * 1024) {
|
||||
if (files.length === 1 && (files[0].file?.size ?? 0) > 10 * 1024 * 1024) {
|
||||
message.error('文件大小不能超过10MB')
|
||||
currentGoodsModel.value.fileList = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onUpdateClick(item: ResponsePointGoodModel) {
|
||||
currentGoodsModel.value = {
|
||||
goods: {
|
||||
@@ -278,8 +257,8 @@ function onUpdateClick(item: ResponsePointGoodModel) {
|
||||
isAllowedPrivacyPolicy.value = true
|
||||
showAddGoodsModal.value = true
|
||||
}
|
||||
//下架
|
||||
function onSetShelfClick(item: ResponsePointGoodModel, status: GoodsStatus) {
|
||||
|
||||
async function onSetShelfClick(item: ResponsePointGoodModel, status: GoodsStatus) {
|
||||
const d = dialog.warning({
|
||||
title: '警告',
|
||||
content: `你确定要${status == GoodsStatus.Normal ? '重新上架' : '下架'}这个礼物吗?`,
|
||||
@@ -288,22 +267,23 @@ function onSetShelfClick(item: ResponsePointGoodModel, status: GoodsStatus) {
|
||||
onPositiveClick: async () => {
|
||||
d.loading = true
|
||||
const originStatus = item.status
|
||||
//item.status = status
|
||||
|
||||
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],
|
||||
status: status,
|
||||
})
|
||||
if (data.code == 200) {
|
||||
|
||||
if (code === 200) {
|
||||
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) {
|
||||
goods.value[index].status = status
|
||||
}
|
||||
} else {
|
||||
message.error('失败: ' + data.message)
|
||||
message.error('失败: ' + errMsg)
|
||||
item.status = originStatus
|
||||
console.error(data.message)
|
||||
console.error(errMsg)
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('失败: ' + err)
|
||||
@@ -315,6 +295,7 @@ function onSetShelfClick(item: ResponsePointGoodModel, status: GoodsStatus) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function onDeleteClick(item: ResponsePointGoodModel) {
|
||||
const d = dialog.warning({
|
||||
title: '警告',
|
||||
@@ -323,16 +304,18 @@ function onDeleteClick(item: ResponsePointGoodModel) {
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
d.loading = true
|
||||
|
||||
try {
|
||||
const data = await QueryGetAPI(POINT_API_URL + 'delete-goods', {
|
||||
const { code, message: errMsg } = await QueryGetAPI(POINT_API_URL + 'delete-goods', {
|
||||
id: item.id,
|
||||
})
|
||||
if (data.code == 200) {
|
||||
|
||||
if (code === 200) {
|
||||
message.success('成功')
|
||||
goods.value = goods.value.filter((g) => g.id != item.id)
|
||||
goods.value = goods.value.filter(g => g.id !== item.id)
|
||||
} else {
|
||||
message.error('失败: ' + data.message)
|
||||
console.error(data.message)
|
||||
message.error('失败: ' + errMsg)
|
||||
console.error(errMsg)
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('失败: ' + err)
|
||||
@@ -343,30 +326,44 @@ function onDeleteClick(item: ResponsePointGoodModel) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function onModalOpen() {
|
||||
if (currentGoodsModel.value.goods.id) {
|
||||
resetGoods()
|
||||
}
|
||||
showAddGoodsModal.value = true
|
||||
}
|
||||
|
||||
function resetGoods() {
|
||||
currentGoodsModel.value = JSON.parse(JSON.stringify(defaultGoodsModel))
|
||||
}
|
||||
function responseGoodsToModel(goods: ResponsePointGoodModel) { }
|
||||
|
||||
onMounted(() => { })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NFlex>
|
||||
<!-- 头部状态卡片 -->
|
||||
<NFlex
|
||||
vertical
|
||||
:size="16"
|
||||
>
|
||||
<NFlex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
:gap="16"
|
||||
>
|
||||
<NAlert
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Point) && accountInfo.eventFetcherState.online
|
||||
? 'success'
|
||||
: 'warning'
|
||||
"
|
||||
style="min-width: 400px"
|
||||
style="flex: 1; min-width: 300px"
|
||||
>
|
||||
启用
|
||||
<NFlex
|
||||
align="center"
|
||||
:gap="8"
|
||||
>
|
||||
<span>启用</span>
|
||||
<NButton
|
||||
text
|
||||
type="primary"
|
||||
@@ -381,8 +378,11 @@ onMounted(() => { })
|
||||
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.Point)"
|
||||
@update:value="setFunctionEnable"
|
||||
/>
|
||||
<br>
|
||||
<NText depth="3">
|
||||
</NFlex>
|
||||
<NText
|
||||
depth="3"
|
||||
style="margin-top: 8px; display: block"
|
||||
>
|
||||
此功能需要部署
|
||||
<NButton
|
||||
text
|
||||
@@ -398,13 +398,19 @@ onMounted(() => { })
|
||||
</NAlert>
|
||||
<EventFetcherStatusCard />
|
||||
</NFlex>
|
||||
|
||||
<!-- 礼物展示页链接 -->
|
||||
<NDivider
|
||||
style="margin: 16px 0 16px 0"
|
||||
style="margin: 16px 0"
|
||||
title-placement="left"
|
||||
>
|
||||
礼物展示页链接
|
||||
</NDivider>
|
||||
<NFlex align="center">
|
||||
|
||||
<NFlex
|
||||
align="center"
|
||||
:gap="12"
|
||||
>
|
||||
<NInputGroup style="max-width: 400px;">
|
||||
<NInput
|
||||
:value="`${useCNUrl ? CN_HOST : CURRENT_HOST}@${accountInfo.name}/goods`"
|
||||
@@ -421,16 +427,26 @@ onMounted(() => { })
|
||||
使用国内镜像(访问更快)
|
||||
</NCheckbox>
|
||||
</NFlex>
|
||||
<NDivider />
|
||||
</NFlex>
|
||||
|
||||
<NDivider style="margin: 16px 0" />
|
||||
|
||||
<!-- 主要内容标签页 -->
|
||||
<NTabs
|
||||
v-model:value="hash"
|
||||
animated
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
<!-- 礼物管理标签页 -->
|
||||
<NTabPane
|
||||
name="goods"
|
||||
tab="礼物"
|
||||
>
|
||||
<NFlex>
|
||||
<NFlex
|
||||
justify="start"
|
||||
:gap="12"
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="onModalOpen"
|
||||
@@ -444,7 +460,8 @@ onMounted(() => { })
|
||||
前往展示页
|
||||
</NButton>
|
||||
</NFlex>
|
||||
<NDivider />
|
||||
|
||||
<!-- 上架礼物列表 -->
|
||||
<NEmpty
|
||||
v-if="goods.filter((g) => g.status != GoodsStatus.Discontinued).length == 0"
|
||||
description="暂无礼物"
|
||||
@@ -452,17 +469,28 @@ onMounted(() => { })
|
||||
<NGrid
|
||||
v-else
|
||||
cols="1 500:2 700:3 1000:4 1200:5"
|
||||
:x-gap="12"
|
||||
:y-gap="8"
|
||||
:x-gap="16"
|
||||
:y-gap="16"
|
||||
>
|
||||
<NGridItem
|
||||
v-for="item in goods.filter((g) => g.status != GoodsStatus.Discontinued)"
|
||||
:key="item.id"
|
||||
>
|
||||
<PointGoodsItem :goods="item">
|
||||
<PointGoodsItem
|
||||
:goods="item"
|
||||
class="point-goods-card"
|
||||
>
|
||||
<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
|
||||
type="info"
|
||||
size="small"
|
||||
@@ -485,11 +513,16 @@ onMounted(() => { })
|
||||
删除
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</template>
|
||||
</PointGoodsItem>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
<NDivider>已下架</NDivider>
|
||||
|
||||
<!-- 下架礼物列表 -->
|
||||
<NDivider style="margin: 24px 0 16px">
|
||||
已下架
|
||||
</NDivider>
|
||||
<NEmpty
|
||||
v-if="goods.filter((g) => g.status == GoodsStatus.Discontinued).length == 0"
|
||||
description="暂无已下架的礼物"
|
||||
@@ -497,17 +530,28 @@ onMounted(() => { })
|
||||
<NGrid
|
||||
v-else
|
||||
cols="1 500:2 700:3 1000:4 1200:5"
|
||||
:x-gap="12"
|
||||
:y-gap="8"
|
||||
:x-gap="16"
|
||||
:y-gap="16"
|
||||
>
|
||||
<NGridItem
|
||||
v-for="item in goods.filter((g) => g.status == GoodsStatus.Discontinued)"
|
||||
:key="item.id"
|
||||
>
|
||||
<PointGoodsItem :goods="item">
|
||||
<PointGoodsItem
|
||||
:goods="item"
|
||||
class="point-goods-card"
|
||||
>
|
||||
<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
|
||||
type="info"
|
||||
size="small"
|
||||
@@ -530,11 +574,14 @@ onMounted(() => { })
|
||||
删除
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</template>
|
||||
</PointGoodsItem>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</NTabPane>
|
||||
|
||||
<!-- 订单管理标签页 -->
|
||||
<NTabPane
|
||||
name="orders"
|
||||
tab="订单"
|
||||
@@ -542,6 +589,8 @@ onMounted(() => { })
|
||||
>
|
||||
<PointOrderManage :goods="goods" />
|
||||
</NTabPane>
|
||||
|
||||
<!-- 用户管理标签页 -->
|
||||
<NTabPane
|
||||
name="users"
|
||||
tab="用户"
|
||||
@@ -549,6 +598,8 @@ onMounted(() => { })
|
||||
>
|
||||
<PointUserManage :goods="goods" />
|
||||
</NTabPane>
|
||||
|
||||
<!-- 设置标签页 -->
|
||||
<NTabPane
|
||||
name="settings"
|
||||
tab="设置"
|
||||
@@ -557,12 +608,14 @@ onMounted(() => { })
|
||||
<PointSettings />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
<NDivider />
|
||||
|
||||
<!-- 添加/修改礼物模态框 -->
|
||||
<NModal
|
||||
v-model:show="showAddGoodsModal"
|
||||
preset="card"
|
||||
style="width: 600px; max-width: 90%"
|
||||
title="添加/修改礼物信息"
|
||||
class="goods-modal"
|
||||
>
|
||||
<template #header-extra>
|
||||
<NPopconfirm
|
||||
@@ -585,7 +638,19 @@ onMounted(() => { })
|
||||
ref="formRef"
|
||||
:model="currentGoodsModel"
|
||||
: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
|
||||
path="goods.name"
|
||||
@@ -597,6 +662,7 @@ onMounted(() => { })
|
||||
placeholder="必填, 礼物名称"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem
|
||||
path="goods.price"
|
||||
label="所需积分"
|
||||
@@ -608,9 +674,14 @@ onMounted(() => { })
|
||||
min="0"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem
|
||||
path="goods.count"
|
||||
label="库存"
|
||||
>
|
||||
<NFlex
|
||||
:gap="12"
|
||||
align="center"
|
||||
>
|
||||
<NCheckbox
|
||||
:checked="!currentGoodsModel.goods.count"
|
||||
@@ -621,10 +692,25 @@ onMounted(() => { })
|
||||
<NInputNumber
|
||||
v-if="currentGoodsModel.goods.count"
|
||||
v-model:value="currentGoodsModel.goods.count"
|
||||
placeholder="可选, 礼物库存"
|
||||
placeholder="礼物库存"
|
||||
style="max-width: 120px"
|
||||
/>
|
||||
</NFlex>
|
||||
</NFormItem>
|
||||
</NFlex>
|
||||
|
||||
<!-- 详细描述分组 -->
|
||||
<NDivider
|
||||
title-placement="left"
|
||||
style="margin: 16px 0"
|
||||
>
|
||||
详细描述
|
||||
</NDivider>
|
||||
<NFlex
|
||||
vertical
|
||||
:gap="12"
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<NFormItem
|
||||
path="goods.description"
|
||||
label="描述"
|
||||
@@ -636,6 +722,7 @@ onMounted(() => { })
|
||||
type="textarea"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem
|
||||
path="goods.tags"
|
||||
label="标签"
|
||||
@@ -650,11 +737,20 @@ onMounted(() => { })
|
||||
:options="existTags"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem
|
||||
path="goods.cover"
|
||||
label="封面"
|
||||
>
|
||||
<NFlex v-if="currentGoodsModel.goods.cover">
|
||||
<NFlex
|
||||
vertical
|
||||
:gap="8"
|
||||
>
|
||||
<NFlex
|
||||
v-if="currentGoodsModel.goods.cover"
|
||||
:gap="8"
|
||||
align="center"
|
||||
>
|
||||
<NText>当前封面: </NText>
|
||||
<NImage
|
||||
:src="FILE_BASE_URL + currentGoodsModel.goods.cover"
|
||||
@@ -672,12 +768,53 @@ onMounted(() => { })
|
||||
>
|
||||
+ {{ currentGoodsModel.goods.cover ? '更换' : '上传' }}封面
|
||||
</NUpload>
|
||||
</NFlex>
|
||||
</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
|
||||
path="goods.guardFree"
|
||||
label="兑换规则"
|
||||
label="特殊权限"
|
||||
>
|
||||
<NFlex
|
||||
vertical
|
||||
:gap="8"
|
||||
>
|
||||
<NFlex vertical>
|
||||
<NCheckbox
|
||||
:checked="currentGoodsModel.goods.setting?.guardFree != undefined"
|
||||
@update:checked="
|
||||
@@ -705,7 +842,11 @@ onMounted(() => { })
|
||||
中存在对应记录时才能生效
|
||||
</NTooltip>
|
||||
</NCheckbox>
|
||||
<NFlex v-if="currentGoodsModel.goods.setting?.guardFree">
|
||||
|
||||
<NFlex
|
||||
v-if="currentGoodsModel.goods.setting?.guardFree"
|
||||
:gap="8"
|
||||
>
|
||||
<NSelect
|
||||
v-model:value="currentGoodsModel.goods.setting.guardFree.year"
|
||||
:options="allowedYearOptions"
|
||||
@@ -717,6 +858,7 @@ onMounted(() => { })
|
||||
placeholder="请选择月份"
|
||||
/>
|
||||
</NFlex>
|
||||
|
||||
<NText>
|
||||
最低兑换等级
|
||||
<NTooltip>
|
||||
@@ -753,28 +895,21 @@ onMounted(() => { })
|
||||
</NRadioGroup>
|
||||
</NFlex>
|
||||
</NFormItem>
|
||||
<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>
|
||||
</NFlex>
|
||||
|
||||
<!-- 礼物类型特定配置 -->
|
||||
<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
|
||||
path="goods.maxBuyCount"
|
||||
label="最大兑换数量"
|
||||
@@ -785,11 +920,15 @@ onMounted(() => { })
|
||||
min="1"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem
|
||||
path="address"
|
||||
label="收货地址"
|
||||
>
|
||||
<NFlex vertical>
|
||||
<NFlex
|
||||
vertical
|
||||
:gap="8"
|
||||
>
|
||||
<NRadioGroup
|
||||
:value="currentGoodsModel.goods.collectUrl == undefined ? 0 : 1"
|
||||
@update:value="(v) => (currentGoodsModel.goods.collectUrl = v == 1 ? '' : undefined)"
|
||||
@@ -809,6 +948,7 @@ onMounted(() => { })
|
||||
</NRadioGroup>
|
||||
</NFlex>
|
||||
</NFormItem>
|
||||
|
||||
<template v-if="currentGoodsModel.goods.collectUrl != undefined">
|
||||
<NFormItem
|
||||
path="goods.collectUrl"
|
||||
@@ -816,6 +956,7 @@ onMounted(() => { })
|
||||
>
|
||||
<NFlex
|
||||
vertical
|
||||
:gap="8"
|
||||
style="width: 100%"
|
||||
>
|
||||
<NInput
|
||||
@@ -840,8 +981,20 @@ onMounted(() => { })
|
||||
</NCheckbox>
|
||||
</NFormItem>
|
||||
</template>
|
||||
</NFlex>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NDivider
|
||||
title-placement="left"
|
||||
style="margin: 16px 0"
|
||||
>
|
||||
虚拟礼物配置
|
||||
</NDivider>
|
||||
<NFlex
|
||||
vertical
|
||||
:gap="12"
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<NFormItem
|
||||
path="goods.content"
|
||||
required
|
||||
@@ -864,15 +1017,52 @@ onMounted(() => { })
|
||||
clearable
|
||||
/>
|
||||
</NFormItem>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<NFlex
|
||||
justify="center"
|
||||
style="margin-top: 24px"
|
||||
>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="isUpdating"
|
||||
@click="updateGoods"
|
||||
>
|
||||
{{ currentGoodsModel.goods.id ? '修改' : '创建' }}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NForm>
|
||||
</NScrollbar>
|
||||
</NModal>
|
||||
</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>
|
||||
|
||||
@@ -89,9 +89,10 @@ const filteredUsers = computed(() => {
|
||||
|
||||
// 根据关键词搜索
|
||||
if (settings.value.searchKeyword) {
|
||||
const keyword = settings.value.searchKeyword.toLowerCase()
|
||||
return (
|
||||
user.info.name?.toLowerCase().includes(settings.value.searchKeyword.toLowerCase()) == true ||
|
||||
user.info.userId?.toString() == settings.value.searchKeyword
|
||||
user.info.name?.toLowerCase().includes(keyword) == true ||
|
||||
user.info.userId?.toString() == keyword
|
||||
)
|
||||
}
|
||||
|
||||
@@ -103,64 +104,40 @@ const filteredUsers = computed(() => {
|
||||
// 当前查看的用户详情
|
||||
const currentUser = ref<ResponsePointUserModel>()
|
||||
|
||||
// 数据表格列定义
|
||||
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) => {
|
||||
return (
|
||||
row.info?.name ??
|
||||
h(NFlex, null, () => [
|
||||
// 渲染用户名或用户ID
|
||||
const renderUsername = (user: ResponsePointUserModel) => {
|
||||
if (user.info?.name) {
|
||||
return user.info.name
|
||||
}
|
||||
|
||||
return 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})` }),
|
||||
])
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '积分',
|
||||
key: 'point',
|
||||
sorter: 'default',
|
||||
render: (row: ResponsePointUserModel) => {
|
||||
return row.point
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '订单数量',
|
||||
key: 'orders',
|
||||
render: (row: ResponsePointUserModel) => {
|
||||
return row.isAuthed ? row.orderCount : '无'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '最后更新于',
|
||||
key: 'updateAt',
|
||||
sorter: 'default',
|
||||
render: (row: ResponsePointUserModel) => {
|
||||
}
|
||||
|
||||
// 渲染订单数量,更友好的显示方式
|
||||
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: row.updateAt, type: 'relative' }),
|
||||
default: () => h(NTime, { time: row.updateAt }),
|
||||
trigger: () => h(NTime, { time: timestamp, type: 'relative' }),
|
||||
default: () => h(NTime, { time: timestamp }),
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (row: ResponsePointUserModel) => {
|
||||
}
|
||||
|
||||
// 渲染操作按钮
|
||||
const renderActions = (user: ResponsePointUserModel) => {
|
||||
return h(NFlex, { justify: 'center', gap: 8 }, () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
onClick: () => {
|
||||
currentUser.value = row
|
||||
currentUser.value = user
|
||||
showModal.value = true
|
||||
},
|
||||
type: 'info',
|
||||
@@ -170,7 +147,7 @@ const column: DataTableColumns<ResponsePointUserModel> = [
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{ onPositiveClick: () => deleteUser(row) },
|
||||
{ onPositiveClick: () => deleteUser(user) },
|
||||
{
|
||||
default: () => '确定要删除这个用户吗?记录将无法恢复',
|
||||
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 />
|
||||
|
||||
<!-- 无数据提示 -->
|
||||
<template v-if="filteredUsers.length == 0">
|
||||
<NDivider />
|
||||
<NEmpty :description="settings.onlyAuthed ? '没有已认证的用户' : '没有用户'" />
|
||||
</template>
|
||||
<NEmpty
|
||||
v-if="filteredUsers.length == 0"
|
||||
:description="isLoading ? '加载中...' : (settings.onlyAuthed ? '没有已认证的用户' : '没有用户')"
|
||||
/>
|
||||
|
||||
<!-- 用户数据表格 -->
|
||||
<NDataTable
|
||||
@@ -465,6 +488,7 @@ onMounted(async () => {
|
||||
onUpdatePage: (page) => (pn = page),
|
||||
onUpdatePageSize: (pageSize) => (ps = pageSize)
|
||||
}"
|
||||
:loading="isLoading"
|
||||
/>
|
||||
</NSpin>
|
||||
|
||||
@@ -592,8 +616,8 @@ onMounted(async () => {
|
||||
<NButton
|
||||
type="error"
|
||||
:loading="isLoading"
|
||||
@click="resetAllPoints"
|
||||
:disabled="resetConfirmText !== RESET_CONFIRM_TEXT"
|
||||
@click="resetAllPoints"
|
||||
>
|
||||
确认重置所有用户积分
|
||||
</NButton>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
:author-name="message.authorName"
|
||||
:author-type="message.authorType"
|
||||
:privilege-type="message.privilegeType"
|
||||
:rich-content="getShowRichContent(message)"
|
||||
:content-parts="getShowContentParts(message)"
|
||||
:repeated="message.repeated"
|
||||
/>
|
||||
<paid-message
|
||||
@@ -205,7 +205,7 @@ export default defineComponent({
|
||||
},
|
||||
getGiftShowNameAndNum: constants.getGiftShowNameAndNum,
|
||||
getShowContent: constants.getShowContent,
|
||||
getShowRichContent: constants.getShowRichContent,
|
||||
getShowContentParts: constants.getShowContentParts,
|
||||
getShowAuthorName: constants.getShowAuthorName,
|
||||
|
||||
addMessage(message) {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
id="message"
|
||||
class="style-scope yt-live-chat-text-message-renderer"
|
||||
>
|
||||
<template v-for="(content, index) in richContent">
|
||||
<template v-for="(content, index) in contentParts">
|
||||
<span
|
||||
v-if="content.type === CONTENT_TYPE_TEXT"
|
||||
:key="index"
|
||||
@@ -81,7 +81,7 @@ const props = defineProps({
|
||||
time: Date,
|
||||
authorName: String,
|
||||
authorType: Number,
|
||||
richContent: Array,
|
||||
contentParts: Array,
|
||||
privilegeType: Number,
|
||||
repeated: Number
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ const props = defineProps<{
|
||||
userInfo: UserInfo | undefined
|
||||
biliInfo: any | undefined
|
||||
currentData?: any
|
||||
config?: any
|
||||
}>()
|
||||
const isLoading = ref(true)
|
||||
const message = useMessage()
|
||||
|
||||
@@ -4,9 +4,9 @@ import { SongsInfo } from '@/api/api-models'
|
||||
import SongList from '@/components/SongList.vue'
|
||||
import { SongListConfigType } from '@/data/TemplateTypes'
|
||||
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 { h, ref } from 'vue'
|
||||
import { h, ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const accountInfo = useAccount()
|
||||
|
||||
@@ -16,6 +16,50 @@ const emits = defineEmits(['requestSong'])
|
||||
|
||||
const isLoading = ref('')
|
||||
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) => [
|
||||
accountInfo.value?.id != props.userInfo?.id
|
||||
@@ -55,9 +99,41 @@ const buttons = (song: SongsInfo) => [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="song-list-container">
|
||||
<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
|
||||
v-if="data"
|
||||
ref="songListRef"
|
||||
:songs="data ?? []"
|
||||
:is-self="accountInfo?.id == userInfo?.id"
|
||||
:extra-button="buttons"
|
||||
@@ -76,4 +152,36 @@ const buttons = (song: SongsInfo) => [
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
<NDivider />
|
||||
</div>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user