Files
vtsuru.live/src/views/manage/LotteryView.vue
2023-12-24 00:00:00 +08:00

432 lines
14 KiB
Vue

<script setup lang="ts">
import { NavigateToNewTab } from '@/Utils'
import { LotteryUserInfo } from '@/api/api-models'
import { QueryGetAPI } from '@/api/query'
import { LOTTERY_API_URL, TURNSTILE_KEY } from '@/data/constants'
import { useLocalStorage, useStorage } from '@vueuse/core'
import { format } from 'date-fns'
import {
NAvatar,
NButton,
NCard,
NCheckbox,
NCollapseTransition,
NCountdown,
NDivider,
NEmpty,
NGrid,
NGridItem,
NInput,
NInputGroup,
NInputGroupLabel,
NInputNumber,
NList,
NListItem,
NModal,
NRadioButton,
NRadioGroup,
NScrollbar,
NSpace,
NTag,
NTime,
NTooltip,
useMessage,
useNotification,
} from 'naive-ui'
import { computed, h, onUnmounted, ref } from 'vue'
import VueTurnstile from 'vue-turnstile'
interface TempLotteryResponseModel {
users: LotteryUserInfo[]
createTime: number
total: number
}
interface LotteryOption {
resultCount: number
lotteryType: 'single' | 'half'
needVIP: boolean
needFanCard: boolean
needGuard: boolean
needCharge: boolean
fanCardLevel: number
}
interface LotteryHistory {
users: LotteryUserInfo[]
time: number
type: 'comment' | 'forward'
url: string
}
const lotteryHistory = useStorage<LotteryHistory[]>('LotteryHistory', [])
const message = useMessage()
const notification = useNotification()
const token = ref('')
const turnstile = ref()
const defaultOption = {
resultCount: 1,
lotteryType: 'single',
needVIP: false,
needFanCard: false,
needGuard: false,
needCharge: false,
fanCardLevel: 1,
} as LotteryOption
const lotteryOption = useLocalStorage('Settings.LotteryOption', defaultOption)
const isLoading = ref(false)
const isLottering = ref(false)
const isLotteried = ref(false)
const showModal = ref(false)
const inputDynamic = ref<string>()
const inputDynamicId = computed(() => {
try {
var id = BigInt(inputDynamic.value ?? '')
return id
} catch {
try {
const url = new URL(inputDynamic.value ?? '')
if (url.host.endsWith('bilibili.com')) {
const sp = url.pathname.split('/')
const id = BigInt(sp.length > 1 ? sp[sp.length - 1] : sp[0])
return id
}
} catch {
return null
}
}
return null
})
const isCommentCountDown = ref(true)
const originCommentUsers = ref<TempLotteryResponseModel>()
const originForwardUsers = ref<TempLotteryResponseModel>()
const currentType = ref<'comment' | 'forward'>('comment')
const resultUsers = ref<LotteryUserInfo[]>()
const commentUsers = ref<TempLotteryResponseModel>()
const forwardUsers = ref<TempLotteryResponseModel>()
const currentUsers = computed(() => {
return getCurrentUsers()
})
function getCurrentUsers() {
switch (currentType.value) {
case 'comment': {
return commentUsers.value
}
case 'forward': {
return forwardUsers.value
}
}
return undefined
}
const validUsers = computed(() => {
return currentUsers.value?.users.filter((u) => isUserValid(u))
})
async function onGet() {
switch (currentType.value) {
case 'comment': {
await getCommentsUsers()
break
}
case 'forward': {
await getForwardUsers()
break
}
}
currentUsers.value?.users.forEach((u) => (u.visiable = true))
}
async function getCommentsUsers() {
isLoading.value = true
await QueryGetAPI<TempLotteryResponseModel>(
LOTTERY_API_URL + 'comments',
{
id: inputDynamicId.value,
},
[['Turnstile', token.value]],
)
.then((data) => {
if (data.code == 200) {
originCommentUsers.value = JSON.parse(JSON.stringify(data.data))
commentUsers.value = data.data
isCommentCountDown.value = false
} else {
message.error('获取用户失败: ' + data.message)
}
})
.catch((err) => {
message.error('获取失败')
})
.finally(() => {
turnstile.value?.reset()
isLoading.value = false
})
}
async function getForwardUsers() {
isLoading.value = true
await QueryGetAPI<TempLotteryResponseModel>(
LOTTERY_API_URL + 'forward',
{
id: inputDynamicId.value,
},
[['Turnstile', token.value]],
)
.then((data) => {
if (data.code == 200) {
originForwardUsers.value = JSON.parse(JSON.stringify(data.data))
forwardUsers.value = data.data
isCommentCountDown.value = false
} else {
message.error('获取用户失败: ' + data.message)
}
})
.catch((err) => {
message.error('获取失败')
})
.finally(() => {
turnstile.value?.reset()
isLoading.value = false
})
}
function getRandomInt(max: number) {
return Math.floor(Math.random() * max)
}
function isUserValid(u: LotteryUserInfo) {
if (lotteryOption.value.needVIP) {
if (u.isVIP != true) return false
}
if (lotteryOption.value.needFanCard) {
if ((u.card?.level ?? -1) < lotteryOption.value.fanCardLevel) return false
}
if (lotteryOption.value.needGuard) {
if (u.card?.isGuard != true) return false
}
if (lotteryOption.value.needCharge) {
if (u.card?.isCharge != true) return false
}
return true
}
function startLottery() {
if (!isLottering.value && currentUsers.value) {
isLottering.value = true
try {
const data = currentUsers.value
if ((validUsers.value?.length ?? -1) < lotteryOption.value.resultCount) {
message.warning('符合条件的抽奖人数达不到抽选人数')
isLottering.value = false
return
}
const tempUsers = getCurrentUsers()
if (tempUsers) tempUsers.users = validUsers.value ?? []
switch (lotteryOption.value.lotteryType) {
case 'single': {
console.log('开始抽取单个用户')
removeSingleUser()
function removeSingleUser() {
if (data.users.length > lotteryOption.value.resultCount) {
console.log(`[${data.users.length}] 移除` + data.users.splice(getRandomInt(data.users.length), 1)[0].name)
setTimeout(() => {
removeSingleUser()
}, 500)
} else {
onFinishLottery()
}
}
break
}
}
} catch (err) {
message.error('发生错误')
}
}
}
function onFinishLottery() {
resultUsers.value = JSON.parse(JSON.stringify(currentUsers.value?.users))
isLottering.value = false
isLotteried.value = true
notification.create({
title: '抽奖完成',
description: '共' + resultUsers.value?.length + '人',
duration: 3000,
content: () =>
h(
NSpace,
{ vertical: true },
resultUsers.value?.map((user) => h(NSpace, null, [h(NAvatar, { src: user.avatar + '@32w_32h', imgProps: { referrerpolicy: 'no-referrer' } }), h('span', user.name)])),
),
meta: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
onAfterLeave: () => {
message.success('已保存至历史')
},
})
lotteryHistory.value.push({
users: currentUsers.value?.users ?? [],
time: Date.now(),
type: currentType.value,
url: inputDynamicId.value ? 'https://t.bilibili.com/' + inputDynamicId.value : inputDynamic.value ?? '',
})
}
function reset() {
switch (currentType.value) {
case 'comment': {
commentUsers.value = JSON.parse(JSON.stringify(originCommentUsers.value))
break
}
case 'forward': {
forwardUsers.value = JSON.parse(JSON.stringify(originForwardUsers.value))
break
}
}
isLotteried.value = false
}
function getLevelColor(level: number) {
switch (level) {
case 1: {
return 'gray'
}
case 2: {
return '#8bd29d'
}
case 4: {
return '#FEBB8B'
}
case 3: {
return '#7BCDEF'
}
case 5: {
return '#EE672A'
}
case 6: {
return '#F04C49'
}
default: {
return 'gray'
}
}
}
onUnmounted(() => {
turnstile.value?.remove()
})
</script>
<template>
<NCard size="medium" embedded title="动态抽奖">
<template #header-extra>
<NButton @click="showModal = true"> 历史记录 </NButton>
</template>
<NInput v-model:value="inputDynamic" placeholder="动态链接或直接输入动态Id" :status="inputDynamicId ? 'success' : 'warning'" :disabled="isLoading || isLottering" />
<NDivider style="margin: 10px 0 10px 0" />
<NCard size="small" embedded title="选项">
<template #header-extra>
<NButton size="small" secondary @click="lotteryOption = defaultOption"> 恢复默认 </NButton>
</template>
<NSpace justify="center">
<NRadioGroup v-model:value="currentType" name="用户类型" :disabled="isLottering">
<NRadioButton value="comment"> 评论 </NRadioButton>
<NRadioButton value="forward"> 转发 </NRadioButton>
</NRadioGroup>
</NSpace>
<NDivider style="margin: 10px 0 10px 0"></NDivider>
<NSpace align="center">
<NInputGroup style="max-width: 200px">
<NInputGroupLabel> 抽选人数 </NInputGroupLabel>
<NInputNumber v-model:value="lotteryOption.resultCount" placeholder="" min="1" />
</NInputGroup>
<NCheckbox v-model:checked="lotteryOption.needVIP"> 需要大会员 </NCheckbox>
<template v-if="currentType == 'comment'">
<NCheckbox v-model:checked="lotteryOption.needCharge"> 需要充电 </NCheckbox>
<NCheckbox v-model:checked="lotteryOption.needFanCard"> 需要粉丝牌 </NCheckbox>
<NCollapseTransition>
<NInputGroup v-if="lotteryOption.needFanCard" style="max-width: 200px">
<NInputGroupLabel> 最低粉丝牌等级 </NInputGroupLabel>
<NInputNumber v-model:value="lotteryOption.fanCardLevel" min="1" max="50" :default-value="1" :disabled="isLottering" />
</NInputGroup>
</NCollapseTransition>
</template>
<NRadioGroup v-model:value="lotteryOption.lotteryType" name="抽取类型" size="small" :disabled="isLottering">
<NRadioButton value="single"> 单个 </NRadioButton>
<NRadioButton value="half"> 减半 </NRadioButton>
</NRadioGroup>
</NSpace>
</NCard>
<br />
<NSpace justify="center" align="center">
<NButton :disabled="!inputDynamicId || !isCommentCountDown || !token || isLottering" :loading="!token || isLoading" @click="onGet" type="primary"> 加载用户 </NButton>
<NCountdown v-if="!isCommentCountDown" :duration="(currentUsers?.createTime ?? -1) + 60000 - Date.now()" @finish="isCommentCountDown = true" />
</NSpace>
<br />
<NCard v-if="currentUsers" size="small">
<NSpace justify="center">
<NButton type="primary" @click="startLottery" :loading="isLottering" :disabled="isLotteried"> 开始抽取 </NButton>
<NButton type="info" :disabled="isLottering || !isLotteried" @click="reset"> 重置 </NButton>
</NSpace>
<NDivider style="margin: 10px 0 10px 0"> {{ validUsers?.length }} </NDivider>
<NGrid cols="1 500:2 800:3 1000:4" :x-gap="12" :y-gap="8">
<NGridItem v-for="item in validUsers" v-bind:key="item.uId">
<NCard size="small" :title="item.name" style="height: 155px">
<template #header>
<NSpace align="center" vertical :size="5">
<NAvatar round lazy borderd :size="64" :src="item.avatar + '@64w_64h'" :img-props="{ referrerpolicy: 'no-referrer' }" style="box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2)" />
<NSpace>
<NTag v-if="item.isVIP" size="tiny" round :color="{ color: '#fb7299', textColor: 'white', borderColor: 'white' }"> 大会员 </NTag>
<NTooltip>
<template #trigger>
<NTag v-if="item.level" size="tiny" :color="{ color: getLevelColor(item.level), textColor: 'white', borderColor: 'white' }" :borderd="false"> LV {{ item.level }} </NTag>
</template>
用户等级
</NTooltip>
<NTag v-if="item.card" size="tiny" round>
<NTag size="tiny" round :bordered="false">
{{ item.card.level }}
</NTag>
<span style="color: #577fb8">
{{ item.card.name }}
</span>
</NTag>
</NSpace>
{{ item.name }}
</NSpace>
</template>
</NCard>
</NGridItem>
</NGrid>
</NCard>
</NCard>
<NModal v-model:show="showModal" preset="card" title="抽奖结果" style="max-width: 90%; width: 800px" closable>
<template #header-extra>
<NButton type="error" size="small" @click="lotteryHistory = []"> 清空 </NButton>
</template>
<NScrollbar v-if="lotteryHistory.length > 0" style="max-height: 80vh">
<NList>
<NListItem v-for="item in lotteryHistory" :key="item.time">
<NCard size="small">
<template #header>
<NTime :time="item.time" />
</template>
<template #header-extra>
<NButton type="error" size="small" @click="lotteryHistory.splice(lotteryHistory.indexOf(item), 1)"> 删除 </NButton>
</template>
<NSpace vertical>
<NSpace v-for="user in item.users" :key="user.uId">
<NAvatar round lazy :src="user.avatar + '@64w_64h'" :img-props="{ referrerpolicy: 'no-referrer' }" />
{{ user.name }}
</NSpace>
</NSpace>
<NDivider style="margin: 10px 0 10px 0" />
<NButton secondary @click="NavigateToNewTab(item.url)"> 目标动态 </NButton>
</NCard>
</NListItem>
</NList>
</NScrollbar>
<NEmpty v-else description="暂无记录" />
</NModal>
<VueTurnstile ref="turnstile" :site-key="TURNSTILE_KEY" v-model="token" theme="auto" style="text-align: center" />
</template>