mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
add video collect
This commit is contained in:
@@ -3,11 +3,16 @@
|
|||||||
<NNotificationProvider>
|
<NNotificationProvider>
|
||||||
<NConfigProvider :theme-overrides="themeOverrides" :theme="theme" style="height: 100vh" :locale="zhCN" :date-locale="dateZhCN">
|
<NConfigProvider :theme-overrides="themeOverrides" :theme="theme" style="height: 100vh" :locale="zhCN" :date-locale="dateZhCN">
|
||||||
<NElement style="height: 100vh">
|
<NElement style="height: 100vh">
|
||||||
|
<Suspense>
|
||||||
<ViewerLayout v-if="layout == 'viewer'" />
|
<ViewerLayout v-if="layout == 'viewer'" />
|
||||||
<ManageLayout v-else-if="layout == 'manage'" />
|
<ManageLayout v-else-if="layout == 'manage'" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</template>
|
</template>
|
||||||
|
<template #fallback>
|
||||||
|
<NSpin size="large" show/>
|
||||||
|
</template>
|
||||||
|
</Suspense>
|
||||||
</NElement>
|
</NElement>
|
||||||
</NConfigProvider>
|
</NConfigProvider>
|
||||||
</NNotificationProvider>
|
</NNotificationProvider>
|
||||||
@@ -18,7 +23,7 @@
|
|||||||
import ViewerLayout from '@/views/ViewerLayout.vue'
|
import ViewerLayout from '@/views/ViewerLayout.vue'
|
||||||
import ManageLayout from '@/views/ManageLayout.vue'
|
import ManageLayout from '@/views/ManageLayout.vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { NConfigProvider, NMessageProvider, NNotificationProvider, zhCN, dateZhCN, useOsTheme, darkTheme, NElement } from 'naive-ui'
|
import { NConfigProvider, NMessageProvider, NNotificationProvider, zhCN, dateZhCN, useOsTheme, darkTheme, NElement, NSpin } from 'naive-ui'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { ThemeType, UserInfo } from './api/api-models'
|
import { ThemeType, UserInfo } from './api/api-models'
|
||||||
@@ -34,6 +39,7 @@ const layout = computed(() => {
|
|||||||
document.title = route.meta.title + ' · 管理 · VTsuru'
|
document.title = route.meta.title + ' · 管理 · VTsuru'
|
||||||
return 'manage'
|
return 'manage'
|
||||||
} else {
|
} else {
|
||||||
|
document.title = route.meta.title + ' · VTsuru'
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -151,12 +151,59 @@ export enum ThemeType {
|
|||||||
Light = 'light',
|
Light = 'light',
|
||||||
Dark = 'dark',
|
Dark = 'dark',
|
||||||
}
|
}
|
||||||
|
export interface VideoCollectCreateModel {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
endAt: number
|
||||||
|
maxVideoCount: number
|
||||||
|
}
|
||||||
export interface VideoCollectTable{
|
export interface VideoCollectTable{
|
||||||
id: string
|
id: string
|
||||||
|
shortId: string
|
||||||
name: string
|
name: string
|
||||||
title: string
|
|
||||||
description: string
|
description: string
|
||||||
createAt: number
|
createAt: number
|
||||||
endAt: number
|
endAt: number
|
||||||
|
isFinish: boolean
|
||||||
|
videoCount: number
|
||||||
|
maxVideoCount: number
|
||||||
|
owner: UserInfo
|
||||||
|
}
|
||||||
|
export interface VideoCollectVideo {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
publistTime: number
|
||||||
|
ownerName: string
|
||||||
|
ownerUId: number
|
||||||
|
cover: string
|
||||||
|
length: number
|
||||||
|
watched?: boolean
|
||||||
|
}
|
||||||
|
export enum VideoFrom{
|
||||||
|
Collect,
|
||||||
|
Spam
|
||||||
|
}
|
||||||
|
export enum VideoStatus
|
||||||
|
{
|
||||||
|
Pending,
|
||||||
|
Accepted,
|
||||||
|
Rejected,
|
||||||
|
}
|
||||||
|
export interface VideoSender{
|
||||||
|
sendAt: number
|
||||||
|
sender?: string
|
||||||
|
senderId?: number
|
||||||
|
description?: string
|
||||||
|
from: VideoFrom
|
||||||
|
}
|
||||||
|
export interface VideoInfo {
|
||||||
|
bvid: string
|
||||||
|
senders: VideoSender[]
|
||||||
|
status: VideoStatus
|
||||||
|
}
|
||||||
|
export interface VideoCollectDetail {
|
||||||
|
table: VideoCollectTable
|
||||||
|
videos: { info: VideoInfo; video: VideoCollectVideo }[]
|
||||||
}
|
}
|
||||||
|
|||||||
69
src/components/VideoCollectInfoCard.vue
Normal file
69
src/components/VideoCollectInfoCard.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { VideoCollectTable } from '@/api/api-models'
|
||||||
|
import router from '@/router'
|
||||||
|
import { Clock24Filled, Clock24Regular, NumberRow24Regular } from '@vicons/fluent'
|
||||||
|
import { CountdownProps, NCard, NCountdown, NDivider, NEllipsis, NIcon, NSpace, NTag, NText, NTime, NTooltip } from 'naive-ui'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: VideoCollectTable
|
||||||
|
canClick?: boolean
|
||||||
|
}>()
|
||||||
|
const renderCountdown: CountdownProps['render'] = (info: { hours: number; minutes: number; seconds: number }) => {
|
||||||
|
return `${String(info.hours).padStart(2, '0')}时 ${String(info.minutes).padStart(2, '0')}分 ${String(info.seconds).padStart(2, '0')}秒`
|
||||||
|
}
|
||||||
|
function onClick() {
|
||||||
|
if (props.canClick == true) {
|
||||||
|
router.push({ name: 'manage-videoCollect-Detail', params: { id: props.item.id } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NCard size="small" style="width: 100%; max-width: 70vw; cursor: pointer" @click="onClick" embedded hoverable>
|
||||||
|
<template #header>
|
||||||
|
<NSpace :size="5">
|
||||||
|
<NTag v-if="item.isFinish" size="small"> 已结束 </NTag>
|
||||||
|
<NTag v-else type="success" size="small"> 进行中 </NTag>
|
||||||
|
<NDivider vertical />
|
||||||
|
{{ item.name }}
|
||||||
|
</NSpace>
|
||||||
|
</template>
|
||||||
|
<template #header-extra>
|
||||||
|
<slot name="header-extra"></slot>
|
||||||
|
</template>
|
||||||
|
<NText depth="3" style="font-size: 13px">
|
||||||
|
<NTime :time="item.createAt" />
|
||||||
|
</NText>
|
||||||
|
<br />
|
||||||
|
<NText depth="3">
|
||||||
|
<NEllipsis>
|
||||||
|
{{ item.description }}
|
||||||
|
</NEllipsis>
|
||||||
|
</NText>
|
||||||
|
<template #footer>
|
||||||
|
<NSpace :size="5" align="center">
|
||||||
|
<NSpace>
|
||||||
|
<NIcon :component="NumberRow24Regular" />
|
||||||
|
<NTooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<NText> {{ item.videoCount }} / {{ item.maxVideoCount }} </NText>
|
||||||
|
</template>
|
||||||
|
已征集数量 / 最大征集数量
|
||||||
|
</NTooltip>
|
||||||
|
</NSpace>
|
||||||
|
<template v-if="!item.isFinish">
|
||||||
|
<NDivider vertical />
|
||||||
|
<NSpace>
|
||||||
|
<NIcon :component="Clock24Regular" />
|
||||||
|
<NTooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<NText depth="3"> 剩余 <NCountdown :duration="item.endAt - Date.now()" :render="renderCountdown" /> </NText>
|
||||||
|
</template>
|
||||||
|
结束于 <NTime :time="item.endAt" />
|
||||||
|
</NTooltip>
|
||||||
|
</NSpace>
|
||||||
|
</template>
|
||||||
|
</NSpace>
|
||||||
|
</template>
|
||||||
|
</NCard>
|
||||||
|
</template>
|
||||||
@@ -22,6 +22,24 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'resetPassword',
|
name: 'resetPassword',
|
||||||
component: () => import('@/views/ChangePasswordView.vue'),
|
component: () => import('@/views/ChangePasswordView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/video-collect/:id',
|
||||||
|
name: 'video-collect',
|
||||||
|
component: () => import('@/views/VideoCollectPublic.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '推荐 · 视频征集',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/video-collect/list/:id',
|
||||||
|
name: 'video-collect-list',
|
||||||
|
component: () => import('@/views/VideoCollectListView.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '结果 · 视频征集',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/user/:id',
|
path: '/user/:id',
|
||||||
name: 'user',
|
name: 'user',
|
||||||
@@ -148,8 +166,26 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'video-collect/:id',
|
||||||
|
name: 'manage-videoCollect-Detail',
|
||||||
|
component: () => import('@/views/manage/VideoCollectDetailView.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '详情 · 视频征集',
|
||||||
|
keepAlive: true,
|
||||||
|
parent: 'manage-videoCollect',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'notfound',
|
||||||
|
component: import('@/views/NotfoundView.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '页面不存在',
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTi
|
|||||||
</NSpace>
|
</NSpace>
|
||||||
<NDivider title-placement="left"> 更新日志 </NDivider>
|
<NDivider title-placement="left"> 更新日志 </NDivider>
|
||||||
<NTimeline>
|
<NTimeline>
|
||||||
|
<NTimelineItem type="success" title="功能添加" content="视频征集" time="2023-10-30" />
|
||||||
<NTimelineItem type="info" title="功能更新" content="日程表添加 '粉粉' 模板" time="2023-10-27" />
|
<NTimelineItem type="info" title="功能更新" content="日程表添加 '粉粉' 模板" time="2023-10-27" />
|
||||||
<NTimelineItem type="info" title="功能更新" content="提问箱新增公开选项" time="2023-10-26" />
|
<NTimelineItem type="info" title="功能更新" content="提问箱新增公开选项" time="2023-10-26" />
|
||||||
<NTimelineItem type="success" title="功能添加" content="提问箱分享卡片" time="2023-10-25" />
|
<NTimelineItem type="success" title="功能添加" content="提问箱分享卡片" time="2023-10-25" />
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const functions = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '视频征集',
|
name: '视频征集',
|
||||||
desc: '创建用来收集视频链接的页面, 可以从动态爬取, 也可以提前对视频进行筛选 (开发中)',
|
desc: '创建用来收集视频链接的页面, 可以从动态爬取, 也可以提前对视频进行筛选',
|
||||||
icon: Lottery24Filled,
|
icon: Lottery24Filled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ onMounted(() => {
|
|||||||
<NMenu
|
<NMenu
|
||||||
style="margin-top: 12px"
|
style="margin-top: 12px"
|
||||||
:disabled="accountInfo?.isEmailVerified != true"
|
:disabled="accountInfo?.isEmailVerified != true"
|
||||||
:default-value="$route.name?.toString()"
|
:default-value="($route.meta.parent as string) ?? $route.name?.toString()"
|
||||||
:collapsed-width="64"
|
:collapsed-width="64"
|
||||||
:collapsed-icon-size="22"
|
:collapsed-icon-size="22"
|
||||||
:options="menuOptions"
|
:options="menuOptions"
|
||||||
@@ -232,7 +232,12 @@ onMounted(() => {
|
|||||||
<div style="box-sizing: border-box; padding: 20px; min-width: 300px">
|
<div style="box-sizing: border-box; padding: 20px; min-width: 300px">
|
||||||
<RouterView v-slot="{ Component }" v-if="accountInfo?.isEmailVerified">
|
<RouterView v-slot="{ Component }" v-if="accountInfo?.isEmailVerified">
|
||||||
<KeepAlive>
|
<KeepAlive>
|
||||||
|
<Suspense>
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
|
<template #fallback>
|
||||||
|
<NSpin show />
|
||||||
|
</template>
|
||||||
|
</Suspense>
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
</RouterView>
|
</RouterView>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|||||||
13
src/views/NotfoundView.vue
Normal file
13
src/views/NotfoundView.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NButton, NElement, NLayoutContent, NResult } from 'naive-ui'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NLayoutContent tag="div" style="height: 100vh;position: relative;">
|
||||||
|
<NResult status="404" title="404" description="页面不存在" style="position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%);">
|
||||||
|
<template #footer>
|
||||||
|
<NButton type="primary" size="large" @click="$router.push({ name: 'index' })">返回首页</NButton>
|
||||||
|
</template>
|
||||||
|
</NResult>
|
||||||
|
</NLayoutContent>
|
||||||
|
</template>
|
||||||
@@ -1,31 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ACCOUNT, useAccount } from '@/api/account'
|
import { ACCOUNT } from '@/api/account'
|
||||||
import { AccountInfo } from '@/api/api-models'
|
import { AccountInfo } from '@/api/api-models'
|
||||||
import { QueryGetAPI } from '@/api/query'
|
import { QueryGetAPI } from '@/api/query'
|
||||||
import { ACCOUNT_API_URL, TURNSTILE_KEY } from '@/data/constants'
|
import { ACCOUNT_API_URL } from '@/data/constants'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import { NAlert, NButton, NCard, NLayoutContent, NSpace, NSpin, useMessage } from 'naive-ui'
|
import { NButton, NCard, NLayoutContent, NSpace, useMessage } from 'naive-ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import VueTurnstile from 'vue-turnstile'
|
|
||||||
|
|
||||||
const accountInfo = useAccount()
|
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const token = ref('')
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
|
||||||
async function VerifyAccount() {
|
async function VerifyAccount() {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
await QueryGetAPI<AccountInfo>(
|
await QueryGetAPI<AccountInfo>(ACCOUNT_API_URL + 'verify', {
|
||||||
ACCOUNT_API_URL + 'verify',
|
|
||||||
{
|
|
||||||
target: route.query.target,
|
target: route.query.target,
|
||||||
},
|
})
|
||||||
[['Turnstile', token.value]]
|
|
||||||
)
|
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
ACCOUNT.value = data.data
|
ACCOUNT.value = data.data
|
||||||
@@ -46,12 +38,9 @@ async function VerifyAccount() {
|
|||||||
<div style="display: flex; align-items: center; justify-content: center; height: 100%">
|
<div style="display: flex; align-items: center; justify-content: center; height: 100%">
|
||||||
<NCard embedded style="max-width: 500px">
|
<NCard embedded style="max-width: 500px">
|
||||||
<template #header> 激活账户 </template>
|
<template #header> 激活账户 </template>
|
||||||
<NSpin :show="!token">
|
|
||||||
<NSpace justify="center" align="center" vertical>
|
<NSpace justify="center" align="center" vertical>
|
||||||
<NButton @click="VerifyAccount" type="primary" size="large" :loading="isLoading || !token"> 进行账户激活 </NButton>
|
<NButton @click="VerifyAccount" type="primary" size="large" :loading="isLoading"> 进行账户激活 </NButton>
|
||||||
<VueTurnstile :site-key="TURNSTILE_KEY" v-model="token" theme="auto" style="text-align: center" />
|
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NSpin>
|
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
</NLayoutContent>
|
</NLayoutContent>
|
||||||
|
|||||||
161
src/views/VideoCollectListView.vue
Normal file
161
src/views/VideoCollectListView.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NavigateToNewTab } from '@/Utils'
|
||||||
|
import { VideoCollectCreateModel, VideoCollectDetail, VideoCollectVideo, VideoInfo, VideoStatus } from '@/api/api-models'
|
||||||
|
import { QueryGetAPI } from '@/api/query'
|
||||||
|
import { VIDEO_COLLECT_API_URL } from '@/data/constants'
|
||||||
|
import { Clock24Regular, Person24Regular, Question24Regular } from '@vicons/fluent'
|
||||||
|
import { useElementSize } from '@vueuse/core'
|
||||||
|
import { List } from 'linqts'
|
||||||
|
import { NAlert, NButton, NCard, NDivider, NElement, NEllipsis, NIcon, NImage, NLayoutContent, NList, NListItem, NProgress, NResult, NSpace, NText, NTooltip, useMessage } from 'naive-ui'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const card = ref()
|
||||||
|
|
||||||
|
const { width } = useElementSize(card)
|
||||||
|
|
||||||
|
const videoDetail = ref<VideoCollectDetail | null>(await get())
|
||||||
|
const acceptVideos = computed(() => {
|
||||||
|
return videoDetail.value?.videos.filter((v) => v.info.status == VideoStatus.Accepted)
|
||||||
|
})
|
||||||
|
const watchedVideos = computed(() => {
|
||||||
|
return videoDetail.value?.videos.filter((v) => v.video.watched == true) ?? []
|
||||||
|
})
|
||||||
|
const watchedTime = computed(() => {
|
||||||
|
return new List(watchedVideos.value).Sum((v) => v?.video.length ?? 0)
|
||||||
|
})
|
||||||
|
const totalTime = computed(() => {
|
||||||
|
return new List(videoDetail.value?.videos).Sum((v) => v?.video.length ?? 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function get() {
|
||||||
|
try {
|
||||||
|
const data = await QueryGetAPI<VideoCollectDetail>(VIDEO_COLLECT_API_URL + 'get', { id: route.params.id })
|
||||||
|
if (data.code == 200) {
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
message.error('获取失败')
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
function onClick(video: VideoCollectVideo) {
|
||||||
|
if (video.watched != true) {
|
||||||
|
video.watched = true
|
||||||
|
}
|
||||||
|
// 将视频对象移动到数组的末尾
|
||||||
|
const index = videoDetail.value?.videos.findIndex((v) => v.video == video) ?? -1
|
||||||
|
const tempVideo = videoDetail.value?.videos[index] ?? ({} as { info: VideoInfo; video: VideoCollectVideo })
|
||||||
|
if (index > -1) {
|
||||||
|
videoDetail.value?.videos.splice(index, 1)
|
||||||
|
videoDetail.value?.videos.push(tempVideo)
|
||||||
|
}
|
||||||
|
NavigateToNewTab('https://bilibili.com/video/' + video.id)
|
||||||
|
}
|
||||||
|
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 formatSecondsToTime(seconds: number): string {
|
||||||
|
const hours = Math.floor(seconds / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
const remainingSeconds = seconds % 60
|
||||||
|
|
||||||
|
const formattedHours = hours.toString().padStart(2, '0')
|
||||||
|
const formattedMinutes = minutes.toString().padStart(2, '0')
|
||||||
|
const formattedSeconds = remainingSeconds.toString().padStart(2, '0')
|
||||||
|
|
||||||
|
let formattedTime = ''
|
||||||
|
if (hours > 0) {
|
||||||
|
formattedTime += `${formattedHours}时`
|
||||||
|
}
|
||||||
|
if (minutes > 0) {
|
||||||
|
formattedTime += `${formattedMinutes}分`
|
||||||
|
}
|
||||||
|
formattedTime += `${formattedSeconds}秒`
|
||||||
|
|
||||||
|
return formattedTime
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NLayoutContent style="height: 100vh; position: relative">
|
||||||
|
<NResult v-if="!videoDetail" status="404" title="未找到指定视频征集表" description="请检查链接" />
|
||||||
|
<NCard v-else style="width: 600px; max-width: 90vw; top: 30px; margin: 0 auto">
|
||||||
|
<template #header> 视频征集表 | {{ videoDetail.table.name }} </template>
|
||||||
|
<template #header-extra>
|
||||||
|
<NTooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<NButton circle size="tiny">
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="Question24Regular" />
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
只会显示已通过的视频
|
||||||
|
</NTooltip>
|
||||||
|
</template>
|
||||||
|
<NProgress type="line" :percentage="Math.round((watchedTime / totalTime) * 100)" />
|
||||||
|
<NSpace justify="center" :size="5">
|
||||||
|
共 [<NText depth="3">{{ formatSecondsToTime(totalTime) }}</NText
|
||||||
|
>]
|
||||||
|
<NDivider vertical />
|
||||||
|
已观看 [<NText style="color: #4ea555">{{ formatSecondsToTime(watchedTime) }}</NText
|
||||||
|
>]
|
||||||
|
</NSpace>
|
||||||
|
<NDivider>
|
||||||
|
共 {{ acceptVideos?.length }} 条
|
||||||
|
<NDivider vertical />
|
||||||
|
已观看 {{ watchedVideos.length }} 条
|
||||||
|
</NDivider>
|
||||||
|
<NAlert v-if="watchedVideos.length == acceptVideos?.length" type="success">
|
||||||
|
已观看全部视频
|
||||||
|
</NAlert>
|
||||||
|
<NList ref="card">
|
||||||
|
<NListItem v-for="item in acceptVideos" v-bind:key="item.info.bvid">
|
||||||
|
<NCard size="small" :hoverable="!item.video.watched" :embedded="!item.video.watched">
|
||||||
|
<NSpace>
|
||||||
|
<NImage :src="item.video.cover + '@100h'" lazy :img-props="{ referrerpolicy: 'no-referrer' }" height="75" @click="onClick(item.video)" preview-disabled style="cursor: pointer"/>
|
||||||
|
<NSpace vertical :size="5">
|
||||||
|
<NButton style="width: 100%; max-width: 100px" @click="onClick(item.video)" text>
|
||||||
|
<NText :title="item.video.title" :delete="item.video.watched" :style="`color: ${item.video.watched ? '#a54e4e' : ''};width: ${width - 20}px;`">
|
||||||
|
{{ item.video.title }}
|
||||||
|
</NText>
|
||||||
|
</NButton>
|
||||||
|
<NText depth="3" style="white-space: pre-line; font-size: small">
|
||||||
|
<NEllipsis line-clamp="1">
|
||||||
|
<template #tooltip>
|
||||||
|
<div style="white-space: pre-line; max-width: 300px">
|
||||||
|
{{ item.video.description }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
{{ item.video.description }}
|
||||||
|
</NEllipsis>
|
||||||
|
</NText>
|
||||||
|
<NSpace style="font-size: 12px">
|
||||||
|
<NSpace>
|
||||||
|
<NIcon :component="Clock24Regular" />
|
||||||
|
{{ formatSeconds(item.video.length) }}
|
||||||
|
</NSpace>
|
||||||
|
<NSpace>
|
||||||
|
<NIcon :component="Person24Regular" />
|
||||||
|
{{ item.video.ownerName }}
|
||||||
|
</NSpace>
|
||||||
|
</NSpace>
|
||||||
|
</NSpace>
|
||||||
|
</NSpace>
|
||||||
|
</NCard>
|
||||||
|
</NListItem>
|
||||||
|
</NList>
|
||||||
|
</NCard>
|
||||||
|
</NLayoutContent>
|
||||||
|
</template>
|
||||||
96
src/views/VideoCollectPublic.vue
Normal file
96
src/views/VideoCollectPublic.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { VideoCollectDetail, VideoCollectTable } from '@/api/api-models'
|
||||||
|
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||||
|
import VideoCollectInfoCard from '@/components/VideoCollectInfoCard.vue'
|
||||||
|
import { TURNSTILE_KEY, VIDEO_COLLECT_API_URL } from '@/data/constants'
|
||||||
|
import { NAlert, NButton, NCard, NDivider, NInput, NInputNumber, NLayoutContent, NResult, NSpace, NText, useMessage } from 'naive-ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import VueTurnstile from 'vue-turnstile'
|
||||||
|
|
||||||
|
interface AddVideoModel {
|
||||||
|
id: string
|
||||||
|
video: string
|
||||||
|
name: string
|
||||||
|
uid: number
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const token = ref('')
|
||||||
|
const turnstile = ref()
|
||||||
|
|
||||||
|
const table = ref<VideoCollectTable | null>(await get())
|
||||||
|
const addModel = ref({} as AddVideoModel)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
async function get() {
|
||||||
|
try {
|
||||||
|
const data = await QueryGetAPI<VideoCollectDetail>(VIDEO_COLLECT_API_URL + 'get', { id: route.params.id })
|
||||||
|
if (data.code == 200) {
|
||||||
|
return data.data.table
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
message.error('获取失败')
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
async function add() {
|
||||||
|
if (!addModel.value.video) {
|
||||||
|
message.error('请输入视频')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading.value = true
|
||||||
|
addModel.value.id = table.value?.id ?? route.params.id.toString()
|
||||||
|
await QueryPostAPI(VIDEO_COLLECT_API_URL + 'add', addModel.value, [['Turnstile', token.value]])
|
||||||
|
.then((data) => {
|
||||||
|
if (data.code == 200) {
|
||||||
|
message.success('已成功推荐视频')
|
||||||
|
setTimeout(() => {
|
||||||
|
location.reload()
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
message.error('添加失败: ' + data.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
message.error('添加失败')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
turnstile.value?.reset()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NLayoutContent style="position: relative; height: 100vh">
|
||||||
|
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)">
|
||||||
|
<NResult v-if="!table" status="404" title="指定收集表不存在" description="检查一下你输入的链接吧" />
|
||||||
|
<NCard v-else style="width: 500px; max-width: 90vw">
|
||||||
|
<template #header>
|
||||||
|
视频征集
|
||||||
|
<NDivider vertical />
|
||||||
|
<NButton text @click="$router.push({ name: 'user-index', params: { id: table.owner?.name } })">
|
||||||
|
<NText depth="3" style="font-size: 14px">{{ table.owner?.name }}</NText>
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
<VideoCollectInfoCard :item="table" />
|
||||||
|
<NDivider />
|
||||||
|
<NAlert v-if="table.isFinish" type="error" title="该征集表已截止" />
|
||||||
|
<NSpace v-else vertical>
|
||||||
|
<NInput v-model:value="addModel.video" placeholder="B站视频链接或BVID" />
|
||||||
|
<NInput v-model:value="addModel.name" placeholder="(选填) 推荐人" />
|
||||||
|
<NInputNumber v-model:value="addModel.uid" placeholder="(选填) 推荐人UId" :show-button="false" />
|
||||||
|
<NInput v-model:value="addModel.description" placeholder="(选填) 推荐理由" />
|
||||||
|
<NButton @click="add" type="primary" :loading="isLoading || !token"> 推荐视频 </NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NCard>
|
||||||
|
<VueTurnstile ref="turnstile" :site-key="TURNSTILE_KEY" v-model="token" theme="auto" style="text-align: center" />
|
||||||
|
</div>
|
||||||
|
</NLayoutContent>
|
||||||
|
</template>
|
||||||
@@ -18,6 +18,7 @@ const newEmailAddress = ref('')
|
|||||||
const newEmailVerifyCode = ref('')
|
const newEmailVerifyCode = ref('')
|
||||||
const canSendEmailVerifyCode = ref(true)
|
const canSendEmailVerifyCode = ref(true)
|
||||||
|
|
||||||
|
const oldPassword = ref('')
|
||||||
const newPassword = ref('')
|
const newPassword = ref('')
|
||||||
const newPassword2 = ref('')
|
const newPassword2 = ref('')
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { VideoCollectCreateModel, VideoCollectDetail, VideoCollectTable, VideoCollectVideo, VideoFrom, VideoInfo, VideoStatus } from '@/api/api-models'
|
||||||
|
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||||
|
import VideoCollectInfoCard from '@/components/VideoCollectInfoCard.vue'
|
||||||
|
import { VIDEO_COLLECT_API_URL } from '@/data/constants'
|
||||||
|
import router from '@/router'
|
||||||
|
import { Clock24Filled, Person24Filled } from '@vicons/fluent'
|
||||||
|
import { useWindowSize } from '@vueuse/core'
|
||||||
|
import { formatDuration } from 'date-fns'
|
||||||
|
import { List } from 'linqts'
|
||||||
|
import {
|
||||||
|
FormRules,
|
||||||
|
NButton,
|
||||||
|
NCard,
|
||||||
|
NDatePicker,
|
||||||
|
NDivider,
|
||||||
|
NEllipsis,
|
||||||
|
NEmpty,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NGrid,
|
||||||
|
NGridItem,
|
||||||
|
NIcon,
|
||||||
|
NImage,
|
||||||
|
NInput,
|
||||||
|
NInputNumber,
|
||||||
|
NModal,
|
||||||
|
NPopconfirm,
|
||||||
|
NScrollbar,
|
||||||
|
NSpace,
|
||||||
|
NTabPane,
|
||||||
|
NTabs,
|
||||||
|
NTag,
|
||||||
|
NText,
|
||||||
|
NThing,
|
||||||
|
useMessage,
|
||||||
|
} from 'naive-ui'
|
||||||
|
import Qrcode from 'qrcode.vue'
|
||||||
|
import { VNode, computed, h, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const message = useMessage()
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
|
||||||
|
const shareModalVisiable = ref(false)
|
||||||
|
const editModalVisiable = ref(false)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const defaultModel = { maxVideoCount: 50 } as VideoCollectCreateModel
|
||||||
|
const updateModel = ref<VideoCollectCreateModel>(JSON.parse(JSON.stringify(defaultModel)))
|
||||||
|
|
||||||
|
const videoDetail = ref<VideoCollectDetail>(await getData())
|
||||||
|
|
||||||
|
const createRules: FormRules = {
|
||||||
|
name: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入征集表名称',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
endAt: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入结束日期',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '结束时间不能低于一小时',
|
||||||
|
validator: (rule: unknown, value: string) => {
|
||||||
|
const date = new Date(value)
|
||||||
|
if (date.getTime() < new Date().getTime() + 1000 * 60 * 60) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
maxVideoCount: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入最大视频数量',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '视频不能少于1个',
|
||||||
|
trigger: ['input', 'blur'],
|
||||||
|
validator: (rule: unknown, value: string) => {
|
||||||
|
if (Number(value) < 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const paddingVideos = computed(() => {
|
||||||
|
return videoDetail.value.videos.filter((v) => v.info.status == VideoStatus.Pending)
|
||||||
|
})
|
||||||
|
const rejectVideos = computed(() => {
|
||||||
|
return videoDetail.value.videos.filter((v) => v.info.status == VideoStatus.Rejected)
|
||||||
|
})
|
||||||
|
const acceptVideos = computed(() => {
|
||||||
|
return videoDetail.value.videos.filter((v) => v.info.status == VideoStatus.Accepted)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
try {
|
||||||
|
const data = await QueryGetAPI<VideoCollectDetail>(VIDEO_COLLECT_API_URL + 'get', { id: route.params.id })
|
||||||
|
if (data.code == 200) {
|
||||||
|
updateModel.value = {
|
||||||
|
id: data.data.table.id,
|
||||||
|
name: data.data.table.name,
|
||||||
|
endAt: data.data.table.endAt,
|
||||||
|
description: data.data.table.description,
|
||||||
|
maxVideoCount: data.data.table.maxVideoCount,
|
||||||
|
} as VideoCollectCreateModel
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
message.error('获取失败')
|
||||||
|
}
|
||||||
|
return {} as VideoCollectDetail
|
||||||
|
}
|
||||||
|
const 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,
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const paddingButtonGroup = (v: VideoInfo) =>
|
||||||
|
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) }, () => '拒绝'),
|
||||||
|
])
|
||||||
|
const acceptButtonGroup = (v: VideoInfo) =>
|
||||||
|
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) }, () => '拒绝'),
|
||||||
|
])
|
||||||
|
const rejectButtonGroup = (v: VideoInfo) =>
|
||||||
|
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', {
|
||||||
|
id: videoDetail.value.table.id,
|
||||||
|
bvid: video.bvid,
|
||||||
|
status: status,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data.code == 200) {
|
||||||
|
video.status = status
|
||||||
|
message.success('设置成功')
|
||||||
|
} else {
|
||||||
|
message.error('设置失败: ' + data.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
message.error('设置失败')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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 dateDisabled(ts: number) {
|
||||||
|
return ts < Date.now() + 1000 * 60 * 60
|
||||||
|
}
|
||||||
|
function updateTable() {
|
||||||
|
isLoading.value = true
|
||||||
|
updateModel.value.id = videoDetail.value.table.id
|
||||||
|
QueryPostAPI<VideoCollectTable>(VIDEO_COLLECT_API_URL + 'update', updateModel.value)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.code == 200) {
|
||||||
|
message.success('更新成功')
|
||||||
|
videoDetail.value.table = data.data
|
||||||
|
} else {
|
||||||
|
message.error('更新失败: ' + data.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
message.error('更新失败')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function deleteTable() {
|
||||||
|
isLoading.value = true
|
||||||
|
QueryGetAPI(VIDEO_COLLECT_API_URL + 'del', {
|
||||||
|
id: videoDetail.value.table.id,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data.code == 200) {
|
||||||
|
message.success('已删除')
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push({ name: 'manage-videoCollect' })
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
message.error('删除失败: ' + data.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
message.error('删除失败')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function closeTable() {
|
||||||
|
isLoading.value = true
|
||||||
|
QueryGetAPI(VIDEO_COLLECT_API_URL + 'finish', {
|
||||||
|
id: videoDetail.value.table.id,
|
||||||
|
finish: !videoDetail.value.table.isFinish,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data.code == 200) {
|
||||||
|
message.success('已' + (videoDetail.value.table.isFinish ? '开启表' : '关闭表'))
|
||||||
|
videoDetail.value.table.isFinish = !videoDetail.value.table.isFinish
|
||||||
|
} else {
|
||||||
|
message.error('关闭失败: ' + data.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
message.error('关闭失败')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<NSpace>
|
||||||
|
<NButton @click="$router.go(-1)" text>
|
||||||
|
<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>
|
||||||
|
<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">
|
||||||
|
<template v-if="width > 1000" #header-extra>
|
||||||
|
<NSpace>
|
||||||
|
<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>
|
||||||
|
<NPopconfirm :on-positive-click="deleteTable">
|
||||||
|
<template #trigger>
|
||||||
|
<NButton type="error" size="small"> 删除 </NButton>
|
||||||
|
</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="'https://vtsuru.live/video-collect/' + videoDetail.table.shortId" level="Q" :size="100" background="#00000000" foreground="#ffffff" :margin="1" />
|
||||||
|
<NInput :value="'https://vtsuru.live/video-collect/' + videoDetail.table.shortId" />
|
||||||
|
</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="结束征集的时间" :isDateDisabled="dateDisabled" />
|
||||||
|
<NDivider vertical />
|
||||||
|
<NText depth="3"> 最低为一小时 </NText>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem>
|
||||||
|
<NSpace>
|
||||||
|
<NButton type="primary" @click="updateTable" :loading="isLoading"> 更新 </NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
</NModal>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,49 +1,176 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAccount } from '@/api/account'
|
import { useAccount } from '@/api/account'
|
||||||
import { VideoCollectTable } from '@/api/api-models'
|
import { VideoCollectTable } from '@/api/api-models'
|
||||||
import { QueryGetAPI } from '@/api/query'
|
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||||
|
import VideoCollectInfoCard from '@/components/VideoCollectInfoCard.vue'
|
||||||
import { VIDEO_COLLECT_API_URL } from '@/data/constants'
|
import { VIDEO_COLLECT_API_URL } from '@/data/constants'
|
||||||
import { NCard, NDivider, NList, NListItem, NSpace, NSpin, useMessage } from 'naive-ui'
|
import { Clock24Filled } from '@vicons/fluent'
|
||||||
|
import {
|
||||||
|
CountdownProps,
|
||||||
|
FormRules,
|
||||||
|
NButton,
|
||||||
|
NCard,
|
||||||
|
NCountdown,
|
||||||
|
NDatePicker,
|
||||||
|
NDivider,
|
||||||
|
NEllipsis,
|
||||||
|
NEmpty,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NIcon,
|
||||||
|
NInput,
|
||||||
|
NInputNumber,
|
||||||
|
NList,
|
||||||
|
NListItem,
|
||||||
|
NModal,
|
||||||
|
NSpace,
|
||||||
|
NSpin,
|
||||||
|
NTag,
|
||||||
|
NText,
|
||||||
|
NTime,
|
||||||
|
NTooltip,
|
||||||
|
useMessage,
|
||||||
|
} from 'naive-ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const accountInfo = useAccount()
|
const accountInfo = useAccount()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const videoTables = ref<VideoCollectTable[]>([])
|
|
||||||
|
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
|
const createModalVisible = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
const defaultModel = { maxVideoCount: 50 } as VideoCollectTable
|
||||||
|
const createVideoModel = ref<VideoCollectTable>(JSON.parse(JSON.stringify(defaultModel)))
|
||||||
|
|
||||||
function get() {
|
const videoTables = ref<VideoCollectTable[]>(await get())
|
||||||
QueryGetAPI<VideoCollectTable[]>(VIDEO_COLLECT_API_URL + 'get-all')
|
|
||||||
.then((data) => {
|
const createRules: FormRules = {
|
||||||
|
name: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入征集表名称',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
endAt: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入结束日期',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '结束时间不能低于一小时',
|
||||||
|
validator: (rule: unknown, value: string) => {
|
||||||
|
const date = new Date(value)
|
||||||
|
if (date.getTime() < new Date().getTime() + 1000 * 60 * 60) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
maxVideoCount: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入最大视频数量',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '视频不能少于1个',
|
||||||
|
trigger: ['input', 'blur'],
|
||||||
|
validator: (rule: unknown, value: string) => {
|
||||||
|
if (Number(value) < 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
function dateDisabled(ts: number) {
|
||||||
|
return ts < Date.now() + 1000 * 60 * 60
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoading2 = ref(false)
|
||||||
|
async function get() {
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
|
const data = await QueryGetAPI<VideoCollectTable[]>(VIDEO_COLLECT_API_URL + 'get-all')
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
videoTables.value = data.data
|
//videoTables.value = data.data
|
||||||
|
return data.data
|
||||||
} else {
|
} else {
|
||||||
message.error('获取失败: ' + data.message)
|
message.error('获取失败: ' + data.message)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
message.error('获取失败')
|
||||||
|
return []
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createTable() {
|
||||||
|
formRef.value?.validate().then(async () => {
|
||||||
|
isLoading2.value = true
|
||||||
|
QueryPostAPI<VideoCollectTable>(VIDEO_COLLECT_API_URL + 'create', createVideoModel.value)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.code == 200) {
|
||||||
|
videoTables.value.push(data.data)
|
||||||
|
createModalVisible.value = false
|
||||||
|
message.success('创建成功')
|
||||||
|
createVideoModel.value = JSON.parse(JSON.stringify(defaultModel))
|
||||||
|
} else {
|
||||||
|
message.error('创建失败: ' + data.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
message.error('获取失败')
|
message.error('创建失败')
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
isLoading.value = false
|
isLoading2.value = false
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NSpace> </NSpace>
|
<NSpace>
|
||||||
|
<NButton @click="createModalVisible = true" type="primary"> 新建征集表 </NButton>
|
||||||
|
</NSpace>
|
||||||
<NDivider />
|
<NDivider />
|
||||||
<NSpin :show="isLoading">
|
<NSpin :show="isLoading">
|
||||||
<NSpace justify="center">
|
<NSpace justify="center">
|
||||||
<NList>
|
<NEmpty v-if="videoTables.length == 0" />
|
||||||
<NListItem>
|
<NList v-else>
|
||||||
<NCard size="small">
|
<NListItem v-for="item in videoTables" :key="item.id">
|
||||||
<template #header> </template>
|
<VideoCollectInfoCard :item="item" canClick style="width: 500px; max-width: 70vw" />
|
||||||
</NCard>
|
|
||||||
</NListItem>
|
</NListItem>
|
||||||
</NList>
|
</NList>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
|
<NModal v-model:show="createModalVisible" preset="card" title="创建视频征集" style="width: 600px; max-width: 90vw">
|
||||||
|
<NForm ref="formRef" :model="createVideoModel" :rules="createRules">
|
||||||
|
<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" 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="结束征集的时间" :isDateDisabled="dateDisabled" />
|
||||||
|
<NDivider vertical />
|
||||||
|
<NText depth="3"> 最低为一小时 </NText>
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem>
|
||||||
|
<NSpace>
|
||||||
|
<NButton type="primary" @click="createTable" :loading="isLoading2"> 创建 </NButton>
|
||||||
|
</NSpace>
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
</NModal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user