mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: 使用新组件BiliUserSelector替换原有的UID输入组件并优化相关交互
This commit is contained in:
@@ -36,6 +36,7 @@ import DataManager from './components/autoaction/DataManager.vue'
|
||||
import CheckInSettings from './components/autoaction/settings/CheckInSettings.vue'
|
||||
import GlobalScheduledSettings from './components/autoaction/settings/GlobalScheduledSettings.vue'
|
||||
import TimerCountdown from './components/autoaction/TimerCountdown.vue'
|
||||
import BiliUserSelector from '@/components/common/BiliUserSelector.vue'
|
||||
|
||||
const autoActionStore = useAutoAction()
|
||||
const message = useMessage()
|
||||
@@ -66,7 +67,7 @@ const editingActionId = ref<string | null>(null)
|
||||
const showSetNextModal = ref(false)
|
||||
const targetNextActionId = ref<string | null>(null)
|
||||
const showTestModal = ref(false)
|
||||
const testUid = ref<string>('10004')
|
||||
const testUid = ref<number | undefined>(10004)
|
||||
const currentTestType = ref<TriggerType | null>(null)
|
||||
|
||||
const triggerTypeOptions = [
|
||||
@@ -425,7 +426,7 @@ function handleTestClick(type: TriggerType) {
|
||||
if (type === TriggerType.GUARD) {
|
||||
// 为舰长相关(私信)测试显示UID输入对话框
|
||||
currentTestType.value = type
|
||||
testUid.value = '10004' // 默认值
|
||||
testUid.value = 10004 // 默认值
|
||||
showTestModal.value = true
|
||||
} else {
|
||||
// 其他类型直接测试
|
||||
@@ -441,8 +442,8 @@ function confirmTest() {
|
||||
showTestModal.value = false
|
||||
return
|
||||
}
|
||||
const uid = Number.parseInt(testUid.value)
|
||||
if (isNaN(uid) || uid <= 0) {
|
||||
const uid = Number(testUid.value)
|
||||
if (!Number.isFinite(uid) || uid <= 0) {
|
||||
message.error('请输入有效的UID')
|
||||
return
|
||||
}
|
||||
@@ -818,10 +819,9 @@ function confirmTest() {
|
||||
>
|
||||
<NSpace vertical>
|
||||
<div>请输入私信接收者的UID:</div>
|
||||
<NInput
|
||||
<BiliUserSelector
|
||||
v-model:value="testUid"
|
||||
placeholder="请输入UID"
|
||||
type="text"
|
||||
placeholder="请输入B站用户UID"
|
||||
/>
|
||||
<NText
|
||||
type="info"
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useAutoAction } from '@/client/store/useAutoAction'
|
||||
import { CHECKIN_API_URL } from '@/data/constants'
|
||||
import AutoActionEditor from '../AutoActionEditor.vue'
|
||||
import TemplateHelper from '../TemplateHelper.vue'
|
||||
import BiliUserSelector from '@/components/common/BiliUserSelector.vue'
|
||||
|
||||
interface LiveInfo {
|
||||
roomId?: number
|
||||
@@ -775,11 +776,10 @@ onMounted(() => {
|
||||
|
||||
<NForm :label-width="100">
|
||||
<NFormItem label="用户UID">
|
||||
<NInputNumber
|
||||
<BiliUserSelector
|
||||
v-model:value="testUid"
|
||||
:min="1"
|
||||
style="width: 100%"
|
||||
placeholder="输入用户数字ID"
|
||||
placeholder="请输入B站用户UID"
|
||||
@user-info-loaded="(u) => { if (u?.name && (!testUsername || testUsername === '测试用户')) testUsername = u.name }"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="用户名">
|
||||
|
||||
3
src/components.d.ts
vendored
3
src/components.d.ts
vendored
@@ -9,6 +9,7 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AddressDisplay: typeof import('./components/manage/AddressDisplay.vue')['default']
|
||||
BiliUserSelector: typeof import('./components/common/BiliUserSelector.vue')['default']
|
||||
DanmakuContainer: typeof import('./components/DanmakuContainer.vue')['default']
|
||||
DanmakuItem: typeof import('./components/DanmakuItem.vue')['default']
|
||||
DynamicForm: typeof import('./components/DynamicForm.vue')['default']
|
||||
@@ -21,6 +22,8 @@ declare module 'vue' {
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
|
||||
188
src/components/common/BiliUserSelector.vue
Normal file
188
src/components/common/BiliUserSelector.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<script setup lang="ts">
|
||||
import { NAutoComplete, NAvatar, NFlex, NText } from 'naive-ui'
|
||||
import type { AutoCompleteOption } from 'naive-ui'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { computed, h, ref, watch } from 'vue'
|
||||
import { VTSURU_API_URL } from '@/data/constants'
|
||||
|
||||
interface BiliUserInfo {
|
||||
mid: number
|
||||
name: string
|
||||
face: string
|
||||
}
|
||||
|
||||
interface BiliApiResponse {
|
||||
code: number
|
||||
data?: {
|
||||
card?: BiliUserInfo
|
||||
}
|
||||
}
|
||||
|
||||
type BiliUserSelectorOption = AutoCompleteOption & { userInfo?: BiliUserInfo }
|
||||
|
||||
const props = defineProps<{
|
||||
placeholder?: string
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'userInfoLoaded': [userInfo: BiliUserInfo | null]
|
||||
}>()
|
||||
|
||||
// 使用 defineModel 作为外部 v-model:value 绑定
|
||||
const model = defineModel<number | undefined>('value')
|
||||
|
||||
const inputValue = ref('')
|
||||
const options = ref<BiliUserSelectorOption[]>([])
|
||||
const loading = ref(false)
|
||||
const selectedUserInfo = ref<BiliUserInfo | null>(null)
|
||||
|
||||
// 监听外部 v-model:value 变化,当外部设置了值时加载用户信息
|
||||
watch(
|
||||
() => model.value,
|
||||
async (newValue) => {
|
||||
if (newValue) {
|
||||
inputValue.value = String(newValue)
|
||||
if (!selectedUserInfo.value || selectedUserInfo.value.mid !== newValue) {
|
||||
await loadUserInfo(newValue)
|
||||
}
|
||||
}
|
||||
else {
|
||||
inputValue.value = ''
|
||||
selectedUserInfo.value = null
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 加载用户信息
|
||||
async function loadUserInfo(uid: number) {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await fetch(`${VTSURU_API_URL}bili-user-info/${uid}`)
|
||||
const data: BiliApiResponse = await response.json()
|
||||
|
||||
if (data.code === 0 && data.data?.card) {
|
||||
const userInfo = data.data.card
|
||||
selectedUserInfo.value = userInfo
|
||||
|
||||
options.value = [{
|
||||
label: `${userInfo.name} (${userInfo.mid})`,
|
||||
value: String(userInfo.mid),
|
||||
userInfo,
|
||||
}] as BiliUserSelectorOption[]
|
||||
|
||||
emit('userInfoLoaded', userInfo)
|
||||
}
|
||||
else {
|
||||
selectedUserInfo.value = null
|
||||
emit('userInfoLoaded', null)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('加载用户信息失败:', error)
|
||||
selectedUserInfo.value = null
|
||||
emit('userInfoLoaded', null)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 防抖搜索函数
|
||||
const debouncedSearch = useDebounceFn(async (value: string) => {
|
||||
const uid = Number.parseInt(value)
|
||||
if (Number.isNaN(uid) || uid <= 0) {
|
||||
options.value = []
|
||||
return
|
||||
}
|
||||
|
||||
await loadUserInfo(uid)
|
||||
}, 500)
|
||||
|
||||
// 处理输入变化
|
||||
function handleInput(value: string) {
|
||||
inputValue.value = value
|
||||
const uid = Number.parseInt(value)
|
||||
|
||||
if (Number.isNaN(uid) || uid <= 0) {
|
||||
model.value = undefined
|
||||
selectedUserInfo.value = null
|
||||
options.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 有效的数字输入时,立即同步给外部 v-model
|
||||
model.value = uid
|
||||
|
||||
debouncedSearch(value)
|
||||
}
|
||||
|
||||
// 处理选择
|
||||
function handleSelect(value: string) {
|
||||
inputValue.value = value
|
||||
const numeric = Number.parseInt(value)
|
||||
model.value = Number.isNaN(numeric) ? undefined : numeric
|
||||
const option = options.value.find(opt => opt.value === value)
|
||||
if (option?.userInfo) {
|
||||
selectedUserInfo.value = option.userInfo
|
||||
emit('userInfoLoaded', option.userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义渲染选项
|
||||
function renderOption(option: { option: BiliUserSelectorOption }) {
|
||||
const { userInfo } = option.option
|
||||
if (!userInfo) {
|
||||
return h(NText, { depth: 3 }, { default: () => '加载中...' })
|
||||
}
|
||||
|
||||
return h(
|
||||
NFlex,
|
||||
{ align: 'center', gap: 8 },
|
||||
{
|
||||
default: () => [
|
||||
h(NAvatar, {
|
||||
src: userInfo.face,
|
||||
size: 32,
|
||||
round: true,
|
||||
imgProps: {
|
||||
referrerpolicy: 'no-referrer'
|
||||
}
|
||||
}),
|
||||
h(
|
||||
NFlex,
|
||||
{ vertical: true, gap: 2 },
|
||||
{
|
||||
default: () => [
|
||||
h(NText, { strong: true }, { default: () => userInfo.name }),
|
||||
h(NText, { depth: 3, size: 'small' }, { default: () => `UID: ${userInfo.mid}` }),
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// 计算当前显示的值 - 只显示UID
|
||||
const displayValue = computed(() => {
|
||||
return inputValue.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAutoComplete
|
||||
:value="displayValue"
|
||||
:options="options"
|
||||
:loading="loading"
|
||||
:placeholder="placeholder || '请输入B站用户UID'"
|
||||
:size="size || 'medium'"
|
||||
:disabled="disabled"
|
||||
clearable
|
||||
:render-option="renderOption"
|
||||
@update:value="handleInput"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</template>
|
||||
@@ -37,6 +37,7 @@ import { QueryGetAPI } from '@/api/query'
|
||||
import { POINT_API_URL } from '@/data/constants'
|
||||
import { objectsToCSV } from '@/Utils'
|
||||
import PointUserDetailCard from './PointUserDetailCard.vue'
|
||||
import BiliUserSelector from '@/components/common/BiliUserSelector.vue'
|
||||
|
||||
// 用户积分设置类型定义
|
||||
interface PointUserSettings {
|
||||
@@ -73,6 +74,7 @@ const isLoading = ref(true)
|
||||
const addPointCount = ref(0)
|
||||
const addPointReason = ref<string>('')
|
||||
const addPointTarget = ref<number>()
|
||||
const selectedTargetUserName = ref<string>()
|
||||
|
||||
// 重置所有积分确认
|
||||
const resetConfirmText = ref('')
|
||||
@@ -125,7 +127,7 @@ const userStats = computed(() => {
|
||||
total: users.value.length,
|
||||
authed: users.value.filter(u => u.isAuthed).length,
|
||||
totalPoints: Number(totalPoints.toFixed(1)),
|
||||
totalOrders: users.value.reduce((sum, u) => sum + (u.orderCount || 0), 0),
|
||||
totalOrders: users.value.reduce((sum, u) => sum + ((u.orderCount || 0) > 0 ? (u.orderCount || 0) : 0), 0),
|
||||
avgPoints: Number(avgPoints.toFixed(1)),
|
||||
filtered: filteredUsers.value.length,
|
||||
}
|
||||
@@ -282,14 +284,16 @@ async function givePoint() {
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const data = await QueryGetAPI(`${POINT_API_URL}give-point`, {
|
||||
const data = await QueryGetAPI<{ totalPoint: number, userName?: string, uId?: number }>(`${POINT_API_URL}give-point`, {
|
||||
uId: addPointTarget.value,
|
||||
count: addPointCount.value,
|
||||
reason: addPointReason.value || '',
|
||||
})
|
||||
|
||||
if (data.code == 200) {
|
||||
message.success('添加成功')
|
||||
const userName = data.data?.userName || selectedTargetUserName.value || `UID: ${addPointTarget.value}`
|
||||
const action = addPointCount.value > 0 ? '添加' : '扣除'
|
||||
message.success(`成功为 ${userName} ${action}了 ${Math.abs(addPointCount.value)} 积分`)
|
||||
showGivePointModal.value = false
|
||||
|
||||
// 重新加载用户数据
|
||||
@@ -301,6 +305,7 @@ async function givePoint() {
|
||||
addPointCount.value = 0
|
||||
addPointReason.value = ''
|
||||
addPointTarget.value = undefined
|
||||
selectedTargetUserName.value = undefined
|
||||
} else {
|
||||
message.error(`添加失败: ${data.message}`)
|
||||
}
|
||||
@@ -615,21 +620,26 @@ onMounted(async () => {
|
||||
align="center"
|
||||
:gap="8"
|
||||
>
|
||||
<NInputGroup style="max-width: 300px">
|
||||
<NInputGroupLabel> 目标用户 </NInputGroupLabel>
|
||||
<NInputNumber
|
||||
<NFlex
|
||||
vertical
|
||||
:gap="4"
|
||||
style="flex: 1"
|
||||
>
|
||||
<NText depth="3">
|
||||
目标用户
|
||||
</NText>
|
||||
<BiliUserSelector
|
||||
v-model:value="addPointTarget"
|
||||
type="number"
|
||||
placeholder="请输入目标用户UID"
|
||||
min="0"
|
||||
placeholder="请输入B站用户UID"
|
||||
@user-info-loaded="(userInfo) => selectedTargetUserName = userInfo?.name"
|
||||
/>
|
||||
</NInputGroup>
|
||||
</NFlex>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NIcon :component="Info24Filled" />
|
||||
</template>
|
||||
<div class="tooltip-content">
|
||||
<p>如果目标用户没在直播间发言过则无法显示用户名, 不过不影响使用</p>
|
||||
<p>输入UID后会自动从B站获取用户信息</p>
|
||||
<p>因为UID和B站提供的OpenID不兼容, 未认证用户可能会出现两个记录, 不过在认证完成后会合并成一个</p>
|
||||
</div>
|
||||
</NTooltip>
|
||||
|
||||
Reference in New Issue
Block a user