mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: 重构直播信息卡片组件并优化默认设置
- 将 LiveInfoContainer 组件从传统布局重构为现代卡片式布局 - 优化直播封面展示:添加 LIVE 标识、悬停缩放效果和 16:9 宽高比 - 改进信息展示层次:标题、元数据和统计数据分区显示 - 使用图标增强统计数据可读性(弹幕、互动、收益) - 优化响应式布局,支持移动端和桌面端自适应 - 修改默认启动设置:bootAsMinimized 改为 false - 新增 Man
This commit is contained in:
@@ -30,7 +30,7 @@ export const useSettings = defineStore('settings', () => {
|
||||
const defaultSettings: VTsuruClientSettings = {
|
||||
useDanmakuClientType: 'openlive',
|
||||
fallbackToOpenLive: true,
|
||||
bootAsMinimized: true,
|
||||
bootAsMinimized: false,
|
||||
|
||||
danmakuHistorySize: 100,
|
||||
loginType: 'qrcode',
|
||||
|
||||
6
src/components.d.ts
vendored
6
src/components.d.ts
vendored
@@ -22,6 +22,7 @@ declare module 'vue' {
|
||||
FeedbackItem: typeof import('./components/FeedbackItem.vue')['default']
|
||||
LabelItem: typeof import('./components/LabelItem.vue')['default']
|
||||
LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default']
|
||||
ManagePageHeader: typeof import('./components/manage/ManagePageHeader.vue')['default']
|
||||
MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
@@ -45,6 +46,7 @@ declare module 'vue' {
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
@@ -72,6 +74,7 @@ declare module 'vue' {
|
||||
UserBasicInfoCard: typeof import('./components/UserBasicInfoCard.vue')['default']
|
||||
VEditor: typeof import('./components/VEditor.vue')['default']
|
||||
VideoCollectInfoCard: typeof import('./components/VideoCollectInfoCard.vue')['default']
|
||||
VideoItemCard: typeof import('./components/VideoItemCard.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +90,7 @@ declare global {
|
||||
const FeedbackItem: typeof import('./components/FeedbackItem.vue')['default']
|
||||
const LabelItem: typeof import('./components/LabelItem.vue')['default']
|
||||
const LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default']
|
||||
const ManagePageHeader: typeof import('./components/manage/ManagePageHeader.vue')['default']
|
||||
const MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default']
|
||||
const NAlert: typeof import('naive-ui')['NAlert']
|
||||
const NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
@@ -110,6 +114,7 @@ declare global {
|
||||
const NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
const NSelect: typeof import('naive-ui')['NSelect']
|
||||
const NSpace: typeof import('naive-ui')['NSpace']
|
||||
const NSpin: typeof import('naive-ui')['NSpin']
|
||||
const NStatistic: typeof import('naive-ui')['NStatistic']
|
||||
const NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
const NTag: typeof import('naive-ui')['NTag']
|
||||
@@ -137,4 +142,5 @@ declare global {
|
||||
const UserBasicInfoCard: typeof import('./components/UserBasicInfoCard.vue')['default']
|
||||
const VEditor: typeof import('./components/VEditor.vue')['default']
|
||||
const VideoCollectInfoCard: typeof import('./components/VideoCollectInfoCard.vue')['default']
|
||||
const VideoItemCard: typeof import('./components/VideoItemCard.vue')['default']
|
||||
}
|
||||
@@ -1,22 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { ResponseLiveInfoModel } from '@/api/api-models'
|
||||
import { Info24Filled } from '@vicons/fluent'
|
||||
import {
|
||||
NButton,
|
||||
NDivider,
|
||||
Info24Filled,
|
||||
Chat24Regular,
|
||||
HandRight24Regular,
|
||||
Money24Regular
|
||||
} from '@vicons/fluent'
|
||||
import {
|
||||
NIcon,
|
||||
NNumberAnimation,
|
||||
NPopover,
|
||||
NSpace,
|
||||
NStatistic,
|
||||
NTag,
|
||||
NTime,
|
||||
NTooltip,
|
||||
NText
|
||||
} from 'naive-ui'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const { live } = defineProps<{
|
||||
const props = defineProps<{
|
||||
live: ResponseLiveInfoModel
|
||||
}>()
|
||||
const route = useRoute()
|
||||
@@ -26,126 +29,105 @@ const defaultDanmakusCount = ref(0)
|
||||
function OnClickCover() {
|
||||
router.push({
|
||||
name: 'manage-liveDetail',
|
||||
params: { id: live.liveId },
|
||||
params: { id: props.live.liveId },
|
||||
})
|
||||
}
|
||||
const guartPriceStartData = new Date(Date.UTC(2024, 2, 24, 10, 0, 0))
|
||||
|
||||
watch(
|
||||
() => live,
|
||||
() => props.live,
|
||||
(newValue) => {
|
||||
defaultDanmakusCount.value = newValue.danmakusCount
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="display: flex; flex-wrap: wrap">
|
||||
<NSpace style="flex-flow: nowrap">
|
||||
<span style="display: flex; align-items: center; height: 100%">
|
||||
<img
|
||||
referrerpolicy="no-referrer"
|
||||
:style="!live.isFinish ? 'animation: animated-border 2.5s infinite;cursor: pointer' : 'cursor: pointer'"
|
||||
class="liveCover"
|
||||
:src="`${live.coverUrl}@200w`"
|
||||
lazy
|
||||
preview-disabled
|
||||
@click="OnClickCover()"
|
||||
>
|
||||
</span>
|
||||
<NSpace
|
||||
vertical
|
||||
justify="center"
|
||||
style="gap: 2px"
|
||||
>
|
||||
<NButton
|
||||
text
|
||||
@click="OnClickCover()"
|
||||
>
|
||||
<span style="font-size: 18px; white-space: break-spaces">
|
||||
<div class="live-info-container">
|
||||
<!-- Cover Image -->
|
||||
<div class="cover-wrapper" @click.stop="OnClickCover">
|
||||
<img referrerpolicy="no-referrer" :class="['live-cover', { 'is-live': !live.isFinish }]"
|
||||
:src="`${live.coverUrl}@200w`" loading="lazy">
|
||||
<div v-if="!live.isFinish" class="live-badge">LIVE</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="content-wrapper">
|
||||
<div class="info-section">
|
||||
<!-- Title -->
|
||||
<div class="title-row" @click.stop="OnClickCover">
|
||||
<NText class="live-title">
|
||||
{{ live.title }}
|
||||
</span>
|
||||
</NButton>
|
||||
<span>
|
||||
<span v-if="!live.isFinish">
|
||||
<NTag
|
||||
size="tiny"
|
||||
:bordered="false"
|
||||
type="success"
|
||||
style="justify-items: center; box-shadow: 0 0 3px #589580"
|
||||
>
|
||||
</NText>
|
||||
</div>
|
||||
|
||||
<!-- Meta Data -->
|
||||
<div class="meta-row">
|
||||
<NSpace align="center" size="small" wrap>
|
||||
<NTag v-if="!live.isFinish" size="small" :bordered="false" type="success" class="status-tag">
|
||||
直播中
|
||||
</NTag>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
style="color: gray"
|
||||
>
|
||||
{{ (((live.stopAt ?? 0) - (live.startAt ?? 0)) / (3600 * 1000)).toFixed(1) }}
|
||||
时
|
||||
</span>
|
||||
<NDivider vertical />
|
||||
<NPopover trigger="hover">
|
||||
<template #trigger>
|
||||
<div style="color: grey; font-size: small; display: inline">
|
||||
<NTime
|
||||
style="font-size: small"
|
||||
:time="live.startAt"
|
||||
/>
|
||||
<NTag v-else size="small" :bordered="false" disabled>
|
||||
已结束
|
||||
</NTag>
|
||||
|
||||
<span class="meta-divider">|</span>
|
||||
|
||||
<span class="meta-text">{{ live.parentArea }} / {{ live.area }}</span>
|
||||
|
||||
<span class="meta-divider">|</span>
|
||||
|
||||
<NPopover trigger="hover">
|
||||
<template #trigger>
|
||||
<span class="meta-text">
|
||||
<NTime :time="live.startAt" format="yyyy-MM-dd HH:mm" />
|
||||
</span>
|
||||
</template>
|
||||
<div v-if="live.isFinish">
|
||||
结束于:
|
||||
<NTime :time="live.stopAt ?? 0" />
|
||||
<br>
|
||||
时长: {{ (((live.stopAt ?? 0) - (live.startAt ?? 0)) / (3600 * 1000)).toFixed(1) }} 小时
|
||||
</div>
|
||||
</template>
|
||||
<span v-if="live.isFinish">
|
||||
结束于:
|
||||
<NTime :time="live.stopAt ?? 0" />
|
||||
</span>
|
||||
<span v-else>
|
||||
已直播:
|
||||
{{ ((Date.now() - (live.startAt ?? 0)) / (3600 * 1000)).toFixed(1) }}
|
||||
时
|
||||
</span>
|
||||
</NPopover>
|
||||
</span>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
<div class="liveListItem">
|
||||
<NStatistic label="分区">
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<span style="font-size: 16px; font-weight: 500">
|
||||
{{ live.area }}
|
||||
</span>
|
||||
</template>
|
||||
{{ live.parentArea }}
|
||||
</NTooltip>
|
||||
</NStatistic>
|
||||
<NStatistic label="弹幕">
|
||||
<span style="font-size: 18px; font-weight: 500">
|
||||
<NNumberAnimation
|
||||
:from="defaultDanmakusCount"
|
||||
:to="live.danmakusCount"
|
||||
show-separator
|
||||
/>
|
||||
</span>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="互动"
|
||||
tabular-nums
|
||||
>
|
||||
<span style="font-size: 18px; font-weight: 500">
|
||||
<NNumberAnimation
|
||||
:from="0"
|
||||
:to="live.interactionCount"
|
||||
show-separator
|
||||
/>
|
||||
</span>
|
||||
</NStatistic>
|
||||
<transition>
|
||||
<NStatistic tabular-nums>
|
||||
<template #label>
|
||||
<div v-else>
|
||||
已直播: {{ ((Date.now() - (live.startAt ?? 0)) / (3600 * 1000)).toFixed(1) }} 小时
|
||||
</div>
|
||||
</NPopover>
|
||||
</NSpace>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="stats-section">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">
|
||||
<NIcon :component="Chat24Regular" depth="3" size="14" style="margin-right: 4px; vertical-align: -2px;" />
|
||||
弹幕
|
||||
</span>
|
||||
<span class="stat-value">
|
||||
<NNumberAnimation :from="defaultDanmakusCount" :to="live.danmakusCount" show-separator />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">
|
||||
<NIcon :component="HandRight24Regular" depth="3" size="14"
|
||||
style="margin-right: 4px; vertical-align: -2px;" />
|
||||
互动
|
||||
</span>
|
||||
<span class="stat-value">
|
||||
<NNumberAnimation :from="0" :to="live.interactionCount" show-separator />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="stat-item income">
|
||||
<span class="stat-label">
|
||||
<NIcon :component="Money24Regular" depth="3" size="14" style="margin-right: 4px; vertical-align: -2px;" />
|
||||
收益
|
||||
<NTooltip v-if="new Date(live.startAt) < guartPriceStartData">
|
||||
<template #trigger>
|
||||
<NIcon :component="Info24Filled" />
|
||||
<NIcon :component="Info24Filled" style="vertical-align: middle; cursor: help;" />
|
||||
</template>
|
||||
因为官方并没有提供上舰的价格, 所以记录中的舰长价格一律按照打折价格计算
|
||||
<br>
|
||||
@@ -153,95 +135,195 @@ watch(
|
||||
<br>
|
||||
把鼠标放在下面的价格上就可以查看排除舰长后的收益
|
||||
</NTooltip>
|
||||
</template>
|
||||
</span>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<span style="font-size: 18px; font-weight: 500; color: #a35353">
|
||||
<NNumberAnimation
|
||||
:from="0"
|
||||
<span class="stat-value income-value">
|
||||
¥
|
||||
<NNumberAnimation :from="0"
|
||||
:to="new Date(live.startAt) < guartPriceStartData ? live.totalIncomeWithGuard : live.totalIncome"
|
||||
show-separator
|
||||
/>
|
||||
show-separator :precision="1" />
|
||||
</span>
|
||||
</template>
|
||||
{{ live.totalIncome }}
|
||||
纯收益: ¥{{ live.totalIncome }}
|
||||
</NTooltip>
|
||||
</NStatistic>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-statistic {
|
||||
text-align: right;
|
||||
min-width: 62px;
|
||||
.live-info-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 750px) {
|
||||
.liveCover {
|
||||
width: 90px;
|
||||
height: fit-content;
|
||||
border-radius: 4px;
|
||||
.cover-wrapper {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 140px;
|
||||
/* 16:9 approx height 78px */
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.live-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.cover-wrapper:hover .live-cover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.live-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
background-color: #d03050;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
z-index: 2;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
/* prevent flex item overflow */
|
||||
}
|
||||
|
||||
.info-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
padding-right: 24px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
cursor: pointer;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.live-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.title-row:hover .live-title {
|
||||
color: var(--n-primary-color);
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 12px;
|
||||
color: var(--n-text-color-3);
|
||||
}
|
||||
|
||||
.meta-divider {
|
||||
color: var(--n-divider-color);
|
||||
font-size: 12px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
box-shadow: 0 0 3px rgba(88, 149, 128, 0.4);
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
padding-left: 24px;
|
||||
border-left: 1px solid var(--n-divider-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--n-text-color-3);
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.stat-value.income-value {
|
||||
color: #d03050;
|
||||
}
|
||||
|
||||
/* Mobile / Tablet Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.live-info-container {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.liveList {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: 8px 10px;
|
||||
}
|
||||
|
||||
.liveListItem {
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
.cover-wrapper {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
max-width: 320px;
|
||||
/* Limit max width on phone */
|
||||
margin: 0 auto;
|
||||
/* Center cover if column */
|
||||
}
|
||||
|
||||
.dateEChartStyle {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 750px) {
|
||||
.liveCover {
|
||||
border-radius: 4px;
|
||||
width: 120px;
|
||||
.content-wrapper {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.liveList {
|
||||
display: flex;
|
||||
.info-section {
|
||||
padding-right: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.liveListItem {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-grow: 1;
|
||||
justify-content: end;
|
||||
.stats-section {
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
border-top: 1px solid var(--n-divider-color);
|
||||
padding-top: 8px;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dateEChartStyle {
|
||||
height: 150px;
|
||||
.stat-item {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animated-border {
|
||||
0% {
|
||||
box-shadow: 0 0 0px #589580;
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -42,7 +42,7 @@ function onClick() {
|
||||
<template>
|
||||
<NCard
|
||||
size="small"
|
||||
style="width: 100%; max-width: 70vw; cursor: pointer"
|
||||
style="width: 100%; cursor: pointer"
|
||||
embedded
|
||||
hoverable
|
||||
:bordered="bordered"
|
||||
|
||||
340
src/components/VideoItemCard.vue
Normal file
340
src/components/VideoItemCard.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<script setup lang="ts">
|
||||
import type { VideoCollectVideo, VideoInfo } from '@/api/api-models'
|
||||
import { VideoStatus } from '@/api/api-models'
|
||||
import { Clock24Filled, Person24Filled } from '@vicons/fluent'
|
||||
import {
|
||||
NButton,
|
||||
NCard,
|
||||
NEllipsis,
|
||||
NIcon,
|
||||
NPopconfirm,
|
||||
NScrollbar,
|
||||
NSpace,
|
||||
NTag,
|
||||
NText,
|
||||
NTooltip,
|
||||
} from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
videoInfo: VideoInfo
|
||||
videoData: VideoCollectVideo
|
||||
type: 'padding' | 'accept' | 'reject'
|
||||
isLoading?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update-status', status: VideoStatus, video: VideoInfo): void
|
||||
}>()
|
||||
|
||||
function handleStatusChange(status: VideoStatus) {
|
||||
emit('update-status', status, props.videoInfo)
|
||||
}
|
||||
|
||||
function formatSeconds(seconds: number): string {
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const remainingSeconds = seconds % 60
|
||||
const formattedMinutes = minutes.toString().padStart(2, '0')
|
||||
const formattedSeconds = remainingSeconds.toString().padStart(2, '0')
|
||||
return `${formattedMinutes}:${formattedSeconds}`
|
||||
}
|
||||
|
||||
function openVideo() {
|
||||
window.open(`https://www.bilibili.com/video/${props.videoInfo.bvid}`, '_blank')
|
||||
}
|
||||
|
||||
const statusTag = computed(() => {
|
||||
switch (props.videoInfo.status) {
|
||||
case VideoStatus.Accepted:
|
||||
return { type: 'success', text: '已通过' }
|
||||
case VideoStatus.Rejected:
|
||||
return { type: 'error', text: '已拒绝' }
|
||||
default:
|
||||
return { type: 'default', text: '待审核' }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard
|
||||
size="small"
|
||||
hoverable
|
||||
embedded
|
||||
class="video-card"
|
||||
content-style="padding: 0;"
|
||||
>
|
||||
<template #cover>
|
||||
<div
|
||||
class="cover-container"
|
||||
@click="openVideo"
|
||||
>
|
||||
<img
|
||||
:src="videoData.cover.replace('http://', 'https://')"
|
||||
referrerpolicy="no-referrer"
|
||||
class="cover-img"
|
||||
>
|
||||
<div class="cover-info">
|
||||
<span class="info-item">
|
||||
<NIcon
|
||||
:component="Clock24Filled"
|
||||
color="lightgrey"
|
||||
/>
|
||||
<NText style="color: lightgrey; font-size: 12px; margin-left: 4px">
|
||||
{{ formatSeconds(videoData.length) }}
|
||||
</NText>
|
||||
</span>
|
||||
<span class="info-item">
|
||||
<NIcon
|
||||
:component="Person24Filled"
|
||||
color="lightgrey"
|
||||
/>
|
||||
<NText style="color: lightgrey; font-size: 12px; margin-left: 4px">
|
||||
{{ videoData.ownerName }}
|
||||
</NText>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="title-row">
|
||||
<NButton
|
||||
text
|
||||
style="width: 100%; justify-content: flex-start; text-align: left;"
|
||||
@click="openVideo"
|
||||
>
|
||||
<NEllipsis style="max-width: 100%">
|
||||
<template #tooltip>
|
||||
<div style="max-width: 300px">
|
||||
{{ videoData.title }}
|
||||
</div>
|
||||
</template>
|
||||
<span style="font-weight: 500; font-size: 15px;">{{ videoData.title }}</span>
|
||||
</NEllipsis>
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<div class="sender-info">
|
||||
<NScrollbar style="max-height: 80px">
|
||||
<div
|
||||
v-for="(sender, index) in videoInfo.senders"
|
||||
:key="index"
|
||||
class="sender-item"
|
||||
>
|
||||
<div class="sender-row">
|
||||
<NTag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
round
|
||||
style="margin-right: 6px; transform: scale(0.85); transform-origin: left center;"
|
||||
>
|
||||
推荐人
|
||||
</NTag>
|
||||
<NText depth="2">
|
||||
{{ sender.sender ?? '未填写' }}
|
||||
<span style="opacity: 0.5">[{{ sender.senderId ?? '未填写' }}]</span>
|
||||
</NText>
|
||||
</div>
|
||||
<div
|
||||
v-if="sender.description"
|
||||
class="sender-desc"
|
||||
>
|
||||
<NText depth="3">
|
||||
{{ sender.description }}
|
||||
</NText>
|
||||
</div>
|
||||
<NDivider
|
||||
v-if="index < videoInfo.senders.length - 1"
|
||||
style="margin: 8px 0"
|
||||
/>
|
||||
</div>
|
||||
</NScrollbar>
|
||||
</div>
|
||||
|
||||
<div class="action-area">
|
||||
<template v-if="type === 'padding'">
|
||||
<NSpace
|
||||
justify="space-between"
|
||||
:wrap="false"
|
||||
>
|
||||
<NButton
|
||||
strong
|
||||
secondary
|
||||
type="success"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
:loading="isLoading"
|
||||
@click="handleStatusChange(VideoStatus.Accepted)"
|
||||
>
|
||||
通过
|
||||
</NButton>
|
||||
<NButton
|
||||
strong
|
||||
secondary
|
||||
type="error"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
:loading="isLoading"
|
||||
@click="handleStatusChange(VideoStatus.Rejected)"
|
||||
>
|
||||
拒绝
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<template v-if="type === 'accept'">
|
||||
<NSpace
|
||||
justify="space-between"
|
||||
:wrap="false"
|
||||
>
|
||||
<NButton
|
||||
secondary
|
||||
size="small"
|
||||
class="flex-1"
|
||||
:loading="isLoading"
|
||||
@click="handleStatusChange(VideoStatus.Pending)"
|
||||
>
|
||||
重置
|
||||
</NButton>
|
||||
<NPopconfirm @positive-click="handleStatusChange(VideoStatus.Rejected)">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
strong
|
||||
secondary
|
||||
type="error"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
:loading="isLoading"
|
||||
>
|
||||
拒绝
|
||||
</NButton>
|
||||
</template>
|
||||
确定要拒绝这个已通过的视频吗?
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<template v-if="type === 'reject'">
|
||||
<NSpace
|
||||
justify="space-between"
|
||||
:wrap="false"
|
||||
>
|
||||
<NButton
|
||||
strong
|
||||
secondary
|
||||
type="success"
|
||||
size="small"
|
||||
class="flex-1"
|
||||
:loading="isLoading"
|
||||
@click="handleStatusChange(VideoStatus.Accepted)"
|
||||
>
|
||||
通过
|
||||
</NButton>
|
||||
<NButton
|
||||
secondary
|
||||
size="small"
|
||||
class="flex-1"
|
||||
:loading="isLoading"
|
||||
@click="handleStatusChange(VideoStatus.Pending)"
|
||||
>
|
||||
重置
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.video-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.video-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
position: relative;
|
||||
height: 140px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.cover-container:hover .cover-img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.cover-info {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 6px 8px;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sender-info {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.sender-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.sender-desc {
|
||||
padding-left: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.action-area {
|
||||
margin-top: auto;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
90
src/components/manage/ManagePageHeader.vue
Normal file
90
src/components/manage/ManagePageHeader.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { FunctionTypes } from '@/api/api-models'
|
||||
import { DisableFunction, EnableFunction, useAccount } from '@/api/account'
|
||||
import { useMessage, NFlex, NSpace, NSwitch, NText } from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
subtitle?: string
|
||||
functionType?: FunctionTypes // 如果不传,则不显示开关
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const accountInfo = useAccount()
|
||||
const message = useMessage()
|
||||
const switchLoading = ref(false)
|
||||
|
||||
async function setFunctionEnable(enable: boolean) {
|
||||
if (!props.functionType) return
|
||||
switchLoading.value = true
|
||||
try {
|
||||
const success = enable
|
||||
? await EnableFunction(props.functionType)
|
||||
: await DisableFunction(props.functionType)
|
||||
|
||||
if (success) {
|
||||
message.success(`${props.title}功能已${enable ? '启用' : '禁用'}`)
|
||||
// 更新本地状态
|
||||
if (accountInfo.value?.settings?.enableFunctions) {
|
||||
const list = accountInfo.value.settings.enableFunctions
|
||||
if (enable && !list.includes(props.functionType)) {
|
||||
list.push(props.functionType)
|
||||
} else if (!enable) {
|
||||
const index = list.indexOf(props.functionType)
|
||||
if (index > -1) list.splice(index, 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error(`无法${enable ? '启用' : '禁用'}${props.title}功能`)
|
||||
}
|
||||
} catch (err) {
|
||||
message.error(`操作失败: ${err}`)
|
||||
} finally {
|
||||
switchLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="margin-bottom: 16px;">
|
||||
<NFlex justify="space-between" align="center" wrap>
|
||||
<NSpace align="center" size="large">
|
||||
<div>
|
||||
<h2 style="margin: 0; font-weight: 500; line-height: 1.2;">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<NText
|
||||
v-if="props.subtitle"
|
||||
depth="3"
|
||||
style="font-size: 12px; margin-top: 4px; display: block;"
|
||||
>
|
||||
{{ props.subtitle }}
|
||||
</NText>
|
||||
</div>
|
||||
<!-- 功能启用开关 -->
|
||||
<NFlex v-if="functionType && accountInfo" align="center" size="small">
|
||||
<NSwitch
|
||||
:value="accountInfo.settings?.enableFunctions?.includes(functionType)"
|
||||
:loading="switchLoading"
|
||||
:disabled="loading || switchLoading"
|
||||
@update:value="setFunctionEnable"
|
||||
>
|
||||
<template #checked>已启用</template>
|
||||
<template #unchecked>已禁用</template>
|
||||
</NSwitch>
|
||||
</NFlex>
|
||||
</NSpace>
|
||||
|
||||
<!-- 右侧操作按钮插槽 -->
|
||||
<NSpace>
|
||||
<slot name="action" />
|
||||
</NSpace>
|
||||
</NFlex>
|
||||
|
||||
<!-- 底部额外内容插槽 (如提示信息) -->
|
||||
<div style="margin-top: 12px;">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { RefreshOutline, TrendingDown, TrendingUp } from '@vicons/ionicons5'
|
||||
import {
|
||||
CalendarOutline,
|
||||
ChatbubblesOutline,
|
||||
PeopleOutline,
|
||||
RefreshOutline,
|
||||
TimeOutline,
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
WalletOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import { BarChart, LineChart } from 'echarts/charts'
|
||||
import {
|
||||
DataZoomComponent,
|
||||
@@ -12,7 +21,26 @@ import {
|
||||
} from 'echarts/components'
|
||||
import * as echarts from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { NButton, NCard, NDivider, NEmpty, NGrid, NGridItem, NIcon, NSpace, NSpin, NStatistic, NTabPane, NTabs, NTag, NTime, NTooltip, useMessage, useThemeVars } from 'naive-ui'
|
||||
import {
|
||||
NButton,
|
||||
NCard,
|
||||
NDivider,
|
||||
NEmpty,
|
||||
NGrid,
|
||||
NGridItem,
|
||||
NIcon,
|
||||
NProgress,
|
||||
NSkeleton,
|
||||
NSpace,
|
||||
NStatistic,
|
||||
NTabPane,
|
||||
NTabs,
|
||||
NTag,
|
||||
NTime,
|
||||
NTooltip,
|
||||
useMessage,
|
||||
useThemeVars,
|
||||
} from 'naive-ui'
|
||||
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
@@ -517,343 +545,397 @@ onUnmounted(() => {
|
||||
</NSpace>
|
||||
<NDivider />
|
||||
|
||||
<NSpin :show="loading">
|
||||
<!-- 空状态 -->
|
||||
<NEmpty
|
||||
v-if="!loading && !hasData"
|
||||
description="暂无数据"
|
||||
size="large"
|
||||
style="margin: 60px 0"
|
||||
>
|
||||
<template #extra>
|
||||
<NButton @click="() => fetchAnalyzeData()">
|
||||
重新加载
|
||||
</NButton>
|
||||
</template>
|
||||
</NEmpty>
|
||||
|
||||
<!-- 数据展示 -->
|
||||
<template v-else>
|
||||
<!-- 数据概览卡片 -->
|
||||
<div class="summary-cards">
|
||||
<NGrid
|
||||
cols="1 800:2 1200:3"
|
||||
:x-gap="16"
|
||||
:y-gap="16"
|
||||
>
|
||||
<NGridItem>
|
||||
<NCard
|
||||
title="近7天统计"
|
||||
size="small"
|
||||
class="summary-card"
|
||||
hoverable
|
||||
>
|
||||
<template #header-extra>
|
||||
<NTag :bordered="false" size="small" type="info">
|
||||
最近一周
|
||||
</NTag>
|
||||
</template>
|
||||
<div class="stat-grid">
|
||||
<NStatistic
|
||||
label="总收入"
|
||||
tabular-nums
|
||||
>
|
||||
<template #default>
|
||||
<span class="stat-value-primary">
|
||||
{{ formatCurrency(summaryData?.last7Days?.totalIncome || 0) }}
|
||||
</span>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="总互动数"
|
||||
:value="summaryData?.last7Days?.totalInteractions || 0"
|
||||
tabular-nums
|
||||
/>
|
||||
<NStatistic
|
||||
label="弹幕数"
|
||||
:value="summaryData?.last7Days?.totalDanmakuCount || 0"
|
||||
tabular-nums
|
||||
/>
|
||||
<NStatistic
|
||||
label="直播时长"
|
||||
tabular-nums
|
||||
>
|
||||
<template #default>
|
||||
{{ ((summaryData?.last7Days?.totalLiveMinutes || 0) / 60).toFixed(1) }} 小时
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="互动人数"
|
||||
:value="summaryData?.last7Days?.interactionUsers || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #suffix>
|
||||
<NTag
|
||||
:type="getTrendType(summaryData?.last7Days?.interactionUsersTrend || 0)"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
>
|
||||
<NIcon v-if="(summaryData?.last7Days?.interactionUsersTrend || 0) > 0" :component="TrendingUp" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
<NIcon v-else-if="(summaryData?.last7Days?.interactionUsersTrend || 0) < 0" :component="TrendingDown" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
{{ formatTrend(summaryData?.last7Days?.interactionUsersTrend || 0) }}
|
||||
</NTag>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="付费人数"
|
||||
:value="summaryData?.last7Days?.payingUsers || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #suffix>
|
||||
<NTag
|
||||
:type="getTrendType(summaryData?.last7Days?.payingUsersTrend || 0)"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
>
|
||||
<NIcon v-if="(summaryData?.last7Days?.payingUsersTrend || 0) > 0" :component="TrendingUp" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
<NIcon v-else-if="(summaryData?.last7Days?.payingUsersTrend || 0) < 0" :component="TrendingDown" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
{{ formatTrend(summaryData?.last7Days?.payingUsersTrend || 0) }}
|
||||
</NTag>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="日均收入"
|
||||
tabular-nums
|
||||
>
|
||||
<template #default>
|
||||
{{ formatCurrency(summaryData?.last7Days?.dailyAvgIncome || 0) }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="日均弹幕"
|
||||
:value="(summaryData?.last7Days?.dailyAvgDanmaku || 0).toFixed(0)"
|
||||
tabular-nums
|
||||
/>
|
||||
<NStatistic
|
||||
label="活跃直播天数"
|
||||
:value="summaryData?.last7Days?.activeLiveDays || 0"
|
||||
tabular-nums
|
||||
/>
|
||||
<!-- 加载骨架屏 -->
|
||||
<div v-if="loading" class="skeleton-container">
|
||||
<div class="summary-cards">
|
||||
<NGrid cols="1 800:2 1200:3" :x-gap="16" :y-gap="16">
|
||||
<NGridItem v-for="i in 3" :key="i">
|
||||
<NCard size="small" class="summary-card">
|
||||
<template #header>
|
||||
<NSkeleton text width="30%" />
|
||||
</template>
|
||||
<div class="stat-grid">
|
||||
<div v-for="j in 8" :key="j" class="skeleton-item">
|
||||
<NSkeleton text width="60px" style="margin-bottom: 8px" />
|
||||
<NSkeleton text width="80%" height="24px" />
|
||||
</div>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
</div>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</div>
|
||||
<div class="chart-skeleton">
|
||||
<NSkeleton height="450px" width="100%" border-radius="8px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NGridItem>
|
||||
<NCard
|
||||
title="近30天统计"
|
||||
size="small"
|
||||
class="summary-card"
|
||||
hoverable
|
||||
>
|
||||
<template #header-extra>
|
||||
<NTag :bordered="false" size="small" type="warning">
|
||||
最近一月
|
||||
</NTag>
|
||||
</template>
|
||||
<div class="stat-grid">
|
||||
<NStatistic
|
||||
label="总收入"
|
||||
tabular-nums
|
||||
>
|
||||
<template #default>
|
||||
<span class="stat-value-primary">
|
||||
{{ formatCurrency(summaryData?.last30Days?.totalIncome || 0) }}
|
||||
</span>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="总互动数"
|
||||
:value="summaryData?.last30Days?.totalInteractions || 0"
|
||||
tabular-nums
|
||||
/>
|
||||
<NStatistic
|
||||
label="弹幕数"
|
||||
:value="summaryData?.last30Days?.totalDanmakuCount || 0"
|
||||
tabular-nums
|
||||
/>
|
||||
<NStatistic
|
||||
label="直播时长"
|
||||
tabular-nums
|
||||
>
|
||||
<template #default>
|
||||
{{ ((summaryData?.last30Days?.totalLiveMinutes || 0) / 60).toFixed(1) }} 小时
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="互动人数"
|
||||
:value="summaryData?.last30Days?.interactionUsers || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #suffix>
|
||||
<NTag
|
||||
:type="getTrendType(summaryData?.last30Days?.interactionTrend || 0)"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
>
|
||||
<NIcon v-if="(summaryData?.last30Days?.interactionTrend || 0) > 0" :component="TrendingUp" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
<NIcon v-else-if="(summaryData?.last30Days?.interactionTrend || 0) < 0" :component="TrendingDown" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
{{ formatTrend(summaryData?.last30Days?.interactionTrend || 0) }}
|
||||
</NTag>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="付费人数"
|
||||
:value="summaryData?.last30Days?.payingUsers || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #suffix>
|
||||
<NTag
|
||||
:type="getTrendType(summaryData?.last30Days?.incomeTrend || 0)"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
>
|
||||
<NIcon v-if="(summaryData?.last30Days?.incomeTrend || 0) > 0" :component="TrendingUp" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
<NIcon v-else-if="(summaryData?.last30Days?.incomeTrend || 0) < 0" :component="TrendingDown" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
{{ formatTrend(summaryData?.last30Days?.incomeTrend || 0) }}
|
||||
</NTag>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="日均收入"
|
||||
tabular-nums
|
||||
>
|
||||
<template #default>
|
||||
{{ formatCurrency(summaryData?.last30Days?.dailyAvgIncome || 0) }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="日均弹幕"
|
||||
:value="(summaryData?.last30Days?.dailyAvgDanmaku || 0).toFixed(0)"
|
||||
tabular-nums
|
||||
/>
|
||||
<NStatistic
|
||||
label="活跃直播天数"
|
||||
:value="summaryData?.last30Days?.activeLiveDays || 0"
|
||||
tabular-nums
|
||||
/>
|
||||
</div>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
|
||||
<NGridItem>
|
||||
<NCard
|
||||
title="关键指标"
|
||||
size="small"
|
||||
class="summary-card summary-card-highlight"
|
||||
hoverable
|
||||
>
|
||||
<template #header-extra>
|
||||
<NTag :bordered="false" size="small" type="success">
|
||||
核心数据
|
||||
</NTag>
|
||||
</template>
|
||||
<div class="stat-grid">
|
||||
<NStatistic
|
||||
label="月收入增长"
|
||||
tabular-nums
|
||||
>
|
||||
<template #default>
|
||||
<span class="trend-value" :class="(summaryData?.last30Days?.incomeTrend || 0) >= 0 ? 'trend-up' : 'trend-down'">
|
||||
{{ formatTrend(summaryData?.last30Days?.incomeTrend || 0) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #prefix>
|
||||
<NIcon :color="(summaryData?.last30Days?.incomeTrend || 0) >= 0 ? '#18A058' : '#D03050'">
|
||||
<TrendingUp v-if="(summaryData?.last30Days?.incomeTrend || 0) >= 0" />
|
||||
<TrendingDown v-else />
|
||||
</NIcon>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="月互动增长"
|
||||
tabular-nums
|
||||
>
|
||||
<template #default>
|
||||
<span class="trend-value" :class="(summaryData?.last30Days?.interactionTrend || 0) >= 0 ? 'trend-up' : 'trend-down'">
|
||||
{{ formatTrend(summaryData?.last30Days?.interactionTrend || 0) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #prefix>
|
||||
<NIcon
|
||||
:component="(summaryData?.last30Days?.interactionTrend || 0) >= 0 ? TrendingUp : TrendingDown"
|
||||
:color="(summaryData?.last30Days?.interactionTrend || 0) >= 0 ? '#18A058' : '#D03050'"
|
||||
/>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="单次直播平均时长"
|
||||
tabular-nums
|
||||
>
|
||||
<template #default>
|
||||
{{ ((summaryData?.last30Days?.totalLiveMinutes || 0) / (summaryData?.last30Days?.activeLiveDays || 1) / 60).toFixed(1) }} 小时
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="互动转化率"
|
||||
tabular-nums
|
||||
>
|
||||
<template #default>
|
||||
{{ ((summaryData?.last30Days?.payingUsers || 0) / (summaryData?.last30Days?.interactionUsers || 1) * 100).toFixed(1) }}%
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="每付费用户平均收入"
|
||||
tabular-nums
|
||||
>
|
||||
<template #default>
|
||||
{{ formatCurrency((summaryData?.last30Days?.totalIncome || 0) / (summaryData?.last30Days?.payingUsers || 1)) }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
</div>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</div>
|
||||
|
||||
<NDivider />
|
||||
|
||||
<!-- 图表选择器 -->
|
||||
<div class="chart-selector">
|
||||
<NTabs
|
||||
v-model:value="activeChart"
|
||||
type="line"
|
||||
animated
|
||||
@update:value="onTabChange"
|
||||
>
|
||||
<NTabPane
|
||||
name="income"
|
||||
tab="收入分析"
|
||||
display-directive="show"
|
||||
>
|
||||
<div
|
||||
ref="incomeChartRef"
|
||||
class="chart"
|
||||
/>
|
||||
</NTabPane>
|
||||
<NTabPane
|
||||
name="interaction"
|
||||
tab="互动分析"
|
||||
display-directive="show"
|
||||
>
|
||||
<div
|
||||
ref="interactionChartRef"
|
||||
class="chart"
|
||||
/>
|
||||
</NTabPane>
|
||||
<NTabPane
|
||||
name="users"
|
||||
tab="用户分析"
|
||||
display-directive="show"
|
||||
>
|
||||
<div
|
||||
ref="usersChartRef"
|
||||
class="chart"
|
||||
/>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</div>
|
||||
<!-- 空状态 -->
|
||||
<NEmpty
|
||||
v-else-if="!hasData"
|
||||
description="暂无数据"
|
||||
size="large"
|
||||
style="margin: 60px 0"
|
||||
>
|
||||
<template #extra>
|
||||
<NButton @click="() => fetchAnalyzeData()">
|
||||
重新加载
|
||||
</NButton>
|
||||
</template>
|
||||
</NSpin>
|
||||
</NEmpty>
|
||||
|
||||
<!-- 数据展示 -->
|
||||
<template v-else>
|
||||
<!-- 数据概览卡片 -->
|
||||
<div class="summary-cards">
|
||||
<NGrid
|
||||
cols="1 800:2 1200:3"
|
||||
:x-gap="16"
|
||||
:y-gap="16"
|
||||
>
|
||||
<NGridItem>
|
||||
<NCard
|
||||
title="近7天统计"
|
||||
size="small"
|
||||
class="summary-card"
|
||||
hoverable
|
||||
>
|
||||
<template #header-extra>
|
||||
<NTag :bordered="false" size="small" type="info">
|
||||
最近一周
|
||||
</NTag>
|
||||
</template>
|
||||
<div class="stat-grid">
|
||||
<NStatistic label="总收入" tabular-nums>
|
||||
<template #prefix>
|
||||
<NIcon :component="WalletOutline" color="#f5a623" />
|
||||
</template>
|
||||
<template #default>
|
||||
<span class="stat-value-primary">
|
||||
{{ formatCurrency(summaryData?.last7Days?.totalIncome || 0) }}
|
||||
</span>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="总互动数"
|
||||
:value="summaryData?.last7Days?.totalInteractions || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #prefix>
|
||||
<NIcon :component="ChatbubblesOutline" color="#2080f0" />
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="弹幕数"
|
||||
:value="summaryData?.last7Days?.totalDanmakuCount || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #prefix>
|
||||
<NIcon :component="ChatbubblesOutline" color="#2080f0" />
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic label="直播时长" tabular-nums>
|
||||
<template #prefix>
|
||||
<NIcon :component="TimeOutline" />
|
||||
</template>
|
||||
<template #default>
|
||||
{{ ((summaryData?.last7Days?.totalLiveMinutes || 0) / 60).toFixed(1) }} 小时
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="互动人数"
|
||||
:value="summaryData?.last7Days?.interactionUsers || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #prefix>
|
||||
<NIcon :component="PeopleOutline" color="#18a058" />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<NTag
|
||||
:type="getTrendType(summaryData?.last7Days?.interactionUsersTrend || 0)"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
>
|
||||
<NIcon v-if="(summaryData?.last7Days?.interactionUsersTrend || 0) > 0" :component="TrendingUp" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
<NIcon v-else-if="(summaryData?.last7Days?.interactionUsersTrend || 0) < 0" :component="TrendingDown" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
{{ formatTrend(summaryData?.last7Days?.interactionUsersTrend || 0) }}
|
||||
</NTag>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="付费人数"
|
||||
:value="summaryData?.last7Days?.payingUsers || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #prefix>
|
||||
<NIcon :component="PeopleOutline" color="#f5a623" />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<NTag
|
||||
:type="getTrendType(summaryData?.last7Days?.payingUsersTrend || 0)"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
>
|
||||
<NIcon v-if="(summaryData?.last7Days?.payingUsersTrend || 0) > 0" :component="TrendingUp" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
<NIcon v-else-if="(summaryData?.last7Days?.payingUsersTrend || 0) < 0" :component="TrendingDown" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
{{ formatTrend(summaryData?.last7Days?.payingUsersTrend || 0) }}
|
||||
</NTag>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic label="日均收入" tabular-nums>
|
||||
<template #prefix>
|
||||
<NIcon :component="WalletOutline" color="#f5a623" />
|
||||
</template>
|
||||
<template #default>
|
||||
{{ formatCurrency(summaryData?.last7Days?.dailyAvgIncome || 0) }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="活跃直播天数"
|
||||
:value="summaryData?.last7Days?.activeLiveDays || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #prefix>
|
||||
<NIcon :component="CalendarOutline" />
|
||||
</template>
|
||||
</NStatistic>
|
||||
</div>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
|
||||
<NGridItem>
|
||||
<NCard
|
||||
title="近30天统计"
|
||||
size="small"
|
||||
class="summary-card"
|
||||
hoverable
|
||||
>
|
||||
<template #header-extra>
|
||||
<NTag :bordered="false" size="small" type="warning">
|
||||
最近一月
|
||||
</NTag>
|
||||
</template>
|
||||
<div class="stat-grid">
|
||||
<NStatistic label="总收入" tabular-nums>
|
||||
<template #prefix>
|
||||
<NIcon :component="WalletOutline" color="#f5a623" />
|
||||
</template>
|
||||
<template #default>
|
||||
<span class="stat-value-primary">
|
||||
{{ formatCurrency(summaryData?.last30Days?.totalIncome || 0) }}
|
||||
</span>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="总互动数"
|
||||
:value="summaryData?.last30Days?.totalInteractions || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #prefix>
|
||||
<NIcon :component="ChatbubblesOutline" color="#2080f0" />
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="弹幕数"
|
||||
:value="summaryData?.last30Days?.totalDanmakuCount || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #prefix>
|
||||
<NIcon :component="ChatbubblesOutline" color="#2080f0" />
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic label="直播时长" tabular-nums>
|
||||
<template #prefix>
|
||||
<NIcon :component="TimeOutline" />
|
||||
</template>
|
||||
<template #default>
|
||||
{{ ((summaryData?.last30Days?.totalLiveMinutes || 0) / 60).toFixed(1) }} 小时
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="互动人数"
|
||||
:value="summaryData?.last30Days?.interactionUsers || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #prefix>
|
||||
<NIcon :component="PeopleOutline" color="#18a058" />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<NTag
|
||||
:type="getTrendType(summaryData?.last30Days?.interactionTrend || 0)"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
>
|
||||
<NIcon v-if="(summaryData?.last30Days?.interactionTrend || 0) > 0" :component="TrendingUp" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
<NIcon v-else-if="(summaryData?.last30Days?.interactionTrend || 0) < 0" :component="TrendingDown" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
{{ formatTrend(summaryData?.last30Days?.interactionTrend || 0) }}
|
||||
</NTag>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="付费人数"
|
||||
:value="summaryData?.last30Days?.payingUsers || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #prefix>
|
||||
<NIcon :component="PeopleOutline" color="#f5a623" />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<NTag
|
||||
:type="getTrendType(summaryData?.last30Days?.incomeTrend || 0)"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
>
|
||||
<NIcon v-if="(summaryData?.last30Days?.incomeTrend || 0) > 0" :component="TrendingUp" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
<NIcon v-else-if="(summaryData?.last30Days?.incomeTrend || 0) < 0" :component="TrendingDown" style="vertical-align: -0.15em; margin-right: 2px;" />
|
||||
{{ formatTrend(summaryData?.last30Days?.incomeTrend || 0) }}
|
||||
</NTag>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic label="日均收入" tabular-nums>
|
||||
<template #prefix>
|
||||
<NIcon :component="WalletOutline" color="#f5a623" />
|
||||
</template>
|
||||
<template #default>
|
||||
{{ formatCurrency(summaryData?.last30Days?.dailyAvgIncome || 0) }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic
|
||||
label="活跃直播天数"
|
||||
:value="summaryData?.last30Days?.activeLiveDays || 0"
|
||||
tabular-nums
|
||||
>
|
||||
<template #prefix>
|
||||
<NIcon :component="CalendarOutline" />
|
||||
</template>
|
||||
</NStatistic>
|
||||
</div>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
|
||||
<NGridItem>
|
||||
<NCard
|
||||
title="关键指标"
|
||||
size="small"
|
||||
class="summary-card summary-card-highlight"
|
||||
hoverable
|
||||
>
|
||||
<template #header-extra>
|
||||
<NTag :bordered="false" size="small" type="success">
|
||||
核心数据
|
||||
</NTag>
|
||||
</template>
|
||||
<div class="stat-grid">
|
||||
<NStatistic label="月收入增长" tabular-nums>
|
||||
<template #default>
|
||||
<span class="trend-value" :class="(summaryData?.last30Days?.incomeTrend || 0) >= 0 ? 'trend-up' : 'trend-down'">
|
||||
{{ formatTrend(summaryData?.last30Days?.incomeTrend || 0) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #prefix>
|
||||
<NIcon :color="(summaryData?.last30Days?.incomeTrend || 0) >= 0 ? '#18A058' : '#D03050'">
|
||||
<TrendingUp v-if="(summaryData?.last30Days?.incomeTrend || 0) >= 0" />
|
||||
<TrendingDown v-else />
|
||||
</NIcon>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic label="月互动增长" tabular-nums>
|
||||
<template #default>
|
||||
<span class="trend-value" :class="(summaryData?.last30Days?.interactionTrend || 0) >= 0 ? 'trend-up' : 'trend-down'">
|
||||
{{ formatTrend(summaryData?.last30Days?.interactionTrend || 0) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #prefix>
|
||||
<NIcon
|
||||
:component="(summaryData?.last30Days?.interactionTrend || 0) >= 0 ? TrendingUp : TrendingDown"
|
||||
:color="(summaryData?.last30Days?.interactionTrend || 0) >= 0 ? '#18A058' : '#D03050'"
|
||||
/>
|
||||
</template>
|
||||
</NStatistic>
|
||||
<NStatistic label="单次直播平均时长" tabular-nums>
|
||||
<template #prefix>
|
||||
<NIcon :component="TimeOutline" />
|
||||
</template>
|
||||
<template #default>
|
||||
{{ ((summaryData?.last30Days?.totalLiveMinutes || 0) / (summaryData?.last30Days?.activeLiveDays || 1) / 60).toFixed(1) }} 小时
|
||||
</template>
|
||||
</NStatistic>
|
||||
|
||||
<!-- 互动转化率 - 进度条优化 -->
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">互动转化率</div>
|
||||
<NSpace align="center" :size="10">
|
||||
<NProgress
|
||||
type="line"
|
||||
:percentage="Math.min(100, Math.round(((summaryData?.last30Days?.payingUsers || 0) / (summaryData?.last30Days?.interactionUsers || 1) * 100) * 10) / 10)"
|
||||
:indicator-placement="'inside'"
|
||||
:height="18"
|
||||
color="#18a058"
|
||||
rail-color="rgba(24, 160, 88, 0.2)"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<span class="stat-value-small">
|
||||
{{ ((summaryData?.last30Days?.payingUsers || 0) / (summaryData?.last30Days?.interactionUsers || 1) * 100).toFixed(1) }}%
|
||||
</span>
|
||||
</NSpace>
|
||||
</div>
|
||||
|
||||
<NStatistic label="每付费用户平均收入" tabular-nums>
|
||||
<template #prefix>
|
||||
<NIcon :component="WalletOutline" color="#f5a623" />
|
||||
</template>
|
||||
<template #default>
|
||||
{{ formatCurrency((summaryData?.last30Days?.totalIncome || 0) / (summaryData?.last30Days?.payingUsers || 1)) }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
</div>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</div>
|
||||
|
||||
<NDivider />
|
||||
|
||||
<!-- 图表选择器 -->
|
||||
<div class="chart-selector">
|
||||
<NTabs
|
||||
v-model:value="activeChart"
|
||||
type="line"
|
||||
animated
|
||||
@update:value="onTabChange"
|
||||
>
|
||||
<NTabPane
|
||||
name="income"
|
||||
tab="收入分析"
|
||||
display-directive="show"
|
||||
>
|
||||
<div
|
||||
ref="incomeChartRef"
|
||||
class="chart"
|
||||
/>
|
||||
</NTabPane>
|
||||
<NTabPane
|
||||
name="interaction"
|
||||
tab="互动分析"
|
||||
display-directive="show"
|
||||
>
|
||||
<div
|
||||
ref="interactionChartRef"
|
||||
class="chart"
|
||||
/>
|
||||
</NTabPane>
|
||||
<NTabPane
|
||||
name="users"
|
||||
tab="用户分析"
|
||||
display-directive="show"
|
||||
>
|
||||
<div
|
||||
ref="usersChartRef"
|
||||
class="chart"
|
||||
/>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -871,10 +953,6 @@ onUnmounted(() => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.analyze-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-cards {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -897,7 +975,7 @@ onUnmounted(() => {
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
gap: 16px 24px;
|
||||
}
|
||||
|
||||
.stat-value-primary {
|
||||
@@ -906,6 +984,22 @@ onUnmounted(() => {
|
||||
color: var(--n-text-color);
|
||||
}
|
||||
|
||||
.stat-value-small {
|
||||
font-size: 0.9em;
|
||||
color: var(--n-text-color-2);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--n-text-color-3);
|
||||
}
|
||||
|
||||
.trend-value {
|
||||
font-weight: 600;
|
||||
font-size: 1.1em;
|
||||
@@ -933,6 +1027,24 @@ onUnmounted(() => {
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
|
||||
/* Skeleton Styles */
|
||||
.skeleton-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.skeleton-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.chart-skeleton {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: var(--n-card-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1400px) {
|
||||
.chart {
|
||||
@@ -953,7 +1065,7 @@ onUnmounted(() => {
|
||||
|
||||
.stat-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
@@ -985,22 +1097,6 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 骨架屏动画 */
|
||||
@keyframes skeleton-loading {
|
||||
0% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 标签优化 */
|
||||
:deep(.n-statistic-value__prefix) {
|
||||
margin-right: 8px;
|
||||
@@ -1029,6 +1125,6 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.summary-card:hover :deep(.n-statistic) {
|
||||
transform: scale(1.02);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { ResponseLiveInfoModel } from '@/api/api-models'
|
||||
import { NAlert, NButton, NDivider, NEmpty, NInput, NInputNumber, NSelect, NSkeleton, NList, NListItem, NPagination, NSpace, NSwitch, useMessage } from 'naive-ui'
|
||||
import {
|
||||
ArrowSort24Filled,
|
||||
ArrowSync24Filled,
|
||||
Search24Filled
|
||||
} from '@vicons/fluent'
|
||||
import { NAlert, NButton, NCard, NDivider, NEmpty, NIcon, NInput, NInputNumber, NList, NListItem, NPagination, NSelect, NSkeleton, NSpace, NSwitch, useMessage } from 'naive-ui'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useLocalStorage, useSessionStorage, useStorage } from '@vueuse/core'
|
||||
@@ -127,7 +132,7 @@ function syncStateToQuery() {
|
||||
sort: sortKey.value !== 'startAt' ? sortKey.value : undefined,
|
||||
order: sortOrder.value !== 'desc' ? sortOrder.value : undefined,
|
||||
},
|
||||
}).catch(() => {})
|
||||
}).catch(() => { })
|
||||
}
|
||||
|
||||
watch([page, pageSize, keyword, statusFilter, sortKey, sortOrder], syncStateToQuery)
|
||||
@@ -162,124 +167,117 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace
|
||||
vertical
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<NSpace vertical justify="center" align="center" :size="24">
|
||||
<EventFetcherAlert />
|
||||
<EventFetcherStatusCard />
|
||||
</NSpace>
|
||||
<NDivider />
|
||||
<NAlert
|
||||
v-if="accountInfo?.isBiliVerified != true"
|
||||
type="info"
|
||||
>
|
||||
尚未进行Bilibili认证
|
||||
</NAlert>
|
||||
|
||||
<div v-if="accountInfo?.isBiliVerified != true" style="margin-top: 24px;">
|
||||
<NAlert type="info" title="未认证">
|
||||
尚未进行Bilibili认证,部分功能可能受限。
|
||||
</NAlert>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<NSpace
|
||||
wrap
|
||||
align="center"
|
||||
justify="space-between"
|
||||
style="width: 100%"
|
||||
>
|
||||
<NSpace align="center" wrap>
|
||||
<NInput
|
||||
v-model:value="keyword"
|
||||
placeholder="搜索标题或ID"
|
||||
clearable
|
||||
style="min-width: 220px"
|
||||
/>
|
||||
<NSelect
|
||||
v-model:value="statusFilter"
|
||||
:options="[
|
||||
{ label: '全部', value: 'all' },
|
||||
<NCard style="margin-top: 24px; margin-bottom: 24px; max-width: 1200px; left: 50%; transform: translateX(-50%);" size="small">
|
||||
<NSpace justify="space-between" align="center" wrap item-style="flex-grow: 1">
|
||||
<!-- Left: Search and Filter -->
|
||||
<NSpace align="center" wrap>
|
||||
<NInput v-model:value="keyword" placeholder="搜索标题或ID" clearable style="width: 280px">
|
||||
<template #prefix>
|
||||
<NIcon :component="Search24Filled" />
|
||||
</template>
|
||||
</NInput>
|
||||
|
||||
<NSelect v-model:value="statusFilter" :options="[
|
||||
{ label: '全部状态', value: 'all' },
|
||||
{ label: '直播中', value: 'live' },
|
||||
{ label: '已结束', value: 'finished' },
|
||||
]"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<NSelect
|
||||
v-model:value="sortKey"
|
||||
:options="[
|
||||
{ label: '开始时间', value: 'startAt' },
|
||||
{ label: '弹幕数', value: 'danmakusCount' },
|
||||
{ label: '互动数', value: 'interactionCount' },
|
||||
{ label: '收益', value: 'totalIncome' },
|
||||
]"
|
||||
style="width: 140px"
|
||||
/>
|
||||
<NSelect
|
||||
v-model:value="sortOrder"
|
||||
:options="[
|
||||
{ label: '降序', value: 'desc' },
|
||||
{ label: '升序', value: 'asc' },
|
||||
]"
|
||||
style="width: 100px"
|
||||
/>
|
||||
]" style="width: 140px">
|
||||
</NSelect>
|
||||
</NSpace>
|
||||
|
||||
<!-- Right: Sort and Actions -->
|
||||
<NSpace align="center" wrap>
|
||||
<NSpace align="center" :size="12">
|
||||
<span style="color: var(--n-text-color-3); font-size: 12px;">排序:</span>
|
||||
<NSelect v-model:value="sortKey" size="small" :options="[
|
||||
{ label: '开始时间', value: 'startAt' },
|
||||
{ label: '弹幕数', value: 'danmakusCount' },
|
||||
{ label: '互动数', value: 'interactionCount' },
|
||||
{ label: '收益', value: 'totalIncome' },
|
||||
]" style="width: 120px" />
|
||||
<NSelect v-model:value="sortOrder" size="small" :options="[
|
||||
{ label: '降序', value: 'desc' },
|
||||
{ label: '升序', value: 'asc' },
|
||||
]" style="width: 90px" />
|
||||
</NSpace>
|
||||
|
||||
<NDivider vertical />
|
||||
|
||||
<NSpace align="center">
|
||||
<NSwitch v-model:value="enableAutoRefresh" size="small">
|
||||
<template #checked>自动刷新</template>
|
||||
<template #unchecked>自动刷新</template>
|
||||
</NSwitch>
|
||||
<NInputNumber v-if="enableAutoRefresh" v-model:value="refreshSeconds" size="small" style="width: 80px"
|
||||
:min="10" placeholder="秒">
|
||||
<template #suffix>s</template>
|
||||
</NInputNumber>
|
||||
<NButton size="small" secondary type="primary" :loading="isLoading" @click="getAll()">
|
||||
<template #icon>
|
||||
<NIcon :component="ArrowSync24Filled" />
|
||||
</template>
|
||||
刷新
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
<NSpace align="center">
|
||||
<NSwitch v-model:value="enableAutoRefresh">
|
||||
<template #checked>自动刷新</template>
|
||||
<template #unchecked>自动刷新</template>
|
||||
</NSwitch>
|
||||
<NInputNumber
|
||||
v-model:value="refreshSeconds"
|
||||
style="width: 100px"
|
||||
:min="10"
|
||||
:disabled="!enableAutoRefresh"
|
||||
placeholder="刷新秒数"
|
||||
/>
|
||||
<NButton
|
||||
type="primary"
|
||||
tertiary
|
||||
:loading="isLoading"
|
||||
@click="getAll()"
|
||||
>
|
||||
刷新
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
<NSpace
|
||||
vertical
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<NPagination
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
show-quick-jumper
|
||||
show-size-picker
|
||||
:page-sizes="[10, 20, 30, 40]"
|
||||
:item-count="filteredAndSortedLives.length"
|
||||
/>
|
||||
</NSpace>
|
||||
<NDivider />
|
||||
<NSkeleton v-if="isLoading" text :repeat="5" />
|
||||
</NCard>
|
||||
|
||||
<NSkeleton v-if="isLoading && !lives.length" text :repeat="5" />
|
||||
<template v-else>
|
||||
<NEmpty v-if="!filteredAndSortedLives.length" description="无数据">
|
||||
<NEmpty v-if="!filteredAndSortedLives.length" description="没有找到符合条件的直播记录">
|
||||
<template #extra>
|
||||
<NButton type="primary" @click="getAll">重试</NButton>
|
||||
<NButton type="primary" @click="getAll">重新加载</NButton>
|
||||
</template>
|
||||
</NEmpty>
|
||||
<NList
|
||||
v-else
|
||||
bordered
|
||||
hoverable
|
||||
clickable
|
||||
>
|
||||
<NListItem
|
||||
v-for="live in pagedLives"
|
||||
:key="live.liveId"
|
||||
@click="OnClickCover(live)"
|
||||
>
|
||||
<LiveInfoContainer
|
||||
:key="live.liveId"
|
||||
:live="live"
|
||||
/>
|
||||
</NListItem>
|
||||
</NList>
|
||||
|
||||
<div v-else class="list-container">
|
||||
<NList hoverable clickable class="live-list">
|
||||
<NListItem v-for="live in pagedLives" :key="live.liveId" @click="OnClickCover(live)" class="live-list-item">
|
||||
<LiveInfoContainer :key="live.liveId" :live="live" />
|
||||
</NListItem>
|
||||
</NList>
|
||||
|
||||
<NSpace justify="center" align="center" style="margin-top: 24px; margin-bottom: 48px;">
|
||||
<NPagination v-model:page="page" v-model:page-size="pageSize" show-quick-jumper show-size-picker
|
||||
:page-sizes="[10, 20, 30, 40]" :item-count="filteredAndSortedLives.length" />
|
||||
</NSpace>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.list-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.live-list {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.live-list-item {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s;
|
||||
margin-bottom: 8px;
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.live-list-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { QAInfo, Setting_QuestionDisplay } from '@/api/api-models'
|
||||
import { Delete24Filled, Delete24Regular, Eye24Filled, EyeOff24Filled, Info24Filled } from '@vicons/fluent'
|
||||
import { Heart, HeartOutline, TrashBin } from '@vicons/ionicons5'
|
||||
import { ArrowSync24Filled, Copy24Filled, Delete24Filled, Delete24Regular, Eye24Filled, EyeOff24Filled, Info24Filled, Link24Filled, Share24Filled } from '@vicons/fluent'
|
||||
import { Heart, HeartOutline, SettingsOutline, TrashBin } from '@vicons/ionicons5'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
// @ts-ignore
|
||||
import { saveAs } from 'file-saver'
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
NList,
|
||||
NListItem,
|
||||
NModal,
|
||||
NPageHeader,
|
||||
NPagination,
|
||||
NPopconfirm,
|
||||
NSelect,
|
||||
@@ -36,12 +37,19 @@ import {
|
||||
NTime,
|
||||
NTooltip,
|
||||
useMessage,
|
||||
NGrid,
|
||||
NGi,
|
||||
NStatistic,
|
||||
NThing,
|
||||
NCollapse,
|
||||
NCollapseItem,
|
||||
} from 'naive-ui'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import { computed, h, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { DisableFunction, EnableFunction, SaveAccountSettings, SaveSetting, useAccount } from '@/api/account'
|
||||
import { SaveAccountSettings, SaveSetting, useAccount } from '@/api/account'
|
||||
import { FunctionTypes } from '@/api/api-models'
|
||||
import ManagePageHeader from '@/components/manage/ManagePageHeader.vue'
|
||||
import QuestionItem from '@/components/QuestionItem.vue'
|
||||
import QuestionItems from '@/components/QuestionItems.vue'
|
||||
import { CURRENT_HOST } from '@/data/constants'
|
||||
@@ -230,40 +238,6 @@ async function saveNotificationSetting() {
|
||||
}
|
||||
}
|
||||
|
||||
// 启用或禁用提问箱功能
|
||||
async function setFunctionEnable(enable: boolean) {
|
||||
let success = false
|
||||
try {
|
||||
if (enable) {
|
||||
success = await EnableFunction(FunctionTypes.QuestionBox) // 调用启用API
|
||||
} else {
|
||||
success = await DisableFunction(FunctionTypes.QuestionBox) // 调用禁用API
|
||||
}
|
||||
if (success) {
|
||||
message.success(`提问箱功能已${enable ? '启用' : '禁用'}`)
|
||||
// 成功后可能需要更新 accountInfo 中的 enableFunctions 状态, useAccount 可能需要提供更新方法或自动刷新
|
||||
// 假设 useAccount() 会自动更新或有刷新机制
|
||||
if (accountInfo.value?.settings?.enableFunctions) {
|
||||
if (enable && !accountInfo.value.settings.enableFunctions.includes(FunctionTypes.QuestionBox)) {
|
||||
accountInfo.value.settings.enableFunctions.push(FunctionTypes.QuestionBox)
|
||||
} else if (!enable) {
|
||||
const index = accountInfo.value.settings.enableFunctions.indexOf(FunctionTypes.QuestionBox)
|
||||
if (index > -1) {
|
||||
accountInfo.value.settings.enableFunctions.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error(`无法${enable ? '启用' : '禁用'}提问箱功能`)
|
||||
}
|
||||
} catch (err) {
|
||||
message.error(`操作失败: ${err}`)
|
||||
console.error('Enable/Disable Function error:', err)
|
||||
// 操作失败时可能需要恢复 Switch 的状态,防止UI与实际状态不一致
|
||||
// 这需要更复杂的逻辑,暂时不加
|
||||
}
|
||||
}
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(() => {
|
||||
// 组件挂载时获取初始数据
|
||||
@@ -293,118 +267,143 @@ watch(() => accountInfo.value?.settings?.questionBox?.saftyLevel, (newLevel) =>
|
||||
<template>
|
||||
<NSpin :show="!accountInfo">
|
||||
<template v-if="accountInfo">
|
||||
<!-- 顶部操作区域 -->
|
||||
<NSpace
|
||||
align="center"
|
||||
wrap
|
||||
item-style="margin-bottom: 8px;"
|
||||
<!-- 页面头部 -->
|
||||
<ManagePageHeader
|
||||
title="提问箱管理"
|
||||
:function-type="FunctionTypes.QuestionBox"
|
||||
:loading="useQB.isLoading"
|
||||
>
|
||||
<!-- 提问箱启用开关 -->
|
||||
<NAlert
|
||||
:type="accountInfo.settings?.enableFunctions?.includes(FunctionTypes.QuestionBox) ? 'success' : 'warning'"
|
||||
style="padding: 5px 10px;"
|
||||
:show-icon="false"
|
||||
>
|
||||
<NFlex align="center">
|
||||
启用提问箱
|
||||
<NSwitch
|
||||
:value="accountInfo.settings?.enableFunctions?.includes(FunctionTypes.QuestionBox)"
|
||||
:disabled="useQB.isLoading"
|
||||
@update:value="setFunctionEnable"
|
||||
/>
|
||||
</NFlex>
|
||||
</NAlert>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<NButton
|
||||
type="primary"
|
||||
:loading="useQB.isLoading"
|
||||
@click="refresh"
|
||||
>
|
||||
刷新
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
secondary
|
||||
@click="shareModalVisiable = true"
|
||||
>
|
||||
分享
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
secondary
|
||||
@click="$router.push({ name: 'user-questionBox', params: { id: accountInfo.name } })"
|
||||
>
|
||||
前往提问页
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
secondary
|
||||
@click="showOBSModal = true"
|
||||
>
|
||||
预览OBS组件
|
||||
</NButton>
|
||||
|
||||
<!-- 功能提示 -->
|
||||
<NAlert
|
||||
type="success"
|
||||
closable
|
||||
style="max-width: 550px;"
|
||||
>
|
||||
2025.3.1 本站已支持内容审查, 可前往提问箱设置页进行开启
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NIcon :component="Info24Filled" />
|
||||
</template>
|
||||
新功能还不稳定, 如果启用后遇到任何问题请向我反馈
|
||||
</NTooltip>
|
||||
</NAlert>
|
||||
</NSpace>
|
||||
|
||||
<!-- 提问页链接 -->
|
||||
<NDivider
|
||||
title-placement="left"
|
||||
style="margin: 16px 0;"
|
||||
>
|
||||
提问页链接
|
||||
</NDivider>
|
||||
<NFlex align="center">
|
||||
<!-- 主链接区域输入框和复制按钮 -->
|
||||
<NInputGroup style="flex-grow: 1; max-width: 500px;">
|
||||
<NInput
|
||||
:value="directShareUrl"
|
||||
readonly
|
||||
/>
|
||||
<template #action>
|
||||
<NButton
|
||||
secondary
|
||||
@click="copyToClipboard(directShareUrl)"
|
||||
circle
|
||||
type="primary"
|
||||
:loading="useQB.isLoading"
|
||||
@click="refresh"
|
||||
>
|
||||
复制
|
||||
<template #icon>
|
||||
<NIcon :component="ArrowSync24Filled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NInputGroup>
|
||||
<!-- 主链接区域标签选择器 -->
|
||||
<NSelect
|
||||
v-model:value="selectedDirectShareTag"
|
||||
placeholder="附加话题 (可选)"
|
||||
filterable
|
||||
clearable
|
||||
:options="useQB.tags.filter(t => t.visiable).map((s) => ({ label: s.name, value: s.name }))"
|
||||
style="min-width: 150px; max-width: 200px;"
|
||||
/>
|
||||
</NFlex>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
secondary
|
||||
circle
|
||||
@click="shareModalVisiable = true"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="Share24Filled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
分享提问箱
|
||||
</NTooltip>
|
||||
<NButton
|
||||
secondary
|
||||
@click="showOBSModal = true"
|
||||
>
|
||||
OBS 组件
|
||||
</NButton>
|
||||
<NButton
|
||||
primary
|
||||
@click="$router.push({ name: 'user-questionBox', params: { id: accountInfo.name } })"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="Link24Filled" />
|
||||
</template>
|
||||
前往提问页
|
||||
</NButton>
|
||||
</template>
|
||||
|
||||
<!-- 审核中提示 -->
|
||||
<template v-if="useQB.reviewing > 0">
|
||||
<NDivider style="margin: 10px 0" />
|
||||
<NAlert
|
||||
type="warning"
|
||||
title="有提问正在审核中"
|
||||
<!-- 提示信息 -->
|
||||
<NCollapse
|
||||
v-if="useQB.reviewing > 0 || !accountInfo.settings?.questionBox?.saftyLevel"
|
||||
style="margin-top: 12px;"
|
||||
>
|
||||
当前有 {{ useQB.reviewing }} 条提问正在等待审核。
|
||||
</NAlert>
|
||||
</template>
|
||||
<NCollapseItem
|
||||
title="通知与提示"
|
||||
name="1"
|
||||
>
|
||||
<NSpace vertical>
|
||||
<NAlert
|
||||
v-if="useQB.reviewing > 0"
|
||||
type="warning"
|
||||
show-icon
|
||||
>
|
||||
当前有 {{ useQB.reviewing }} 条提问正在等待审核。
|
||||
</NAlert>
|
||||
<NAlert
|
||||
type="info"
|
||||
show-icon
|
||||
closable
|
||||
>
|
||||
2025.3.1 本站已支持内容审查, 可前往提问箱设置页进行开启
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NIcon :component="Info24Filled" />
|
||||
</template>
|
||||
新功能还不稳定, 如果启用后遇到任何问题请向我反馈
|
||||
</NTooltip>
|
||||
</NAlert>
|
||||
</NSpace>
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
</ManagePageHeader>
|
||||
|
||||
<NDivider style="margin: 16px 0;" />
|
||||
<!-- 提问页链接卡片 -->
|
||||
<NCard
|
||||
size="small"
|
||||
style="margin-bottom: 16px; border-radius: 8px;"
|
||||
embedded
|
||||
>
|
||||
<NFlex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
wrap
|
||||
>
|
||||
<NFlex
|
||||
align="center"
|
||||
style="flex-grow: 1;"
|
||||
>
|
||||
<NIcon
|
||||
:component="Link24Filled"
|
||||
size="20"
|
||||
depth="3"
|
||||
/>
|
||||
<NText depth="3">
|
||||
我的提问链接:
|
||||
</NText>
|
||||
<NInputGroup style="max-width: 400px;">
|
||||
<NInput
|
||||
:value="directShareUrl"
|
||||
readonly
|
||||
size="small"
|
||||
/>
|
||||
<NButton
|
||||
secondary
|
||||
size="small"
|
||||
@click="copyToClipboard(directShareUrl)"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="Copy24Filled" />
|
||||
</template>
|
||||
复制
|
||||
</NButton>
|
||||
</NInputGroup>
|
||||
</NFlex>
|
||||
|
||||
<NSelect
|
||||
v-model:value="selectedDirectShareTag"
|
||||
placeholder="附加话题参数 (可选)"
|
||||
filterable
|
||||
clearable
|
||||
size="small"
|
||||
:options="useQB.tags.filter(t => t.visiable).map((s) => ({ label: s.name, value: s.name }))"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
|
||||
<!-- 主要内容区域: 标签页 -->
|
||||
<NSpin :show="useQB.isLoading">
|
||||
@@ -412,6 +411,7 @@ watch(() => accountInfo.value?.settings?.questionBox?.saftyLevel, (newLevel) =>
|
||||
v-model:value="selectedTabItem"
|
||||
animated
|
||||
type="line"
|
||||
size="large"
|
||||
>
|
||||
<!-- 我收到的 -->
|
||||
<NTabPane
|
||||
@@ -577,21 +577,24 @@ watch(() => accountInfo.value?.settings?.questionBox?.saftyLevel, (newLevel) =>
|
||||
<NEmpty
|
||||
v-if="useQB.sendQuestions.length === 0"
|
||||
description="暂无发送的提问"
|
||||
style="margin-top: 40px;"
|
||||
/>
|
||||
<NList
|
||||
v-else
|
||||
hoverable
|
||||
clickable
|
||||
style="background-color: transparent;"
|
||||
:show-divider="false"
|
||||
>
|
||||
<NListItem
|
||||
v-for="item in useQB.sendQuestions"
|
||||
:key="item.id"
|
||||
style="padding: 0 0 12px 0;"
|
||||
>
|
||||
<NCard
|
||||
size="small"
|
||||
:bordered="false"
|
||||
style="background-color: var(--n-color);"
|
||||
hoverable
|
||||
embedded
|
||||
content-style="padding: 12px 16px;"
|
||||
style="border-radius: 8px;"
|
||||
>
|
||||
<!-- 发送目标和时间 -->
|
||||
<template #header>
|
||||
@@ -600,13 +603,19 @@ watch(() => accountInfo.value?.settings?.questionBox?.saftyLevel, (newLevel) =>
|
||||
justify="space-between"
|
||||
>
|
||||
<NSpace
|
||||
:size="4"
|
||||
:size="8"
|
||||
align="center"
|
||||
>
|
||||
<NText>发给</NText>
|
||||
<NTag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
type="info"
|
||||
>
|
||||
发给
|
||||
</NTag>
|
||||
<NButton
|
||||
text
|
||||
type="info"
|
||||
type="primary"
|
||||
@click="router.push(`/user/${item.target.id}`)"
|
||||
>
|
||||
{{ item.target.name }}
|
||||
@@ -614,7 +623,7 @@ watch(() => accountInfo.value?.settings?.questionBox?.saftyLevel, (newLevel) =>
|
||||
</NSpace>
|
||||
<NText
|
||||
depth="3"
|
||||
style="font-size: small;"
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
<NTooltip placement="top-end">
|
||||
<template #trigger>
|
||||
@@ -634,64 +643,65 @@ watch(() => accountInfo.value?.settings?.questionBox?.saftyLevel, (newLevel) =>
|
||||
</template>
|
||||
<!-- 问题内容 -->
|
||||
<template v-if="item.questionImages && item.questionImages.length > 0">
|
||||
<NSpace
|
||||
vertical
|
||||
<NFlex
|
||||
size="small"
|
||||
wrap
|
||||
style="margin-bottom: 12px;"
|
||||
>
|
||||
<NImage
|
||||
v-for="(img, index) in item.questionImages"
|
||||
:key="index"
|
||||
:src="img.path"
|
||||
width="100"
|
||||
height="80"
|
||||
object-fit="cover"
|
||||
lazy
|
||||
style="border-radius: 4px; margin-bottom: 5px;"
|
||||
style="border-radius: 4px;"
|
||||
/>
|
||||
</NSpace>
|
||||
<br>
|
||||
</NFlex>
|
||||
</template>
|
||||
<NText>{{ item.question?.message }}</NText>
|
||||
<NText style="font-size: 15px; line-height: 1.6; display: block; margin-bottom: 8px;">
|
||||
{{ item.question?.message }}
|
||||
</NText>
|
||||
|
||||
<!-- 回复内容 -->
|
||||
<template
|
||||
v-if="item.answer"
|
||||
#footer
|
||||
>
|
||||
<NDivider style="margin-top: 8px; margin-bottom: 8px;" />
|
||||
<NCard
|
||||
size="small"
|
||||
:bordered="false"
|
||||
style="background-color: var(--n-action-color);"
|
||||
style="background-color: rgba(128, 128, 128, 0.08); border-radius: 6px;"
|
||||
>
|
||||
<template #header>
|
||||
<NText depth="2">
|
||||
对方的回复
|
||||
</NText>
|
||||
</template>
|
||||
<NText>{{ item.answer.message }}</NText>
|
||||
<template #header-extra>
|
||||
<NText
|
||||
depth="3"
|
||||
style="font-size: small;"
|
||||
>
|
||||
<NTooltip
|
||||
v-if="item.answer.createdAt"
|
||||
placement="top-end"
|
||||
>
|
||||
<template #trigger>
|
||||
<NTime
|
||||
:time="item.answer.createdAt"
|
||||
:to="Date.now()"
|
||||
type="relative"
|
||||
/>
|
||||
</template>
|
||||
<NTime
|
||||
:time="item.answer.createdAt"
|
||||
format="yyyy-MM-dd HH:mm:ss"
|
||||
/>
|
||||
</NTooltip>
|
||||
</NText>
|
||||
<NFlex justify="space-between" align="center">
|
||||
<NText depth="3" style="font-size: 13px;">
|
||||
对方的回复
|
||||
</NText>
|
||||
<NText
|
||||
depth="3"
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
<NTooltip
|
||||
v-if="item.answer.createdAt"
|
||||
placement="top-end"
|
||||
>
|
||||
<template #trigger>
|
||||
<NTime
|
||||
:time="item.answer.createdAt"
|
||||
:to="Date.now()"
|
||||
type="relative"
|
||||
/>
|
||||
</template>
|
||||
<NTime
|
||||
:time="item.answer.createdAt"
|
||||
format="yyyy-MM-dd HH:mm:ss"
|
||||
/>
|
||||
</NTooltip>
|
||||
</NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
<NText style="line-height: 1.5;">{{ item.answer.message }}</NText>
|
||||
</NCard>
|
||||
</template>
|
||||
</NCard>
|
||||
@@ -791,174 +801,213 @@ watch(() => accountInfo.value?.settings?.questionBox?.saftyLevel, (newLevel) =>
|
||||
name="3"
|
||||
display-directive="show:lazy"
|
||||
>
|
||||
<NSpace vertical>
|
||||
<!-- 基础设定 -->
|
||||
<NDivider title-placement="left">
|
||||
基础设定
|
||||
</NDivider>
|
||||
<NCheckbox
|
||||
v-model:checked="accountInfo.settings.questionBox.allowUnregistedUser"
|
||||
:disabled="useQB.isLoading"
|
||||
@update:checked="saveQuestionBoxSettings"
|
||||
>
|
||||
允许未注册/匿名用户进行提问
|
||||
</NCheckbox>
|
||||
<NCheckbox
|
||||
v-model:checked="accountInfo.settings.questionBox.allowImageUpload"
|
||||
:disabled="useQB.isLoading"
|
||||
@update:checked="saveQuestionBoxSettings"
|
||||
>
|
||||
允许上传图片
|
||||
</NCheckbox>
|
||||
<!-- 内容审查 -->
|
||||
<NDivider title-placement="left">
|
||||
内容审查等级
|
||||
<NTag
|
||||
type="success"
|
||||
:bordered="false"
|
||||
size="tiny"
|
||||
style="margin-left: 5px;"
|
||||
>
|
||||
新
|
||||
</NTag>
|
||||
</NDivider>
|
||||
<NSlider
|
||||
v-model:value="tempSaftyLevel"
|
||||
:marks="remarkLevel"
|
||||
step="mark"
|
||||
:max="3"
|
||||
style="max-width: 90%; margin: 10px auto;"
|
||||
:format-tooltip="(v) => remarkLevelString[v]"
|
||||
:disabled="useQB.isLoading"
|
||||
@dragend="() => { if (accountInfo?.settings?.questionBox) { accountInfo.settings.questionBox.saftyLevel = tempSaftyLevel; saveQuestionBoxSettings(); } }"
|
||||
/>
|
||||
|
||||
<!-- 标签/话题管理 -->
|
||||
<NDivider title-placement="left">
|
||||
标签/话题管理
|
||||
<NTooltip placement="right">
|
||||
<template #trigger>
|
||||
<NIcon
|
||||
:component="Info24Filled"
|
||||
style="margin-left: 5px; cursor: help; vertical-align: middle;"
|
||||
/>
|
||||
</template>
|
||||
用于对收到的提问进行分类,或让提问者选择相关话题。
|
||||
</NTooltip>
|
||||
</NDivider>
|
||||
<NFlex align="center">
|
||||
<NInputGroup style="max-width: 400px">
|
||||
<NInputGroupLabel> 新标签 </NInputGroupLabel>
|
||||
<NInput
|
||||
v-model:value="addTagName"
|
||||
placeholder="输入标签名称"
|
||||
maxlength="30"
|
||||
show-count
|
||||
clearable
|
||||
/>
|
||||
<NButton
|
||||
type="primary"
|
||||
:disabled="!addTagName.trim()"
|
||||
@click="useQB.addTag(addTagName); addTagName = ''"
|
||||
<NGrid
|
||||
x-gap="12"
|
||||
y-gap="12"
|
||||
cols="1 800:2"
|
||||
>
|
||||
<!-- 左侧设置项 -->
|
||||
<NGi>
|
||||
<NSpace vertical>
|
||||
<!-- 基础设定 -->
|
||||
<NCard
|
||||
title="基础设定"
|
||||
size="small"
|
||||
segmented
|
||||
>
|
||||
添加
|
||||
</NButton>
|
||||
</NInputGroup>
|
||||
</NFlex>
|
||||
<br>
|
||||
<NEmpty
|
||||
v-if="useQB.tags.length === 0"
|
||||
description="暂无标签"
|
||||
/>
|
||||
<NList
|
||||
v-else
|
||||
bordered
|
||||
hoverable
|
||||
style="max-width: 500px; background-color: var(--n-color);"
|
||||
>
|
||||
<NListItem
|
||||
v-for="item in useQB.tags.sort((a, b) => b.createAt - a.createAt)"
|
||||
:key="item.name"
|
||||
>
|
||||
<NFlex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
<!-- 标签名和状态 -->
|
||||
<NTag
|
||||
:bordered="false"
|
||||
:type="item.visiable ? 'success' : 'default'"
|
||||
:style="!item.visiable ? { textDecoration: 'line-through', color: 'grey' } : {}"
|
||||
>
|
||||
{{ item.name }}
|
||||
</NTag>
|
||||
<!-- 操作按钮 -->
|
||||
<NSpace>
|
||||
<!-- 显示/隐藏 -->
|
||||
<NTooltip placement="top">
|
||||
<template #trigger>
|
||||
<NPopconfirm @positive-click="useQB.updateTagVisiable(item.name, !item.visiable)">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
:type="item.visiable ? 'success' : 'warning'"
|
||||
text
|
||||
style="font-size: 18px;"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="item.visiable ? Eye24Filled : EyeOff24Filled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
确定要{{ item.visiable ? '隐藏' : '显示' }}这个标签吗? (隐藏后提问者无法选择)
|
||||
</NPopconfirm>
|
||||
</template>
|
||||
{{ item.visiable ? '隐藏标签' : '显示标签' }}
|
||||
</NTooltip>
|
||||
<!-- 删除 -->
|
||||
<NTooltip placement="top">
|
||||
<template #trigger>
|
||||
<NPopconfirm @positive-click="useQB.delTag(item.name)">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
type="error"
|
||||
text
|
||||
style="font-size: 18px;"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="Delete24Regular" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
确定要删除这个标签吗? 删除后不可恢复。
|
||||
</NPopconfirm>
|
||||
</template>
|
||||
删除标签
|
||||
</NTooltip>
|
||||
<template #header-extra>
|
||||
<NIcon
|
||||
:component="SettingsOutline"
|
||||
size="18"
|
||||
/>
|
||||
</template>
|
||||
<NSpace vertical>
|
||||
<NCheckbox
|
||||
v-model:checked="accountInfo.settings.questionBox.allowUnregistedUser"
|
||||
:disabled="useQB.isLoading"
|
||||
@update:checked="saveQuestionBoxSettings"
|
||||
>
|
||||
允许未注册/匿名用户进行提问
|
||||
</NCheckbox>
|
||||
<NCheckbox
|
||||
v-model:checked="accountInfo.settings.questionBox.allowImageUpload"
|
||||
:disabled="useQB.isLoading"
|
||||
@update:checked="saveQuestionBoxSettings"
|
||||
>
|
||||
允许上传图片
|
||||
</NCheckbox>
|
||||
</NSpace>
|
||||
</NFlex>
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NCard>
|
||||
|
||||
<!-- 通知设置 -->
|
||||
<NDivider title-placement="left">
|
||||
通知设置
|
||||
</NDivider>
|
||||
<NCheckbox
|
||||
v-model:checked="accountInfo.settings.sendEmail.recieveQA"
|
||||
:disabled="useQB.isLoading"
|
||||
@update:checked="saveNotificationSetting"
|
||||
>
|
||||
收到新提问时发送邮件通知
|
||||
</NCheckbox>
|
||||
<NCheckbox
|
||||
v-model:checked="accountInfo.settings.sendEmail.recieveQAReply"
|
||||
:disabled="useQB.isLoading"
|
||||
@update:checked="saveNotificationSetting"
|
||||
>
|
||||
我发送的提问收到回复时发送邮件通知
|
||||
</NCheckbox>
|
||||
</NSpace>
|
||||
<NDivider />
|
||||
<!-- 内容审查 -->
|
||||
<NCard
|
||||
title="内容审查"
|
||||
size="small"
|
||||
segmented
|
||||
>
|
||||
<template #header-extra>
|
||||
<NTag
|
||||
type="success"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
round
|
||||
>
|
||||
新功能
|
||||
</NTag>
|
||||
</template>
|
||||
<div style="padding: 0 10px 10px 10px;">
|
||||
<div style="margin-bottom: 15px; font-size: 13px; color: gray;">
|
||||
设置过滤强度,自动拦截恶意提问
|
||||
</div>
|
||||
<NSlider
|
||||
v-model:value="tempSaftyLevel"
|
||||
:marks="remarkLevel"
|
||||
step="mark"
|
||||
:max="3"
|
||||
:format-tooltip="(v) => remarkLevelString[v]"
|
||||
:disabled="useQB.isLoading"
|
||||
@dragend="() => { if (accountInfo?.settings?.questionBox) { accountInfo.settings.questionBox.saftyLevel = tempSaftyLevel; saveQuestionBoxSettings(); } }"
|
||||
/>
|
||||
</div>
|
||||
</NCard>
|
||||
|
||||
<!-- 通知设置 -->
|
||||
<NCard
|
||||
title="通知设置"
|
||||
size="small"
|
||||
segmented
|
||||
>
|
||||
<NSpace vertical>
|
||||
<NCheckbox
|
||||
v-model:checked="accountInfo.settings.sendEmail.recieveQA"
|
||||
:disabled="useQB.isLoading"
|
||||
@update:checked="saveNotificationSetting"
|
||||
>
|
||||
收到新提问时发送邮件通知
|
||||
</NCheckbox>
|
||||
<NCheckbox
|
||||
v-model:checked="accountInfo.settings.sendEmail.recieveQAReply"
|
||||
:disabled="useQB.isLoading"
|
||||
@update:checked="saveNotificationSetting"
|
||||
>
|
||||
我发送的提问收到回复时发送邮件通知
|
||||
</NCheckbox>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
</NSpace>
|
||||
</NGi>
|
||||
|
||||
<!-- 右侧设置项: 标签管理 -->
|
||||
<NGi>
|
||||
<NCard
|
||||
title="标签/话题管理"
|
||||
size="small"
|
||||
style="height: 100%;"
|
||||
segmented
|
||||
>
|
||||
<template #header-extra>
|
||||
<NTooltip placement="left">
|
||||
<template #trigger>
|
||||
<NIcon
|
||||
:component="Info24Filled"
|
||||
style="cursor: help;"
|
||||
/>
|
||||
</template>
|
||||
用于对收到的提问进行分类,或让提问者选择相关话题。
|
||||
</NTooltip>
|
||||
</template>
|
||||
|
||||
<NInputGroup style="margin-bottom: 12px;">
|
||||
<NInput
|
||||
v-model:value="addTagName"
|
||||
placeholder="输入新标签名称"
|
||||
maxlength="30"
|
||||
show-count
|
||||
clearable
|
||||
/>
|
||||
<NButton
|
||||
type="primary"
|
||||
:disabled="!addTagName.trim()"
|
||||
@click="useQB.addTag(addTagName); addTagName = ''"
|
||||
>
|
||||
添加
|
||||
</NButton>
|
||||
</NInputGroup>
|
||||
|
||||
<NEmpty
|
||||
v-if="useQB.tags.length === 0"
|
||||
description="暂无标签"
|
||||
/>
|
||||
<NList
|
||||
v-else
|
||||
bordered
|
||||
hoverable
|
||||
style="max-height: 500px; overflow-y: auto;"
|
||||
>
|
||||
<NListItem
|
||||
v-for="item in useQB.tags.sort((a, b) => b.createAt - a.createAt)"
|
||||
:key="item.name"
|
||||
>
|
||||
<NFlex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
<!-- 标签名 -->
|
||||
<NTag
|
||||
:bordered="false"
|
||||
:type="item.visiable ? 'success' : 'default'"
|
||||
:style="!item.visiable ? { textDecoration: 'line-through', color: 'grey' } : {}"
|
||||
>
|
||||
{{ item.name }}
|
||||
</NTag>
|
||||
<!-- 操作按钮 -->
|
||||
<NSpace size="small">
|
||||
<NTooltip placement="top">
|
||||
<template #trigger>
|
||||
<NPopconfirm @positive-click="useQB.updateTagVisiable(item.name, !item.visiable)">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
:type="item.visiable ? 'success' : 'warning'"
|
||||
text
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="item.visiable ? Eye24Filled : EyeOff24Filled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
确定要{{ item.visiable ? '隐藏' : '显示' }}这个标签吗? (隐藏后提问者无法选择)
|
||||
</NPopconfirm>
|
||||
</template>
|
||||
{{ item.visiable ? '隐藏标签' : '显示标签' }}
|
||||
</NTooltip>
|
||||
|
||||
<NTooltip placement="top">
|
||||
<template #trigger>
|
||||
<NPopconfirm @positive-click="useQB.delTag(item.name)">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
type="error"
|
||||
text
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="Delete24Regular" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
确定要删除这个标签吗? 删除后不可恢复。
|
||||
</NPopconfirm>
|
||||
</template>
|
||||
删除标签
|
||||
</NTooltip>
|
||||
</NSpace>
|
||||
</NFlex>
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NCard>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</NSpin>
|
||||
@@ -1237,10 +1286,6 @@ watch(() => accountInfo.value?.settings?.questionBox?.saftyLevel, (newLevel) =>
|
||||
padding-right: 20px; /* 与二维码的间距 */
|
||||
}
|
||||
|
||||
.share-card-text {
|
||||
/* 包含标题和名字 */
|
||||
}
|
||||
|
||||
.share-card-title {
|
||||
font-size: 28px; /* 调整大小 */
|
||||
font-weight: bold; /* 加粗 */
|
||||
|
||||
@@ -157,12 +157,13 @@ onUnmounted(() => {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
border: 2 solid rgb(255, 255, 255);
|
||||
border: 2px solid rgb(255, 255, 255); /* 修正 border 语法 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden; /* 防止圆角溢出 */
|
||||
}
|
||||
|
||||
.question-display-content {
|
||||
@@ -170,62 +171,64 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-evenly;
|
||||
/* justify-content: space-evenly; 移除这个,让内容自然排列,长内容更友好 */
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
overflow-y: auto; /* 明确只在Y轴滚动 */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.question-display-user-name {
|
||||
text-align: center;
|
||||
margin: 5px;
|
||||
height: 32px;
|
||||
margin: 8px 5px;
|
||||
min-height: 32px; /* 使用 min-height 避免文字被切 */
|
||||
line-height: 1.4;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
}
|
||||
|
||||
.question-display-text {
|
||||
min-height: 50px;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6; /* 增加行高,提高可读性 */
|
||||
word-break: break-word; /* 防止长单词溢出 */
|
||||
}
|
||||
|
||||
.question-display-images {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.question-display-image {
|
||||
max-width: 40%;
|
||||
max-height: 150px;
|
||||
max-width: 100%;
|
||||
max-height: 200px;
|
||||
border-radius: 8px;
|
||||
object-fit: contain; /* 确保图片完整显示 */
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* 轻微阴影 */
|
||||
}
|
||||
|
||||
/* 滚动条美化 */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
|
||||
height: 10px;
|
||||
width: 6px; /* 变细 */
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: hsla(0, 0%, 51%, 0.377);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
-webkit-box-shadow: none;
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
-webkit-box-shadow: none;
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
border-radius: 10px;
|
||||
|
||||
background: #cccccc4b;
|
||||
|
||||
-webkit-box-shadow: none;
|
||||
|
||||
-webkit-box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ import {
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, h, onMounted, ref, watch } from 'vue'
|
||||
import { DisableFunction, EnableFunction, useAccount } from '@/api/account'
|
||||
import { useAccount } from '@/api/account'
|
||||
import { FunctionTypes } from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import ManagePageHeader from '@/components/manage/ManagePageHeader.vue'
|
||||
import ScheduleList from '@/components/ScheduleList.vue'
|
||||
import { CURRENT_HOST, SCHEDULE_API_URL } from '@/data/constants'
|
||||
import { copyToClipboard } from '@/Utils'
|
||||
@@ -584,19 +585,6 @@ function renderOption({ node, option }: { node: VNode, option: SelectOption }) {
|
||||
node,
|
||||
])
|
||||
}
|
||||
async function setFunctionEnable(enable: boolean) {
|
||||
let success = false
|
||||
if (enable) {
|
||||
success = await EnableFunction(FunctionTypes.Schedule)
|
||||
} else {
|
||||
success = await DisableFunction(FunctionTypes.Schedule)
|
||||
}
|
||||
if (success) {
|
||||
message.success(`已${enable ? '启用' : '禁用'}`)
|
||||
} else {
|
||||
message.error(`无法${enable ? '启用' : '禁用'}`)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
get()
|
||||
@@ -604,77 +592,72 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace align="center">
|
||||
<NAlert
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Schedule) ? 'success' : 'warning'"
|
||||
style="max-width: 200px"
|
||||
>
|
||||
启用日程表
|
||||
<NDivider vertical />
|
||||
<NSwitch
|
||||
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.Schedule)"
|
||||
@update:value="setFunctionEnable"
|
||||
/>
|
||||
</NAlert>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="showAddModal = true"
|
||||
>
|
||||
添加周程
|
||||
</NButton>
|
||||
<NButton @click="$router.push({ name: 'manage-index', query: { tab: 'setting', setting: 'template', template: 'schedule' } })">
|
||||
修改模板
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<NDivider
|
||||
style="margin: 16px 0 16px 0"
|
||||
title-placement="left"
|
||||
<ManagePageHeader
|
||||
title="日程表管理"
|
||||
:function-type="FunctionTypes.Schedule"
|
||||
>
|
||||
日程表展示页链接
|
||||
</NDivider>
|
||||
<NFlex align="center">
|
||||
<NInputGroup style="max-width: 400px;">
|
||||
<NInput
|
||||
:value="`${CURRENT_HOST}@${accountInfo.name}/schedule`"
|
||||
readonly
|
||||
/>
|
||||
<template #action>
|
||||
<NButton
|
||||
secondary
|
||||
@click="copyToClipboard(`${CURRENT_HOST}@${accountInfo.name}/schedule`)"
|
||||
type="primary"
|
||||
@click="showAddModal = true"
|
||||
>
|
||||
复制
|
||||
添加周程
|
||||
</NButton>
|
||||
</NInputGroup>
|
||||
</NFlex>
|
||||
<NDivider
|
||||
style="margin: 16px 0 16px 0"
|
||||
title-placement="left"
|
||||
>
|
||||
订阅链接
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NIcon>
|
||||
<TagQuestionMark16Filled />
|
||||
</NIcon>
|
||||
</template>
|
||||
通过订阅链接可以订阅日程表到日历软件中
|
||||
</NTooltip>
|
||||
</NDivider>
|
||||
<NFlex align="center">
|
||||
<NInputGroup style="max-width: 400px;">
|
||||
<NInput
|
||||
:value="`${SCHEDULE_API_URL}${accountInfo.id}.ics`"
|
||||
readonly
|
||||
/>
|
||||
<NButton
|
||||
secondary
|
||||
@click="copyToClipboard(`${SCHEDULE_API_URL}${accountInfo.id}.ics`)"
|
||||
>
|
||||
复制
|
||||
<NButton @click="$router.push({ name: 'manage-index', query: { tab: 'setting', setting: 'template', template: 'schedule' } })">
|
||||
修改模板
|
||||
</NButton>
|
||||
</NInputGroup>
|
||||
</NFlex>
|
||||
<NDivider />
|
||||
</template>
|
||||
|
||||
<NDivider
|
||||
style="margin: 16px 0 16px 0"
|
||||
title-placement="left"
|
||||
>
|
||||
日程表展示页链接
|
||||
</NDivider>
|
||||
<NFlex align="center">
|
||||
<NInputGroup style="max-width: 400px;">
|
||||
<NInput
|
||||
:value="`${CURRENT_HOST}@${accountInfo.name}/schedule`"
|
||||
readonly
|
||||
/>
|
||||
<NButton
|
||||
secondary
|
||||
@click="copyToClipboard(`${CURRENT_HOST}@${accountInfo.name}/schedule`)"
|
||||
>
|
||||
复制
|
||||
</NButton>
|
||||
</NInputGroup>
|
||||
</NFlex>
|
||||
<NDivider
|
||||
style="margin: 16px 0 16px 0"
|
||||
title-placement="left"
|
||||
>
|
||||
订阅链接
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NIcon>
|
||||
<TagQuestionMark16Filled />
|
||||
</NIcon>
|
||||
</template>
|
||||
通过订阅链接可以订阅日程表到日历软件中
|
||||
</NTooltip>
|
||||
</NDivider>
|
||||
<NFlex align="center">
|
||||
<NInputGroup style="max-width: 400px;">
|
||||
<NInput
|
||||
:value="`${SCHEDULE_API_URL}${accountInfo.id}.ics`"
|
||||
readonly
|
||||
/>
|
||||
<NButton
|
||||
secondary
|
||||
@click="copyToClipboard(`${SCHEDULE_API_URL}${accountInfo.id}.ics`)"
|
||||
>
|
||||
复制
|
||||
</NButton>
|
||||
</NInputGroup>
|
||||
</NFlex>
|
||||
</ManagePageHeader>
|
||||
|
||||
<NModal
|
||||
v-model:show="showAddModal"
|
||||
style="width: 600px; max-width: 90vw"
|
||||
|
||||
@@ -49,9 +49,10 @@ import {
|
||||
} from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import * as XLSX from 'xlsx'
|
||||
import { DisableFunction, EnableFunction, useAccount } from '@/api/account'
|
||||
import { useAccount } from '@/api/account'
|
||||
import { FunctionTypes, SongFrom } from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import ManagePageHeader from '@/components/manage/ManagePageHeader.vue'
|
||||
import SongList from '@/components/SongList.vue'
|
||||
import { CURRENT_HOST, FETCH_API, SONG_API_URL } from '@/data/constants'
|
||||
import { copyToClipboard, objectsToCSV } from '@/Utils'
|
||||
@@ -945,25 +946,6 @@ function beforeUpload(data: { file: UploadFileInfo, fileList: UploadFileInfo[] }
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置歌单功能启用状态
|
||||
*/
|
||||
async function setFunctionEnable(enable: boolean) {
|
||||
let success = false
|
||||
|
||||
if (enable) {
|
||||
success = await EnableFunction(FunctionTypes.SongList)
|
||||
} else {
|
||||
success = await DisableFunction(FunctionTypes.SongList)
|
||||
}
|
||||
|
||||
if (success) {
|
||||
message.success(`已${enable ? '启用' : '禁用'}`)
|
||||
} else {
|
||||
message.error(`无法${enable ? '启用' : '禁用'}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置添加歌曲表单
|
||||
*/
|
||||
@@ -1011,85 +993,77 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace align="center">
|
||||
<!-- 歌单功能启用状态 -->
|
||||
<NAlert
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.SongList) ? 'success' : 'warning'"
|
||||
style="max-width: 200px"
|
||||
>
|
||||
启用歌单
|
||||
<NDivider vertical />
|
||||
<NSwitch
|
||||
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.SongList)"
|
||||
@update:value="setFunctionEnable"
|
||||
/>
|
||||
</NAlert>
|
||||
|
||||
<!-- 功能按钮区 -->
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="showModal = true"
|
||||
>
|
||||
添加歌曲
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="$router.push({ name: 'manage-index', query: { tab: 'setting', setting: 'template', template: 'songlist' } })"
|
||||
>
|
||||
修改展示模板
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
secondary
|
||||
@click="exportData"
|
||||
>
|
||||
导出为 CSV
|
||||
</NButton>
|
||||
<NButton
|
||||
secondary
|
||||
@click="$router.push({ name: 'manage-liveRequest' })"
|
||||
>
|
||||
前往点播管理页
|
||||
</NButton>
|
||||
<NButton
|
||||
secondary
|
||||
@click="$router.push({ name: 'user-songList', params: { id: accountInfo?.name } })"
|
||||
>
|
||||
前往歌单展示页
|
||||
</NButton>
|
||||
<NButton
|
||||
:loading="isLoading"
|
||||
@click="() => {
|
||||
getSongs()
|
||||
message.success('完成')
|
||||
}"
|
||||
>
|
||||
刷新
|
||||
</NButton>
|
||||
</NSpace>
|
||||
|
||||
<!-- 歌单展示页链接 -->
|
||||
<NDivider
|
||||
style="margin: 16px 0 16px 0"
|
||||
title-placement="left"
|
||||
<ManagePageHeader
|
||||
title="歌单管理"
|
||||
:function-type="FunctionTypes.SongList"
|
||||
>
|
||||
歌单展示页链接
|
||||
</NDivider>
|
||||
<NFlex align="center">
|
||||
<NInputGroup style="max-width: 400px;">
|
||||
<NInput
|
||||
:value="`${CURRENT_HOST}@${accountInfo.name}/song-list`"
|
||||
readonly
|
||||
/>
|
||||
<template #action>
|
||||
<!-- 功能按钮区 -->
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="showModal = true"
|
||||
>
|
||||
添加歌曲
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="$router.push({ name: 'manage-index', query: { tab: 'setting', setting: 'template', template: 'songlist' } })"
|
||||
>
|
||||
修改展示模板
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
secondary
|
||||
@click="exportData"
|
||||
>
|
||||
导出为 CSV
|
||||
</NButton>
|
||||
<NButton
|
||||
secondary
|
||||
@click="copyToClipboard(`${CURRENT_HOST}@${accountInfo.name}/song-list`)"
|
||||
@click="$router.push({ name: 'manage-liveRequest' })"
|
||||
>
|
||||
复制
|
||||
前往点播管理页
|
||||
</NButton>
|
||||
</NInputGroup>
|
||||
</NFlex>
|
||||
<NDivider style="margin: 16px 0 16px 0" />
|
||||
<NButton
|
||||
secondary
|
||||
@click="$router.push({ name: 'user-songList', params: { id: accountInfo?.name } })"
|
||||
>
|
||||
前往歌单展示页
|
||||
</NButton>
|
||||
<NButton
|
||||
:loading="isLoading"
|
||||
@click="() => {
|
||||
getSongs()
|
||||
message.success('完成')
|
||||
}"
|
||||
>
|
||||
刷新
|
||||
</NButton>
|
||||
</template>
|
||||
|
||||
<!-- 歌单展示页链接 -->
|
||||
<NDivider
|
||||
style="margin: 16px 0 16px 0"
|
||||
title-placement="left"
|
||||
>
|
||||
歌单展示页链接
|
||||
</NDivider>
|
||||
<NFlex align="center">
|
||||
<NInputGroup style="max-width: 400px;">
|
||||
<NInput
|
||||
:value="`${CURRENT_HOST}@${accountInfo.name}/song-list`"
|
||||
readonly
|
||||
/>
|
||||
<NButton
|
||||
secondary
|
||||
@click="copyToClipboard(`${CURRENT_HOST}@${accountInfo.name}/song-list`)"
|
||||
>
|
||||
复制
|
||||
</NButton>
|
||||
</NInputGroup>
|
||||
</NFlex>
|
||||
<NDivider style="margin: 16px 0 16px 0" />
|
||||
</ManagePageHeader>
|
||||
|
||||
<!-- 添加歌曲模态框 -->
|
||||
<NModal
|
||||
|
||||
@@ -10,15 +10,22 @@ import type {
|
||||
VideoCollectVideo,
|
||||
VideoInfo,
|
||||
} from '@/api/api-models'
|
||||
import { Clock24Filled, Person24Filled } from '@vicons/fluent'
|
||||
import {
|
||||
ArrowLeft24Regular,
|
||||
Delete24Regular,
|
||||
Edit24Regular,
|
||||
MoreVertical24Regular,
|
||||
Share24Regular,
|
||||
TableDismiss24Regular,
|
||||
} from '@vicons/fluent'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { List } from 'linqts'
|
||||
import {
|
||||
NBadge,
|
||||
NButton,
|
||||
NCard,
|
||||
NDatePicker,
|
||||
NDivider,
|
||||
NEllipsis,
|
||||
NDropdown,
|
||||
NEmpty,
|
||||
NForm,
|
||||
NFormItem,
|
||||
@@ -29,21 +36,22 @@ import {
|
||||
NInputNumber,
|
||||
NModal,
|
||||
NPopconfirm,
|
||||
NScrollbar,
|
||||
NSpace,
|
||||
NSpin,
|
||||
NTabPane,
|
||||
NTabs,
|
||||
NText,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import Qrcode from 'qrcode.vue'
|
||||
import { computed, h, onActivated, ref } from 'vue'
|
||||
import { computed, onActivated, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
VideoStatus,
|
||||
} from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import VideoCollectInfoCard from '@/components/VideoCollectInfoCard.vue'
|
||||
import VideoItemCard from '@/components/VideoItemCard.vue'
|
||||
import { CURRENT_HOST, VIDEO_COLLECT_API_URL } from '@/data/constants'
|
||||
import router from '@/router'
|
||||
import { downloadImage } from '@/Utils'
|
||||
@@ -115,6 +123,55 @@ const acceptVideos = computed(() => {
|
||||
return videoDetail.value?.videos?.filter(v => v.info.status == VideoStatus.Accepted) ?? []
|
||||
})
|
||||
|
||||
// 移动端下拉菜单选项
|
||||
const mobileMenuOptions = computed(() => [
|
||||
{
|
||||
label: '分享',
|
||||
key: 'share',
|
||||
icon: () => h(NIcon, null, { default: () => h(Share24Regular) }),
|
||||
},
|
||||
{
|
||||
label: '更新信息',
|
||||
key: 'edit',
|
||||
icon: () => h(NIcon, null, { default: () => h(Edit24Regular) }),
|
||||
},
|
||||
{
|
||||
label: videoDetail.value.table.isFinish ? '开启表' : '关闭表',
|
||||
key: 'toggle-status',
|
||||
icon: () => h(NIcon, null, { default: () => h(TableDismiss24Regular) }),
|
||||
},
|
||||
{
|
||||
label: '结果页面',
|
||||
key: 'result',
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
key: 'delete',
|
||||
icon: () => h(NIcon, { color: '#d03050' }, { default: () => h(Delete24Regular) }),
|
||||
},
|
||||
])
|
||||
|
||||
function handleMobileMenuSelect(key: string) {
|
||||
switch (key) {
|
||||
case 'share':
|
||||
shareModalVisiable.value = true
|
||||
break
|
||||
case 'edit':
|
||||
editModalVisiable.value = true
|
||||
break
|
||||
case 'toggle-status':
|
||||
closeTable()
|
||||
break
|
||||
case 'result':
|
||||
router.push({ name: 'video-collect-list', params: { id: videoDetail.value.table.id } })
|
||||
break
|
||||
case 'delete':
|
||||
deleteTable() // 这里最好加个确认,但在下拉菜单里直接触发确认比较麻烦,暂时直接调用,原逻辑是有Popconfirm的
|
||||
// 由于移动端下拉菜单难以直接嵌入Popconfirm,建议改为点击后弹窗确认
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
try {
|
||||
const data = await QueryGetAPI<VideoCollectDetail>(`${VIDEO_COLLECT_API_URL}get`, { id: route.params.id })
|
||||
@@ -133,133 +190,7 @@ async function getData() {
|
||||
}
|
||||
return {} as VideoCollectDetail
|
||||
}
|
||||
function gridRender(type: 'padding' | 'reject' | 'accept') {
|
||||
let footer: (arg0: VideoInfo) => VNode
|
||||
let videos: { info: VideoInfo, video: VideoCollectVideo }[]
|
||||
switch (type) {
|
||||
case 'padding':
|
||||
footer = paddingButtonGroup
|
||||
videos = paddingVideos.value
|
||||
break
|
||||
case 'reject':
|
||||
footer = rejectButtonGroup
|
||||
videos = rejectVideos.value
|
||||
break
|
||||
case 'accept':
|
||||
footer = acceptButtonGroup
|
||||
videos = acceptVideos.value
|
||||
break
|
||||
}
|
||||
return videos.length == 0
|
||||
? h(NEmpty)
|
||||
: h(NGrid, { cols: '1 500:2 700:3 900:4 1200:5 ', xGap: '12', yGap: '12', responsive: 'self' }, () =>
|
||||
videos?.map(v =>
|
||||
h(NGridItem, () =>
|
||||
h(
|
||||
NCard,
|
||||
{ style: 'height: 330px;', embedded: true, size: 'small' },
|
||||
{
|
||||
cover: () =>
|
||||
h('div', { style: 'position: relative;height: 150px;' }, [
|
||||
h('img', {
|
||||
src: v.video.cover.replace('http://', 'https://'),
|
||||
referrerpolicy: 'no-referrer',
|
||||
style: 'max-height: 100%; object-fit: contain;cursor: pointer',
|
||||
onClick: () => window.open(`https://www.bilibili.com/video/${v.info.bvid}`, '_blank'),
|
||||
}),
|
||||
h(
|
||||
NSpace,
|
||||
{
|
||||
style: { position: 'relative', bottom: '20px', background: '#00000073' },
|
||||
justify: 'space-around',
|
||||
},
|
||||
() => [
|
||||
h('span', [
|
||||
h(NIcon, { component: Clock24Filled, color: 'lightgrey' }),
|
||||
h(NText, { style: 'color: lightgrey;size:small;' }, () => formatSeconds(v.video.length)),
|
||||
]),
|
||||
h('span', [
|
||||
h(NIcon, { component: Person24Filled, color: 'lightgrey' }),
|
||||
h(NText, { style: 'color: lightgrey;size:small;' }, () => v.video.ownerName),
|
||||
]),
|
||||
],
|
||||
),
|
||||
]),
|
||||
header: () =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
style: 'width: 100%;',
|
||||
text: true,
|
||||
onClick: () => window.open(`https://www.bilibili.com/video/${v.info.bvid}`, '_blank'),
|
||||
},
|
||||
() =>
|
||||
h(
|
||||
NEllipsis,
|
||||
{ style: 'max-width: 100%;' },
|
||||
{
|
||||
default: () => v.video.title,
|
||||
tooltip: () => h('div', { style: 'max-width: 300px' }, v.video.title),
|
||||
},
|
||||
),
|
||||
),
|
||||
default: () =>
|
||||
h(NScrollbar, { style: 'height: 65px;' }, () =>
|
||||
h(NCard, { contentStyle: 'padding: 5px;' }, () =>
|
||||
v.info.senders.map(s => [
|
||||
h('div', { style: 'font-size: 12px;' }, [
|
||||
h('div', `推荐人: ${s.sender ?? '未填写'} [${s.senderId ?? '未填写'}]`),
|
||||
h('div', `推荐理由: ${s.description ?? '未填写'}`),
|
||||
]),
|
||||
h(NSpace, { style: 'margin: 0;' }),
|
||||
]))),
|
||||
footer: () => footer(v.info),
|
||||
},
|
||||
)),
|
||||
))
|
||||
}
|
||||
function paddingButtonGroup(v: VideoInfo) {
|
||||
return h(NSpace, { size: 'small', justify: 'space-around' }, () => [
|
||||
h(
|
||||
NButton,
|
||||
{ type: 'success', loading: isLoading.value, onClick: () => setStatus(VideoStatus.Accepted, v) },
|
||||
() => '通过',
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{ type: 'error', loading: isLoading.value, onClick: () => setStatus(VideoStatus.Rejected, v) },
|
||||
() => '拒绝',
|
||||
),
|
||||
])
|
||||
}
|
||||
function acceptButtonGroup(v: VideoInfo) {
|
||||
return h(NSpace, { size: 'small', justify: 'space-around' }, () => [
|
||||
h(
|
||||
NButton,
|
||||
{ type: 'info', loading: isLoading.value, onClick: () => setStatus(VideoStatus.Pending, v) },
|
||||
() => '重设为未审核',
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{ type: 'error', loading: isLoading.value, onClick: () => setStatus(VideoStatus.Rejected, v) },
|
||||
() => '拒绝',
|
||||
),
|
||||
])
|
||||
}
|
||||
function rejectButtonGroup(v: VideoInfo) {
|
||||
return h(NSpace, { size: 'small', justify: 'space-around' }, () => [
|
||||
h(
|
||||
NButton,
|
||||
{ type: 'success', loading: isLoading.value, onClick: () => setStatus(VideoStatus.Accepted, v) },
|
||||
() => '通过',
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{ type: 'info', loading: isLoading.value, onClick: () => setStatus(VideoStatus.Pending, v) },
|
||||
() => '重设为未审核',
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
function setStatus(status: VideoStatus, video: VideoInfo) {
|
||||
isLoading.value = true
|
||||
QueryGetAPI(`${VIDEO_COLLECT_API_URL}set-status`, {
|
||||
@@ -282,6 +213,7 @@ function setStatus(status: VideoStatus, video: VideoInfo) {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function formatSeconds(seconds: number): string {
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const remainingSeconds = seconds % 60
|
||||
@@ -291,9 +223,11 @@ function formatSeconds(seconds: number): string {
|
||||
|
||||
return `${formattedMinutes}:${formattedSeconds}`
|
||||
}
|
||||
|
||||
function dateDisabled(ts: number) {
|
||||
return ts < Date.now() + 1000 * 60 * 60
|
||||
}
|
||||
|
||||
function updateTable() {
|
||||
isLoading.value = true
|
||||
updateModel.value.id = videoDetail.value.table.id
|
||||
@@ -301,6 +235,7 @@ function updateTable() {
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
message.success('更新成功')
|
||||
editModalVisiable.value = false
|
||||
videoDetail.value.table = data.data
|
||||
} else {
|
||||
message.error(`更新失败: ${data.message}`)
|
||||
@@ -313,6 +248,7 @@ function updateTable() {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function deleteTable() {
|
||||
isLoading.value = true
|
||||
QueryGetAPI(`${VIDEO_COLLECT_API_URL}del`, {
|
||||
@@ -335,6 +271,7 @@ function deleteTable() {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function closeTable() {
|
||||
isLoading.value = true
|
||||
QueryGetAPI(`${VIDEO_COLLECT_API_URL}finish`, {
|
||||
@@ -356,6 +293,7 @@ function closeTable() {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function saveQRCode() {
|
||||
downloadImage(
|
||||
`https://api.qrserver.com/v1/create-qr-code/?data=${`https://vtsuru.live/video-collect/${videoDetail.value.table.shortId}`}`,
|
||||
@@ -371,256 +309,416 @@ onActivated(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace>
|
||||
<NButton
|
||||
text
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
<NText depth="3">
|
||||
{{ '< 返回' }}
|
||||
</NText>
|
||||
</NButton>
|
||||
<template v-if="width <= 1000">
|
||||
<NButton
|
||||
type="success"
|
||||
size="small"
|
||||
@click="shareModalVisiable = true"
|
||||
>
|
||||
分享
|
||||
</NButton>
|
||||
<NButton
|
||||
type="info"
|
||||
size="small"
|
||||
@click="editModalVisiable = true"
|
||||
>
|
||||
更新
|
||||
</NButton>
|
||||
<NButton
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="closeTable"
|
||||
>
|
||||
{{ videoDetail.table.isFinish ? '开启表' : '关闭表' }}
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
@click="$router.push({ name: 'video-collect-list', params: { id: videoDetail.table.id } })"
|
||||
>
|
||||
结果页面
|
||||
</NButton>
|
||||
<NPopconfirm :on-positive-click="deleteTable">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
type="error"
|
||||
size="small"
|
||||
>
|
||||
删除
|
||||
</NButton>
|
||||
</template>
|
||||
确定删除表? 此操作无法撤销
|
||||
</NPopconfirm>
|
||||
</template>
|
||||
</NSpace>
|
||||
<VideoCollectInfoCard
|
||||
:item="videoDetail.table"
|
||||
style="width: 100%; max-width: 90vw"
|
||||
from="owner"
|
||||
>
|
||||
<template
|
||||
v-if="width > 1000"
|
||||
#header-extra
|
||||
>
|
||||
<NSpace>
|
||||
<div class="detail-container">
|
||||
<!-- Header Section -->
|
||||
<div class="header-section">
|
||||
<div class="header-left">
|
||||
<NButton
|
||||
type="success"
|
||||
size="small"
|
||||
@click="shareModalVisiable = true"
|
||||
text
|
||||
style="font-size: 16px"
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
分享
|
||||
</NButton>
|
||||
<NButton
|
||||
type="info"
|
||||
size="small"
|
||||
@click="editModalVisiable = true"
|
||||
>
|
||||
更新
|
||||
</NButton>
|
||||
<NButton
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="closeTable"
|
||||
>
|
||||
{{ videoDetail.table.isFinish ? '开启表' : '关闭表' }}
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
@click="$router.push({ name: 'video-collect-list', params: { id: videoDetail.table.id } })"
|
||||
>
|
||||
结果表
|
||||
</NButton>
|
||||
<NPopconfirm :on-positive-click="deleteTable">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
type="error"
|
||||
size="small"
|
||||
>
|
||||
删除
|
||||
</NButton>
|
||||
<template #icon>
|
||||
<NIcon><ArrowLeft24Regular /></NIcon>
|
||||
</template>
|
||||
确定删除表? 此操作无法撤销
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
</template>
|
||||
</VideoCollectInfoCard>
|
||||
<NDivider> 已通过时长: {{ formatSeconds(new List(acceptVideos).Sum((v) => v?.video.length ?? 0)) }} </NDivider>
|
||||
<NEmpty
|
||||
v-if="videoDetail?.videos?.length == 0"
|
||||
description="暂无视频"
|
||||
/>
|
||||
<template v-else>
|
||||
<NTabs
|
||||
animated
|
||||
type="segment"
|
||||
>
|
||||
<NTabPane name="padding">
|
||||
<template #tab>
|
||||
未审核
|
||||
<NDivider
|
||||
vertical
|
||||
style="margin: 0 3px 0 3px"
|
||||
/>
|
||||
<NText depth="3">
|
||||
{{ paddingVideos.length }}
|
||||
</NText>
|
||||
</template>
|
||||
<component :is="gridRender('padding')" />
|
||||
</NTabPane>
|
||||
<NTabPane name="accept">
|
||||
<template #tab>
|
||||
<NText style="color: #5bb85f">
|
||||
通过
|
||||
</NText>
|
||||
<NDivider
|
||||
vertical
|
||||
style="margin: 0 5px 0 5px"
|
||||
/>
|
||||
<NText depth="3">
|
||||
{{ acceptVideos.length }}
|
||||
</NText>
|
||||
</template>
|
||||
<component :is="gridRender('accept')" />
|
||||
</NTabPane>
|
||||
<NTabPane name="reject">
|
||||
<template #tab>
|
||||
<NText style="color: #a85f5f">
|
||||
拒绝
|
||||
</NText>
|
||||
<NDivider
|
||||
vertical
|
||||
style="margin: 0 3px 0 3px"
|
||||
/>
|
||||
<NText depth="3">
|
||||
{{ rejectVideos.length }}
|
||||
</NText>
|
||||
</template>
|
||||
<component :is="gridRender('reject')" />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</template>
|
||||
<NModal
|
||||
v-model:show="shareModalVisiable"
|
||||
title="分享"
|
||||
preset="card"
|
||||
style="width: 600px; max-width: 90vw"
|
||||
>
|
||||
<Qrcode
|
||||
:value="`${CURRENT_HOST}video-collect/${videoDetail.table.shortId}`"
|
||||
level="Q"
|
||||
:size="100"
|
||||
background="#fff"
|
||||
:margin="1"
|
||||
/>
|
||||
<NInput :value="`${CURRENT_HOST}video-collect/${videoDetail.table.shortId}`" />
|
||||
<NDivider />
|
||||
<NSpace justify="center">
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="saveQRCode"
|
||||
>
|
||||
保存二维码
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NModal>
|
||||
<NModal
|
||||
v-model:show="editModalVisiable"
|
||||
title="更新信息"
|
||||
preset="card"
|
||||
style="width: 600px; max-width: 90vw"
|
||||
>
|
||||
<NForm
|
||||
ref="formRef"
|
||||
:model="updateModel"
|
||||
:rules="createRules"
|
||||
>
|
||||
<NFormItem
|
||||
label="标题"
|
||||
path="name"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="updateModel.name"
|
||||
placeholder="征集表的标题"
|
||||
maxlength="30"
|
||||
show-count
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
label="描述"
|
||||
path="description"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="updateModel.description"
|
||||
placeholder="可以是备注之类的"
|
||||
maxlength="300"
|
||||
show-count
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
label="视频数量"
|
||||
path="maxVideoCount"
|
||||
>
|
||||
<NInputNumber
|
||||
v-model:value="updateModel.maxVideoCount"
|
||||
placeholder="最大数量"
|
||||
type="number"
|
||||
style="max-width: 150px"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
label="结束时间"
|
||||
path="endAt"
|
||||
>
|
||||
<NDatePicker
|
||||
v-model:value="updateModel.endAt"
|
||||
type="datetime"
|
||||
placeholder="结束征集的时间"
|
||||
:is-date-disabled="dateDisabled"
|
||||
/>
|
||||
<NDivider vertical />
|
||||
<NText depth="3">
|
||||
最低为一小时
|
||||
返回列表
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<!-- Desktop Actions -->
|
||||
<NSpace v-if="width > 800">
|
||||
<NButton
|
||||
secondary
|
||||
strong
|
||||
@click="shareModalVisiable = true"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon><Share24Regular /></NIcon>
|
||||
</template>
|
||||
分享
|
||||
</NButton>
|
||||
<NButton
|
||||
secondary
|
||||
strong
|
||||
@click="editModalVisiable = true"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon><Edit24Regular /></NIcon>
|
||||
</template>
|
||||
更新信息
|
||||
</NButton>
|
||||
<NButton
|
||||
secondary
|
||||
strong
|
||||
:type="videoDetail.table.isFinish ? 'success' : 'warning'"
|
||||
@click="closeTable"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon><TableDismiss24Regular /></NIcon>
|
||||
</template>
|
||||
{{ videoDetail.table.isFinish ? '开启征集' : '结束征集' }}
|
||||
</NButton>
|
||||
<NButton
|
||||
secondary
|
||||
strong
|
||||
type="info"
|
||||
@click="$router.push({ name: 'video-collect-list', params: { id: videoDetail.table.id } })"
|
||||
>
|
||||
查看结果
|
||||
</NButton>
|
||||
<NPopconfirm @positive-click="deleteTable">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
secondary
|
||||
strong
|
||||
type="error"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon><Delete24Regular /></NIcon>
|
||||
</template>
|
||||
删除
|
||||
</NButton>
|
||||
</template>
|
||||
确定删除表? 此操作无法撤销
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
|
||||
<!-- Mobile Actions -->
|
||||
<NDropdown
|
||||
v-else
|
||||
trigger="click"
|
||||
:options="mobileMenuOptions"
|
||||
@select="handleMobileMenuSelect"
|
||||
>
|
||||
<NButton
|
||||
secondary
|
||||
strong
|
||||
circle
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon><MoreVertical24Regular /></NIcon>
|
||||
</template>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Card -->
|
||||
<div class="info-card-wrapper">
|
||||
<VideoCollectInfoCard
|
||||
:item="videoDetail.table"
|
||||
style="width: 100%"
|
||||
from="owner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-bar">
|
||||
<NText depth="3">
|
||||
已通过视频总时长:
|
||||
<NText
|
||||
strong
|
||||
style="color: var(--n-text-color)"
|
||||
>
|
||||
{{ formatSeconds(new List(acceptVideos).Sum((v) => v?.video.length ?? 0)) }}
|
||||
</NText>
|
||||
</NFormItem>
|
||||
<NFormItem>
|
||||
<NSpace>
|
||||
</NText>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="content-area">
|
||||
<NEmpty
|
||||
v-if="videoDetail?.videos?.length == 0"
|
||||
description="暂无视频提交"
|
||||
style="margin-top: 48px"
|
||||
/>
|
||||
<NTabs
|
||||
v-else
|
||||
animated
|
||||
type="line"
|
||||
justify-content="space-evenly"
|
||||
class="custom-tabs"
|
||||
>
|
||||
<NTabPane name="padding">
|
||||
<template #tab>
|
||||
<div class="tab-label">
|
||||
<span>待审核</span>
|
||||
<NBadge
|
||||
v-if="paddingVideos.length > 0"
|
||||
:value="paddingVideos.length"
|
||||
:max="99"
|
||||
type="warning"
|
||||
class="tab-badge"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="video-grid">
|
||||
<NGrid
|
||||
x-gap="16"
|
||||
y-gap="16"
|
||||
cols="1 520:2 800:3 1100:4 1400:5"
|
||||
responsive="self"
|
||||
>
|
||||
<NGridItem
|
||||
v-for="v in paddingVideos"
|
||||
:key="v.info.bvid"
|
||||
>
|
||||
<VideoItemCard
|
||||
:video-info="v.info"
|
||||
:video-data="v.video"
|
||||
type="padding"
|
||||
:is-loading="isLoading"
|
||||
@update-status="setStatus"
|
||||
/>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</div>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane name="accept">
|
||||
<template #tab>
|
||||
<div class="tab-label">
|
||||
<span style="color: #18a058">已通过</span>
|
||||
<NBadge
|
||||
v-if="acceptVideos.length > 0"
|
||||
:value="acceptVideos.length"
|
||||
:max="99"
|
||||
type="success"
|
||||
class="tab-badge"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="video-grid">
|
||||
<NGrid
|
||||
x-gap="16"
|
||||
y-gap="16"
|
||||
cols="1 520:2 800:3 1100:4 1400:5"
|
||||
responsive="self"
|
||||
>
|
||||
<NGridItem
|
||||
v-for="v in acceptVideos"
|
||||
:key="v.info.bvid"
|
||||
>
|
||||
<VideoItemCard
|
||||
:video-info="v.info"
|
||||
:video-data="v.video"
|
||||
type="accept"
|
||||
:is-loading="isLoading"
|
||||
@update-status="setStatus"
|
||||
/>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</div>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane name="reject">
|
||||
<template #tab>
|
||||
<div class="tab-label">
|
||||
<span style="color: #d03050">已拒绝</span>
|
||||
<NBadge
|
||||
v-if="rejectVideos.length > 0"
|
||||
:value="rejectVideos.length"
|
||||
:max="99"
|
||||
color="#d03050"
|
||||
class="tab-badge"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="video-grid">
|
||||
<NGrid
|
||||
x-gap="16"
|
||||
y-gap="16"
|
||||
cols="1 520:2 800:3 1100:4 1400:5"
|
||||
responsive="self"
|
||||
>
|
||||
<NGridItem
|
||||
v-for="v in rejectVideos"
|
||||
:key="v.info.bvid"
|
||||
>
|
||||
<VideoItemCard
|
||||
:video-info="v.info"
|
||||
:video-data="v.video"
|
||||
type="reject"
|
||||
:is-loading="isLoading"
|
||||
@update-status="setStatus"
|
||||
/>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</div>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
<NModal
|
||||
v-model:show="shareModalVisiable"
|
||||
title="分享"
|
||||
preset="card"
|
||||
style="width: 600px; max-width: 90vw"
|
||||
>
|
||||
<div style="display: flex; flex-direction: column; align-items: center; gap: 24px; padding: 12px;">
|
||||
<div style="padding: 12px; background: white; border-radius: 8px;">
|
||||
<Qrcode
|
||||
:value="`${CURRENT_HOST}video-collect/${videoDetail.table.shortId}`"
|
||||
level="Q"
|
||||
:size="200"
|
||||
background="#fff"
|
||||
:margin="1"
|
||||
/>
|
||||
</div>
|
||||
<NInput
|
||||
:value="`${CURRENT_HOST}video-collect/${videoDetail.table.shortId}`"
|
||||
readonly
|
||||
@click="(e: MouseEvent) => (e.target as HTMLInputElement).select()"
|
||||
/>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="saveQRCode"
|
||||
>
|
||||
保存二维码图片
|
||||
</NButton>
|
||||
</div>
|
||||
</NModal>
|
||||
|
||||
<NModal
|
||||
v-model:show="editModalVisiable"
|
||||
title="更新信息"
|
||||
preset="card"
|
||||
style="width: 600px; max-width: 90vw"
|
||||
>
|
||||
<NForm
|
||||
ref="formRef"
|
||||
:model="updateModel"
|
||||
:rules="createRules"
|
||||
label-placement="left"
|
||||
label-width="80"
|
||||
>
|
||||
<NFormItem
|
||||
label="标题"
|
||||
path="name"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="updateModel.name"
|
||||
placeholder="征集表的标题"
|
||||
maxlength="30"
|
||||
show-count
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
label="描述"
|
||||
path="description"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="updateModel.description"
|
||||
type="textarea"
|
||||
placeholder="可以是备注之类的"
|
||||
maxlength="300"
|
||||
show-count
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NGrid
|
||||
:cols="2"
|
||||
:x-gap="24"
|
||||
>
|
||||
<NGridItem>
|
||||
<NFormItem
|
||||
label="最大数量"
|
||||
path="maxVideoCount"
|
||||
>
|
||||
<NInputNumber
|
||||
v-model:value="updateModel.maxVideoCount"
|
||||
placeholder="最大数量"
|
||||
type="number"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NFormItem
|
||||
label="结束时间"
|
||||
path="endAt"
|
||||
>
|
||||
<NDatePicker
|
||||
v-model:value="updateModel.endAt"
|
||||
type="datetime"
|
||||
placeholder="结束征集的时间"
|
||||
:is-date-disabled="dateDisabled"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 12px;">
|
||||
<NButton
|
||||
type="primary"
|
||||
:loading="isLoading"
|
||||
@click="updateTable"
|
||||
>
|
||||
更新
|
||||
保存更改
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NModal>
|
||||
</div>
|
||||
</NForm>
|
||||
</NModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.detail-container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
|
||||
.info-card-wrapper {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 4px 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.content-area {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.custom-tabs :deep(.n-tabs-nav) {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
transform: scale(0.85);
|
||||
}
|
||||
|
||||
.video-grid {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.header-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,10 +11,11 @@ import {
|
||||
NEmpty,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NGrid,
|
||||
NGridItem,
|
||||
NIcon,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NList,
|
||||
NListItem,
|
||||
NModal,
|
||||
NSpace,
|
||||
NSpin,
|
||||
@@ -22,10 +23,12 @@ import {
|
||||
NText,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { Add20Regular } from '@vicons/fluent'
|
||||
import { ref } from 'vue'
|
||||
import { UpdateFunctionEnable, useAccount } from '@/api/account'
|
||||
import { useAccount } from '@/api/account'
|
||||
import { FunctionTypes } from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import ManagePageHeader from '@/components/manage/ManagePageHeader.vue'
|
||||
import VideoCollectInfoCard from '@/components/VideoCollectInfoCard.vue'
|
||||
import { VIDEO_COLLECT_API_URL } from '@/data/constants'
|
||||
|
||||
@@ -130,119 +133,208 @@ function createTable() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAlert
|
||||
v-if="accountInfo.id"
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.VideoCollect) ? 'success' : 'warning'"
|
||||
style="max-width: 300px"
|
||||
>
|
||||
在个人主页展示进行中的征集表
|
||||
<NSwitch
|
||||
:value="accountInfo.settings.enableFunctions.includes(FunctionTypes.VideoCollect)"
|
||||
@update:value="UpdateFunctionEnable(FunctionTypes.VideoCollect)"
|
||||
/>
|
||||
</NAlert>
|
||||
<NDivider />
|
||||
<NSpace>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="createModalVisible = true"
|
||||
<div class="manage-container">
|
||||
<ManagePageHeader
|
||||
title="视频征集管理"
|
||||
subtitle="创建并管理您的视频征集活动"
|
||||
:function-type="FunctionTypes.VideoCollect"
|
||||
>
|
||||
新建征集表
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<NDivider />
|
||||
<NSpin :show="isLoading">
|
||||
<NSpace justify="center">
|
||||
<NEmpty v-if="videoTables.length == 0" />
|
||||
<NList
|
||||
v-else
|
||||
bordered
|
||||
>
|
||||
<NListItem
|
||||
v-for="item in videoTables"
|
||||
:key="item.id"
|
||||
style="padding: 0"
|
||||
<template #action>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="medium"
|
||||
@click="createModalVisible = true"
|
||||
>
|
||||
<VideoCollectInfoCard
|
||||
:item="item"
|
||||
can-click
|
||||
style="width: 500px; max-width: 70vw"
|
||||
from="owner"
|
||||
/>
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NSpace>
|
||||
</NSpin>
|
||||
<NModal
|
||||
v-model:show="createModalVisible"
|
||||
preset="card"
|
||||
title="创建视频征集"
|
||||
style="width: 600px; max-width: 90vw"
|
||||
>
|
||||
<NForm
|
||||
ref="formRef"
|
||||
:model="createVideoModel"
|
||||
:rules="createRules"
|
||||
<template #icon>
|
||||
<NIcon><Add20Regular /></NIcon>
|
||||
</template>
|
||||
新建征集表
|
||||
</NButton>
|
||||
</template>
|
||||
</ManagePageHeader>
|
||||
|
||||
<NSpin :show="isLoading">
|
||||
<div
|
||||
v-if="videoTables.length == 0 && !isLoading"
|
||||
class="empty-state"
|
||||
>
|
||||
<NEmpty
|
||||
description="暂无征集表"
|
||||
size="large"
|
||||
>
|
||||
<template #extra>
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="createModalVisible = true"
|
||||
>
|
||||
创建第一个征集表
|
||||
</NButton>
|
||||
</template>
|
||||
</NEmpty>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="grid-container"
|
||||
>
|
||||
<NGrid
|
||||
x-gap="24"
|
||||
y-gap="24"
|
||||
cols="1 640:2 1024:3 1440:4"
|
||||
responsive="self"
|
||||
>
|
||||
<NGridItem
|
||||
v-for="item in videoTables"
|
||||
:key="item.id"
|
||||
>
|
||||
<div class="card-wrapper">
|
||||
<VideoCollectInfoCard
|
||||
:item="item"
|
||||
can-click
|
||||
from="owner"
|
||||
style="width: 100%"
|
||||
class="collect-card"
|
||||
/>
|
||||
</div>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</div>
|
||||
</NSpin>
|
||||
|
||||
<NModal
|
||||
v-model:show="createModalVisible"
|
||||
preset="card"
|
||||
title="创建视频征集"
|
||||
style="width: 600px; max-width: 90vw"
|
||||
class="custom-modal"
|
||||
>
|
||||
<NFormItem
|
||||
label="标题"
|
||||
path="name"
|
||||
<NForm
|
||||
ref="formRef"
|
||||
:model="createVideoModel"
|
||||
:rules="createRules"
|
||||
label-placement="left"
|
||||
label-width="80"
|
||||
require-mark-placement="right-hanging"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="createVideoModel.name"
|
||||
placeholder="征集表的标题"
|
||||
maxlength="30"
|
||||
show-count
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
label="描述"
|
||||
path="description"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="createVideoModel.description"
|
||||
placeholder="可以是备注之类的"
|
||||
maxlength="300"
|
||||
show-count
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
label="视频数量"
|
||||
path="maxVideoCount"
|
||||
>
|
||||
<NInputNumber
|
||||
v-model:value="createVideoModel.maxVideoCount"
|
||||
placeholder="最大数量"
|
||||
type="number"
|
||||
style="max-width: 150px"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
label="结束时间"
|
||||
path="endAt"
|
||||
>
|
||||
<NDatePicker
|
||||
v-model:value="createVideoModel.endAt"
|
||||
type="datetime"
|
||||
placeholder="结束征集的时间"
|
||||
:is-date-disabled="dateDisabled"
|
||||
/>
|
||||
<NDivider vertical />
|
||||
<NText depth="3">
|
||||
最低为一小时
|
||||
</NText>
|
||||
</NFormItem>
|
||||
<NFormItem>
|
||||
<NSpace>
|
||||
<NFormItem
|
||||
label="标题"
|
||||
path="name"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="createVideoModel.name"
|
||||
placeholder="给征集活动起个响亮的名字"
|
||||
maxlength="30"
|
||||
show-count
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
label="描述"
|
||||
path="description"
|
||||
>
|
||||
<NInput
|
||||
v-model:value="createVideoModel.description"
|
||||
type="textarea"
|
||||
placeholder="简要描述活动规则或备注"
|
||||
maxlength="300"
|
||||
show-count
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NGrid
|
||||
x-gap="24"
|
||||
:cols="2"
|
||||
>
|
||||
<NGridItem>
|
||||
<NFormItem
|
||||
label="最大数量"
|
||||
path="maxVideoCount"
|
||||
>
|
||||
<NInputNumber
|
||||
v-model:value="createVideoModel.maxVideoCount"
|
||||
placeholder="限制数量"
|
||||
:min="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<NFormItem
|
||||
label="结束时间"
|
||||
path="endAt"
|
||||
>
|
||||
<NDatePicker
|
||||
v-model:value="createVideoModel.endAt"
|
||||
type="datetime"
|
||||
placeholder="选择截止时间"
|
||||
:is-date-disabled="dateDisabled"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
|
||||
<div class="modal-footer">
|
||||
<NText
|
||||
depth="3"
|
||||
style="font-size: 12px"
|
||||
>
|
||||
* 结束时间至少需要在当前时间一小时后
|
||||
</NText>
|
||||
<NButton
|
||||
type="primary"
|
||||
:loading="isLoading2"
|
||||
@click="createTable"
|
||||
>
|
||||
创建
|
||||
立即创建
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</NModal>
|
||||
</div>
|
||||
</NForm>
|
||||
</NModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.manage-container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
margin-top: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 深度选择器修改卡片样式 */
|
||||
:deep(.collect-card) {
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
:deep(.collect-card:hover) {
|
||||
transform: translateY(-4px);
|
||||
border-color: var(--n-color-target);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
/* 可以在这里添加针对小屏幕的样式调整 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { DisableFunction, EnableFunction, useAccount } from '@/api/account'
|
||||
import { useAccount } from '@/api/account'
|
||||
import {
|
||||
FunctionTypes,
|
||||
GoodsStatus,
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
} from '@/api/api-models'
|
||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||
import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue'
|
||||
import ManagePageHeader from '@/components/manage/ManagePageHeader.vue'
|
||||
import PointGoodsItem from '@/components/manage/PointGoodsItem.vue'
|
||||
import { CURRENT_HOST, POINT_API_URL } from '@/data/constants'
|
||||
import { uploadFiles, UploadStage } from '@/data/fileUpload'
|
||||
@@ -222,17 +223,6 @@ const rules = {
|
||||
},
|
||||
}
|
||||
|
||||
// 方法
|
||||
async function setFunctionEnable(enable: boolean) {
|
||||
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()
|
||||
@@ -427,77 +417,73 @@ onMounted(() => { })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 头部状态卡片 -->
|
||||
<NFlex
|
||||
vertical
|
||||
:size="16"
|
||||
<!-- 头部 -->
|
||||
<ManagePageHeader
|
||||
title="积分管理"
|
||||
:function-type="FunctionTypes.Point"
|
||||
>
|
||||
<NFlex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
:gap="16"
|
||||
vertical
|
||||
:size="16"
|
||||
>
|
||||
<NAlert
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Point) && accountInfo.eventFetcherState.online
|
||||
? 'success'
|
||||
: 'warning'
|
||||
"
|
||||
style="flex: 1; min-width: 300px"
|
||||
<NFlex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
:gap="16"
|
||||
>
|
||||
<NFlex
|
||||
align="center"
|
||||
:gap="8"
|
||||
<NAlert
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Point) && accountInfo.eventFetcherState.online
|
||||
? 'success'
|
||||
: 'warning'
|
||||
"
|
||||
style="flex: 1; min-width: 300px"
|
||||
>
|
||||
<span>启用</span>
|
||||
<NButton
|
||||
text
|
||||
type="primary"
|
||||
tag="a"
|
||||
href="https://www.wolai.com/ueENtfAm9gPEqHrAVSB2Co"
|
||||
target="_blank"
|
||||
<NFlex
|
||||
align="center"
|
||||
:gap="8"
|
||||
>
|
||||
积分系统
|
||||
</NButton>
|
||||
<NDivider vertical />
|
||||
<NSwitch
|
||||
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.Point)"
|
||||
@update:value="setFunctionEnable"
|
||||
/>
|
||||
</NFlex>
|
||||
<NText
|
||||
depth="3"
|
||||
style="margin-top: 8px; display: block"
|
||||
>
|
||||
此功能需要部署
|
||||
<NButton
|
||||
text
|
||||
type="primary"
|
||||
tag="a"
|
||||
href="https://www.wolai.com/fje5wLtcrDoZcb9rk2zrFs"
|
||||
target="_blank"
|
||||
>
|
||||
VtsuruEventFetcher
|
||||
</NButton>
|
||||
, 否则将无法记录各种事件
|
||||
</NText>
|
||||
</NAlert>
|
||||
<EventFetcherStatusCard />
|
||||
</NFlex>
|
||||
<NText>
|
||||
此功能依赖
|
||||
<NButton
|
||||
text
|
||||
type="primary"
|
||||
tag="a"
|
||||
href="https://www.wolai.com/fje5wLtcrDoZcb9rk2zrFs"
|
||||
target="_blank"
|
||||
>
|
||||
VtsuruEventFetcher
|
||||
</NButton>
|
||||
(事件监听器), 否则将无法自动记录礼物/舰长等事件
|
||||
</NText>
|
||||
<NDivider vertical />
|
||||
<NButton
|
||||
text
|
||||
type="info"
|
||||
tag="a"
|
||||
href="https://www.wolai.com/ueENtfAm9gPEqHrAVSB2Co"
|
||||
target="_blank"
|
||||
>
|
||||
积分系统说明
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NAlert>
|
||||
<EventFetcherStatusCard />
|
||||
</NFlex>
|
||||
|
||||
<!-- 礼物展示页链接 -->
|
||||
<NDivider
|
||||
style="margin: 0"
|
||||
title-placement="left"
|
||||
>
|
||||
礼物展示页链接
|
||||
</NDivider>
|
||||
<!-- 礼物展示页链接 -->
|
||||
<NDivider
|
||||
style="margin: 0"
|
||||
title-placement="left"
|
||||
>
|
||||
礼物展示页链接
|
||||
</NDivider>
|
||||
|
||||
<NFlex
|
||||
align="center"
|
||||
:gap="12"
|
||||
>
|
||||
<NInputGroup style="max-width: 400px;">
|
||||
<NInput
|
||||
<NFlex
|
||||
align="center"
|
||||
:gap="12"
|
||||
>
|
||||
<NInputGroup style="max-width: 400px;">
|
||||
<NInput
|
||||
:value="`${CURRENT_HOST}@${accountInfo.name}/goods`"
|
||||
readonly
|
||||
/>
|
||||
@@ -510,6 +496,7 @@ onMounted(() => { })
|
||||
</NInputGroup>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</ManagePageHeader>
|
||||
|
||||
<NDivider style="margin: 16px 0" />
|
||||
|
||||
|
||||
@@ -44,6 +44,12 @@ async function getUsers() {
|
||||
type: OpenLiveLotteryType.Waiting,
|
||||
} as UpdateLiveLotteryUsersModel
|
||||
}
|
||||
|
||||
function handleImageError(e: Event) {
|
||||
const img = e.target as HTMLImageElement
|
||||
img.src = 'https://i2.hdslb.com/bfs/face/member/noface.jpg'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.$mitt.on('onOBSComponentUpdate', () => {
|
||||
getUsers()
|
||||
@@ -93,6 +99,7 @@ onUnmounted(() => {
|
||||
class="lottery-avatar"
|
||||
:src="`${user.avatar}@30h`"
|
||||
referrerpolicy="no-referrer"
|
||||
@error="handleImageError"
|
||||
>
|
||||
<div>
|
||||
<p class="lottery-name">{{ user.name }}</p>
|
||||
@@ -138,6 +145,7 @@ onUnmounted(() => {
|
||||
style="border-radius: 50%"
|
||||
:src="`${user.avatar}@50h_50w`"
|
||||
referrerpolicy="no-referrer"
|
||||
@error="handleImageError"
|
||||
>
|
||||
<NText style="font-size: large">
|
||||
{{ user.name }}
|
||||
@@ -145,7 +153,10 @@ onUnmounted(() => {
|
||||
</NSpace>
|
||||
</div>
|
||||
</Vue3Marquee>
|
||||
<NSpace justify="center">
|
||||
<NSpace
|
||||
v-else
|
||||
justify="center"
|
||||
>
|
||||
<div
|
||||
v-for="user in result.resultUsers"
|
||||
:key="user.uId"
|
||||
@@ -168,6 +179,7 @@ onUnmounted(() => {
|
||||
style="border-radius: 50%"
|
||||
:src="`${user.avatar}@50h_50w`"
|
||||
referrerpolicy="no-referrer"
|
||||
@error="handleImageError"
|
||||
>
|
||||
<NText style="font-size: large; margin-top: 10px">
|
||||
{{ user.name }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { OpenLiveInfo, OpenLiveLotteryUserInfo, UpdateLiveLotteryUsersModel } from '@/api/api-models'
|
||||
import type { DanmakuInfo, GiftInfo } from '@/data/DanmakuClients/OpenLiveClient'
|
||||
import { Add24Filled, Delete24Filled, Info24Filled, PersonAdd24Filled, Sparkle24Filled, Target24Filled } from '@vicons/fluent'
|
||||
import { Add24Filled, Delete24Filled, Info24Filled, Pause24Filled, PersonAdd24Filled, Play24Filled, Sparkle24Filled, Target24Filled } from '@vicons/fluent'
|
||||
import { useLocalStorage, useStorage } from '@vueuse/core'
|
||||
import { format } from 'date-fns'
|
||||
import {
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
NEmpty,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NGi,
|
||||
NGrid,
|
||||
NIcon,
|
||||
NInput,
|
||||
NInputGroup,
|
||||
@@ -26,12 +28,14 @@ import {
|
||||
NList,
|
||||
NListItem,
|
||||
NModal,
|
||||
NNumberAnimation,
|
||||
NProgress,
|
||||
NRadioButton,
|
||||
NRadioGroup,
|
||||
NResult,
|
||||
NScrollbar,
|
||||
NSpace,
|
||||
NStatistic,
|
||||
NTag,
|
||||
NTime,
|
||||
NTooltip,
|
||||
@@ -784,318 +788,331 @@ onUnmounted(() => {
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
<NCard
|
||||
size="small"
|
||||
embedded
|
||||
title="抽奖选项"
|
||||
>
|
||||
<template #header-extra>
|
||||
<div class="settings-wrapper">
|
||||
<div class="settings-header">
|
||||
<NSpace align="center">
|
||||
<NIcon :component="Sparkle24Filled" color="#f0a020" />
|
||||
<span style="font-weight: bold; font-size: 16px">抽奖设置</span>
|
||||
</NSpace>
|
||||
<NButton
|
||||
size="small"
|
||||
size="tiny"
|
||||
secondary
|
||||
:disabled="isStartLottery"
|
||||
@click="lotteryOption = defaultOption"
|
||||
>
|
||||
恢复默认
|
||||
</NButton>
|
||||
</template>
|
||||
<NSpace
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<NTag :bordered="false">
|
||||
抽奖类型
|
||||
</NTag>
|
||||
<NRadioGroup
|
||||
v-model:value="lotteryOption.type"
|
||||
:disabled="isLottering"
|
||||
size="small"
|
||||
>
|
||||
<NRadioButton
|
||||
value="danmaku"
|
||||
:disabled="isStartLottery"
|
||||
>
|
||||
弹幕
|
||||
</NRadioButton>
|
||||
<NRadioButton
|
||||
value="gift"
|
||||
:disabled="isStartLottery"
|
||||
>
|
||||
礼物
|
||||
</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NSpace>
|
||||
<NDivider style="margin: 10px 0 10px 0" />
|
||||
<NSpace align="center">
|
||||
<NInputGroup style="max-width: 200px">
|
||||
<NInputGroupLabel> 抽选人数 </NInputGroupLabel>
|
||||
<NInputNumber
|
||||
v-model:value="lotteryOption.resultCount"
|
||||
:disabled="isStartLottery"
|
||||
placeholder=""
|
||||
min="1"
|
||||
/>
|
||||
</NInputGroup>
|
||||
<NCheckbox
|
||||
v-model:checked="lotteryOption.needGuard"
|
||||
:disabled="isStartLottery"
|
||||
>
|
||||
需要上舰
|
||||
</NCheckbox>
|
||||
<NCheckbox
|
||||
v-model:checked="lotteryOption.needFanMedal"
|
||||
:disabled="isStartLottery"
|
||||
>
|
||||
需要粉丝牌
|
||||
</NCheckbox>
|
||||
<NCollapseTransition>
|
||||
<NInputGroup
|
||||
v-if="lotteryOption.needFanMedal"
|
||||
style="max-width: 200px"
|
||||
>
|
||||
<NInputGroupLabel> 最低粉丝牌等级 </NInputGroupLabel>
|
||||
<NInputNumber
|
||||
v-model:value="lotteryOption.fanCardLevel"
|
||||
min="1"
|
||||
max="50"
|
||||
:default-value="1"
|
||||
:disabled="isLottering || isStartLottery"
|
||||
/>
|
||||
</NInputGroup>
|
||||
</NCollapseTransition>
|
||||
<template v-if="lotteryOption.type == 'danmaku'">
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NInputGroup style="max-width: 250px">
|
||||
<NInputGroupLabel> 弹幕内容 </NInputGroupLabel>
|
||||
<NInput
|
||||
v-model:value="lotteryOption.danmakuKeyword"
|
||||
:disabled="isStartLottery"
|
||||
placeholder="留空则任何弹幕都可以"
|
||||
/>
|
||||
</NInputGroup>
|
||||
</template>
|
||||
符合规则的弹幕才会被添加到抽奖队列中
|
||||
</NTooltip>
|
||||
<NRadioGroup
|
||||
v-model:value="lotteryOption.danmakuFilterType"
|
||||
name="判定类型"
|
||||
:disabled="isLottering"
|
||||
size="small"
|
||||
>
|
||||
<NRadioButton
|
||||
:disabled="isStartLottery"
|
||||
value="all"
|
||||
</div>
|
||||
|
||||
<div class="settings-layout">
|
||||
<!-- 左侧:参与规则 -->
|
||||
<div class="setting-column">
|
||||
<div class="setting-section">
|
||||
<div class="section-header">
|
||||
<NIcon :component="Target24Filled" />
|
||||
参与规则
|
||||
</div>
|
||||
<NForm
|
||||
label-placement="left"
|
||||
label-width="80"
|
||||
size="small"
|
||||
>
|
||||
完全一致
|
||||
</NRadioButton>
|
||||
<NRadioButton
|
||||
:disabled="isStartLottery"
|
||||
value="contains"
|
||||
<NFormItem label="参与方式">
|
||||
<NRadioGroup
|
||||
v-model:value="lotteryOption.type"
|
||||
:disabled="isLottering || isStartLottery"
|
||||
>
|
||||
<NRadioButton value="danmaku">
|
||||
弹幕
|
||||
</NRadioButton>
|
||||
<NRadioButton value="gift">
|
||||
礼物
|
||||
</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
|
||||
<template v-if="lotteryOption.type == 'danmaku'">
|
||||
<NFormItem label="弹幕内容">
|
||||
<NInput
|
||||
v-model:value="lotteryOption.danmakuKeyword"
|
||||
:disabled="isStartLottery"
|
||||
placeholder="留空则任意弹幕"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem
|
||||
v-if="lotteryOption.danmakuKeyword"
|
||||
label="匹配规则"
|
||||
>
|
||||
<NRadioGroup
|
||||
v-model:value="lotteryOption.danmakuFilterType"
|
||||
:disabled="isStartLottery"
|
||||
>
|
||||
<NRadioButton value="all">
|
||||
完全一致
|
||||
</NRadioButton>
|
||||
<NRadioButton value="contains">
|
||||
包含
|
||||
</NRadioButton>
|
||||
<NRadioButton value="regex">
|
||||
正则
|
||||
</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
</template>
|
||||
|
||||
<template v-else-if="lotteryOption.type == 'gift'">
|
||||
<NFormItem label="礼物限制">
|
||||
<NInputGroup>
|
||||
<NInputNumber
|
||||
v-model:value="lotteryOption.giftMinPrice"
|
||||
:disabled="isStartLottery"
|
||||
placeholder="最低价格"
|
||||
:min="0"
|
||||
style="width: 50%"
|
||||
>
|
||||
<template #suffix>
|
||||
元
|
||||
</template>
|
||||
</NInputNumber>
|
||||
<NInput
|
||||
v-model:value="lotteryOption.giftName"
|
||||
:disabled="isStartLottery"
|
||||
placeholder="指定礼物名称"
|
||||
style="width: 50%"
|
||||
/>
|
||||
</NInputGroup>
|
||||
</NFormItem>
|
||||
</template>
|
||||
|
||||
<NFormItem label="身份限制">
|
||||
<NSpace>
|
||||
<NCheckbox
|
||||
v-model:checked="lotteryOption.needGuard"
|
||||
:disabled="isStartLottery"
|
||||
>
|
||||
舰长
|
||||
</NCheckbox>
|
||||
<NCheckbox
|
||||
v-model:checked="lotteryOption.needFanMedal"
|
||||
:disabled="isStartLottery"
|
||||
>
|
||||
粉丝牌
|
||||
</NCheckbox>
|
||||
<NCheckbox
|
||||
v-model:checked="lotteryOption.needWearFanMedal"
|
||||
:disabled="isStartLottery"
|
||||
>
|
||||
佩戴
|
||||
</NCheckbox>
|
||||
</NSpace>
|
||||
</NFormItem>
|
||||
|
||||
<NCollapseTransition :show="lotteryOption.needFanMedal">
|
||||
<NFormItem label="粉丝牌等级">
|
||||
<NInputNumber
|
||||
v-model:value="lotteryOption.fanCardLevel"
|
||||
:min="1"
|
||||
:max="50"
|
||||
:disabled="isStartLottery"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NCollapseTransition>
|
||||
</NForm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:玩法设置 -->
|
||||
<div class="setting-column">
|
||||
<div class="setting-section">
|
||||
<div class="section-header">
|
||||
<NIcon :component="Sparkle24Filled" />
|
||||
玩法设置
|
||||
</div>
|
||||
<NForm
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
size="small"
|
||||
>
|
||||
包含
|
||||
</NRadioButton>
|
||||
<NRadioButton
|
||||
:disabled="isStartLottery"
|
||||
value="regex"
|
||||
>
|
||||
正则
|
||||
</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</template>
|
||||
<template v-else-if="lotteryOption.type == 'gift'">
|
||||
<NInputGroup style="max-width: 250px">
|
||||
<NInputGroupLabel> 最低价格 </NInputGroupLabel>
|
||||
<NInputNumber
|
||||
v-model:value="lotteryOption.giftMinPrice"
|
||||
:disabled="isStartLottery"
|
||||
placeholder="留空则不限制"
|
||||
/>
|
||||
</NInputGroup>
|
||||
<NInputGroup style="max-width: 200px">
|
||||
<NInputGroupLabel> 礼物名称 </NInputGroupLabel>
|
||||
<NInput
|
||||
v-model:value="lotteryOption.giftName"
|
||||
:disabled="isStartLottery"
|
||||
placeholder="留空则不限制"
|
||||
/>
|
||||
</NInputGroup>
|
||||
</template>
|
||||
</NSpace>
|
||||
<NDivider style="margin: 10px 0 10px 0" />
|
||||
<NSpace
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<NTag :bordered="false">
|
||||
抽取方式
|
||||
</NTag>
|
||||
<NRadioGroup
|
||||
v-model:value="lotteryOption.lotteryType"
|
||||
name="抽取类型"
|
||||
size="small"
|
||||
:disabled="isLottering"
|
||||
>
|
||||
<NRadioButton value="single">
|
||||
单个淘汰
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NIcon :component="Info24Filled" />
|
||||
</template>
|
||||
{{ lotteryTypeDescriptions.single }}
|
||||
</NTooltip>
|
||||
</NRadioButton>
|
||||
<NRadioButton value="half">
|
||||
减半淘汰
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NIcon :component="Info24Filled" />
|
||||
</template>
|
||||
{{ lotteryTypeDescriptions.half }}
|
||||
</NTooltip>
|
||||
</NRadioButton>
|
||||
<NRadioButton value="flip">
|
||||
翻牌抽取
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NIcon :component="Info24Filled" />
|
||||
</template>
|
||||
{{ lotteryTypeDescriptions.flip }}
|
||||
</NTooltip>
|
||||
</NRadioButton>
|
||||
<NRadioButton value="wheel" :disabled="currentUsers.length < 2">
|
||||
转轮抽取
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NIcon :component="Info24Filled" />
|
||||
</template>
|
||||
{{ lotteryTypeDescriptions.wheel }}
|
||||
</NTooltip>
|
||||
</NRadioButton>
|
||||
<NRadioButton value="cards">
|
||||
抽卡模式
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NIcon :component="Info24Filled" />
|
||||
</template>
|
||||
{{ lotteryTypeDescriptions.cards }}
|
||||
</NTooltip>
|
||||
</NRadioButton>
|
||||
<NRadioButton value="elimination">
|
||||
淘汰赛
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NIcon :component="Info24Filled" />
|
||||
</template>
|
||||
{{ lotteryTypeDescriptions.elimination }}
|
||||
</NTooltip>
|
||||
</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NSpace>
|
||||
<NDivider style="margin: 10px 0 10px 0" />
|
||||
<NSpace align="center" justify="center">
|
||||
<NTag :bordered="false">
|
||||
动画速度
|
||||
</NTag>
|
||||
<NInputGroup style="max-width: 200px">
|
||||
<NInputGroupLabel> 动画延迟(毫秒) </NInputGroupLabel>
|
||||
<NInputNumber
|
||||
v-model:value="lotteryOption.animationSpeed"
|
||||
:disabled="isLottering"
|
||||
min="100"
|
||||
max="5000"
|
||||
step="100"
|
||||
/>
|
||||
</NInputGroup>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
<div class="form-row">
|
||||
<NFormItem label="抽取人数" style="flex: 1">
|
||||
<NInputNumber
|
||||
v-model:value="lotteryOption.resultCount"
|
||||
:min="1"
|
||||
:disabled="isStartLottery"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="动画速度" style="flex: 1">
|
||||
<NInputNumber
|
||||
v-model:value="lotteryOption.animationSpeed"
|
||||
:step="100"
|
||||
:min="100"
|
||||
:max="5000"
|
||||
:disabled="isLottering"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #suffix>
|
||||
ms
|
||||
</template>
|
||||
</NInputNumber>
|
||||
</NFormItem>
|
||||
</div>
|
||||
|
||||
<NFormItem label="玩法模式">
|
||||
<div class="mode-selector-grid">
|
||||
<div
|
||||
v-for="(desc, key) in lotteryTypeDescriptions"
|
||||
:key="key"
|
||||
class="mode-card"
|
||||
:class="{
|
||||
active: lotteryOption.lotteryType === key,
|
||||
disabled: isLottering || (key === 'wheel' && currentUsers.length < 2)
|
||||
}"
|
||||
@click="!isLottering && (key !== 'wheel' || currentUsers.length >= 2) && (lotteryOption.lotteryType = key as any)"
|
||||
>
|
||||
<div class="mode-icon">
|
||||
<NIcon v-if="key === 'single'" :component="Delete24Filled" />
|
||||
<NIcon v-else-if="key === 'half'" :component="Pause24Filled" style="transform: rotate(90deg)" />
|
||||
<NIcon v-else-if="key === 'flip'" :component="Sparkle24Filled" />
|
||||
<NIcon v-else-if="key === 'wheel'" :component="Target24Filled" />
|
||||
<NIcon v-else-if="key === 'cards'" :component="Add24Filled" />
|
||||
<NIcon v-else-if="key === 'elimination'" :component="Play24Filled" />
|
||||
</div>
|
||||
<div class="mode-info">
|
||||
<div class="mode-title">
|
||||
{{ key === 'single' ? '单个淘汰' :
|
||||
key === 'half' ? '减半淘汰' :
|
||||
key === 'flip' ? '翻牌抽取' :
|
||||
key === 'wheel' ? '转轮抽取' :
|
||||
key === 'cards' ? '抽卡模式' : '淘汰赛' }}
|
||||
</div>
|
||||
<div class="mode-desc">{{ desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NCard
|
||||
v-if="originUsers"
|
||||
size="small"
|
||||
style="margin-top: 16px; min-height: 400px"
|
||||
>
|
||||
<NSpace
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<NButton
|
||||
type="primary"
|
||||
:loading="isStartLottery"
|
||||
:disabled="isStartLottery || isLotteried || !client"
|
||||
@click="continueLottery"
|
||||
<template #header>
|
||||
<NSpace
|
||||
align="center"
|
||||
justify="space-between"
|
||||
>
|
||||
开始监听
|
||||
</NButton>
|
||||
<NButton
|
||||
type="warning"
|
||||
:disabled="!isStartLottery"
|
||||
@click="pause"
|
||||
>
|
||||
停止
|
||||
</NButton>
|
||||
<NButton
|
||||
type="error"
|
||||
:disabled="isLottering || originUsers.length == 0"
|
||||
@click="clear"
|
||||
>
|
||||
清空
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<NDivider style="margin: 20px 0 20px 0">
|
||||
<template v-if="isStartLottery">
|
||||
进行抽取前需要先停止
|
||||
</template>
|
||||
<template v-else-if="lotteryProgress > 0 && lotteryProgress < 100">
|
||||
抽取进行中 ({{ Math.round(lotteryProgress) }}%)
|
||||
</template>
|
||||
<template v-else-if="currentLotteryStep > 0 && lotteryOption.lotteryType === 'elimination'">
|
||||
淘汰赛第 {{ currentLotteryStep }} 轮
|
||||
</template>
|
||||
</NDivider>
|
||||
<div class="user-count-stat">
|
||||
<span class="label">当前参与</span>
|
||||
<NNumberAnimation
|
||||
:from="0"
|
||||
:to="currentUsers.length"
|
||||
active
|
||||
/>
|
||||
<span class="unit">人</span>
|
||||
</div>
|
||||
<NSpace>
|
||||
<NButton
|
||||
:type="isStartLottery ? 'warning' : 'success'"
|
||||
:loading="isStartLottery && !isLotteried"
|
||||
@click="isStartLottery ? pause() : continueLottery()"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="isStartLottery ? Pause24Filled : Play24Filled" />
|
||||
</template>
|
||||
{{ isStartLottery ? '暂停监听' : '开始监听' }}
|
||||
</NButton>
|
||||
<NButton
|
||||
type="error"
|
||||
secondary
|
||||
:disabled="isLottering || originUsers.length == 0"
|
||||
@click="clear"
|
||||
>
|
||||
清空
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div v-if="isLottering || lotteryProgress > 0" style="margin: 10px 0">
|
||||
<NProgress
|
||||
:percentage="lotteryProgress"
|
||||
:show-indicator="true"
|
||||
type="line"
|
||||
:status="isLottering ? 'info' : 'success'"
|
||||
/>
|
||||
<div
|
||||
v-if="isLottering || lotteryProgress > 0 || isStartLottery"
|
||||
class="status-bar"
|
||||
>
|
||||
<div
|
||||
v-if="isStartLottery"
|
||||
style="color: var(--n-primary-color)"
|
||||
>
|
||||
<NSpace
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<NIcon
|
||||
:component="Sparkle24Filled"
|
||||
class="n-icon-spin"
|
||||
/>
|
||||
正在监听弹幕/礼物中...
|
||||
</NSpace>
|
||||
</div>
|
||||
<div v-else-if="lotteryProgress > 0 && lotteryProgress < 100">
|
||||
<NProgress
|
||||
type="line"
|
||||
:percentage="lotteryProgress"
|
||||
:indicator-placement="'inside'"
|
||||
processing
|
||||
/>
|
||||
<div style="margin-top: 8px">
|
||||
<template v-if="currentLotteryStep > 0 && lotteryOption.lotteryType === 'elimination'">
|
||||
淘汰赛第 {{ currentLotteryStep }} 轮
|
||||
</template>
|
||||
<template v-else>
|
||||
正在抽取中...
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NSpace justify="center">
|
||||
|
||||
<div class="action-bar">
|
||||
<NButton
|
||||
type="success"
|
||||
size="large"
|
||||
:loading="isLottering"
|
||||
:disabled="isStartLottery || isLotteried"
|
||||
:disabled="isStartLottery || isLotteried || currentUsers.length === 0"
|
||||
data-umami-event="Open-Live Use Lottery"
|
||||
:data-umami-event-uid="client?.authInfo?.anchor_info?.uid"
|
||||
style="width: 180px; height: 48px; font-size: 18px"
|
||||
@click="startLottery"
|
||||
>
|
||||
进行抽取
|
||||
<template #icon>
|
||||
<NIcon :component="Sparkle24Filled" />
|
||||
</template>
|
||||
开始抽取
|
||||
</NButton>
|
||||
<NButton
|
||||
type="info"
|
||||
secondary
|
||||
:disabled="isStartLottery || isLottering || !isLotteried"
|
||||
size="large"
|
||||
:disabled="isLottering || !isLotteried"
|
||||
style="width: 120px; height: 48px"
|
||||
@click="reset"
|
||||
>
|
||||
重置
|
||||
重置结果
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<NDivider style="margin: 10px 0 10px 0">
|
||||
共 {{ currentUsers?.length }} 人
|
||||
</NDivider>
|
||||
<!-- 翻牌模式:洗牌按钮 -->
|
||||
<div v-if="lotteryOption.lotteryType === 'flip'" style="display: flex; justify-content: center; margin-bottom: 10px;">
|
||||
<NButton
|
||||
size="small"
|
||||
v-if="lotteryOption.lotteryType === 'flip'"
|
||||
size="large"
|
||||
type="info"
|
||||
secondary
|
||||
:disabled="!flipEnabled || isLottering || isStartLottery || currentUsers.length === 0"
|
||||
style="height: 48px"
|
||||
@click="shuffleFlipCards"
|
||||
>
|
||||
洗牌
|
||||
</NButton>
|
||||
</div>
|
||||
<NDivider style="margin: 10px 0 20px 0" />
|
||||
<!-- 转轮模式特殊显示 -->
|
||||
<div v-if="lotteryOption.lotteryType === 'wheel' && currentUsers.length >= 2" class="wheel-container">
|
||||
<div class="wheel-area">
|
||||
@@ -1406,6 +1423,165 @@ onUnmounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-wrapper {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
|
||||
.settings-layout {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.setting-column {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
background: var(--n-card-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
border: 1px solid var(--n-border-color);
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.setting-section:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--n-text-color);
|
||||
border-bottom: 1px dashed var(--n-border-color);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.user-count-stat {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--n-text-color-2);
|
||||
}
|
||||
.user-count-stat .n-number-animation {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: var(--n-primary-color);
|
||||
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
.user-count-stat .unit {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
margin: 16px 0;
|
||||
text-align: center;
|
||||
color: var(--n-text-color-2);
|
||||
}
|
||||
|
||||
.mode-selector-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mode-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--n-border-color);
|
||||
border-radius: 8px;
|
||||
background-color: var(--n-card-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mode-card:hover:not(.disabled) {
|
||||
border-color: var(--n-primary-color);
|
||||
background-color: rgba(var(--n-primary-color-rgb), 0.05);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.mode-card.active {
|
||||
border-color: var(--n-primary-color);
|
||||
background-color: rgba(var(--n-primary-color-rgb), 0.1);
|
||||
color: var(--n-primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(var(--n-primary-color-rgb), 0.2);
|
||||
}
|
||||
|
||||
.mode-card.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.mode-icon {
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mode-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mode-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mode-desc {
|
||||
font-size: 12px;
|
||||
color: var(--n-text-color-3);
|
||||
display: none; /* 默认不显示描述,hover或大屏可以显示,目前保持简洁 */
|
||||
}
|
||||
|
||||
/* 卡片容器 */
|
||||
.lottery-cards-container {
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user