-
+
@@ -1024,43 +937,182 @@ async function uploadConfigToServer() {
.danmuji-split {
flex: 1;
- min-height: 0; /* 重要:防止flex子项超出容器 */
+ min-height: 0;
}
.left-panel-scroll-container {
height: 100%;
- overflow-y: auto;
- padding: 0;
+ background-color: var(--n-color);
}
-.danmuji-obs-preview {
- --danmuji-bg: #333;
- background-color: #222;
- background-image: linear-gradient(45deg, var(--danmuji-bg) 25%, transparent 25%),
- linear-gradient(-45deg, var(--danmuji-bg) 25%, transparent 25%),
- linear-gradient(45deg, transparent 75%, var(--danmuji-bg) 75%),
- linear-gradient(-45deg, transparent 75%, var(--danmuji-bg) 75%);
+.obs-link-card {
+ flex-shrink: 0;
+}
+
+.obs-label {
+ display: flex;
+ flex-direction: column;
+ margin-right: 12px;
+}
+
+.label-text {
+ font-weight: 500;
+}
+
+.label-desc {
+ font-size: 12px;
+ color: var(--n-text-color-3);
+}
+
+.main-tabs {
+ height: 100%;
+}
+
+.tab-content-wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ padding-top: 12px;
+}
+
+.editor-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.editor-title {
+ font-weight: 500;
+}
+
+.editor-container {
+ flex: 1;
+ min-height: 0;
+ border: 1px solid var(--n-border-color);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.config-scroll-container {
+ height: 100%;
+ overflow-y: auto;
+ padding-right: 4px;
+ padding-top: 4px;
+}
+
+/* 隐藏滚动条但保持可滚动 (Webkit) */
+.config-scroll-container::-webkit-scrollbar {
+ width: 6px;
+}
+.config-scroll-container::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, 0.2);
+ border-radius: 3px;
+}
+.config-scroll-container::-webkit-scrollbar-track {
+ background-color: transparent;
+}
+
+.form-section-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--n-text-color-2);
+ margin-top: 16px;
+ margin-bottom: 8px;
+ padding-left: 4px;
+ border-left: 3px solid var(--n-primary-color);
+ line-height: 1;
+}
+.form-section-title:first-child {
+ margin-top: 0;
+}
+
+.checkbox-card {
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.checkbox-card:hover {
+ background-color: var(--n-action-color);
+}
+
+.right-panel-container {
+ height: 100%;
+ width: 100%;
+ padding: 16px;
+ box-sizing: border-box;
+ background-color: var(--n-color-embedded);
+ display: flex;
+ flex-direction: column;
+}
+
+.preview-window {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ background: #1a1a1a; /* 默认深色背景,模拟OBS */
+ border-radius: 8px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
+ overflow: hidden;
+ border: 1px solid var(--n-border-color);
+}
+
+.preview-toolbar {
+ height: 36px;
+ background: #2d2d2d;
+ display: flex;
+ align-items: center;
+ padding: 0 12px;
+ border-bottom: 1px solid #3d3d3d;
+}
+
+.window-controls {
+ display: flex;
+ gap: 6px;
+ margin-right: 16px;
+}
+
+.dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+}
+
+.dot.red { background: #ff5f56; }
+.dot.yellow { background: #ffbd2e; }
+.dot.green { background: #27c93f; }
+
+.address-bar {
+ flex: 1;
+ background: #1a1a1a;
+ height: 24px;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 11px;
+ color: #888;
+ user-select: none;
+}
+
+.preview-content {
+ flex: 1;
+ min-height: 0;
+ position: relative;
+ /* 棋盘格背景 */
+ background-color: #1a1a1a;
+ background-image:
+ linear-gradient(45deg, #222 25%, transparent 25%),
+ linear-gradient(-45deg, #222 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, #222 75%),
+ linear-gradient(-45deg, transparent 75%, #222 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
}
-/* 优化滚动条样式 */
-:deep(::-webkit-scrollbar) {
- width: 8px;
- height: 8px;
+:deep(.n-card-header) {
+ padding: 12px 16px 8px 16px;
}
-
-:deep(::-webkit-scrollbar-track) {
- background: rgba(0, 0, 0, 0.05);
- border-radius: 4px;
-}
-
-:deep(::-webkit-scrollbar-thumb) {
- background: rgba(0, 0, 0, 0.2);
- border-radius: 4px;
-}
-
-:deep(::-webkit-scrollbar-thumb:hover) {
- background: rgba(0, 0, 0, 0.3);
+:deep(.n-card__content) {
+ padding: 0 16px 12px 16px;
}
diff --git a/src/views/manage/LiveManager.vue b/src/views/manage/LiveManager.vue
index 114597a..f350cf6 100644
--- a/src/views/manage/LiveManager.vue
+++ b/src/views/manage/LiveManager.vue
@@ -1,6 +1,11 @@
-
+
-
-
- 尚未进行Bilibili认证
-
+
+
+
+ 尚未进行Bilibili认证,部分功能可能受限。
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+ ]" style="width: 140px">
+
+
+
+
+
+
+ 排序:
+
+
+
+
+
+
+
+
+ 自动刷新
+ 自动刷新
+
+
+ s
+
+
+
+
+
+ 刷新
+
+
+
-
-
- 自动刷新
- 自动刷新
-
-
-
- 刷新
-
-
-
-
-
-
-
-
+
+
+
-
+
- 重试
+ 重新加载
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/manage/QuestionBoxManageView.vue b/src/views/manage/QuestionBoxManageView.vue
index e154500..10cfd3d 100644
--- a/src/views/manage/QuestionBoxManageView.vue
+++ b/src/views/manage/QuestionBoxManageView.vue
@@ -1,7 +1,7 @@
-
-
- 启用日程表
-
-
-
-
- 添加周程
-
-
- 修改模板
-
-
-
- 日程表展示页链接
-
-
-
-
+
- 复制
+ 添加周程
-
-
-
- 订阅链接
-
-
-
-
-
-
- 通过订阅链接可以订阅日程表到日历软件中
-
-
-
-
-
-
- 复制
+
+ 修改模板
-
-
-
+
+
+
+ 日程表展示页链接
+
+
+
+
+
+ 复制
+
+
+
+
+ 订阅链接
+
+
+
+
+
+
+ 通过订阅链接可以订阅日程表到日历软件中
+
+
+
+
+
+
+ 复制
+
+
+
+
+
{
-
-
-
- 启用歌单
-
-
-
-
-
-
- 添加歌曲
-
-
- 修改展示模板
-
-
- 导出为 CSV
-
-
- 前往点播管理页
-
-
- 前往歌单展示页
-
- {
- getSongs()
- message.success('完成')
- }"
- >
- 刷新
-
-
-
-
-
- 歌单展示页链接
-
-
-
-
+
+
+
+ 添加歌曲
+
+
+ 修改展示模板
+
+
+ 导出为 CSV
+
- 复制
+ 前往点播管理页
-
-
-
+
+ 前往歌单展示页
+
+ {
+ getSongs()
+ message.success('完成')
+ }"
+ >
+ 刷新
+
+
+
+
+
+ 歌单展示页链接
+
+
+
+
+
+ 复制
+
+
+
+
+
{
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(`${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 () => {
-
-
-
- {{ '< 返回' }}
-
-
-
-
- 分享
-
-
- 更新
-
-
- {{ videoDetail.table.isFinish ? '开启表' : '关闭表' }}
-
-
- 结果页面
-
-
-
-
- 删除
-
-
- 确定删除表? 此操作无法撤销
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ 已通过视频总时长:
+
+ {{ formatSeconds(new List(acceptVideos).Sum((v) => v?.video.length ?? 0)) }}
-
-
-
+
+
+
+
+
+
+
+
+
+
+ 待审核
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 已通过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 已拒绝
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
(e.target as HTMLInputElement).select()"
+ />
+
+ 保存二维码图片
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- 更新
+ 保存更改
-
-
-
-
+
+
+
+
+
+
diff --git a/src/views/manage/VideoCollectManageView.vue b/src/views/manage/VideoCollectManageView.vue
index e9ecc6d..634176b 100644
--- a/src/views/manage/VideoCollectManageView.vue
+++ b/src/views/manage/VideoCollectManageView.vue
@@ -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() {
-
- 在个人主页展示进行中的征集表
-
-
-
-
-
+
- 新建征集表
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
+
+ 新建征集表
+
+
+
+
+
+
+
+
+
+ 创建第一个征集表
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
- 最低为一小时
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/manage/point/PointManage.vue b/src/views/manage/point/PointManage.vue
index 2272f95..7d59232 100644
--- a/src/views/manage/point/PointManage.vue
+++ b/src/views/manage/point/PointManage.vue
@@ -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(() => { })