Expand publishing and virtual product workflows

This commit is contained in:
王鹏
2026-09-30 11:00:01 +08:00
parent 4ed8743934
commit b904dcf52e
54 changed files with 2185 additions and 108 deletions

View File

@@ -0,0 +1,151 @@
:root {
color-scheme: light;
font-family: Arial, "Microsoft YaHei", sans-serif;
color: #273142;
background: #f6f8fb;
}
* { box-sizing: border-box; }
body { min-width: 300px; margin: 0; background: #f6f8fb; }
button { font: inherit; }
.panel-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 18px 16px 14px;
background: #fff;
border-bottom: 1px solid #e7ebf1;
}
.panel-header h1 { margin: 0; font-size: 17px; line-height: 24px; }
.panel-header p { margin: 6px 0 0; color: #7a8495; font-size: 12px; line-height: 1.5; }
.header-mark {
display: inline-grid;
width: 20px;
height: 20px;
margin-right: 4px;
place-items: center;
color: #fff;
background: #28b66f;
border-radius: 5px;
}
.header-action,
.retry-button {
border: 0;
cursor: pointer;
transition: background .15s ease, color .15s ease, transform .15s ease;
}
.header-action {
width: 32px;
height: 32px;
color: #657084;
background: #f1f4f8;
border-radius: 8px;
font-size: 19px;
}
.header-action:hover { color: #1677ff; background: #eaf3ff; }
main { padding: 12px 12px 54px; }
.notice {
margin-bottom: 10px;
padding: 9px 11px;
color: #9b5b00;
background: #fff7e8;
border: 1px solid #ffe0a3;
border-radius: 8px;
font-size: 12px;
line-height: 1.5;
}
.notice.is-success { color: #237849; background: #edf9f2; border-color: #bce8cd; }
.task-list { display: grid; gap: 9px; }
.task-card {
display: flex;
align-items: center;
min-height: 66px;
background: #fff;
border: 1px solid #e5e9f0;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(35, 47, 70, .04);
overflow: hidden;
}
.task-main {
display: flex;
flex: 1;
align-items: center;
min-width: 0;
padding: 11px 8px 11px 12px;
text-align: left;
background: transparent;
border: 0;
cursor: pointer;
}
.task-main:hover { background: #fafcff; }
.platform-icon {
display: inline-grid;
flex: 0 0 34px;
width: 34px;
height: 34px;
margin-right: 10px;
place-items: center;
color: #fff;
border-radius: 9px;
font-size: 14px;
font-weight: 700;
}
.platform-xiaohongshu { background: #ff2442; }
.platform-douyin { background: #15171d; }
.platform-wechat_channels { background: #19b875; }
.platform-bilibili { background: #00aeec; }
.platform-xianyu { color: #322d00; background: #ffe500; }
.task-copy { display: flex; flex: 1; min-width: 0; flex-direction: column; }
.platform-name { color: #273142; font-size: 14px; line-height: 20px; }
.task-title {
margin-top: 2px;
overflow: hidden;
color: #8a93a3;
font-size: 11px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-line { display: flex; align-items: center; margin-top: 4px; color: #697386; font-size: 11px; }
.status-line i { width: 6px; height: 6px; margin-right: 5px; background: #9aa3b2; border-radius: 50%; }
.status-line.is-running i { background: #1677ff; box-shadow: 0 0 0 3px #e8f2ff; }
.status-line.is-ready i { background: #28b66f; box-shadow: 0 0 0 3px #e8f8ef; }
.status-line.is-failed i { background: #f04438; box-shadow: 0 0 0 3px #ffedeb; }
.status-line.is-login i { background: #f59f00; box-shadow: 0 0 0 3px #fff3d6; }
.retry-button {
flex: 0 0 42px;
width: 42px;
height: 42px;
margin-right: 8px;
color: #667085;
background: #f5f7fa;
border-radius: 9px;
font-size: 21px;
}
.retry-button:hover { color: #1677ff; background: #eaf3ff; transform: rotate(-20deg); }
.retry-button:disabled { color: #b7bec9; background: #f5f6f8; cursor: not-allowed; transform: none; }
.retry-button.is-loading { animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.empty-state { padding: 54px 24px; color: #7b8595; text-align: center; }
.empty-state .empty-icon { margin-bottom: 12px; color: #a9b2c0; font-size: 34px; }
.empty-state strong { display: block; color: #4b5567; font-size: 14px; }
.empty-state p { margin: 8px 0 0; font-size: 12px; line-height: 1.6; }
footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
padding: 10px 14px;
color: #7f8998;
background: rgba(255, 255, 255, .96);
border-top: 1px solid #e7ebf1;
font-size: 11px;
}
.connection-dot { width: 7px; height: 7px; margin-right: 6px; background: #28b66f; border-radius: 50%; }
.version { margin-left: auto; }

View File

@@ -0,0 +1,50 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>多平台发布助手</title>
<link rel="stylesheet" href="panel.css">
</head>
<body>
<header class="panel-header">
<div>
<h1><span class="header-mark">✓</span> 多平台发布助手</h1>
<p>点击平台名称定位页面,点击刷新图标重新填写</p>
</div>
<button id="refresh-list" class="header-action" type="button" title="刷新任务列表" aria-label="刷新任务列表">↻</button>
</header>
<main>
<div id="notice" class="notice" hidden></div>
<div id="task-list" class="task-list"></div>
<section id="empty-state" class="empty-state" hidden>
<div class="empty-icon">⇧</div>
<strong>暂无发布任务</strong>
<p>请先在南音工作室系统的“发布管理”中创建任务。</p>
</section>
</main>
<footer>
<span id="connection-dot" class="connection-dot"></span>
<span>扩展已连接</span>
<span id="version" class="version"></span>
</footer>
<template id="task-template">
<article class="task-card">
<button class="task-main" type="button">
<span class="platform-icon"></span>
<span class="task-copy">
<strong class="platform-name"></strong>
<small class="task-title"></small>
<span class="status-line"><i></i><span></span></span>
</span>
</button>
<button class="retry-button" type="button" title="重新打开并重试" aria-label="重新打开并重试">↻</button>
</article>
</template>
<script src="panel.js"></script>
</body>
</html>

View File

@@ -0,0 +1,103 @@
const PLATFORM_META = Object.freeze({
xiaohongshu: { short: '红', className: 'platform-xiaohongshu' },
douyin: { short: '抖', className: 'platform-douyin' },
wechat_channels: { short: '视', className: 'platform-wechat_channels' },
bilibili: { short: 'B', className: 'platform-bilibili' },
xianyu: { short: '闲', className: 'platform-xianyu' }
})
const STATUS_META = Object.freeze({
PENDING: { text: '等待扩展接收', className: '' },
OPENING: { text: '正在打开平台', className: 'is-running' },
LOGIN_REQUIRED: { text: '需要登录', className: 'is-login' },
DOWNLOADING: { text: '正在读取素材', className: 'is-running' },
UPLOADING: { text: '正在上传素材', className: 'is-running' },
FILLING: { text: '正在填写资料', className: 'is-running' },
READY_TO_PUBLISH: { text: '等待手动发布', className: 'is-ready' },
SUCCEEDED: { text: '发布成功', className: 'is-ready' },
FAILED: { text: '处理失败,可重试', className: 'is-failed' },
CANCELLED: { text: '页面已关闭,可重试', className: 'is-failed' }
})
const taskList = document.getElementById('task-list')
const emptyState = document.getElementById('empty-state')
const notice = document.getElementById('notice')
const template = document.getElementById('task-template')
document.getElementById('version').textContent = 'v' + chrome.runtime.getManifest().version
document.getElementById('refresh-list').addEventListener('click', loadTasks)
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'local') return
if (Object.keys(changes).some(key => key.startsWith('publisher_task_'))) loadTasks()
})
async function loadTasks() {
const response = await sendMessage({ type: 'SIDEPANEL_GET_TASKS' })
if (!response || !response.ok) {
showNotice(response && response.message || '读取发布任务失败')
return
}
renderTasks(response.tasks || [])
}
function renderTasks(tasks) {
taskList.replaceChildren()
emptyState.hidden = tasks.length > 0
for (const task of tasks) {
const card = template.content.firstElementChild.cloneNode(true)
const meta = PLATFORM_META[task.platform] || { short: '?', className: '' }
const status = STATUS_META[task.status] || { text: task.status || '未知状态', className: '' }
const icon = card.querySelector('.platform-icon')
icon.textContent = meta.short
icon.classList.add(meta.className)
card.querySelector('.platform-name').textContent = task.platformName || task.platform
card.querySelector('.task-title').textContent = task.title || '当前发布任务'
const statusLine = card.querySelector('.status-line')
statusLine.classList.add(status.className)
statusLine.querySelector('span').textContent = task.statusMessage || status.text
card.querySelector('.task-main').addEventListener('click', async () => {
const response = await sendMessage({ type: 'SIDEPANEL_ACTIVATE_TASK', taskId: task.taskId })
if (!response || !response.ok) showNotice(response && response.message || '无法定位平台页面')
})
const retryButton = card.querySelector('.retry-button')
retryButton.disabled = !task.canRetry
retryButton.title = task.canRetry ? '重新打开并重试' : '已发布成功,不能重复执行'
retryButton.addEventListener('click', async () => {
retryButton.disabled = true
retryButton.classList.add('is-loading')
const response = await sendMessage({ type: 'SIDEPANEL_RETRY_TASK', taskId: task.taskId })
retryButton.classList.remove('is-loading')
if (!response || !response.ok) {
retryButton.disabled = !task.canRetry
showNotice(response && response.message || '重试失败')
return
}
showNotice('已重新打开' + (task.platformName || task.platform) + '并开始填写', true)
await loadTasks()
})
taskList.appendChild(card)
}
}
function showNotice(message, success = false) {
notice.textContent = message
notice.classList.toggle('is-success', success)
notice.hidden = false
window.setTimeout(() => { notice.hidden = true }, 4000)
}
function sendMessage(message) {
return new Promise(resolve => {
chrome.runtime.sendMessage(message, response => {
if (chrome.runtime.lastError) {
resolve({ ok: false, message: chrome.runtime.lastError.message })
return
}
resolve(response)
})
})
}
loadTasks()