platform-vue/src/views/accountpool/cursor/index.vue
2026-04-13 08:37:54 +08:00

462 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import Edit from './components/edit.vue';
import {
addAccountPool,
batchAddAccountPool,
extractAccountPool,
getAccountPoolDetail,
getAccountPoolList,
} from '@/api/accountPool';
const moduleKey = 'cursor';
const loading = ref(false);
const editVisible = ref(false);
const editMode = ref('single');
const detailVisible = ref(false);
const extractVisible = ref(false);
const extractTargetRow = ref(null);
const query = reactive({
keyword: '',
status: '',
});
const activeTypeTab = ref('all');
const extractForm = reactive({
platform: 'local', // local | xianyu
type: 'account', // account | tk | account_tk
});
const detailRow = ref(null);
const tableData = ref([]);
const total = ref(0);
const selectedRows = ref([]);
const pagination = reactive({
page: 1,
pageSize: 30,
});
const pagedList = computed(() => tableData.value);
function resetQuery() {
query.keyword = '';
query.status = '';
}
const typeTabs = computed(() => {
return [
{ label: '全部', value: 'all' },
{ label: '账号密码', value: 'account' },
{ label: '账号密码+Token', value: 'account_tk' },
{ label: 'Token', value: 'tk' },
];
});
watch(
() => [query.keyword, query.status, activeTypeTab.value],
() => {
pagination.page = 1;
fetchList();
}
);
watch(
() => [pagination.page, pagination.pageSize],
() => {
fetchList();
}
);
function openAddDialog(mode = 'single') {
editMode.value = mode;
editVisible.value = true;
}
function nowText() {
const d = new Date();
const p = (v) => String(v).padStart(2, '0');
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(
d.getHours()
)}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}
async function saveRows(rows) {
if (!rows.length) return;
if (rows.length === 1) {
await addAccountPool(moduleKey, rows[0]);
return;
}
await batchAddAccountPool(moduleKey, rows);
}
async function handleEditSubmit(payload) {
loading.value = true;
try {
await saveRows(payload.rows || []);
ElMessage.success(
payload.mode === 'batch' ? '批量添加成功' : '账号添加成功'
);
await fetchList();
} finally {
loading.value = false;
}
}
function handleSelectionChange(rows) {
selectedRows.value = rows;
}
function openDetail(row) {
loading.value = true;
getAccountPoolDetail(moduleKey, row.id)
.then((res) => {
if (res?.code !== 200) {
ElMessage.error(res?.msg || '获取详情失败');
return;
}
detailRow.value = normalizeRow(res.data || {});
detailVisible.value = true;
})
.finally(() => {
loading.value = false;
});
}
function openExtractDialog() {
extractTargetRow.value = null;
extractForm.platform = 'local';
extractForm.type = 'account';
extractVisible.value = true;
}
function openExtractByRow(row) {
extractTargetRow.value = row;
extractForm.platform = 'local';
extractForm.type = row.type;
extractVisible.value = true;
}
async function handleExtract() {
loading.value = true;
try {
const target = extractTargetRow.value;
if (!target) {
ElMessage.warning('未找到提取目标');
return;
}
const res = await extractAccountPool(moduleKey, {
id: target.id,
type: target.type,
platform: extractForm.platform,
});
if (res?.code !== 200) {
ElMessage.error(res?.msg || '提取失败');
return;
}
ElMessage.success('提取成功');
extractVisible.value = false;
await fetchList();
} finally {
loading.value = false;
}
}
function markExtractForSelected() {
if (!selectedRows.value.length) {
ElMessage.warning('请先选择数据');
return;
}
loading.value = true;
Promise.all(
selectedRows.value.map((row) =>
extractAccountPool(moduleKey, {
id: row.id,
type: row.type,
platform: 'local',
})
)
)
.then(() => {
ElMessage.success('批量提取成功');
fetchList();
})
.finally(() => {
loading.value = false;
});
}
function typeText(type) {
if (type === 'account') return '账号密码';
if (type === 'account_tk') return '账号密码+Token';
return 'Token';
}
function platformText(platform) {
if (!platform) return '-';
return platform === 'local' ? '本地' : '闲鱼';
}
function normalizeRow(raw) {
const pick = (...keys) => {
for (const key of keys) {
if (raw?.[key] !== undefined && raw?.[key] !== null) return raw[key];
}
return '';
};
return {
id: pick('id', 'Id'),
type: pick('data_type', 'dataType', 'type'),
account: pick('account', 'Account'),
password: pick('password', 'Password'),
token: pick('token', 'Token'),
remark: pick('remark', 'Remark'),
extracted: Number(pick('is_extracted', 'isExtracted')) === 1 || !!pick('extracted'),
extractedAt: pick('extracted_time', 'extracted_at', 'extractedAt'),
extractedPlatform: pick('extracted_platform', 'extractedPlatform'),
createdAt: pick('create_time', 'created_at', 'createdAt'),
};
}
async function fetchList() {
loading.value = true;
try {
const res = await getAccountPoolList(moduleKey, {
page: pagination.page,
pageSize: pagination.pageSize,
keyword: query.keyword || undefined,
status: query.status || undefined,
type: activeTypeTab.value === 'all' ? undefined : activeTypeTab.value,
});
if (res?.code !== 200) {
ElMessage.error(res?.msg || '获取列表失败');
return;
}
const list = Array.isArray(res?.data?.list) ? res.data.list : [];
tableData.value = list.map(normalizeRow);
total.value = Number(res?.data?.total || 0);
} finally {
loading.value = false;
}
}
onMounted(() => {
fetchList();
});
</script>
<template>
<div class="account-pool-page">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>号池管理Cursor</span>
</div>
</template>
<div class="toolbar">
<div class="toolbar-left">
<el-input
v-model="query.keyword"
placeholder="搜索账号 / token / 备注"
clearable
class="w-260"
/>
<el-select v-model="query.status" placeholder="提取状态" clearable class="w-140">
<el-option label="未提取" value="unused" />
<el-option label="已提取" value="extracted" />
</el-select>
<el-button @click="resetQuery">重置</el-button>
</div>
<div class="toolbar-right">
<el-button type="primary" @click="openAddDialog('single')">添加账号</el-button>
<el-button type="success" @click="openAddDialog('batch')">批量添加</el-button>
<el-button @click="markExtractForSelected">批量标记提取</el-button>
</div>
</div>
<el-tabs v-model="activeTypeTab" class="type-tabs">
<el-tab-pane
v-for="tab in typeTabs"
:key="tab.value"
:label="tab.label"
:name="tab.value"
/>
</el-tabs>
<el-table
:data="pagedList"
border
stripe
style="width: 100%"
:loading="loading"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="52" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="账号类型" width="160" align="center">
<template #default="{ row }">
<el-tag>{{ typeText(row.type) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="account" label="账号" min-width="180" />
<el-table-column label="密码/Token" min-width="240">
<template #default="{ row }">
<span v-if="row.type === 'account'">
{{ row.password || '-' }}
</span>
<span v-else-if="row.type === 'account_tk'">
{{ `${row.password || '-'} / ${row.token || '-'}` }}
</span>
<span v-else>{{ row.token || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" />
<el-table-column label="提取状态" width="100">
<template #default="{ row }">
<el-tag :type="row.extracted ? 'success' : 'info'">
{{ row.extracted ? '已提取' : '未提取' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="extractedAt" label="提取时间" width="180" />
<el-table-column label="提取平台" width="120">
<template #default="{ row }">
{{ platformText(row.extractedPlatform) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
<el-button
link
type="warning"
:disabled="row.extracted"
@click="openExtractByRow(row)"
>
提取
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrap">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
background
layout="total, prev, pager, next, jumper"
:page-sizes="[30, 50, 100]"
:total="total"
/>
</div>
</el-card>
<Edit v-model="editVisible" :mode="editMode" @submit="handleEditSubmit" />
<el-dialog v-model="detailVisible" title="账号详情" width="560px">
<el-descriptions :column="1" border v-if="detailRow">
<el-descriptions-item label="ID">{{ detailRow.id }}</el-descriptions-item>
<el-descriptions-item label="账号类型">
{{ typeText(detailRow.type) }}
</el-descriptions-item>
<el-descriptions-item label="账号">
{{ detailRow.account || '-' }}
</el-descriptions-item>
<el-descriptions-item label="密码">
{{ detailRow.password || '-' }}
</el-descriptions-item>
<el-descriptions-item label="Token">
{{ detailRow.token || '-' }}
</el-descriptions-item>
<el-descriptions-item label="提取状态">
{{ detailRow.extracted ? '已提取' : '未提取' }}
</el-descriptions-item>
<el-descriptions-item label="提取时间">
{{ detailRow.extractedAt || '-' }}
</el-descriptions-item>
<el-descriptions-item label="提取平台">
{{ platformText(detailRow.extractedPlatform) }}
</el-descriptions-item>
<el-descriptions-item label="备注">
{{ detailRow.remark || '-' }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
<el-dialog v-model="extractVisible" title="提取账号" width="420px">
<el-form label-width="84px">
<el-form-item label="提取类型">
<el-input :model-value="typeText(extractForm.type)" disabled />
</el-form-item>
<el-form-item label="提取平台">
<el-radio-group v-model="extractForm.platform">
<el-radio value="local">本地</el-radio>
<el-radio value="xianyu">闲鱼</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="extractVisible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleExtract">
确认提取
</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.account-pool-page {
padding: 12px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.w-260 {
width: 260px;
}
.w-140 {
width: 140px;
}
.w-180 {
width: 180px;
}
.type-tabs {
margin-bottom: 12px;
}
.pagination-wrap {
display: flex;
justify-content: flex-end;
margin-top: 14px;
}
</style>