Add Dashboard video publishing helper

This commit is contained in:
daixiawu 2026-06-29 02:35:51 +08:00
parent e430ecf2ef
commit c7395f6f57
4 changed files with 355 additions and 1 deletions

View File

@ -6292,6 +6292,75 @@ body::after {
border-radius: 0 0 14px 14px;
}
/* ========== Video Helper (视频助手) ========== */
.video-helper-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
gap: 12px;
}
.video-helper-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 14px 16px;
min-width: 0;
}
.video-helper-card:hover {
border-color: rgba(79,140,255,0.25);
}
.video-helper-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
.video-helper-title-wrap {
display: flex;
flex-direction: column;
gap: 5px;
min-width: 0;
}
.video-helper-platform {
display: inline-flex;
align-items: center;
width: fit-content;
padding: 3px 8px;
border-radius: 4px;
background: rgba(79,140,255,0.1);
color: var(--accent-blue);
font-size: 11px;
font-weight: 700;
}
.video-helper-title {
color: var(--text-primary);
font-size: 15px;
font-weight: 700;
line-height: 1.35;
}
.video-helper-note {
color: var(--text-muted);
font-size: 12px;
line-height: 1.5;
margin-bottom: 10px;
}
.video-helper-content {
margin: 0;
padding: 10px 12px;
min-height: 54px;
max-height: 260px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-primary);
color: var(--text-secondary);
font-family: inherit;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
/* ========== Art Development ========== */
.artdev-module .module-body {
padding: 0;

View File

@ -863,6 +863,7 @@
<!-- Sub-tabs -->
<div class="sub-tabs" id="sns-sub-tabs">
<button class="sub-tab" data-sub="tools">常用工具</button>
<button class="sub-tab" data-sub="video">视频助手</button>
<button class="sub-tab" data-sub="xiaoheihe">小黑盒</button>
<button class="sub-tab active" data-sub="steam">Steam商店</button>
<button class="sub-tab" data-sub="bilibili">Bilibili</button>
@ -889,6 +890,20 @@
<div id="qr-list"></div>
</div>
<!-- Sub-panel: 视频助手 -->
<div id="sub-sns-video" class="sub-panel">
<div class="sns-header-bar">
<div class="sns-header-left">
<span class="sns-header-title">视频助手</span>
<span class="sns-header-count" id="video-helper-count">加载中...</span>
</div>
</div>
<div class="toolbar">
<input type="text" id="video-helper-search" class="search-input" placeholder="搜索平台、标题或标签...">
</div>
<div id="video-helper-list"></div>
</div>
<!-- Sub-panel: 小黑盒 -->
<div id="sub-sns-xiaoheihe" class="sub-panel">
<div class="sns-header-bar">
@ -1398,6 +1413,7 @@
<script src="js/sns.js"></script>
<script src="js/community_monitor.js"></script>
<script src="js/quick_replies.js"></script>
<script src="js/video_helper.js"></script>
<script src="js/form_helper.js"></script>
<script src="js/art_dev.js"></script>
<script src="js/email_processing.js"></script>

View File

@ -44,9 +44,11 @@ function snsInit() {
const targetPanel = document.getElementById('sub-sns-' + sub);
if (targetPanel) targetPanel.classList.add('active');
// tools 子标签走常用回复模块,其它走 SNS 平台数据
// tools/video 子标签走静态工具模块,其它走 SNS 平台数据
if (sub === 'tools') {
if (typeof quickRepliesInit === 'function') quickRepliesInit();
} else if (sub === 'video') {
if (typeof videoHelperInit === 'function') videoHelperInit();
} else {
currentSnsPlatform = sub;
snsLoadPlatform(currentSnsPlatform);

View File

@ -0,0 +1,267 @@
/* ============================================================
TH1 Dashboard - SNS Assistant / Video Helper
Static copy blocks for publishing trailers and platform tags.
============================================================ */
const videoHelperState = {
initialized: false,
items: [
{
id: 'bilibili-tags-hakurei-summer',
platform: 'Bilibili',
title: 'Bilibili Tag - 博丽帝国夏促宣传片',
note: 'B站投稿标签优先覆盖东方、策略、Steam、游戏宣传片。',
content: [
'帝国幻想乡',
'TOHOTOPIA',
'东方Project',
'东方同人游戏',
'博丽灵梦',
'博丽帝国',
'策略游戏',
'战棋游戏',
'4X游戏',
'独立游戏',
'Steam游戏',
'Steam夏促'
].join(' ')
},
{
id: 'youtube-tags-hakurei-summer',
platform: 'YouTube',
title: 'YouTube Tags - Hakurei Empire Summer Sale Trailer',
note: 'YouTube 标签栏使用逗号分隔,偏英文搜索。',
content: [
'TOHOTOPIA',
'Tohotopia',
'Touhou',
'Touhou Project',
'Touhou fangame',
'Touhou fan game',
'Hakurei Reimu',
'Reimu Hakurei',
'Hakurei Empire',
'Sumireko Usami',
'Kasen Ibaraki',
'Aunn Komano',
'Suika Ibuki',
'strategy game',
'4X strategy',
'turn based strategy',
'indie game',
'Steam game',
'Steam Summer Sale',
'free update',
'new faction',
'Polytopia'
].join(', ')
},
{
id: 'twitter-tags-hakurei-summer',
platform: 'X / Twitter',
title: 'X/Twitter Hashtags - 英文主推',
note: '主推文建议 2-4 个 hashtag不要塞太多。',
content: '#TOHOTOPIA #Touhou #IndieGame #SteamSummerSale'
},
{
id: 'twitter-tags-alt',
platform: 'X / Twitter',
title: 'X/Twitter Hashtags - 策略玩家向',
note: '当推文更强调玩法、4X、回合制时使用。',
content: '#TOHOTOPIA #TurnBasedStrategy #4XStrategy #IndieGame'
},
{
id: 'youtube-title-hakurei',
platform: 'YouTube',
title: 'YouTube Title - 英文标题',
note: '适合英文版宣传片。',
content: 'Hakurei Empire is Here! Free Faction Update & Steam Summer Sale | TOHOTOPIA'
},
{
id: 'bilibili-title-hakurei',
platform: 'Bilibili',
title: 'Bilibili Title - 中文标题',
note: '延续“参赛确认”系列感。',
content: '博丽帝国参赛确认新阵营免费更新Steam夏促进行中'
},
{
id: 'twitter-post-hakurei',
platform: 'X / Twitter',
title: 'X/Twitter Post - 英文主推文',
note: '主推文建议原生上传视频Steam 链接放回复或正文末尾。',
content: [
'Free Update: Hakurei Empire is Here!',
'',
'TOHOTOPIA is 40% off during the Steam Summer Sale.',
'Build your Gensokyo empire with Reimu, Sumireko, Kasen, Aunn, and Suika.',
'',
'#TOHOTOPIA #Touhou #IndieGame #SteamSummerSale'
].join('\n')
},
{
id: 'steam-link-reply',
platform: 'X / Twitter',
title: 'X/Twitter Reply - Steam 链接回复',
note: '主推文不放外链时,第一条回复放这个。',
content: [
'Play TOHOTOPIA on Steam:',
'https://store.steampowered.com/app/3774440/TOHOTOPIA/'
].join('\n')
},
{
id: 'bilibili-description-hakurei',
platform: 'Bilibili',
title: 'Bilibili Description - 中文简介',
note: '发布中文版本视频时可直接改价格/群号后使用。',
content: [
'大家好,这里是《帝国幻想乡~Tohotopia》开发者 天火人雪糕!',
'',
'《帝国幻想乡~Tohotopia》新阵营【博丽帝国】现已免费更新',
'Steam 夏促同步进行中,欢迎加入愿望单 / 入手体验~',
'',
'--------------------------------',
'本视频是【博丽帝国】阵营宣传片!',
'',
'博丽帝国阵营——幻想乡的巫女、守护者与异变专家们正式参赛。',
'灵梦、堇子、华扇、阿吽、萃香将以全新的英雄与技能组合登场,在棋盘上展开一场神社势力的大会战!',
'',
'【King职阶英雄】博丽灵梦',
'【Queen职阶英雄】宇佐见堇子',
'【Bishop职阶英雄】茨木华扇',
'【Knight职阶英雄】高丽野阿吽',
'【Rook职阶英雄】伊吹萃香',
'',
'Steam商店页https://store.steampowered.com/app/3774440/TOHOTOPIA/',
'玩家群:请替换为群号',
'',
'如果你喜欢文明、Polytopia、战棋、东方同人策略游戏欢迎关注我们'
].join('\n')
},
{
id: 'youtube-description-hakurei',
platform: 'YouTube',
title: 'YouTube Description - English Description',
note: '适合英文版 YouTube 视频简介。',
content: [
'The Hakurei Empire is now available as a free update in TOHOTOPIA!',
'',
'A new faction joins the tournament with Reimu Hakurei, Sumireko Usami, Kasen Ibaraki, Aunn Komano, and Suika Ibuki. Build your empire, explore the board, and turn the battlefield into a very Gensokyo-style strategy game.',
'',
'Steam Summer Sale is live now.',
'',
'Steam page:',
'https://store.steampowered.com/app/3774440/TOHOTOPIA/',
'',
'#TOHOTOPIA #Touhou #IndieGame #SteamSummerSale'
].join('\n')
}
]
};
function videoHelperInit() {
if (!videoHelperState.initialized) {
videoHelperState.initialized = true;
const searchInput = document.getElementById('video-helper-search');
if (searchInput) {
searchInput.addEventListener('input', videoHelperRender);
}
}
videoHelperRender();
}
function videoHelperRender() {
const container = document.getElementById('video-helper-list');
const countEl = document.getElementById('video-helper-count');
if (!container) return;
const searchInput = document.getElementById('video-helper-search');
const term = searchInput ? searchInput.value.toLowerCase().trim() : '';
let items = videoHelperState.items.slice();
if (term) {
items = items.filter(item => {
const haystack = `${item.platform} ${item.title} ${item.note} ${item.content}`.toLowerCase();
return haystack.includes(term);
});
}
if (countEl) {
countEl.textContent = `${items.length} / ${videoHelperState.items.length}`;
}
if (items.length === 0) {
container.innerHTML = '<div class="sns-empty">没有匹配的视频发布文案</div>';
return;
}
container.innerHTML = `<div class="video-helper-grid">${items.map(videoHelperRenderCard).join('')}</div>`;
}
function videoHelperRenderCard(item) {
return `<div class="video-helper-card" data-id="${videoHelperEscHtml(item.id)}">
<div class="video-helper-card-header">
<div class="video-helper-title-wrap">
<span class="video-helper-platform">${videoHelperEscHtml(item.platform)}</span>
<span class="video-helper-title">${videoHelperEscHtml(item.title)}</span>
</div>
<button class="qr-copy-btn" onclick="videoHelperCopy('${videoHelperEscAttr(item.id)}')" title="复制纯文本">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
复制
</button>
</div>
<div class="video-helper-note">${videoHelperEscHtml(item.note || '')}</div>
<pre class="video-helper-content">${videoHelperEscHtml(item.content || '')}</pre>
</div>`;
}
async function videoHelperCopy(id) {
const item = videoHelperState.items.find(entry => entry.id === id);
if (!item) return;
try {
await videoHelperCopyText(item.content || '');
videoHelperToast('已复制到剪贴板', 'success');
} catch (e) {
console.error('Video helper copy failed:', e);
videoHelperToast('复制失败: ' + e.message, 'error');
}
}
async function videoHelperCopyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
function videoHelperEscHtml(str) {
if (str === undefined || str === null) return '';
const div = document.createElement('div');
div.textContent = String(str);
return div.innerHTML;
}
function videoHelperEscAttr(str) {
return String(str || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
function videoHelperToast(message, type) {
if (typeof showToast === 'function') {
showToast(message, type || 'success');
return;
}
if (typeof qrToast === 'function') {
qrToast(message, type || 'success');
return;
}
alert(message);
}