This commit is contained in:
949036910@qq.com
2026-05-28 15:24:21 +08:00
parent 1a32a59635
commit bf22177e80
6 changed files with 387 additions and 40 deletions

View File

@@ -96,13 +96,23 @@ pub async fn publish_login(
params: PublishLoginParams,
) -> Result<Value, String> {
let _ = app_config;
let platform = params.platform.trim().to_string();
if platform.is_empty() {
return Err("platform 不能为空".to_string());
}
// 对齐 Electron先创建账号槽位再走扫码登录保证每个账号独立 userDataDir。
let (effective_account_id, created_slot) = if let Some(id) = params.account_id {
(id, false)
} else {
(store.create_account_slot(platform.clone()).await?.id, true)
};
let result = run_publish_script(
app,
window,
"publish_login.js",
json!({
"platform": params.platform,
"accountId": params.account_id,
"platform": platform,
"accountId": effective_account_id,
}),
)
.await?;
@@ -118,17 +128,23 @@ pub async fn publish_login(
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let avatar = result
.get("avatar")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let cookies = result
.get("cookies")
.map(|v| serde_json::to_string(v).unwrap_or_else(|_| "[]".to_string()))
.unwrap_or_else(|| "[]".to_string());
let rec = store
.upsert_login(
params.platform.clone(),
platform,
nickname,
uid,
avatar,
cookies,
params.account_id,
Some(effective_account_id),
)
.await?;
return Ok(json!({
@@ -138,6 +154,9 @@ pub async fn publish_login(
}));
}
if created_slot {
let _ = store.delete(effective_account_id).await;
}
Ok(result)
}

View File

@@ -138,6 +138,7 @@ impl PublishDb {
platform: &str,
nickname: &str,
uid: &str,
avatar: &str,
cookies_json: &str,
account_id: Option<i64>,
) -> Result<PlatformAccountRecord, PublishDbError> {
@@ -149,9 +150,9 @@ impl PublishDb {
if let Some(id) = account_id {
conn.execute(
"UPDATE platform_accounts SET nickname = ?1, uid = ?2, cookies = ?3,
login_status = 'active', expires_at = ?4, updated_at = ?5 WHERE id = ?6",
params![nickname, uid, cookies_json, expires, now, id],
"UPDATE platform_accounts SET nickname = ?1, uid = ?2, avatar = ?3, cookies = ?4,
login_status = 'active', expires_at = ?5, updated_at = ?6 WHERE id = ?7",
params![nickname, uid, avatar, cookies_json, expires, now, id],
)?;
return self
.get(id)?
@@ -159,11 +160,30 @@ impl PublishDb {
.ok_or_else(|| PublishDbError::Message("更新账号失败".into()));
}
if !uid.trim().is_empty() {
let mut stmt = conn.prepare(
"SELECT id FROM platform_accounts WHERE platform = ?1 AND uid = ?2 LIMIT 1",
)?;
let mut rows = stmt.query(params![platform, uid])?;
if let Some(row) = rows.next()? {
let existing_id: i64 = row.get(0)?;
conn.execute(
"UPDATE platform_accounts SET nickname = ?1, avatar = ?2, cookies = ?3,
login_status = 'active', expires_at = ?4, updated_at = ?5 WHERE id = ?6",
params![nickname, avatar, cookies_json, expires, now, existing_id],
)?;
return self
.get(existing_id)?
.map(|(r, _)| r)
.ok_or_else(|| PublishDbError::Message("更新账号失败".into()));
}
}
conn.execute(
"INSERT INTO platform_accounts (platform, nickname, uid, avatar, cookies, tokens,
login_status, expires_at, created_at, updated_at)
VALUES (?1, ?2, ?3, '', ?4, '', 'active', ?5, ?6, ?6)",
params![platform, nickname, uid, cookies_json, expires, now],
VALUES (?1, ?2, ?3, ?4, ?5, '', 'active', ?6, ?7, ?7)",
params![platform, nickname, uid, avatar, cookies_json, expires, now],
)?;
let id = conn.last_insert_rowid();
self.get(id)?
@@ -171,6 +191,23 @@ impl PublishDb {
.ok_or_else(|| PublishDbError::Message("插入账号失败".into()))
}
pub fn create_account_slot(&self, platform: &str) -> Result<PlatformAccountRecord, PublishDbError> {
let conn = self.conn.lock().map_err(|_| {
PublishDbError::Message("数据库锁异常".into())
})?;
let now = chrono::Utc::now().timestamp_millis();
conn.execute(
"INSERT INTO platform_accounts (platform, nickname, uid, avatar, cookies, tokens,
login_status, expires_at, created_at, updated_at)
VALUES (?1, '', '', '', '[]', '', 'inactive', NULL, ?2, ?2)",
params![platform, now],
)?;
let id = conn.last_insert_rowid();
self.get(id)?
.map(|(r, _)| r)
.ok_or_else(|| PublishDbError::Message("创建账号槽位失败".into()))
}
pub fn set_login_status(&self, id: i64, status: &str) -> Result<(), PublishDbError> {
let conn = self.conn.lock().map_err(|_| {
PublishDbError::Message("数据库锁异常".into())

View File

@@ -64,6 +64,7 @@ impl PublishStore {
platform: String,
nickname: String,
uid: String,
avatar: String,
cookies_json: String,
account_id: Option<i64>,
) -> Result<PlatformAccountRecord, String> {
@@ -72,6 +73,7 @@ impl PublishStore {
&platform,
&nickname,
&uid,
&avatar,
&cookies_json,
account_id,
)
@@ -80,6 +82,11 @@ impl PublishStore {
.await
}
pub async fn create_account_slot(&self, platform: String) -> Result<PlatformAccountRecord, String> {
self.with_db(move |db| db.create_account_slot(&platform).map_err(|e| e.to_string()))
.await
}
pub async fn set_login_status(&self, id: i64, status: String) -> Result<(), String> {
self.with_db(move |db| db.set_login_status(id, &status).map_err(|e| e.to_string()))
.await

View File

@@ -18,6 +18,8 @@ const {
const feedback = ref({ severity: "", message: "" });
const scheduleDialogVisible = ref(false);
const scheduleDate = ref(null);
const accountDialogVisible = ref(false);
const activePlatformKey = ref("douyin");
const hasVideo = computed(
() => Boolean(processedVideoPath.value || generatedVideoPath.value),
@@ -40,6 +42,20 @@ const accountOptions = (platform) =>
value: a.id,
}));
const platformIcons = {
douyin: "📱",
kuaishou: "🎬",
shipin: "📺",
xiaohongshu: "📕",
};
const activePlatform = computed(
() =>
publishPlatforms.value.find((p) => p.key === activePlatformKey.value) ||
publishPlatforms.value[0] ||
null,
);
function showFeedback(result) {
if (!result?.message) return;
feedback.value = {
@@ -52,6 +68,17 @@ onMounted(async () => {
await workflow.refreshPublishAccounts();
});
function openAccountManager() {
if (publishPlatforms.value.length && !activePlatform.value) {
activePlatformKey.value = publishPlatforms.value[0].key;
}
accountDialogVisible.value = true;
}
function selectPlatform(platformKey) {
activePlatformKey.value = platformKey;
}
async function onPublish() {
feedback.value = { severity: "", message: "" };
showFeedback(await workflow.publishVideo({ autoPublish: true }));
@@ -85,22 +112,77 @@ function cancelSchedule() {
feedback.value = { severity: "success", message: "已取消定时发布" };
}
async function onLogin(platform) {
async function onLogin(platform, accountId = undefined) {
feedback.value = { severity: "", message: "" };
showFeedback(await workflow.loginPublishPlatform(platform.key));
showFeedback(await workflow.loginPublishPlatform(platform.key, accountId));
}
async function onLoginAccount(platform, account) {
await onLogin(platform, account.id);
}
async function onAddAccount(platform) {
await onLogin(platform, null);
}
function onDeleteAccount(platform, account) {
platform.accounts = (platform.accounts || []).filter((a) => a.id !== account.id);
if (platform.accountId === account.id) {
platform.accountId = null;
}
if (!platform.accounts.length) {
platform.loggedIn = false;
platform.nickname = "";
}
feedback.value = { severity: "success", message: "已从本地列表移除账号,刷新后可重新获取" };
}
function formatAccountStatus(account) {
if (account?.loginStatus === "active" && account?.hasCookies) {
return { dot: "🟢", text: "已登录", color: "#4ade80" };
}
if (account?.loginStatus === "expired") {
return { dot: "🟠", text: "登录过期", color: "#f59e0b" };
}
return { dot: "⚪", text: "未登录", color: "#999999" };
}
function formatUpdateTime(account) {
const raw = account?.updatedAt || account?.lastLoginAt || account?.createdAt;
if (!raw) return "";
const d = new Date(raw);
if (Number.isNaN(d.getTime())) return "";
return d.toLocaleString("zh-CN", {
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
</script>
<template>
<DashboardCard title="视频发布" step="07" :grow="true">
<div class="mb-2 flex justify-end">
<Button
label="账号管理"
icon="pi pi-users"
size="small"
text
class="text-gray-400 hover:text-blue-400"
@click="openAccountManager"
/>
</div>
<div
v-for="platform in publishPlatforms"
:key="platform.key"
class="dashboard-platform-row mb-2"
class="publish-platform-row mb-2 flex items-center justify-between gap-2 rounded border border-white/5 bg-white/5 p-2 hover:bg-white/10"
>
<label class="flex shrink-0 items-center gap-2 text-sm" style="min-width: 86px">
<label class="flex shrink-0 items-center text-sm" style="min-width: 86px">
<Checkbox v-model="platform.checked" binary />
<span>{{ platform.label }}</span>
<span class="ml-1">{{ platform.label }}</span>
<span
v-if="platform.loggedIn"
class="text-xs text-emerald-400"
@@ -116,16 +198,9 @@ async function onLogin(platform) {
option-value="value"
placeholder="选择账号"
size="small"
class="min-w-0 flex-1"
class="min-w-0 max-w-[200px] flex-1"
:disabled="!accountOptions(platform).length"
/>
<Button
label="登录"
size="small"
text
:loading="publishLoggingIn === platform.key"
@click="onLogin(platform)"
/>
</div>
<p v-if="!hasVideo" class="mt-1 text-xs text-amber-400/90">
@@ -196,6 +271,94 @@ async function onLogin(platform) {
首次发布请手动关闭浏览器内出现的任何弹窗提示说明等防止干扰脚本下次即可流畅运行
</p>
<Dialog
v-model:visible="accountDialogVisible"
header="平台账号管理"
modal
:style="{ width: '640px' }"
>
<div class="account-manager">
<div class="platform-tabs">
<div
v-for="platform in publishPlatforms"
:key="platform.key"
class="platform-tab"
:class="{ active: activePlatform?.key === platform.key }"
@click="selectPlatform(platform.key)"
>
<span class="tab-icon">{{ platformIcons[platform.key] || "📌" }}</span>
<span class="tab-name">{{ platform.label }}</span>
<span v-if="platform.accounts?.length" class="tab-count">{{ platform.accounts.length }}</span>
</div>
</div>
<div class="account-list">
<div
v-for="account in activePlatform?.accounts || []"
:key="`${activePlatform.key}_${account.id}`"
class="account-card"
>
<div class="account-info">
<span class="status-dot">{{ formatAccountStatus(account).dot }}</span>
<div class="account-detail">
<div class="account-nickname">{{ account.nickname || "未登录" }}</div>
<div class="account-meta">
<span class="status-text" :style="{ color: formatAccountStatus(account).color }">
{{ formatAccountStatus(account).text }}
</span>
<span v-if="formatUpdateTime(account)" class="update-time">
· {{ formatUpdateTime(account) }}
</span>
</div>
</div>
</div>
<div class="account-actions">
<Button
label="扫码登录"
size="small"
severity="secondary"
:loading="publishLoggingIn === activePlatform.key"
@click="onLoginAccount(activePlatform, account)"
/>
<Button
label="删除"
size="small"
severity="danger"
outlined
@click="onDeleteAccount(activePlatform, account)"
/>
</div>
</div>
<div v-if="!(activePlatform?.accounts || []).length" class="account-card">
<div class="account-info">
<span class="status-dot"></span>
<div class="account-detail">
<div class="account-nickname">未登录</div>
<div class="account-meta">
<span class="status-text" style="color: #999999">未登录</span>
</div>
</div>
</div>
<div class="account-actions">
<Button
label="扫码登录"
size="small"
severity="secondary"
:loading="publishLoggingIn === activePlatform?.key"
@click="onAddAccount(activePlatform)"
/>
</div>
</div>
<div class="add-account-btn" @click="onAddAccount(activePlatform)">
<span class="add-icon">+</span>
<span>添加{{ activePlatform?.label || "" }}账号</span>
</div>
</div>
</div>
</Dialog>
<Dialog
v-model:visible="scheduleDialogVisible"
header="定时发布"
@@ -217,3 +380,118 @@ async function onLogin(platform) {
</Dialog>
</DashboardCard>
</template>
<style scoped>
.account-manager {
display: flex;
flex-direction: column;
gap: 12px;
}
.platform-tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.platform-tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border: 1px solid #333333;
border-radius: 10px;
background: #1c1c1c;
color: #d1d5db;
cursor: pointer;
transition: all 0.2s ease;
}
.platform-tab.active {
border-color: #2563eb;
background: rgba(37, 99, 235, 0.15);
color: #e2e8f0;
}
.tab-count {
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 999px;
background: rgba(37, 99, 235, 0.25);
color: #bfdbfe;
font-size: 11px;
line-height: 18px;
text-align: center;
}
.account-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.account-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px;
border: 1px solid #333333;
border-radius: 10px;
background: #1a1a1a;
}
.account-info {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.account-detail {
min-width: 0;
}
.account-nickname {
color: #e5e7eb;
font-size: 14px;
font-weight: 600;
}
.account-meta {
margin-top: 3px;
color: #9ca3af;
font-size: 12px;
}
.account-actions {
display: flex;
align-items: center;
gap: 8px;
}
.add-account-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px dashed #3f3f46;
border-radius: 10px;
padding: 10px 12px;
color: #cbd5e1;
cursor: pointer;
transition: all 0.2s ease;
}
.add-account-btn:hover {
border-color: #2563eb;
color: #dbeafe;
background: rgba(37, 99, 235, 0.08);
}
.add-icon {
font-size: 18px;
line-height: 1;
}
</style>

View File

@@ -54,15 +54,17 @@ export async function refreshPublishAccounts(store) {
* @param {ReturnType<typeof import('../stores/workflow.js').useWorkflowStore>} store
* @param {string} platformKey
*/
export async function loginPublishPlatform(store, platformKey) {
export async function loginPublishPlatform(store, platformKey, accountId = undefined) {
const row = store.publishPlatforms.find((p) => p.key === platformKey);
if (!row) return { ok: false, message: "未知平台" };
store.publishLoggingIn = platformKey;
try {
const result = await invoke("publish_login", {
params: {
platform: platformKey,
accountId: row.accountId || null,
accountId: accountId === undefined ? row.accountId || null : accountId,
},
});
if (!result?.success) {
return {
@@ -116,9 +118,11 @@ export async function executePublishVideo(store, { autoPublish = true } = {}) {
for (const row of selected) {
const check = await invoke("publish_check_login", {
params: {
platform: row.key,
accountId: row.accountId || null,
checkBrowser: false,
},
});
if (!check?.isLoggedIn) {
const label = row.label || row.key;
@@ -142,6 +146,7 @@ export async function executePublishVideo(store, { autoPublish = true } = {}) {
try {
const result = await invoke("publish_execute", {
params: {
platforms: selected.map((p) => ({
platform: p.key,
accountId: p.accountId || null,
@@ -152,6 +157,7 @@ export async function executePublishVideo(store, { autoPublish = true } = {}) {
description,
tags,
autoPublish,
},
});
store.publishResults = Array.isArray(result?.results) ? result.results : [];

View File

@@ -685,8 +685,8 @@ export const useWorkflowStore = defineStore("workflow", {
return refreshPublishAccounts(this);
},
async loginPublishPlatform(platformKey) {
return loginPublishPlatform(this, platformKey);
async loginPublishPlatform(platformKey, accountId = undefined) {
return loginPublishPlatform(this, platformKey, accountId);
},
async publishVideo(options) {