platform-vue/src/views/accountpool/kiro/index.vue
2026-04-29 18:25:59 +08:00

507 lines
19 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, onUnmounted, reactive, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import Edit from './components/edit.vue';
import DetailDialog from './components/detail.vue';
import ExtractDialog from './components/extract.vue';
import PatchDialog from '../components/patch.vue';
import {
addAccountPool,
batchAddAccountPool,
extractAccountPool,
getAccountPoolDetail,
getAccountPoolList,
updateAccountPoolRemark,
} from '@/api/accountPool';
const moduleKey = 'krio';
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 apiDocVisible = ref(false);
const patchVisible = ref(false);
const query = reactive({ keyword: '', status: '' });
const activeTypeTab = ref('all');
const extractForm = reactive({ platform: 'local', type: 'account', remark: '' });
const tableData = ref([]);
const total = ref(0);
const selectedRows = ref([]);
const detailRow = ref(null);
const detailRemarkSaving = ref(false);
const isMobile = ref(false);
const pagination = reactive({ page: 1, pageSize: 30 });
const pagedList = computed(() => tableData.value);
function resetQuery() {
query.keyword = '';
query.status = '';
}
const typeTabs = computed(() => [
{ 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;
}
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 openExtractByRow(row) {
extractTargetRow.value = row;
extractForm.platform = 'local';
extractForm.type = row.type;
extractForm.remark = row.remark || '';
extractVisible.value = true;
}
function openPatchDialog() {
patchVisible.value = true;
}
function buildCopyTextByRow(row) {
const parts = [];
if (row?.account) parts.push(row.account);
if (row?.password) parts.push(row.password);
if (row?.token) parts.push(row.token);
return parts.join('\n');
}
async function copyToClipboard(text) {
if (!text) { ElMessage.warning('无可复制内容'); return false; }
try {
await navigator.clipboard.writeText(text);
ElMessage.success('已复制');
return true;
} catch (e) {
ElMessage.error('复制失败,请检查浏览器权限');
return false;
}
}
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, remark: extractForm.remark || '',
});
if (res?.code !== 200) { ElMessage.error(res?.msg || '提取失败'); return; }
ElMessage.success('提取成功');
extractVisible.value = false;
await fetchList();
} finally { loading.value = false; }
}
async function handleSaveRemark(payload) {
if (!payload?.id) return;
detailRemarkSaving.value = true;
try {
const res = await updateAccountPoolRemark(moduleKey, payload);
if (res?.code !== 200) { ElMessage.error(res?.msg || '备注更新失败'); return; }
ElMessage.success('备注已更新');
if (detailRow.value?.id === payload.id) {
detailRow.value = { ...detailRow.value, remark: payload.remark || '' };
}
await fetchList();
} finally { detailRemarkSaving.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';
}
const PLATFORM_MAP = {
local: { label: '本地', type: 'info' },
xianyu: { label: '闲鱼', type: 'warning' },
taobao: { label: '淘宝', type: 'info' },
pinduoduo: { label: '拼多多', type: 'danger' },
jingdong: { label: '京东', type: 'primary' },
douyin: { label: '抖音', type: 'success' },
ziyoushangcheng: { label: '自有商城', type: 'warning' },
};
function platformText(platform) { return PLATFORM_MAP[platform]?.label || (platform || '-'); }
function platformTagType(platform) { return PLATFORM_MAP[platform]?.type || 'info'; }
function normalizeRow(raw) {
const pick = (...keys) => {
for (const key of keys) {
if (raw?.[key] !== undefined && raw?.[key] !== null) return raw[key];
}
return '';
};
const pickNullable = (...keys) => {
for (const key of keys) {
if (raw?.[key] !== undefined) return raw[key] ?? null;
}
return null;
};
const formatTime = (val) => {
if (!val) return '';
const d = new Date(val);
if (isNaN(d)) return val;
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())}`;
};
return {
id: pick('id', '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', 'IsExtracted')) === 1,
extractedAt: formatTime(pickNullable('extracted_time', 'extractedAt')),
extractedPlatform: pickNullable('extracted_platform', 'extractedPlatform'),
createdAt: formatTime(pick('create_time', '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; }
}
function updateDeviceType() {
isMobile.value = window.innerWidth <= 768;
}
onMounted(() => {
updateDeviceType();
window.addEventListener('resize', updateDeviceType);
fetchList();
});
onUnmounted(() => {
window.removeEventListener('resize', updateDeviceType);
});
// ---- 接口说明数据 ----
const BASE_URL = 'https://api.yunzer.cn';
const paramDocs = [
{ name: 'type', required: true, desc: '来源平台,用于标记本次提取来自哪个渠道', values: 'xianyu / taobao / pinduoduo / jingdong / douyin / ziyoushangcheng / local' },
{ name: 'module', required: true, desc: '号池模块,指定从哪个产品的号池提取', values: 'cursor / windsurf / krio' },
{ name: 'data_type', required: false, desc: '账号类型,不传则提取任意类型', values: 'account / tk / account_tk' },
];
const platformDocs = [
{ value: 'xianyu', label: '闲鱼', desc: '闲鱼平台发货调用' },
{ value: 'taobao', label: '淘宝', desc: '淘宝平台发货调用' },
{ value: 'pinduoduo', label: '拼多多', desc: '拼多多平台发货调用' },
{ value: 'jingdong', label: '京东', desc: '京东平台发货调用' },
{ value: 'douyin', label: '抖音', desc: '抖音平台发货调用' },
{ value: 'ziyoushangcheng', label: '自有商城', desc: '自有商城平台发货调用' },
{ value: 'local', label: '本地', desc: '本地手动调用' },
];
const moduleDocs = [
{ value: 'cursor', label: 'Cursor', desc: 'Cursor 号池' },
{ value: 'windsurf', label: 'Windsurf', desc: 'Windsurf 号池' },
{ value: 'krio', label: 'Krio', desc: 'Krio 号池' },
];
const examples = [
{ label: '闲鱼 · 提取 Krio Token', url: `${BASE_URL}/api/getcard?type=xianyu&module=krio&data_type=tk` },
{ label: '拼多多 · 提取 Krio 账号密码', url: `${BASE_URL}/api/getcard?type=pinduoduo&module=krio&data_type=account` },
{ label: '京东 · 提取 Cursor 任意类型', url: `${BASE_URL}/api/getcard?type=jingdong&module=cursor` },
{ label: '抖音 · 提取 Windsurf Token', url: `${BASE_URL}/api/getcard?type=douyin&module=windsurf&data_type=tk` },
];
const successResp = `// 纯 Token 类型data_type=tk
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
// 账号密码类型data_type=account
账号user@example.com / 密码your_password
// 账号密码+Token 类型data_type=account_tk
账号user@example.com / 密码your_password / TokeneyJhbGciOiJIUzI1NiIs...`;
const errorResp = `// 无可用卡密
{ "code": 404, "msg": "暂无可用卡密" }
// 参数错误
{ "code": 400, "msg": "缺少参数 type来源平台" }`;
function copyText(text) {
copyToClipboard(text);
}
function copyCardInfo(row) {
copyToClipboard(buildCopyTextByRow(row));
}
</script>
<template>
<div class="account-pool-page">
<el-card shadow="never">
<template #header>
<div class="card-header"><span>号池管理Kiro</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="warning" @click="openPatchDialog">补卡</el-button>
<el-button type="primary" @click="openAddDialog('single')">添加账号</el-button>
<el-button type="success" @click="openAddDialog('batch')">批量添加</el-button>
<el-button @click="markExtractForSelected">批量标记提取</el-button>
<el-button @click="apiDocVisible = true">接口说明</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>
<div class="table-scroll">
<el-table class="pool-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="100" align="center">
<template #default="{ row }"><el-tag>{{ typeText(row.type) }}</el-tag></template>
</el-table-column>
<el-table-column label="提取状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.extracted ? 'success' : 'info'">{{ row.extracted ? '已提取' : '未提取' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="提取平台" width="110" align="center" >
<template #default="{ row }">
<el-tag v-if="row.extractedPlatform" :type="platformTagType(row.extractedPlatform)" size="small">
{{ platformText(row.extractedPlatform) }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="extractedAt" label="提取时间" width="180" align="center" />
<el-table-column prop="remark" label="备注" min-width="140" />
<el-table-column label="操作" width="220" align="center">
<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>
<el-button v-if="row.extracted" link type="success" @click="copyCardInfo(row)">复制</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="pager">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
background
:layout="isMobile ? 'prev, pager, next' : 'total, prev, pager, next, jumper'"
:page-sizes="[30, 50, 100]"
:total="total"
/>
</div>
</el-card>
<Edit v-model="editVisible" :mode="editMode" @submit="handleEditSubmit" />
<DetailDialog
v-model="detailVisible"
:row="detailRow"
:save-loading="detailRemarkSaving"
@save-remark="handleSaveRemark"
/>
<ExtractDialog
v-model="extractVisible"
:loading="loading"
:type="extractForm.type"
:platform="extractForm.platform"
:remark="extractForm.remark"
:platform-map="PLATFORM_MAP"
@update:platform="(v) => (extractForm.platform = v)"
@update:remark="(v) => (extractForm.remark = v)"
@confirm="handleExtract"
/>
<PatchDialog
v-model="patchVisible"
:module="moduleKey"
:platform-map="PLATFORM_MAP"
:default-account-type="activeTypeTab === 'all' ? 'account' : activeTypeTab"
@success="fetchList"
/>
<!-- 接口说明抽屉 -->
<el-drawer v-model="apiDocVisible" title="提卡接口说明" size="560px" direction="rtl">
<div class="api-doc">
<el-alert type="info" :closable="false" style="margin-bottom:16px">
该接口为对外公开接口,无需登录认证,每次调用自动提取一条未使用的卡密并标记为已提取(不可重复)。
</el-alert>
<div class="doc-section">
<div class="doc-title">接口地址</div>
<el-tag type="success" class="method-tag">GET</el-tag>
<code class="url-code">https://api.yunzer.cn/api/getcard</code>
</div>
<div class="doc-section">
<div class="doc-title">请求参数</div>
<el-table :data="paramDocs" border size="small">
<el-table-column prop="name" label="参数名" width="120" />
<el-table-column prop="required" label="必填" width="60" align="center">
<template #default="{ row }">
<el-tag :type="row.required ? 'danger' : 'info'" size="small">{{ row.required ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="desc" label="说明" />
<el-table-column prop="values" label="可选值" min-width="160" />
</el-table>
</div>
<div class="doc-section">
<div class="doc-title">调用示例</div>
<div v-for="ex in examples" :key="ex.label" class="example-item">
<div class="example-label">{{ ex.label }}</div>
<div class="example-url-wrap">
<code class="example-url">{{ ex.url }}</code>
<el-button link type="primary" size="small" @click="copyText(ex.url)">复制</el-button>
</div>
</div>
</div>
<div class="doc-section">
<div class="doc-title">成功响应</div>
<pre class="code-block">{{ successResp }}</pre>
</div>
<div class="doc-section">
<div class="doc-title">失败响应</div>
<pre class="code-block">{{ errorResp }}</pre>
</div>
<div class="doc-section">
<div class="doc-title">支持的平台type 参数)</div>
<el-table :data="platformDocs" border size="small">
<el-table-column prop="value" label="type 值" width="130" />
<el-table-column prop="label" label="平台" width="100" />
<el-table-column prop="desc" label="说明" />
</el-table>
</div>
<div class="doc-section">
<div class="doc-title">支持的号池module 参数)</div>
<el-table :data="moduleDocs" border size="small">
<el-table-column prop="value" label="module 值" width="130" />
<el-table-column prop="label" label="号池" width="100" />
<el-table-column prop="desc" label="说明" />
</el-table>
</div>
</div>
</el-drawer>
</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; }
.type-tabs { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 14px; }
.table-scroll { width: 100%; overflow-x: hidden; }
.pool-table { min-width: 980px; }
@media (max-width: 768px) {
.account-pool-page { padding: 8px; }
.toolbar { gap: 8px; }
.toolbar-left, .toolbar-right { width: 100%; gap: 8px; }
.w-260, .w-140 { width: 100%; }
.toolbar-right .el-button {
flex: 1 1 calc(50% - 8px);
min-width: 120px;
margin: 0;
}
.type-tabs :deep(.el-tabs__nav-wrap) { overflow-x: auto; overflow-y: hidden; }
.pager { justify-content: center; }
}
.api-doc { padding: 0 4px; font-size: 13px; }
.doc-section { margin-bottom: 24px; }
.doc-title { font-weight: 600; font-size: 14px; margin-bottom: 10px; color: #303133; border-left: 3px solid #409eff; padding-left: 8px; }
.method-tag { margin-right: 8px; vertical-align: middle; }
.url-code { background: #f5f7fa; padding: 4px 10px; border-radius: 4px; font-size: 13px; color: #e6a23c; word-break: break-all; }
.example-item { margin-bottom: 10px; }
.example-label { font-size: 12px; color: #909399; margin-bottom: 4px; }
.example-url-wrap { display: flex; align-items: center; gap: 8px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px; }
.example-url { flex: 1; font-size: 12px; color: #409eff; word-break: break-all; }
.code-block { background: #1e1e1e; color: #d4d4d4; padding: 12px 16px; border-radius: 6px; font-size: 12px; line-height: 1.6; overflow-x: auto; white-space: pre-wrap; word-break: break-all; margin: 0; }
</style>