| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260 |
- <template>
- <div class="accounts-page">
- <div class="page-header">
- <h2>账号管理</h2>
- <div class="header-actions">
- <el-button @click="showGroupManage = true">
- <el-icon><Setting /></el-icon>
- 管理分组
- </el-button>
- <el-button @click="refreshAllAccounts" :disabled="!accounts.length">
- <el-icon><Refresh /></el-icon>
- 刷新所有
- </el-button>
- <el-button type="primary" @click="showPlatformSelect = true">
- <el-icon><Monitor /></el-icon>
- 浏览器登录
- </el-button>
- <el-button @click="showAddDialog = true">
- <el-icon><Plus /></el-icon>
- 手动添加
- </el-button>
- </div>
- </div>
-
- <!-- 筛选栏 -->
- <div class="page-card filter-bar">
- <el-select v-model="filter.platform" placeholder="平台" clearable style="width: 150px">
- <el-option
- v-for="platform in platforms"
- :key="platform.type"
- :label="platform.name"
- :value="platform.type"
- />
- </el-select>
- <el-select v-model="filter.groupId" placeholder="分组" clearable style="width: 150px">
- <el-option
- v-for="group in groups"
- :key="group.id"
- :label="group.name"
- :value="group.id"
- />
- </el-select>
- <el-select v-model="filter.status" placeholder="状态" clearable style="width: 120px">
- <el-option label="正常" value="active" />
- <el-option label="已过期" value="expired" />
- <el-option label="已禁用" value="disabled" />
- </el-select>
- <el-button @click="loadAccounts">
- <el-icon><Refresh /></el-icon>
- 刷新
- </el-button>
- </div>
-
- <!-- 账号列表 -->
- <div class="page-card">
- <el-table :data="accounts" v-loading="loading" style="width: 100%">
- <el-table-column label="账号" min-width="200">
- <template #default="{ row }">
- <div class="account-cell">
- <el-avatar :size="40" :src="row.avatarUrl || undefined">
- {{ row.accountName?.[0] }}
- </el-avatar>
- <div class="account-info">
- <div class="account-name">{{ row.accountName }}</div>
- <div class="account-id">ID: {{ row.accountId }}</div>
- </div>
- </div>
- </template>
- </el-table-column>
-
- <el-table-column label="平台" width="120">
- <template #default="{ row }">
- <el-tag>{{ getPlatformName(row.platform) }}</el-tag>
- </template>
- </el-table-column>
-
- <el-table-column label="粉丝数" width="100">
- <template #default="{ row }">
- {{ formatNumber(row.fansCount) }}
- </template>
- </el-table-column>
-
- <el-table-column label="作品数" width="100">
- <template #default="{ row }">
- {{ row.worksCount }}
- </template>
- </el-table-column>
-
- <el-table-column label="状态" width="100">
- <template #default="{ row }">
- <el-tag :type="getStatusType(row.status)">
- {{ getStatusText(row.status) }}
- </el-tag>
- </template>
- </el-table-column>
-
- <el-table-column label="分组" width="150">
- <template #default="{ row }">
- <el-select
- :model-value="row.groupId"
- placeholder="无分组"
- clearable
- size="small"
- style="width: 100%"
- @change="(val: number | undefined) => handleChangeGroup(row.id, val)"
- >
- <el-option
- v-for="group in groups"
- :key="group.id"
- :label="group.name"
- :value="group.id"
- />
- </el-select>
- </template>
- </el-table-column>
-
- <el-table-column label="操作" width="180" fixed="right">
- <template #default="{ row }">
- <el-button type="primary" link size="small" @click="openPlatformAdmin(row)">
- 后台
- </el-button>
- <el-button type="primary" link size="small" @click="refreshAccount(row.id)">
- 刷新
- </el-button>
- <el-button type="danger" link size="small" @click="deleteAccount(row.id)">
- 删除
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
-
- <!-- 添加账号对话框 -->
- <el-dialog v-model="showAddDialog" title="添加账号" width="500px">
- <el-form :model="addForm" label-width="80px">
- <el-form-item label="平台">
- <el-select v-model="addForm.platform" placeholder="选择平台" style="width: 100%">
- <el-option
- v-for="platform in platforms"
- :key="platform.type"
- :label="platform.name"
- :value="platform.type"
- :disabled="!platform.supported"
- >
- <span class="platform-option">
- <span>{{ platform.name }}</span>
- <el-tag v-if="!platform.supported" size="small" type="info">适配中</el-tag>
- </span>
- </el-option>
- </el-select>
- </el-form-item>
-
- <el-form-item label="Cookie">
- <el-input
- v-model="addForm.cookieData"
- type="textarea"
- :rows="4"
- placeholder="粘贴从浏览器复制的 Cookie"
- />
- </el-form-item>
-
- <el-form-item label="分组">
- <el-select v-model="addForm.groupId" placeholder="选择分组" clearable style="width: 100%">
- <el-option
- v-for="group in groups"
- :key="group.id"
- :label="group.name"
- :value="group.id"
- />
- </el-select>
- </el-form-item>
- </el-form>
-
- <template #footer>
- <el-button @click="showAddDialog = false">取消</el-button>
- <el-button type="primary" @click="handleAddAccount" :loading="submitting">
- 添加
- </el-button>
- </template>
- </el-dialog>
-
- <!-- 平台选择对话框 -->
- <el-dialog
- v-model="showPlatformSelect"
- title="选择登录平台"
- width="480px"
- :close-on-click-modal="true"
- >
- <div class="platform-grid">
- <div
- v-for="platform in supportedPlatforms"
- :key="platform.type"
- class="platform-card"
- :style="{ '--platform-color': platform.color }"
- @click="selectPlatformAndLogin(platform.type)"
- >
- <div class="platform-icon" :class="platform.type">
- {{ platform.name[0] }}
- </div>
- <span class="platform-name">{{ platform.name }}</span>
- </div>
- </div>
-
- <div class="platform-hint">
- 点击平台图标后会在右侧标签页中打开登录界面,登录成功后系统会自动获取Cookie
- </div>
- </el-dialog>
-
- <!-- 刷新账号对话框 -->
- <el-dialog
- v-model="showRefreshDialog"
- title="刷新账号信息"
- width="450px"
- :close-on-click-modal="false"
- :show-close="refreshState.status !== 'loading'"
- @closed="resetRefreshState"
- >
- <div class="refresh-status">
- <el-icon
- class="status-icon"
- :class="refreshState.status"
- >
- <Loading v-if="refreshState.status === 'loading'" />
- <CircleCheck v-else-if="refreshState.status === 'success'" />
- <CircleClose v-else />
- </el-icon>
-
- <div class="status-text">
- <template v-if="refreshState.status === 'loading'">
- <h3>
- <span class="fetching-text">正在刷新账号信息</span>
- <span class="fetching-dots"></span>
- </h3>
- <div class="fetching-progress">
- <el-progress :percentage="refreshState.progress" :show-text="false" :stroke-width="4" />
- </div>
- <p class="fetching-hint">正在后台获取最新数据,请稍候</p>
- <div class="fetching-steps">
- <div class="step" :class="{ active: refreshState.step >= 1, done: refreshState.step > 1 }">
- <el-icon v-if="refreshState.step > 1"><CircleCheck /></el-icon>
- <el-icon v-else-if="refreshState.step === 1"><Loading /></el-icon>
- <span v-else>1</span>
- <span class="step-text">验证Cookie</span>
- </div>
- <div class="step" :class="{ active: refreshState.step >= 2, done: refreshState.step > 2 }">
- <el-icon v-if="refreshState.step > 2"><CircleCheck /></el-icon>
- <el-icon v-else-if="refreshState.step === 2"><Loading /></el-icon>
- <span v-else>2</span>
- <span class="step-text">获取资料</span>
- </div>
- <div class="step" :class="{ active: refreshState.step >= 3, done: refreshState.step > 3 }">
- <el-icon v-if="refreshState.step > 3"><CircleCheck /></el-icon>
- <el-icon v-else-if="refreshState.step === 3"><Loading /></el-icon>
- <span v-else>3</span>
- <span class="step-text">获取作品</span>
- </div>
- </div>
- </template>
- <template v-else-if="refreshState.status === 'success'">
- <h3>刷新成功!</h3>
- <div v-if="refreshState.account" class="account-preview">
- <el-avatar :size="48" :src="refreshState.account.avatarUrl || undefined">
- {{ refreshState.account.accountName?.[0] }}
- </el-avatar>
- <div class="account-preview-info">
- <div class="account-preview-name">{{ refreshState.account.accountName }}</div>
- <div class="account-preview-stats">
- <span>粉丝: {{ formatNumber(refreshState.account.fansCount) }}</span>
- <span v-if="refreshState.account.worksCount">
- · 作品: {{ refreshState.account.worksCount }}
- </span>
- </div>
- </div>
- </div>
- <p>账号信息已更新</p>
- </template>
- <template v-else-if="refreshState.status === 'expired'">
- <h3>登录已过期</h3>
- <p>账号Cookie已失效,需要重新登录</p>
- </template>
- <template v-else>
- <h3>刷新失败</h3>
- <p>{{ refreshState.error || '未知错误' }}</p>
- </template>
- </div>
- </div>
-
- <template #footer>
- <template v-if="refreshState.status === 'loading'">
- <el-button disabled>请稍候...</el-button>
- </template>
- <template v-else-if="refreshState.status === 'expired'">
- <el-button @click="showRefreshDialog = false">关闭</el-button>
- <el-button type="primary" @click="handleReLoginFromRefresh">
- 重新登录
- </el-button>
- </template>
- <template v-else>
- <el-button type="primary" @click="showRefreshDialog = false">确定</el-button>
- </template>
- </template>
- </el-dialog>
-
- <!-- 分组管理对话框 -->
- <el-dialog
- v-model="showGroupManage"
- title="管理分组"
- width="500px"
- :close-on-click-modal="false"
- >
- <div class="group-manage">
- <!-- 新增分组 -->
- <div class="group-add">
- <el-input
- v-model="newGroupName"
- placeholder="输入新分组名称"
- style="width: 300px"
- @keyup.enter="handleAddGroup"
- />
- <el-button type="primary" @click="handleAddGroup" :loading="groupSaving">
- <el-icon><Plus /></el-icon>
- 新增
- </el-button>
- </div>
-
- <!-- 分组列表 -->
- <div class="group-list">
- <div v-if="groups.length === 0" class="group-empty">
- <el-empty description="暂无分组" :image-size="80" />
- </div>
- <div
- v-else
- v-for="group in groups"
- :key="group.id"
- class="group-item"
- >
- <template v-if="editingGroupId === group.id">
- <el-input
- v-model="editingGroupName"
- size="small"
- style="flex: 1"
- @keyup.enter="handleSaveGroup(group.id)"
- @keyup.escape="cancelEditGroup"
- />
- <el-button type="primary" size="small" @click="handleSaveGroup(group.id)" :loading="groupSaving">
- 保存
- </el-button>
- <el-button size="small" @click="cancelEditGroup">
- 取消
- </el-button>
- </template>
- <template v-else>
- <span class="group-name">{{ group.name }}</span>
- <span class="group-count">{{ getGroupAccountCount(group.id) }} 个账号</span>
- <div class="group-actions">
- <el-button type="primary" link size="small" @click="startEditGroup(group)">
- 编辑
- </el-button>
- <el-button
- type="danger"
- link
- size="small"
- @click="handleDeleteGroup(group)"
- :disabled="getGroupAccountCount(group.id) > 0"
- >
- 删除
- </el-button>
- </div>
- </template>
- </div>
- </div>
- </div>
-
- <template #footer>
- <el-button @click="showGroupManage = false">关闭</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, reactive, onMounted, onUnmounted, watch } from 'vue';
- import { Plus, Refresh, Monitor, Loading, CircleCheck, CircleClose, Setting } from '@element-plus/icons-vue';
- import { ElMessage, ElMessageBox } from 'element-plus';
- import { accountsApi } from '@/api/accounts';
- import { PLATFORMS, PLATFORM_TYPES } from '@media-manager/shared';
- import type { PlatformAccount, AccountGroup, PlatformType } from '@media-manager/shared';
- import { useTaskQueueStore } from '@/stores/taskQueue';
- import { useTabsStore } from '@/stores/tabs';
- const taskStore = useTaskQueueStore();
- const tabsStore = useTabsStore();
- // 监听任务列表变化,当 sync_account 任务完成时自动刷新账号列表
- watch(() => taskStore.tasks, (newTasks, oldTasks) => {
- const hasSyncAccountComplete = newTasks.some(task => {
- if (task.type !== 'sync_account') return false;
- const oldTask = oldTasks?.find(t => t.id === task.id);
- // 任务状态变为 completed 或 failed
- if (oldTask && oldTask.status !== task.status &&
- (task.status === 'completed' || task.status === 'failed')) {
- return true;
- }
- return false;
- });
-
- if (hasSyncAccountComplete) {
- console.log('[Accounts] Sync account task completed, refreshing list...');
- loadAccounts();
- }
- }, { deep: true });
- // 监听账号刷新信号(当浏览器登录添加账号后或账号同步任务完成时触发)
- watch(() => tabsStore.accountRefreshTrigger, () => {
- console.log('[Accounts] Account refresh triggered (from tabs), reloading list...');
- loadAccounts();
- });
- // 监听任务队列的账号刷新信号
- watch(() => taskStore.accountRefreshTrigger, () => {
- console.log('[Accounts] Account refresh triggered (from task), reloading list...');
- loadAccounts();
- });
- const loading = ref(false);
- const submitting = ref(false);
- const showAddDialog = ref(false);
- const showPlatformSelect = ref(false);
- const showRefreshDialog = ref(false);
- const showGroupManage = ref(false);
- // 分组管理相关
- const newGroupName = ref('');
- const groupSaving = ref(false);
- const editingGroupId = ref<number | null>(null);
- const editingGroupName = ref('');
- // 刷新账号的状态
- const refreshState = reactive({
- status: '' as '' | 'loading' | 'success' | 'expired' | 'failed',
- progress: 0,
- step: 0,
- error: '',
- account: null as PlatformAccount | null,
- platform: '' as PlatformType | '',
- });
- let refreshTimer: ReturnType<typeof setInterval> | null = null;
- const accounts = ref<PlatformAccount[]>([]);
- const groups = ref<AccountGroup[]>([]);
- // 浏览器登录相关
- const browserLoginForm = reactive({
- platform: '' as PlatformType | '',
- groupId: undefined as number | undefined,
- });
- const platforms = PLATFORM_TYPES.map(type => PLATFORMS[type]);
- // 只显示已支持的平台
- const supportedPlatforms = platforms.filter(p => p.supported);
- const filter = reactive({
- platform: '',
- groupId: undefined as number | undefined,
- status: '',
- });
- const addForm = reactive({
- platform: '' as PlatformType | '',
- cookieData: '',
- groupId: undefined as number | undefined,
- });
- function getPlatformName(platform: PlatformType) {
- return PLATFORMS[platform]?.name || platform;
- }
- function getStatusType(status: string): 'success' | 'danger' | 'info' | 'primary' | 'warning' {
- const types: Record<string, 'success' | 'danger' | 'info' | 'primary' | 'warning'> = {
- active: 'success',
- expired: 'danger',
- disabled: 'info',
- };
- return types[status] || 'info';
- }
- function getStatusText(status: string) {
- const texts: Record<string, string> = {
- active: '正常',
- expired: '已过期',
- disabled: '已禁用',
- };
- return texts[status] || status;
- }
- function formatNumber(num: number) {
- if (num >= 10000) {
- return (num / 10000).toFixed(1) + 'w';
- }
- return num.toString();
- }
- async function loadAccounts() {
- loading.value = true;
- try {
- const [accountList, groupList] = await Promise.all([
- accountsApi.getAccounts({
- platform: filter.platform || undefined,
- groupId: filter.groupId,
- status: filter.status || undefined,
- }),
- accountsApi.getGroups(),
- ]);
- accounts.value = accountList;
- groups.value = groupList;
- } catch {
- // 错误已处理
- } finally {
- loading.value = false;
- }
- }
- async function handleAddAccount() {
- if (!addForm.platform || !addForm.cookieData) {
- ElMessage.warning('请填写完整信息');
- return;
- }
-
- submitting.value = true;
- try {
- await accountsApi.addAccount({
- platform: addForm.platform,
- cookieData: addForm.cookieData,
- groupId: addForm.groupId,
- });
- ElMessage.success('账号添加成功');
- showAddDialog.value = false;
- addForm.platform = '';
- addForm.cookieData = '';
- addForm.groupId = undefined;
- loadAccounts();
- } catch {
- // 错误已处理
- } finally {
- submitting.value = false;
- }
- }
- async function refreshAccount(id: number) {
- // 找到要刷新的账号
- const account = accounts.value.find(a => a.id === id);
- if (!account) {
- ElMessage.error('账号不存在');
- return;
- }
-
- // 使用任务队列
- await taskStore.syncAccount(id, account.accountName);
- ElMessage.success('账号刷新任务已创建');
- taskStore.openDialog();
- }
- // 刷新所有账号
- async function refreshAllAccounts() {
- if (!accounts.value.length) {
- ElMessage.warning('暂无账号');
- return;
- }
-
- // 为每个账号创建刷新任务
- for (const account of accounts.value) {
- await taskStore.syncAccount(account.id, account.accountName);
- }
-
- ElMessage.success(`已创建 ${accounts.value.length} 个账号刷新任务`);
- taskStore.openDialog();
- }
- // 启动刷新进度动画
- function startRefreshAnimation() {
- if (refreshTimer) {
- clearInterval(refreshTimer);
- }
-
- let elapsed = 0;
- refreshTimer = setInterval(() => {
- elapsed += 100;
-
- // 进度条模拟
- if (refreshState.progress < 95) {
- const targetProgress = Math.min(95, Math.log(elapsed / 500 + 1) * 30);
- refreshState.progress = Math.min(95, targetProgress);
- }
-
- // 步骤更新
- if (elapsed > 800 && refreshState.step < 2) {
- refreshState.step = 2;
- }
- if (elapsed > 2500 && refreshState.step < 3) {
- refreshState.step = 3;
- }
- }, 100);
- }
- // 停止刷新进度动画
- function stopRefreshAnimation() {
- if (refreshTimer) {
- clearInterval(refreshTimer);
- refreshTimer = null;
- }
- refreshState.progress = 100;
- refreshState.step = 4;
- }
- // 重置刷新状态
- function resetRefreshState() {
- refreshState.status = '';
- refreshState.progress = 0;
- refreshState.step = 0;
- refreshState.error = '';
- refreshState.account = null;
- refreshState.platform = '';
- stopRefreshAnimation();
- }
- // 从刷新对话框触发重新登录
- function handleReLoginFromRefresh() {
- showRefreshDialog.value = false;
- browserLoginForm.platform = refreshState.platform;
- showPlatformSelect.value = true;
- }
- // 修改账号分组
- async function handleChangeGroup(accountId: number, groupId: number | undefined) {
- try {
- await accountsApi.updateAccount(accountId, { groupId: groupId || null });
- // 更新本地数据
- const account = accounts.value.find(a => a.id === accountId);
- if (account) {
- account.groupId = groupId || null;
- }
- ElMessage.success('分组已更新');
- } catch {
- // 错误已处理,重新加载数据以恢复
- loadAccounts();
- }
- }
- // 打开平台后台管理页面
- async function openPlatformAdmin(account: PlatformAccount) {
- const platformInfo = PLATFORMS[account.platform as PlatformType];
- if (!platformInfo) {
- ElMessage.warning('未知平台');
- return;
- }
-
- try {
- // 获取账号的 Cookie 数据
- const { cookieData } = await accountsApi.getAccountCookie(account.id);
-
- // 在标签页中打开平台后台,带上 Cookie,设置为管理后台模式
- tabsStore.openBrowserTab(
- account.platform,
- `${platformInfo.name} - ${account.accountName}`,
- undefined,
- platformInfo.creatorUrl,
- cookieData,
- true // isAdminMode: 管理后台模式,不校验登录和保存账号
- );
- } catch (error) {
- console.error('获取账号 Cookie 失败:', error);
- // 即使获取 Cookie 失败,也打开后台(用户需要重新登录),仍然是管理后台模式
- tabsStore.openBrowserTab(
- account.platform,
- `${platformInfo.name} - ${account.accountName}`,
- undefined,
- platformInfo.creatorUrl,
- undefined,
- true // isAdminMode: 管理后台模式
- );
- }
- }
- async function deleteAccount(id: number) {
- try {
- await ElMessageBox.confirm('确定要删除该账号吗?', '提示', {
- type: 'warning',
- });
- await accountsApi.deleteAccount(id);
- ElMessage.success('删除成功');
- loadAccounts();
- } catch {
- // 取消或错误
- }
- }
- // 分组管理方法
- function getGroupAccountCount(groupId: number): number {
- return accounts.value.filter(a => a.groupId === groupId).length;
- }
- async function handleAddGroup() {
- if (!newGroupName.value.trim()) {
- ElMessage.warning('请输入分组名称');
- return;
- }
-
- groupSaving.value = true;
- try {
- await accountsApi.createGroup({ name: newGroupName.value.trim() });
- ElMessage.success('分组创建成功');
- newGroupName.value = '';
- loadAccounts(); // 重新加载以获取最新分组列表
- } catch {
- // 错误已处理
- } finally {
- groupSaving.value = false;
- }
- }
- function startEditGroup(group: AccountGroup) {
- editingGroupId.value = group.id;
- editingGroupName.value = group.name;
- }
- function cancelEditGroup() {
- editingGroupId.value = null;
- editingGroupName.value = '';
- }
- async function handleSaveGroup(groupId: number) {
- if (!editingGroupName.value.trim()) {
- ElMessage.warning('分组名称不能为空');
- return;
- }
-
- groupSaving.value = true;
- try {
- await accountsApi.updateGroup(groupId, { name: editingGroupName.value.trim() });
- ElMessage.success('分组更新成功');
- cancelEditGroup();
- loadAccounts(); // 重新加载以获取最新分组列表
- } catch {
- // 错误已处理
- } finally {
- groupSaving.value = false;
- }
- }
- async function handleDeleteGroup(group: AccountGroup) {
- const count = getGroupAccountCount(group.id);
- if (count > 0) {
- ElMessage.warning(`该分组下还有 ${count} 个账号,请先移除账号`);
- return;
- }
-
- try {
- await ElMessageBox.confirm(`确定要删除分组"${group.name}"吗?`, '提示', {
- type: 'warning',
- });
- await accountsApi.deleteGroup(group.id);
- ElMessage.success('分组删除成功');
- loadAccounts(); // 重新加载以获取最新分组列表
- } catch {
- // 取消或错误
- }
- }
- // 选择平台并开始登录(九宫格点击)
- function selectPlatformAndLogin(platformType: PlatformType) {
- const platformName = getPlatformName(platformType);
-
- // 在标签页中打开浏览器
- tabsStore.openBrowserTab(
- platformType,
- `${platformName} 登录`
- );
-
- // 关闭平台选择对话框
- showPlatformSelect.value = false;
-
- ElMessage.success('已打开浏览器登录标签页');
- }
- // 浏览器登录功能 - 改为在标签页中打开(保留用于重新登录场景)
- function startBrowserLogin() {
- if (!browserLoginForm.platform) {
- ElMessage.warning('请选择平台');
- return;
- }
-
- selectPlatformAndLogin(browserLoginForm.platform as PlatformType);
- browserLoginForm.platform = '';
- }
- // 检查过期账号并提示重新登录
- async function checkExpiredAccounts() {
- const expiredAccounts = accounts.value.filter(a => a.status === 'expired');
-
- if (expiredAccounts.length > 0) {
- const account = expiredAccounts[0]; // 处理第一个过期账号
-
- ElMessageBox.confirm(
- `账号 "${account.accountName}" 登录已过期,是否重新登录?`,
- '登录过期',
- {
- confirmButtonText: '重新登录',
- cancelButtonText: '稍后再说',
- type: 'warning',
- }
- ).then(() => {
- browserLoginForm.platform = account.platform;
- showPlatformSelect.value = true;
- }).catch(() => {
- // 用户取消
- });
- }
- }
- // 验证单个账号的 Cookie 状态
- async function verifyAccountStatus(account: PlatformAccount) {
- try {
- const result = await accountsApi.checkAccountStatus(account.id);
- if (result.needReLogin) {
- // 更新本地状态
- const idx = accounts.value.findIndex(a => a.id === account.id);
- if (idx !== -1) {
- accounts.value[idx].status = 'expired';
- }
- return false;
- }
- return true;
- } catch {
- return true; // 检查失败时不影响
- }
- }
- onMounted(async () => {
- await loadAccounts();
- // 页面加载后不自动检查过期账号,避免服务启动时网络不稳定导致误判
- // 过期账号会在定时任务刷新后或用户手动刷新后提示
- });
- onUnmounted(() => {
- stopRefreshAnimation();
- });
- </script>
- <style lang="scss" scoped>
- @use '@/styles/variables.scss' as *;
- .accounts-page {
- max-width: 1400px;
- margin: 0 auto;
- }
- .page-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 24px;
-
- h2 {
- margin: 0;
- font-size: 22px;
- font-weight: 600;
- color: $text-primary;
- }
-
- .header-actions {
- display: flex;
- gap: 12px;
-
- .el-button {
- border-radius: $radius-base;
- font-weight: 500;
- }
- }
- }
- .filter-bar {
- display: flex;
- gap: 12px;
- margin-bottom: 20px;
- padding: 16px 20px !important;
-
- :deep(.el-select) {
- .el-input__wrapper {
- border-radius: $radius-base;
- }
- }
- }
- .page-card {
- background: #fff;
- border-radius: $radius-lg;
- padding: 20px 24px;
- box-shadow: $shadow-sm;
- border: 1px solid $border-light;
-
- :deep(.el-table) {
- --el-table-border-color: #{$border-light};
- --el-table-header-bg-color: #{$bg-base};
-
- th.el-table__cell {
- font-weight: 600;
- color: $text-primary;
- background: $bg-base !important;
- }
-
- .el-table__row {
- &:hover > td.el-table__cell {
- background: $primary-color-light !important;
- }
- }
-
- .el-button--link {
- font-weight: 500;
- }
- }
- }
- .account-cell {
- display: flex;
- align-items: center;
- gap: 14px;
-
- :deep(.el-avatar) {
- flex-shrink: 0;
- border: 2px solid $border-light;
- background: linear-gradient(135deg, $primary-color-light, #fff);
- color: $primary-color;
- font-weight: 600;
- }
-
- .account-info {
- .account-name {
- font-weight: 600;
- color: $text-primary;
- }
-
- .account-id {
- font-size: 12px;
- color: $text-secondary;
- margin-top: 2px;
- }
- }
- }
- .refresh-status {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 40px 20px;
- text-align: center;
-
- .status-icon {
- font-size: 56px;
- margin-bottom: 20px;
-
- &.loading {
- color: $primary-color;
- animation: spin 1s linear infinite;
- }
-
- &.success {
- color: $success-color;
- }
-
- &.error,
- &.failed,
- &.expired {
- color: $danger-color;
- }
- }
-
- .status-text {
- h3 {
- margin: 0 0 10px;
- font-size: 18px;
- font-weight: 600;
- color: $text-primary;
- }
-
- p {
- margin: 0;
- color: $text-secondary;
- }
- }
-
- .account-preview {
- display: flex;
- align-items: center;
- gap: 14px;
- margin: 15px 0;
- padding: 16px 24px;
- background: $primary-color-light;
- border-radius: $radius-lg;
- border: 1px solid $border-light;
-
- .account-preview-info {
- text-align: left;
-
- .account-preview-name {
- font-weight: 600;
- font-size: 16px;
- color: $text-primary;
- }
-
- .account-preview-stats {
- font-size: 13px;
- color: $text-secondary;
- margin-top: 4px;
-
- span + span {
- margin-left: 4px;
- }
- }
- }
- }
-
- .fetching-text {
- display: inline-block;
- }
-
- .fetching-dots::after {
- content: '';
- animation: dots 1.5s steps(4, end) infinite;
- }
-
- .fetching-progress {
- width: 220px;
- margin: 16px 0;
-
- :deep(.el-progress-bar__outer) {
- background: $border-light;
- }
-
- :deep(.el-progress-bar__inner) {
- background: linear-gradient(90deg, $primary-color, #64b5f6);
- }
- }
-
- .fetching-hint {
- font-size: 13px;
- color: $text-secondary;
- margin-bottom: 20px;
- }
-
- .fetching-steps {
- display: flex;
- gap: 36px;
-
- .step {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 10px;
- color: $text-secondary;
-
- > span:first-child,
- > .el-icon:first-child {
- width: 32px;
- height: 32px;
- border-radius: 50%;
- background: $bg-base;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 14px;
- font-weight: 600;
- }
-
- .step-text {
- font-size: 13px;
- font-weight: 500;
- }
-
- &.active {
- color: $primary-color;
-
- > span:first-child,
- > .el-icon:first-child {
- background: $primary-color-light;
- color: $primary-color;
- }
-
- .el-icon {
- animation: spin 1s linear infinite;
- }
- }
-
- &.done {
- color: $success-color;
-
- > .el-icon:first-child {
- background: $success-color-light;
- color: $success-color;
- animation: none;
- }
- }
- }
- }
- }
- @keyframes spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
- @keyframes dots {
- 0%, 20% { content: ''; }
- 40% { content: '.'; }
- 60% { content: '..'; }
- 80%, 100% { content: '...'; }
- }
- // 平台选择九宫格
- .platform-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 16px;
- padding: 8px 0;
- }
- .platform-card {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 10px;
- padding: 20px 16px;
- border-radius: 12px;
- background: $bg-light;
- cursor: pointer;
- transition: all 0.2s ease;
- border: 2px solid transparent;
-
- &:hover {
- background: #fff;
- border-color: var(--platform-color, $primary-color);
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-
- .platform-icon {
- transform: scale(1.1);
- }
- }
-
- &:active {
- transform: translateY(0);
- }
- }
- .platform-icon {
- width: 48px;
- height: 48px;
- border-radius: 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 20px;
- font-weight: 600;
- color: #fff;
- transition: transform 0.2s ease;
-
- // 各平台颜色
- &.douyin {
- background: linear-gradient(135deg, #000000, #333333);
- }
- &.xiaohongshu {
- background: linear-gradient(135deg, #FE2C55, #FF5C7C);
- }
- &.kuaishou {
- background: linear-gradient(135deg, #FF5000, #FF7733);
- }
- &.weixin_video {
- background: linear-gradient(135deg, #07C160, #2DC76D);
- }
- &.bilibili {
- background: linear-gradient(135deg, #00A1D6, #33B5E5);
- }
- &.toutiao {
- background: linear-gradient(135deg, #F85959, #FF7B7B);
- }
- &.baijiahao {
- background: linear-gradient(135deg, #2932E1, #5B6AE8);
- }
- }
- .platform-name {
- font-size: 14px;
- font-weight: 500;
- color: $text-primary;
- }
- .platform-hint {
- margin-top: 16px;
- padding: 12px 16px;
- background: $bg-light;
- border-radius: 8px;
- font-size: 13px;
- color: $text-secondary;
- text-align: center;
- }
- // 分组管理样式
- .group-manage {
- .group-add {
- display: flex;
- gap: 12px;
- margin-bottom: 20px;
- padding-bottom: 16px;
- border-bottom: 1px solid $border-light;
- }
-
- .group-list {
- max-height: 400px;
- overflow-y: auto;
- }
-
- .group-empty {
- padding: 20px 0;
- }
-
- .group-item {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 12px 16px;
- background: $bg-light;
- border-radius: 8px;
- margin-bottom: 8px;
-
- &:last-child {
- margin-bottom: 0;
- }
-
- .group-name {
- flex: 1;
- font-weight: 500;
- color: $text-primary;
- }
-
- .group-count {
- font-size: 13px;
- color: $text-secondary;
- }
-
- .group-actions {
- display: flex;
- gap: 4px;
- }
- }
- }
- </style>
|