11
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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", {
|
||||
platform: platformKey,
|
||||
accountId: row.accountId || null,
|
||||
params: {
|
||||
platform: platformKey,
|
||||
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", {
|
||||
platform: row.key,
|
||||
accountId: row.accountId || null,
|
||||
checkBrowser: false,
|
||||
params: {
|
||||
platform: row.key,
|
||||
accountId: row.accountId || null,
|
||||
checkBrowser: false,
|
||||
},
|
||||
});
|
||||
if (!check?.isLoggedIn) {
|
||||
const label = row.label || row.key;
|
||||
@@ -142,16 +146,18 @@ export async function executePublishVideo(store, { autoPublish = true } = {}) {
|
||||
|
||||
try {
|
||||
const result = await invoke("publish_execute", {
|
||||
platforms: selected.map((p) => ({
|
||||
platform: p.key,
|
||||
accountId: p.accountId || null,
|
||||
})),
|
||||
videoPath,
|
||||
coverPath,
|
||||
title: title.split(/\r?\n/)[0].slice(0, 30),
|
||||
description,
|
||||
tags,
|
||||
autoPublish,
|
||||
params: {
|
||||
platforms: selected.map((p) => ({
|
||||
platform: p.key,
|
||||
accountId: p.accountId || null,
|
||||
})),
|
||||
videoPath,
|
||||
coverPath,
|
||||
title: title.split(/\r?\n/)[0].slice(0, 30),
|
||||
description,
|
||||
tags,
|
||||
autoPublish,
|
||||
},
|
||||
});
|
||||
|
||||
store.publishResults = Array.isArray(result?.results) ? result.results : [];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user