mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
refactor: 优化多个视图组件并添加功能
本次提交对多个视图组件进行了重构和功能增强:
PointGoodsView.vue:
- 清理了未使用的导入(`useAccount`)和变量(`accountInfo`, `biliInfo` prop)。
- 通过重组计算属性和方法提高了代码可读性。
- 增强了商品列表的筛选和排序逻辑。
- 为购买商品功能添加了错误处理和加载状态。
PointUserHistoryView.vue:
- 为获取积分历史记录实现了加载状态。
- 改进了 PointHistoryCard 组件的渲染。
QuestionBoxView.vue:
- 优化了可读性和性能(整合状态变量,改进命名)。
- 增强了文件上传处理和验证逻辑。
- 改进了标签选择逻辑和数据获取方法。
- 添加了代码注释以提高可理解性。
UserIndexView.vue:
- 简化了确定要显示的模板组件的逻辑。
- 确保无论用户信息是否存在,都一致返回默认模板。
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { NavigateToNewTab } from '@/Utils'
|
||||
import { useAccount } from '@/api/account'
|
||||
// 移除未使用的 useAccount
|
||||
import {
|
||||
AddressInfo,
|
||||
GoodsTypes,
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
NFlex,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput, // 引入 NInput
|
||||
NInputNumber,
|
||||
NModal,
|
||||
NSelect,
|
||||
@@ -31,386 +32,600 @@ import {
|
||||
NTooltip,
|
||||
SelectOption,
|
||||
useDialog,
|
||||
useMessage
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, h, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// 移除未使用的 biliInfo prop
|
||||
const props = defineProps<{
|
||||
userInfo: UserInfo
|
||||
biliInfo: any
|
||||
}>()
|
||||
const router = useRouter()
|
||||
|
||||
const useAuth = useAuthStore()
|
||||
const accountInfo = useAccount()
|
||||
// 移除未使用的 accountInfo
|
||||
const isLoading = ref(false)
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const biliAuth = computed(() => useAuth.biliAuth)
|
||||
|
||||
const goods = ref<ResponsePointGoodModel[]>([])
|
||||
const currentPoint = ref<number>(-1)
|
||||
// --- 响应式状态 ---
|
||||
const goods = ref<ResponsePointGoodModel[]>([]) // 礼物列表
|
||||
const currentPoint = ref<number>(-1) // 当前用户积分
|
||||
|
||||
// 购买模态框相关状态
|
||||
const showBuyModal = ref(false)
|
||||
const showAddressSelect = ref(false)
|
||||
const currentGoods = ref<ResponsePointGoodModel>() // 当前选中的礼物
|
||||
const buyCount = ref(1) // 购买数量
|
||||
const selectedAddress = ref<AddressInfo>() // 选中的地址
|
||||
|
||||
const currentGoods = ref<ResponsePointGoodModel>()
|
||||
const buyCount = ref(1)
|
||||
const selectedAddress = ref<AddressInfo>()
|
||||
const canDoBuy = computed(() => {
|
||||
return currentGoods.value && currentGoods.value.price * buyCount.value <= currentPoint.value
|
||||
})
|
||||
const tags = computed(() => {
|
||||
return Array.from(new Set(goods.value.flatMap((g) => g.tags)))
|
||||
})
|
||||
const selectedTag = ref<string>()
|
||||
const selectedItems = computed(() => {
|
||||
return goods.value.filter((item) => selectedTag.value ? item.tags.includes(selectedTag.value) : true).filter((item) => !onlyCanBuy.value || getTooltip(item) == '开始兑换').filter((item) => !ignoreGuard.value || item.allowGuardLevel == 0)
|
||||
})
|
||||
// 筛选相关状态
|
||||
const selectedTag = ref<string>() // 选中的标签
|
||||
const onlyCanBuy = ref(false) // 只显示可兑换
|
||||
const ignoreGuard = ref(false) // 忽略舰长限制
|
||||
const priceOrder = ref<'asc' | 'desc' | null>(null) // 价格排序
|
||||
const searchKeyword = ref('') // 搜索关键词
|
||||
|
||||
const onlyCanBuy = ref(false)
|
||||
const ignoreGuard = ref(false)
|
||||
// --- 计算属性 ---
|
||||
|
||||
// 地址选项,用于地址选择器
|
||||
const addressOptions = computed(() => {
|
||||
if (!biliAuth.value.id) return []
|
||||
return (
|
||||
biliAuth.value.address?.map((item) => {
|
||||
return {
|
||||
label: item.address,
|
||||
value: item.id,
|
||||
}
|
||||
}) ?? []
|
||||
biliAuth.value.address?.map((item) => ({
|
||||
label: item.address, // 使用地址作为标签
|
||||
value: item.id, // 使用地址ID作为值
|
||||
})) ?? []
|
||||
)
|
||||
})
|
||||
|
||||
const canBuy = computed(() => {
|
||||
if (!biliAuth.value.id) return false
|
||||
return true
|
||||
// 判断是否可以执行购买操作
|
||||
const canDoBuy = computed(() => {
|
||||
if (!currentGoods.value) return false
|
||||
// 检查积分是否足够
|
||||
const pointCheck = currentGoods.value.price * buyCount.value <= currentPoint.value
|
||||
// 如果是实物礼物且没有外部收集链接,则必须选择地址
|
||||
const addressCheck =
|
||||
currentGoods.value.type !== GoodsTypes.Physical ||
|
||||
currentGoods.value.collectUrl ||
|
||||
!!selectedAddress.value
|
||||
return pointCheck && addressCheck
|
||||
})
|
||||
|
||||
// 礼物标签列表
|
||||
const tags = computed(() => {
|
||||
return Array.from(new Set(goods.value.flatMap((g) => g.tags)))
|
||||
})
|
||||
|
||||
// 经过筛选和排序后的礼物列表
|
||||
const selectedItems = computed(() => {
|
||||
let filteredItems = goods.value
|
||||
// 标签筛选
|
||||
.filter((item) => !selectedTag.value || item.tags.includes(selectedTag.value))
|
||||
// 可兑换筛选 (只显示 getTooltip 返回 '开始兑换' 的礼物)
|
||||
.filter((item) => !onlyCanBuy.value || getTooltip(item) === '开始兑换')
|
||||
// 舰长等级筛选 (只显示允许所有等级或忽略舰长限制的礼物)
|
||||
.filter((item) => !ignoreGuard.value || item.allowGuardLevel === 0)
|
||||
// 关键词搜索 (匹配名称或描述)
|
||||
.filter(
|
||||
(item) =>
|
||||
!searchKeyword.value ||
|
||||
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
(item.description && item.description.toLowerCase().includes(searchKeyword.value.toLowerCase())),
|
||||
)
|
||||
|
||||
// 价格排序
|
||||
if (priceOrder.value) {
|
||||
filteredItems = filteredItems.sort((a, b) => {
|
||||
return priceOrder.value === 'asc' ? a.price - b.price : b.price - a.price
|
||||
})
|
||||
}
|
||||
|
||||
return filteredItems
|
||||
})
|
||||
|
||||
// --- 方法 ---
|
||||
|
||||
// 获取礼物兑换按钮的提示文本
|
||||
function getTooltip(goods: ResponsePointGoodModel): '开始兑换' | '当前积分不足' | '请先进行账号认证' | '库存不足' {
|
||||
if ((currentPoint.value ?? 0) < goods.price) {
|
||||
return '当前积分不足'
|
||||
} else if (!biliAuth.value.id) return '请先进行账号认证'
|
||||
else if ((goods?.count ?? Number.MAX_VALUE) <= 0) return '库存不足'
|
||||
else {
|
||||
return '开始兑换'
|
||||
if (!biliAuth.value.id) return '请先进行账号认证' // 未认证
|
||||
if ((goods?.count ?? Number.MAX_VALUE) <= 0) return '库存不足' // 库存不足
|
||||
if ((currentPoint.value ?? 0) < goods.price) return '当前积分不足' // 积分不足
|
||||
return '开始兑换' // 可以兑换
|
||||
}
|
||||
|
||||
// 重置购买模态框状态
|
||||
function resetBuyModalState() {
|
||||
showBuyModal.value = false
|
||||
showAddressSelect.value = false
|
||||
selectedAddress.value = undefined
|
||||
buyCount.value = 1
|
||||
currentGoods.value = undefined
|
||||
}
|
||||
|
||||
// 处理模态框显示状态变化
|
||||
function handleModalUpdateShow(show: boolean) {
|
||||
if (!show) {
|
||||
resetBuyModalState()
|
||||
}
|
||||
}
|
||||
|
||||
// 执行购买操作
|
||||
async function buyGoods() {
|
||||
// 输入验证
|
||||
if (buyCount.value < 1) {
|
||||
message.error('兑换数量不能小于1')
|
||||
} else if (
|
||||
!selectedAddress.value &&
|
||||
currentGoods.value?.type == GoodsTypes.Physical &&
|
||||
!currentGoods.value.collectUrl
|
||||
return
|
||||
}
|
||||
if (!Number.isInteger(buyCount.value)) {
|
||||
message.error('兑换数量必须为整数')
|
||||
return
|
||||
}
|
||||
if (
|
||||
currentGoods.value?.type === GoodsTypes.Physical && // 是实物
|
||||
!currentGoods.value.collectUrl && // 没有外部收集链接
|
||||
!selectedAddress.value // 且没有选择地址
|
||||
) {
|
||||
message.error('请选择收货地址')
|
||||
} else if (!Number.isInteger(buyCount.value)) {
|
||||
message.error('兑换数量必须为整数')
|
||||
} else {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await useAuth.QueryBiliAuthPostAPI<ResponsePointOrder2UserModel>(POINT_API_URL + 'buy', {
|
||||
vId: props.userInfo.id,
|
||||
goodsId: currentGoods.value?.id,
|
||||
count: buyCount.value,
|
||||
addressId: selectedAddress.value ? selectedAddress.value.id : null,
|
||||
})
|
||||
if (data.code == 200) {
|
||||
message.info('兑换成功')
|
||||
dialog.success({
|
||||
title: '成功',
|
||||
content: `兑换成功,订单号:${data.data.id}`,
|
||||
positiveText: '前往查看',
|
||||
negativeText: '我知道了',
|
||||
onPositiveClick: () => {
|
||||
router.push({ name: 'bili-user', hash: '#orders' })
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
showBuyModal.value = false
|
||||
showAddressSelect.value = false
|
||||
selectedAddress.value = undefined
|
||||
buyCount.value = 1
|
||||
currentGoods.value = undefined
|
||||
},
|
||||
})
|
||||
} else {
|
||||
message.error('兑换失败: ' + data.message)
|
||||
console.error(data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
message.error('兑换失败: ' + err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 确认对话框
|
||||
dialog.warning({
|
||||
title: '确认兑换',
|
||||
content: `确定要花费 ${currentGoods.value!.price * buyCount.value} 积分兑换 ${buyCount.value} 个 "${currentGoods.value!.name}" 吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await useAuth.QueryBiliAuthPostAPI<ResponsePointOrder2UserModel>(POINT_API_URL + 'buy', {
|
||||
vId: props.userInfo.id,
|
||||
goodsId: currentGoods.value?.id,
|
||||
count: buyCount.value,
|
||||
addressId: selectedAddress.value?.id ?? null, // 如果地址未选择,则传 null
|
||||
})
|
||||
|
||||
if (data.code === 200) {
|
||||
message.success('兑换成功')
|
||||
// 更新本地积分显示
|
||||
currentPoint.value -= currentGoods.value!.price * buyCount.value
|
||||
// 显示成功对话框
|
||||
dialog.success({
|
||||
title: '成功',
|
||||
content: `兑换成功,订单号:${data.data.id}`,
|
||||
positiveText: '前往查看',
|
||||
negativeText: '关闭',
|
||||
onPositiveClick: () => {
|
||||
router.push({ name: 'bili-user', hash: '#orders' })
|
||||
resetBuyModalState() // 跳转后也重置状态
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
resetBuyModalState() // 关闭成功提示后重置状态
|
||||
},
|
||||
})
|
||||
// 重新获取礼物列表
|
||||
goods.value = await useAuth.GetGoods(props.userInfo.id, message);
|
||||
} else {
|
||||
message.error('兑换失败: ' + data.message)
|
||||
console.error('Buy failed:', data)
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Buy error:', err)
|
||||
message.error('兑换失败: ' + (err.message || err))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
// 无论成功失败,如果模态框还开着,理论上应该重置部分状态或关闭模态框
|
||||
// 但成功时已有处理,失败时保留模态框让用户修改或取消
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 点击兑换按钮,打开模态框
|
||||
function onBuyClick(good: ResponsePointGoodModel) {
|
||||
showBuyModal.value = true
|
||||
currentGoods.value = good
|
||||
buyCount.value = 1 // 重置购买数量
|
||||
selectedAddress.value = undefined // 重置地址选择
|
||||
showBuyModal.value = true
|
||||
}
|
||||
|
||||
// 自定义渲染地址选择器的标签
|
||||
const renderLabel = (option: SelectOption) => {
|
||||
return h(AddressDisplay, { address: biliAuth.value.address?.find((a) => a.id == option.value), size: 'small' })
|
||||
const address = biliAuth.value.address?.find((a) => a.id === option.value)
|
||||
return h(AddressDisplay, { address: address, size: 'small' })
|
||||
}
|
||||
const renderOption = ({ node, option }: { node: any; option: SelectOption }) => {
|
||||
|
||||
// 自定义渲染地址选择器的选项
|
||||
const renderOption = ({ option }: { node: any; option: SelectOption }) => {
|
||||
const address = biliAuth.value.address?.find((a) => a.id === option.value)
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
style: 'width: 100%;height: 100%;margin: 5px;padding: 12px;',
|
||||
style: 'width: 100%; height: 100%; margin: 5px; padding: 12px; justify-content: flex-start;', // 优化样式
|
||||
secondary: true,
|
||||
type: selectedAddress.value?.id != option.value ? 'default' : 'info',
|
||||
type: selectedAddress.value?.id !== option.value ? 'default' : 'info', // 根据是否选中改变类型
|
||||
onClick: () => {
|
||||
selectedAddress.value = biliAuth.value.address?.find((a) => a.id == option.value)
|
||||
showAddressSelect.value = false
|
||||
selectedAddress.value = address // 选中地址
|
||||
showAddressSelect.value = false // 关闭选择下拉框
|
||||
},
|
||||
},
|
||||
() => h(AddressDisplay, { address: biliAuth.value.address?.find((a) => a.id == option.value) }),
|
||||
// 按钮内容为地址显示组件
|
||||
() => h(AddressDisplay, { address: address }),
|
||||
)
|
||||
}
|
||||
|
||||
// 跳转到 Bilibili 用户中心页面
|
||||
function gotoAuthPage() {
|
||||
/*if (!accountInfo.value?.biliUserAuthInfo) {
|
||||
message.error('你尚未进行 Bilibili 认证, 请前往面板进行认证和绑定')
|
||||
return
|
||||
}
|
||||
useAuthStore()
|
||||
.setCurrentAuth(accountInfo.value?.biliUserAuthInfo.token)
|
||||
.then(() => {
|
||||
NavigateToNewTab('/bili-user')
|
||||
})*/
|
||||
// 移除旧的注释代码
|
||||
NavigateToNewTab('/bili-user')
|
||||
}
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(async () => {
|
||||
if (props.userInfo && useAuth.isAuthed) {
|
||||
if (!biliAuth.value.id) {
|
||||
isLoading.value = true
|
||||
await useAuth.getAuthInfo()
|
||||
}
|
||||
if (biliAuth.value.id) {
|
||||
currentPoint.value = (await useAuth.GetSpecificPoint(props.userInfo.id)) ?? -1
|
||||
isLoading.value = true // 开始加载
|
||||
try {
|
||||
// 如果用户已登录 B站 认证系统
|
||||
if (useAuth.isAuthed) {
|
||||
// 如果本地没有 B站 用户信息,则获取
|
||||
if (!biliAuth.value.id) {
|
||||
await useAuth.getAuthInfo()
|
||||
}
|
||||
// 如果获取到 B站 用户信息,则获取该主播直播间的积分
|
||||
if (biliAuth.value.id) {
|
||||
currentPoint.value = (await useAuth.GetSpecificPoint(props.userInfo.id)) ?? -1
|
||||
}
|
||||
}
|
||||
// 获取礼物列表
|
||||
goods.value = await useAuth.GetGoods(props.userInfo.id, message)
|
||||
} catch (error) {
|
||||
console.error("Error loading initial data:", error)
|
||||
message.error("加载数据时出错")
|
||||
} finally {
|
||||
isLoading.value = false // 结束加载
|
||||
}
|
||||
|
||||
goods.value = await useAuth.GetGoods(props.userInfo.id, message)
|
||||
isLoading.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAlert
|
||||
v-if="!useAuth.isAuthed"
|
||||
type="warning"
|
||||
>
|
||||
你尚未进行 Bilibili 账号认证, 无法兑换积分
|
||||
<br>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="small"
|
||||
style="margin-top: 12px"
|
||||
@click="$router.push({ name: 'bili-auth' })"
|
||||
>
|
||||
立即认证
|
||||
</NButton>
|
||||
</NAlert>
|
||||
<NCard
|
||||
v-else
|
||||
style="max-width: 600px; margin: 0 auto;"
|
||||
embedded
|
||||
hoverable
|
||||
>
|
||||
<template #header>
|
||||
你好, {{ useAuth.biliAuth.name }}
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<NFlex>
|
||||
<NButton
|
||||
type="info"
|
||||
secondary
|
||||
size="small"
|
||||
@click="gotoAuthPage"
|
||||
>
|
||||
前往认证用户中心
|
||||
</NButton>
|
||||
<NButton
|
||||
secondary
|
||||
size="small"
|
||||
@click="NavigateToNewTab('/bili-user#settings')"
|
||||
>
|
||||
切换账号
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</template>
|
||||
<NText> 你在 {{ userInfo.extra?.streamerInfo?.name ?? userInfo.name }} 的直播间的积分为 {{ currentPoint }} </NText>
|
||||
</NCard>
|
||||
<NDivider />
|
||||
<NCard
|
||||
v-if="tags.length > 0"
|
||||
size="small"
|
||||
title="标签筛选"
|
||||
>
|
||||
<NFlex
|
||||
align="center"
|
||||
justify="center"
|
||||
<div>
|
||||
<!-- 未认证提示 -->
|
||||
<NAlert
|
||||
v-if="!useAuth.isAuthed"
|
||||
type="warning"
|
||||
title="需要认证"
|
||||
>
|
||||
你尚未进行 Bilibili 账号认证, 无法查看积分或兑换礼物。
|
||||
<br>
|
||||
<NButton
|
||||
v-for="tag in tags"
|
||||
:key="tag"
|
||||
:type="tag == selectedTag ? 'success' : 'default'"
|
||||
:borderd="false"
|
||||
style="margin: 4px"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="selectedTag = selectedTag == tag ? undefined : tag"
|
||||
style="margin-top: 12px"
|
||||
@click="$router.push({ name: 'bili-auth' })"
|
||||
>
|
||||
{{ tag }}
|
||||
立即认证
|
||||
</NButton>
|
||||
</NFlex>
|
||||
<NDivider />
|
||||
<NCheckbox v-model:checked="onlyCanBuy">
|
||||
只显示可兑换的礼物
|
||||
</NCheckbox>
|
||||
<NCheckbox v-model:checked="ignoreGuard">
|
||||
忽略需要舰长的礼物
|
||||
</NCheckbox>
|
||||
</NCard>
|
||||
<NDivider />
|
||||
<NSpin :show="isLoading">
|
||||
<NEmpty v-if="selectedItems.length == 0">
|
||||
暂无礼物
|
||||
</NEmpty>
|
||||
<NFlex justify="center">
|
||||
<PointGoodsItem
|
||||
v-for="item in selectedItems"
|
||||
:key="item.id"
|
||||
:goods="item"
|
||||
content-style="max-width: 300px;height: 365px"
|
||||
>
|
||||
<template #footer>
|
||||
<NFlex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
:disabled="getTooltip(item) != '开始兑换'"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="onBuyClick(item)"
|
||||
>
|
||||
兑换
|
||||
</NButton>
|
||||
</template>
|
||||
{{ getTooltip(item) }}
|
||||
</NTooltip>
|
||||
<NFlex
|
||||
style="flex: 1"
|
||||
justify="end"
|
||||
>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NText
|
||||
style="size: 34px"
|
||||
:delete="item.canFreeBuy"
|
||||
>
|
||||
🪙
|
||||
{{ item.price > 0 ? item.price : '免费' }}
|
||||
</NText>
|
||||
</template>
|
||||
{{ item.canFreeBuy ? '你可以免费兑换此礼物' : '所需积分' }}
|
||||
</NTooltip>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</template>
|
||||
</PointGoodsItem>
|
||||
</NFlex>
|
||||
</NSpin>
|
||||
<NModal
|
||||
v-if="currentGoods"
|
||||
v-model:show="showBuyModal"
|
||||
preset="card"
|
||||
title="确认兑换"
|
||||
style="width: 500px; max-width: 90vw; height: auto"
|
||||
>
|
||||
<template #header>
|
||||
<NFlex align="baseline">
|
||||
<NTag
|
||||
:type="currentGoods.type == GoodsTypes.Physical ? 'info' : 'default'"
|
||||
:bordered="false"
|
||||
>
|
||||
{{ currentGoods.type == GoodsTypes.Physical ? '实体礼物' : '虚拟物品' }}
|
||||
</NTag>
|
||||
<NText> {{ currentGoods.name }} </NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
<PointGoodsItem
|
||||
v-if="currentGoods"
|
||||
:goods="currentGoods"
|
||||
/>
|
||||
<template v-if="currentGoods.type == GoodsTypes.Physical">
|
||||
<NDivider> 选项 </NDivider>
|
||||
<NForm>
|
||||
<NFormItem
|
||||
label="兑换数量"
|
||||
required
|
||||
>
|
||||
<NInputNumber
|
||||
v-model:value="buyCount"
|
||||
:min="1"
|
||||
:max="currentGoods.maxBuyCount ?? 100000"
|
||||
style="max-width: 120px"
|
||||
step="1"
|
||||
:precision="0"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
v-if="
|
||||
currentGoods.type == GoodsTypes.Physical &&
|
||||
(currentGoods.collectUrl == null || currentGoods.collectUrl == undefined)
|
||||
"
|
||||
label="收货地址"
|
||||
required
|
||||
>
|
||||
<NSelect
|
||||
v-model:show="showAddressSelect"
|
||||
:value="selectedAddress?.id"
|
||||
:options="addressOptions"
|
||||
:render-label="renderLabel"
|
||||
:render-option="renderOption"
|
||||
placeholder="请选择地址"
|
||||
/>
|
||||
|
||||
<NButton
|
||||
size="small"
|
||||
type="info"
|
||||
tag="a"
|
||||
href="/bili-user#settings"
|
||||
target="_blank"
|
||||
>
|
||||
管理收货地址
|
||||
</NButton>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</template>
|
||||
<NDivider>
|
||||
<NTag :type="currentGoods.price * buyCount > currentPoint ? 'error' : 'success'">
|
||||
{{ currentGoods.price * buyCount > currentPoint ? '积分不足' : '可兑换' }}
|
||||
</NTag>
|
||||
</NDivider>
|
||||
<NButton
|
||||
type="primary"
|
||||
:disabled="!canDoBuy"
|
||||
:loading="isLoading"
|
||||
@click="buyGoods"
|
||||
</NAlert>
|
||||
|
||||
<!-- 用户信息与积分展示 -->
|
||||
<NCard
|
||||
v-else
|
||||
style="max-width: 600px; margin: 0 auto;"
|
||||
embedded
|
||||
hoverable
|
||||
>
|
||||
确认兑换
|
||||
</NButton>
|
||||
<NText>
|
||||
<NDivider vertical />
|
||||
所需积分: {{ currentGoods.price * buyCount }}
|
||||
<NDivider vertical />
|
||||
当前积分: {{ currentPoint }}
|
||||
</NText>
|
||||
</NModal>
|
||||
<template #header>
|
||||
你好, {{ biliAuth.name }} <!-- 直接使用计算属性 -->
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<NFlex>
|
||||
<NButton
|
||||
type="info"
|
||||
secondary
|
||||
size="small"
|
||||
@click="gotoAuthPage"
|
||||
>
|
||||
前往认证用户中心
|
||||
</NButton>
|
||||
<NButton
|
||||
secondary
|
||||
size="small"
|
||||
@click="NavigateToNewTab('/bili-user#settings')"
|
||||
>
|
||||
切换账号
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</template>
|
||||
<NText v-if="currentPoint >= 0">
|
||||
你在 {{ userInfo.extra?.streamerInfo?.name ?? userInfo.name }} 的直播间的积分为 {{ currentPoint }}
|
||||
</NText>
|
||||
<NText v-else>
|
||||
正在加载积分...
|
||||
</NText>
|
||||
</NCard>
|
||||
|
||||
<NDivider />
|
||||
|
||||
<!-- 礼物筛选区域 -->
|
||||
<NCard
|
||||
v-if="tags.length > 0 || goods.length > 0"
|
||||
size="small"
|
||||
title="礼物筛选与排序"
|
||||
style="margin-bottom: 16px;"
|
||||
>
|
||||
<!-- 标签筛选 -->
|
||||
<NFlex
|
||||
v-if="tags.length > 0"
|
||||
align="center"
|
||||
justify="start"
|
||||
wrap
|
||||
style="margin-bottom: 12px;"
|
||||
>
|
||||
<NText style="margin-right: 8px;">
|
||||
标签:
|
||||
</NText>
|
||||
<NButton
|
||||
v-for="tag in tags"
|
||||
:key="tag"
|
||||
:type="tag === selectedTag ? 'success' : 'default'"
|
||||
:ghost="tag !== selectedTag"
|
||||
style="margin: 2px;"
|
||||
size="small"
|
||||
@click="selectedTag = selectedTag === tag ? undefined : tag"
|
||||
>
|
||||
{{ tag }}
|
||||
</NButton>
|
||||
<NButton
|
||||
v-if="selectedTag"
|
||||
text
|
||||
type="warning"
|
||||
size="small"
|
||||
style="margin-left: 8px;"
|
||||
@click="selectedTag = undefined"
|
||||
>
|
||||
清除标签
|
||||
</NButton>
|
||||
</NFlex>
|
||||
|
||||
<!-- 搜索与选项 -->
|
||||
<NFlex
|
||||
wrap
|
||||
justify="space-between"
|
||||
align="center"
|
||||
:size="[12, 8]"
|
||||
>
|
||||
<!-- 搜索框 -->
|
||||
<NInput
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索礼物名称或描述"
|
||||
clearable
|
||||
style="min-width: 200px; flex-grow: 1;"
|
||||
/>
|
||||
|
||||
<!-- 筛选选项 -->
|
||||
<NFlex
|
||||
wrap
|
||||
:gap="12"
|
||||
align="center"
|
||||
>
|
||||
<NCheckbox v-model:checked="onlyCanBuy">
|
||||
只显示可兑换
|
||||
</NCheckbox>
|
||||
<NCheckbox v-model:checked="ignoreGuard">
|
||||
忽略舰长限制
|
||||
</NCheckbox>
|
||||
<!-- 价格排序 -->
|
||||
<NSelect
|
||||
v-model:value="priceOrder"
|
||||
:options="[
|
||||
{ label: '默认排序', value: null },
|
||||
{ label: '价格 低→高', value: 'asc' },
|
||||
{ label: '价格 高→低', value: 'desc' }
|
||||
]"
|
||||
placeholder="价格排序"
|
||||
clearable
|
||||
style="min-width: 140px"
|
||||
/>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
|
||||
<!-- 礼物列表区域 -->
|
||||
<NSpin :show="isLoading">
|
||||
<template #description>
|
||||
加载中...
|
||||
</template>
|
||||
<NEmpty
|
||||
v-if="!isLoading && selectedItems.length === 0"
|
||||
description="没有找到符合条件的礼物"
|
||||
>
|
||||
<template #extra>
|
||||
<NButton
|
||||
v-if="selectedTag || searchKeyword || onlyCanBuy || ignoreGuard || priceOrder"
|
||||
size="small"
|
||||
@click="() => { selectedTag = undefined; searchKeyword = ''; onlyCanBuy = false; ignoreGuard = false; priceOrder = null; }"
|
||||
>
|
||||
清空筛选条件
|
||||
</NButton>
|
||||
</template>
|
||||
</NEmpty>
|
||||
<NFlex
|
||||
v-else
|
||||
wrap
|
||||
justify="center"
|
||||
:gap="16"
|
||||
>
|
||||
<PointGoodsItem
|
||||
v-for="item in selectedItems"
|
||||
:key="item.id"
|
||||
:goods="item"
|
||||
content-style="max-width: 300px; min-width: 250px; height: 380px;"
|
||||
style="flex-grow: 1;"
|
||||
>
|
||||
<template #footer>
|
||||
<NFlex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<NTooltip placement="bottom">
|
||||
<template #trigger>
|
||||
<!-- 按钮禁用状态由 getTooltip 控制 -->
|
||||
<NButton
|
||||
:disabled="getTooltip(item) !== '开始兑换'"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="onBuyClick(item)"
|
||||
>
|
||||
兑换
|
||||
</NButton>
|
||||
</template>
|
||||
{{ getTooltip(item) }} <!-- 显示提示信息 -->
|
||||
</NTooltip>
|
||||
<NFlex
|
||||
align="center"
|
||||
justify="end"
|
||||
style="flex-grow: 1;"
|
||||
>
|
||||
<NTooltip placement="bottom">
|
||||
<template #trigger>
|
||||
<NText
|
||||
style="font-size: 1.1em; font-weight: bold;"
|
||||
:delete="item.canFreeBuy"
|
||||
>
|
||||
🪙
|
||||
{{ item.price > 0 ? item.price : '免费' }}
|
||||
</NText>
|
||||
</template>
|
||||
{{ item.canFreeBuy ? '你可以免费兑换此礼物' : '所需积分' }}
|
||||
</NTooltip>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</template>
|
||||
</PointGoodsItem>
|
||||
</NFlex>
|
||||
</NSpin>
|
||||
|
||||
<!-- 兑换确认模态框 -->
|
||||
<NModal
|
||||
v-if="currentGoods"
|
||||
:show="showBuyModal"
|
||||
preset="card"
|
||||
:title="`确认兑换: ${currentGoods.name}`"
|
||||
style="width: 500px; max-width: 90vw;"
|
||||
:mask-closable="!isLoading"
|
||||
:close-on-esc="!isLoading"
|
||||
@update:show="handleModalUpdateShow"
|
||||
>
|
||||
<template #header>
|
||||
<NFlex align="baseline">
|
||||
<NTag
|
||||
:type="currentGoods.type === GoodsTypes.Physical ? 'info' : 'default'"
|
||||
:bordered="false"
|
||||
>
|
||||
{{ currentGoods.type === GoodsTypes.Physical ? '实体礼物' : '虚拟物品' }}
|
||||
</NTag>
|
||||
<NText strong>
|
||||
{{ currentGoods.name }}
|
||||
</NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<!-- 礼物信息展示 -->
|
||||
<PointGoodsItem
|
||||
:goods="currentGoods"
|
||||
:show-footer="false"
|
||||
content-style="height: auto;"
|
||||
/>
|
||||
|
||||
<!-- 兑换选项 (仅对实物或需要数量选择的礼物显示) -->
|
||||
<template v-if="currentGoods.type === GoodsTypes.Physical || (currentGoods.maxBuyCount ?? 1) > 1">
|
||||
<NDivider style="margin-top: 12px; margin-bottom: 12px;">
|
||||
兑换选项
|
||||
</NDivider>
|
||||
<NForm
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
>
|
||||
<NFormItem
|
||||
label="兑换数量"
|
||||
required
|
||||
>
|
||||
<NInputNumber
|
||||
v-model:value="buyCount"
|
||||
:min="1"
|
||||
:max="currentGoods.maxBuyCount ?? 100000"
|
||||
style="max-width: 120px"
|
||||
step="1"
|
||||
:precision="0"
|
||||
/>
|
||||
<NText
|
||||
depth="3"
|
||||
style="margin-left: 8px;"
|
||||
>
|
||||
(最多可兑换 {{ currentGoods.maxBuyCount ?? '无限' }} 个)
|
||||
</NText>
|
||||
</NFormItem>
|
||||
<!-- 地址选择 (仅对无外部收集链接的实物礼物显示) -->
|
||||
<NFormItem
|
||||
v-if="currentGoods.type === GoodsTypes.Physical && !currentGoods.collectUrl"
|
||||
label="收货地址"
|
||||
required
|
||||
>
|
||||
<NSelect
|
||||
v-model:show="showAddressSelect"
|
||||
:value="selectedAddress?.id"
|
||||
:options="addressOptions"
|
||||
:render-label="renderLabel"
|
||||
:render-option="renderOption"
|
||||
placeholder="请选择地址"
|
||||
style="flex-grow: 1; margin-right: 8px;"
|
||||
/>
|
||||
<NButton
|
||||
size="small"
|
||||
type="info"
|
||||
secondary
|
||||
@click="NavigateToNewTab('/bili-user#settings')"
|
||||
>
|
||||
管理地址
|
||||
</NButton>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</template>
|
||||
|
||||
<NDivider style="margin-top: 16px; margin-bottom: 16px;">
|
||||
<NTag :type="!canDoBuy ? 'error' : 'success'">
|
||||
{{ !canDoBuy ? (currentGoods.price * buyCount > currentPoint ? '积分不足' : '信息不完整') : '可兑换' }}
|
||||
</NTag>
|
||||
</NDivider>
|
||||
|
||||
<!-- 操作按钮和信息 -->
|
||||
<NFlex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
>
|
||||
<NButton
|
||||
type="primary"
|
||||
:disabled="!canDoBuy || isLoading"
|
||||
:loading="isLoading"
|
||||
@click="buyGoods"
|
||||
>
|
||||
确认兑换
|
||||
</NButton>
|
||||
<NText depth="2">
|
||||
所需积分: {{ currentGoods.price * buyCount }}
|
||||
<NDivider vertical />
|
||||
当前积分: {{ currentPoint >= 0 ? currentPoint : '加载中' }}
|
||||
</NText>
|
||||
</NFlex>
|
||||
</NModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 可以添加一些 scoped 样式来优化布局或外观 */
|
||||
.n-card {
|
||||
margin-bottom: 16px; /* 为卡片添加一些底部间距 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,6 +12,7 @@ const isLoading = ref(false)
|
||||
|
||||
const history = ref<ResponsePointHisrotyModel[]>([])
|
||||
|
||||
// 获取积分历史记录
|
||||
async function getHistories() {
|
||||
try {
|
||||
isLoading.value = true
|
||||
@@ -39,6 +40,8 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<NSpin :show="isLoading">
|
||||
<PointHistoryCard :histories="history" />
|
||||
<PointHistoryCard
|
||||
:histories="history"
|
||||
/>
|
||||
</NSpin>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user