批量更新

This commit is contained in:
扫地僧 2026-04-29 18:25:59 +08:00
parent 19828ea396
commit 9c9bbb00f2
13 changed files with 968 additions and 147 deletions

View File

@ -1,5 +1,5 @@
<template>
<el-aside :width="width" class="common-aside">
<el-aside :width="width" :class="['common-aside', { 'mobile-open': isMobile && !isCollapse }]">
<!-- 加载状态 -->
<div v-if="loading" class="loading-spinner">
<i class="el-icon-loading" style="font-size: 24px; color: #fff"></i>
@ -143,13 +143,19 @@
</el-sub-menu>
</template>
</el-menu>
<div v-if="!loading && !hasError && !isCollapse" class="aside-toggle-bottom">
<el-button class="aside-toggle-btn" size="small" @click="handleCollapse">
<el-icon><Fold /></el-icon>
</el-button>
</div>
</el-aside>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { useRouter, useRoute } from "vue-router";
import { Document, Warning } from "@element-plus/icons-vue";
import { Document, Warning, Fold } from "@element-plus/icons-vue";
import { useAllDataStore, useMenuStore } from "@/stores";
const emit = defineEmits(["menu-click"]);
@ -163,7 +169,13 @@ const errorMsg = computed(() => menuStore.error || "加载菜单失败");
const store = useAllDataStore();
const isCollapse = computed(() => store.state.isCollapse);
const width = computed(() => (store.state.isCollapse ? "64px" : "200px"));
const isMobile = ref(false);
const width = computed(() => {
if (isMobile.value) {
return store.state.isCollapse ? "0px" : "220px";
}
return store.state.isCollapse ? "64px" : "200px";
});
const asideBgColor = ref("#304156");
const asideTextColor = ref("#bfcbd9");
@ -171,6 +183,13 @@ const activeColor = ref("#3973FF");
const activeBgColor = ref("#3973FF");
const currentModuleId = ref(null);
const updateDeviceType = () => {
isMobile.value = window.innerWidth <= 768;
//
if (isMobile.value) {
store.state.isCollapse = true;
}
};
const findMenuItem = (menus, targetIndex) => {
for (const menu of menus) {
@ -328,6 +347,9 @@ const handleMenuSelect = (index) => {
const menuItem = findMenuItem(list.value, index);
if (menuItem) {
emit("menu-click", menuItem);
if (isMobile.value) {
store.state.isCollapse = true;
}
}
};
@ -337,6 +359,10 @@ const fetchMenus = async () => {
} catch (error) {}
};
const handleCollapse = () => {
store.state.isCollapse = !store.state.isCollapse;
};
const handleMenuRefresh = () => {
fetchMenus();
};
@ -350,6 +376,9 @@ watch(
);
onMounted(() => {
updateDeviceType();
window.addEventListener("resize", updateDeviceType);
if (!menuStore.menus || menuStore.menus.length === 0) {
setTimeout(() => {
fetchMenus();
@ -360,6 +389,7 @@ onMounted(() => {
});
onUnmounted(() => {
window.removeEventListener("resize", updateDeviceType);
window.removeEventListener("menu-cache-refreshed", handleMenuRefresh);
});
</script>
@ -386,6 +416,10 @@ onUnmounted(() => {
}
}
.common-aside.mobile-open {
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.35), 2px 0 12px rgba(0, 0, 0, 0.18);
}
.loading-spinner {
display: flex;
align-items: center;
@ -422,7 +456,7 @@ h3 {
//
:deep(.el-menu) {
border-right: none;
height: calc(100% - 80px);
height: calc(100% - 128px);
padding: 16px 8px;
background: transparent;
@ -519,14 +553,48 @@ h3 {
}
}
.aside-toggle-bottom {
position: absolute;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
padding: 12px 8px 14px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0));
}
.aside-toggle-btn {
width: 100%;
max-width: 180px;
background-color: rgba(255, 255, 255, 0.18);
border-color: rgba(255, 255, 255, 0.3);
color: #fff;
}
.aside-toggle-btn:hover {
background-color: rgba(255, 255, 255, 0.28);
border-color: rgba(255, 255, 255, 0.45);
color: #fff;
}
//
@media (max-width: 768px) {
.common-aside {
width: 100% !important;
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 1200;
max-width: 80vw;
}
:deep(.el-menu) {
padding: 12px 4px;
}
.aside-toggle-bottom {
padding: 10px 8px 12px;
}
}
</style>

View File

@ -1,8 +1,8 @@
<template>
<div class="header">
<div class="l-content">
<el-button size="small" @click="handleCollapse">
<i class="fa fa-bars"></i>
<el-button v-if="showTopToggle" size="small" @click="handleCollapse">
<el-icon><Expand /></el-icon>
</el-button>
</div>
<div class="r-content">
@ -75,7 +75,7 @@ import { useRouter, useRoute } from "vue-router";
import { useAllDataStore, useMenuStore, useTabsStore } from "@/stores";
import { useAuthStore } from "@/stores/auth";
import { logout, getCurrentUser } from "@/api/login";
import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled } from '@element-plus/icons-vue';
import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Expand } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
const router = useRouter();
@ -201,6 +201,8 @@ const handleCollapse = () => {
store.state.isCollapse = !store.state.isCollapse;
};
const showTopToggle = computed(() => store.state.isCollapse);
const goHome = () => {
tabsStore.closeAll();
router.push('/home');
@ -457,6 +459,46 @@ onUnmounted(() => {
}
}
@media (max-width: 768px) {
.header {
padding: 0 10px;
gap: 8px;
}
.l-content .el-button {
margin-right: 6px;
}
.r-content {
gap: 6px;
.refresh-cache-btn,
.home-btn,
.theme-toggle-btn,
.message-btn {
width: 30px;
height: 30px;
min-height: 30px;
min-width: 30px;
padding: 0;
}
.el-dropdown-link {
gap: 6px;
.user {
width: 30px;
height: 30px;
}
.user-name,
.user-role-tag {
display: none;
}
}
}
}
// - 使
:deep(.el-dropdown) {
.el-dropdown__popper {

View File

@ -658,6 +658,34 @@ const canCloseRight = computed(() => {
transform: translateY(0);
}
}
@media (max-width: 768px) {
.common-layout,
.main-container {
.main-header {
height: 64px;
}
.right-main {
padding: 8px;
.multi-tabs-wrapper {
margin-bottom: 10px;
padding: 6px 8px;
gap: 8px;
}
.tabs-extra-actions {
display: none;
}
}
}
.backtop-button {
width: 34px;
height: 34px;
}
}
</style>
<style lang="less">

View File

@ -0,0 +1,201 @@
<script setup>
import { reactive, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { extractAccountPool } from '@/api/accountPool';
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
/** cursor / windsurf / krio */
module: {
type: String,
required: true,
},
platformMap: {
type: Object,
default: () => ({}),
},
/** 打开弹窗时的默认账号类型(与列表 Tab 对齐:全部时用 account */
defaultAccountType: {
type: String,
default: 'account',
},
});
const emit = defineEmits(['update:modelValue', 'success']);
const confirmLoading = ref(false);
const copiedText = ref('');
const form = reactive({
platform: 'local',
type: 'account',
remark: '',
});
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')),
};
}
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;
}
}
function resetWhenOpen() {
form.platform = 'local';
form.type = props.defaultAccountType || 'account';
form.remark = '';
copiedText.value = '';
}
watch(
() => props.modelValue,
(visible) => {
if (visible) resetWhenOpen();
}
);
function close() {
emit('update:modelValue', false);
}
async function handleConfirm() {
confirmLoading.value = true;
try {
const res = await extractAccountPool(props.module, {
id: 0,
type: form.type,
platform: form.platform,
remark: form.remark || '',
});
if (res?.code !== 200) {
ElMessage.error(res?.msg || '补卡失败');
return;
}
const extractedRow = normalizeRow(res?.data || {});
const text = buildCopyTextByRow(extractedRow);
copiedText.value = text;
const copied = await copyToClipboard(text);
emit('success');
if (copied) close();
} finally {
confirmLoading.value = false;
}
}
</script>
<template>
<el-dialog
:model-value="modelValue"
title="补卡"
width="520px"
:close-on-click-modal="false"
@update:model-value="emit('update:modelValue', $event)"
>
<el-form label-width="92px">
<el-form-item label="账号类型">
<el-select v-model="form.type" placeholder="请选择账号类型" class="field-full">
<el-option label="账号密码" value="account" />
<el-option label="账号密码+Token" value="account_tk" />
<el-option label="Token" value="tk" />
</el-select>
</el-form-item>
<el-form-item label="提取平台">
<el-select v-model="form.platform" placeholder="请选择平台" class="field-full">
<el-option
v-for="(meta, key) in platformMap"
:key="key"
:label="meta.label"
:value="key"
/>
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="form.remark"
class="field-full"
type="textarea"
:rows="3"
placeholder="可选"
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-form-item v-if="copiedText" label="复制内容">
<el-input v-model="copiedText" type="textarea" :rows="4" readonly />
<div class="patch-copy-actions">
<el-button @click="copyToClipboard(copiedText)">复制</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" :loading="confirmLoading" @click="handleConfirm">
确定并复制
</el-button>
</template>
</el-dialog>
</template>
<style scoped>
.field-full {
width: 100%;
}
.patch-copy-actions {
margin-top: 8px;
width: 100%;
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -1,5 +1,6 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
const props = defineProps({
modelValue: {
@ -28,9 +29,11 @@ function typeText(type) {
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' },
};
const platformLabel = computed(() => {
@ -55,13 +58,37 @@ function onSaveRemark() {
if (!props.row?.id) return;
emit('save-remark', { id: props.row.id, remark: remarkText.value || '' });
}
function copyAccountPassword() {
const account = props.row?.account || '';
const password = props.row?.password || '';
if (!account && !password) {
ElMessage.warning('暂无账号密码可复制');
return;
}
navigator.clipboard.writeText(`${account}\n${password}`.trim()).then(() => {
ElMessage.success('已复制账号+密码');
});
}
function copyToken() {
const token = props.row?.token || '';
if (!token) {
ElMessage.warning('暂无 Token 可复制');
return;
}
navigator.clipboard.writeText(token).then(() => {
ElMessage.success('已复制 Token');
});
}
</script>
<template>
<el-dialog
class="pool-detail-dialog"
:model-value="modelValue"
title="账号详情"
width="560px"
width="90%"
@update:model-value="(v) => emit('update:modelValue', v)"
>
<el-descriptions :column="1" border v-if="row">
@ -82,6 +109,12 @@ function onSaveRemark() {
</el-tag>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="复制">
<div class="copy-actions">
<el-button size="small" @click="copyAccountPassword">账号+密码</el-button>
<el-button size="small" type="primary" @click="copyToken">Token</el-button>
</div>
</el-descriptions-item>
<el-descriptions-item label="备注">
<div class="remark-edit-wrap">
<el-input v-model="remarkText" type="textarea" :rows="3" placeholder="请输入备注" />
@ -106,4 +139,42 @@ function onSaveRemark() {
flex-direction: column;
gap: 8px;
}
.copy-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.remark-edit-wrap .el-button {
align-self: flex-start;
}
:deep(.pool-detail-dialog) {
max-width: 560px;
}
@media (max-width: 768px) {
:deep(.pool-detail-dialog) {
width: calc(100vw - 24px) !important;
margin: 0 auto;
}
:deep(.pool-detail-dialog .el-dialog__body) {
padding: 12px;
max-height: 70vh;
overflow-y: auto;
}
:deep(.pool-detail-dialog .el-descriptions__label) {
width: 72px;
word-break: break-all;
white-space: normal;
}
.remark-edit-wrap .el-button {
width: 100%;
align-self: stretch;
}
}
</style>

View File

@ -37,9 +37,10 @@ function typeText(type) {
<template>
<el-dialog
class="pool-extract-dialog"
:model-value="modelValue"
title="提取账号"
width="420px"
width="90%"
@update:model-value="(v) => emit('update:modelValue', v)"
>
<el-form label-width="84px">
@ -78,3 +79,35 @@ function typeText(type) {
</template>
</el-dialog>
</template>
<style scoped>
:deep(.pool-extract-dialog) {
max-width: 420px;
}
@media (max-width: 768px) {
:deep(.pool-extract-dialog) {
width: calc(100vw - 24px) !important;
margin: 0 auto;
}
:deep(.pool-extract-dialog .el-dialog__body) {
padding: 12px;
}
:deep(.pool-extract-dialog .el-form-item__label) {
width: 74px !important;
}
:deep(.pool-extract-dialog .el-dialog__footer .el-button) {
width: calc(50% - 6px);
margin: 0;
}
:deep(.pool-extract-dialog .el-dialog__footer) {
display: flex;
gap: 12px;
padding: 8px 12px 12px;
}
}
</style>

View File

@ -1,9 +1,10 @@
<script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue';
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,
@ -22,6 +23,7 @@ const detailVisible = ref(false);
const extractVisible = ref(false);
const extractTargetRow = ref(null);
const apiDocVisible = ref(false);
const patchVisible = ref(false);
const query = reactive({
keyword: '',
@ -41,6 +43,7 @@ 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: 20,
@ -132,6 +135,33 @@ function openExtractByRow(row) {
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 {
@ -207,17 +237,14 @@ function typeText(type) {
return 'Token';
}
const tooltipOpts = {
popperClass: 'pool-tooltip',
popperStyle: { maxWidth: '600px', wordBreak: 'break-all', whiteSpace: 'pre-wrap' },
};
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) {
@ -285,24 +312,36 @@ async function fetchList() {
}
}
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 / local' },
{ 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: '本地手动调用' },
];
@ -341,15 +380,9 @@ function copyText(text) {
}
function copyCardInfo(row) {
const parts = [];
if (row.account) parts.push(row.account);
if (row.password) parts.push(row.password);
if (row.token) parts.push(row.token);
if (!parts.length) { ElMessage.warning('无可复制内容'); return; }
navigator.clipboard.writeText(parts.join('\n')).then(() => {
ElMessage.success('已复制');
});
copyToClipboard(buildCopyTextByRow(row));
}
</script>
<template>
@ -376,6 +409,7 @@ function copyCardInfo(row) {
<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>
@ -392,38 +426,31 @@ function copyCardInfo(row) {
/>
</el-tabs>
<el-table
:data="pagedList"
border
stripe
style="width: 100%"
:loading="loading"
@selection-change="handleSelectionChange"
>
<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="160" align="center">
<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 prop="account" label="账号" min-width="180" show-overflow-tooltip :tooltip-options="tooltipOpts" />
<el-table-column prop="password" label="密码" min-width="160" show-overflow-tooltip :tooltip-options="tooltipOpts">
<template #default="{ row }">{{ row.password || '-' }}</template>
</el-table-column>
<el-table-column label="Token" min-width="200" show-overflow-tooltip :tooltip-options="tooltipOpts">
<template #default="{ row }">{{ row.token || '-' }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip :tooltip-options="tooltipOpts" />
<el-table-column label="提取状态" width="100">
<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 prop="extractedAt" label="提取时间" width="180" />
<el-table-column label="提取平台" width="120">
<el-table-column label="提取平台" width="120" align="center" >
<template #default="{ row }">
<el-tag v-if="row.extractedPlatform" :type="platformTagType(row.extractedPlatform)" size="small">
{{ platformText(row.extractedPlatform) }}
@ -431,21 +458,24 @@ function copyCardInfo(row) {
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<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>
</el-table>
</div>
<div class="pagination-wrap">
<div class="pager">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
background
layout="total, prev, pager, next, jumper"
:layout="isMobile ? 'prev, pager, next' : 'total, prev, pager, next, jumper'"
:page-sizes="[20, 50, 100]"
:total="total"
/>
@ -473,6 +503,14 @@ function copyCardInfo(row) {
@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">
@ -589,12 +627,63 @@ function copyCardInfo(row) {
margin-bottom: 12px;
}
.pagination-wrap {
.pager {
display: flex;
justify-content: flex-end;
margin-top: 14px;
}
.table-scroll {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-scroll :deep(.el-scrollbar__bar.is-horizontal) {
display: none;
}
.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;
@ -668,14 +757,3 @@ function copyCardInfo(row) {
}
</style>
<style>
.pool-tooltip.el-popper {
max-width: 600px !important;
white-space: pre-wrap !important;
word-break: break-all !important;
}
.pool-tooltip .el-popper__arrow {
display: block;
}
</style>

View File

@ -1,5 +1,6 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
const props = defineProps({
modelValue: { type: Boolean, default: false },
@ -19,9 +20,11 @@ function typeText(type) {
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' },
};
const platformLabel = computed(() => {
@ -46,13 +49,37 @@ function onSaveRemark() {
if (!props.row?.id) return;
emit('save-remark', { id: props.row.id, remark: remarkText.value || '' });
}
function copyAccountPassword() {
const account = props.row?.account || '';
const password = props.row?.password || '';
if (!account && !password) {
ElMessage.warning('暂无账号密码可复制');
return;
}
navigator.clipboard.writeText(`${account}\n${password}`.trim()).then(() => {
ElMessage.success('已复制账号+密码');
});
}
function copyToken() {
const token = props.row?.token || '';
if (!token) {
ElMessage.warning('暂无 Token 可复制');
return;
}
navigator.clipboard.writeText(token).then(() => {
ElMessage.success('已复制 Token');
});
}
</script>
<template>
<el-dialog
class="pool-detail-dialog"
:model-value="modelValue"
title="账号详情"
width="560px"
width="90%"
@update:model-value="(v) => emit('update:modelValue', v)"
>
<el-descriptions :column="1" border v-if="row">
@ -69,6 +96,12 @@ function onSaveRemark() {
<el-tag v-if="row.extractedPlatform" :type="platformType" size="small">{{ platformLabel }}</el-tag>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="复制">
<div class="copy-actions">
<el-button size="small" @click="copyAccountPassword">账号+密码</el-button>
<el-button size="small" type="primary" @click="copyToken">Token</el-button>
</div>
</el-descriptions-item>
<el-descriptions-item label="备注">
<div class="remark-edit-wrap">
<el-input v-model="remarkText" type="textarea" :rows="3" placeholder="请输入备注" />
@ -93,4 +126,42 @@ function onSaveRemark() {
flex-direction: column;
gap: 8px;
}
.copy-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.remark-edit-wrap .el-button {
align-self: flex-start;
}
:deep(.pool-detail-dialog) {
max-width: 560px;
}
@media (max-width: 768px) {
:deep(.pool-detail-dialog) {
width: calc(100vw - 24px) !important;
margin: 0 auto;
}
:deep(.pool-detail-dialog .el-dialog__body) {
padding: 12px;
max-height: 70vh;
overflow-y: auto;
}
:deep(.pool-detail-dialog .el-descriptions__label) {
width: 72px;
word-break: break-all;
white-space: normal;
}
.remark-edit-wrap .el-button {
width: 100%;
align-self: stretch;
}
}
</style>

View File

@ -19,9 +19,10 @@ function typeText(type) {
<template>
<el-dialog
class="pool-extract-dialog"
:model-value="modelValue"
title="提取账号"
width="420px"
width="90%"
@update:model-value="(v) => emit('update:modelValue', v)"
>
<el-form label-width="84px">
@ -53,3 +54,35 @@ function typeText(type) {
</template>
</el-dialog>
</template>
<style scoped>
:deep(.pool-extract-dialog) {
max-width: 420px;
}
@media (max-width: 768px) {
:deep(.pool-extract-dialog) {
width: calc(100vw - 24px) !important;
margin: 0 auto;
}
:deep(.pool-extract-dialog .el-dialog__body) {
padding: 12px;
}
:deep(.pool-extract-dialog .el-form-item__label) {
width: 74px !important;
}
:deep(.pool-extract-dialog .el-dialog__footer .el-button) {
width: calc(50% - 6px);
margin: 0;
}
:deep(.pool-extract-dialog .el-dialog__footer) {
display: flex;
gap: 12px;
padding: 8px 12px 12px;
}
}
</style>

View File

@ -1,9 +1,10 @@
<script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue';
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,
@ -22,6 +23,7 @@ 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');
@ -33,6 +35,7 @@ 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);
@ -96,6 +99,30 @@ function openExtractByRow(row) {
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 {
@ -142,17 +169,14 @@ function typeText(type) {
return 'Token';
}
const tooltipOpts = {
popperClass: 'pool-tooltip',
popperStyle: { maxWidth: '600px', wordBreak: 'break-all', whiteSpace: 'pre-wrap' },
};
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 || '-'); }
@ -208,22 +232,36 @@ async function fetchList() {
} finally { loading.value = false; }
}
onMounted(() => { fetchList(); });
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 / pinduoduo / jingdong / douyin / local' },
{ 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: '本地手动调用' },
];
@ -256,17 +294,13 @@ const errorResp = `// 无可用卡密
{ "code": 400, "msg": "缺少参数 type来源平台" }`;
function copyText(text) {
navigator.clipboard.writeText(text).then(() => { ElMessage.success('已复制'); });
copyToClipboard(text);
}
function copyCardInfo(row) {
const parts = [];
if (row.account) parts.push(row.account);
if (row.password) parts.push(row.password);
if (row.token) parts.push(row.token);
if (!parts.length) { ElMessage.warning('无可复制内容'); return; }
navigator.clipboard.writeText(parts.join('\n')).then(() => { ElMessage.success('已复制'); });
copyToClipboard(buildCopyTextByRow(row));
}
</script>
<template>
@ -286,6 +320,7 @@ function copyCardInfo(row) {
<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>
@ -297,27 +332,19 @@ function copyCardInfo(row) {
<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">
<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="160" align="center">
<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 prop="account" label="账号" min-width="180" show-overflow-tooltip :tooltip-options="tooltipOpts" />
<el-table-column prop="password" label="密码" min-width="160" show-overflow-tooltip :tooltip-options="tooltipOpts">
<template #default="{ row }">{{ row.password || '-' }}</template>
</el-table-column>
<el-table-column label="Token" min-width="200" show-overflow-tooltip :tooltip-options="tooltipOpts">
<template #default="{ row }">{{ row.token || '-' }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip :tooltip-options="tooltipOpts" />
<el-table-column label="提取状态" width="100">
<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 prop="extractedAt" label="提取时间" width="180" />
<el-table-column label="提取平台" width="110">
<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) }}
@ -325,21 +352,24 @@ function copyCardInfo(row) {
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<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>
</el-table>
</div>
<div class="pagination-wrap">
<div class="pager">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
background
layout="total, prev, pager, next, jumper"
:layout="isMobile ? 'prev, pager, next' : 'total, prev, pager, next, jumper'"
:page-sizes="[30, 50, 100]"
:total="total"
/>
@ -367,6 +397,14 @@ function copyCardInfo(row) {
@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">
@ -438,7 +476,23 @@ function copyCardInfo(row) {
.w-260 { width: 260px; }
.w-140 { width: 140px; }
.type-tabs { margin-bottom: 12px; }
.pagination-wrap { display: flex; justify-content: flex-end; margin-top: 14px; }
.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; }
@ -450,11 +504,3 @@ function copyCardInfo(row) {
.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>
<style>
.pool-tooltip.el-popper {
max-width: 600px;
white-space: pre-wrap;
word-break: break-all;
}
</style>

View File

@ -1,5 +1,6 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
const props = defineProps({
modelValue: { type: Boolean, default: false },
@ -19,9 +20,11 @@ function typeText(type) {
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' },
};
const platformLabel = computed(() => {
@ -46,13 +49,37 @@ function onSaveRemark() {
if (!props.row?.id) return;
emit('save-remark', { id: props.row.id, remark: remarkText.value || '' });
}
function copyAccountPassword() {
const account = props.row?.account || '';
const password = props.row?.password || '';
if (!account && !password) {
ElMessage.warning('暂无账号密码可复制');
return;
}
navigator.clipboard.writeText(`${account}\n${password}`.trim()).then(() => {
ElMessage.success('已复制账号+密码');
});
}
function copyToken() {
const token = props.row?.token || '';
if (!token) {
ElMessage.warning('暂无 Token 可复制');
return;
}
navigator.clipboard.writeText(token).then(() => {
ElMessage.success('已复制 Token');
});
}
</script>
<template>
<el-dialog
class="pool-detail-dialog"
:model-value="modelValue"
title="账号详情"
width="560px"
width="90%"
@update:model-value="(v) => emit('update:modelValue', v)"
>
<el-descriptions :column="1" border v-if="row">
@ -69,6 +96,12 @@ function onSaveRemark() {
<el-tag v-if="row.extractedPlatform" :type="platformType" size="small">{{ platformLabel }}</el-tag>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="复制">
<div class="copy-actions">
<el-button size="small" @click="copyAccountPassword">账号+密码</el-button>
<el-button size="small" type="primary" @click="copyToken">Token</el-button>
</div>
</el-descriptions-item>
<el-descriptions-item label="备注">
<div class="remark-edit-wrap">
<el-input v-model="remarkText" type="textarea" :rows="3" placeholder="请输入备注" />
@ -93,4 +126,42 @@ function onSaveRemark() {
flex-direction: column;
gap: 8px;
}
.copy-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.remark-edit-wrap .el-button {
align-self: flex-start;
}
:deep(.pool-detail-dialog) {
max-width: 560px;
}
@media (max-width: 768px) {
:deep(.pool-detail-dialog) {
width: calc(100vw - 24px) !important;
margin: 0 auto;
}
:deep(.pool-detail-dialog .el-dialog__body) {
padding: 12px;
max-height: 70vh;
overflow-y: auto;
}
:deep(.pool-detail-dialog .el-descriptions__label) {
width: 72px;
word-break: break-all;
white-space: normal;
}
.remark-edit-wrap .el-button {
width: 100%;
align-self: stretch;
}
}
</style>

View File

@ -19,9 +19,10 @@ function typeText(type) {
<template>
<el-dialog
class="pool-extract-dialog"
:model-value="modelValue"
title="提取账号"
width="420px"
width="90%"
@update:model-value="(v) => emit('update:modelValue', v)"
>
<el-form label-width="84px">
@ -53,3 +54,35 @@ function typeText(type) {
</template>
</el-dialog>
</template>
<style scoped>
:deep(.pool-extract-dialog) {
max-width: 420px;
}
@media (max-width: 768px) {
:deep(.pool-extract-dialog) {
width: calc(100vw - 24px) !important;
margin: 0 auto;
}
:deep(.pool-extract-dialog .el-dialog__body) {
padding: 12px;
}
:deep(.pool-extract-dialog .el-form-item__label) {
width: 74px !important;
}
:deep(.pool-extract-dialog .el-dialog__footer .el-button) {
width: calc(50% - 6px);
margin: 0;
}
:deep(.pool-extract-dialog .el-dialog__footer) {
display: flex;
gap: 12px;
padding: 8px 12px 12px;
}
}
</style>

View File

@ -1,9 +1,10 @@
<script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue';
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,
@ -22,6 +23,7 @@ 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');
@ -33,6 +35,7 @@ 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);
@ -96,6 +99,30 @@ function openExtractByRow(row) {
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 {
@ -142,17 +169,14 @@ function typeText(type) {
return 'Token';
}
const tooltipOpts = {
popperClass: 'pool-tooltip',
popperStyle: { maxWidth: '600px', wordBreak: 'break-all', whiteSpace: 'pre-wrap' },
};
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 || '-'); }
@ -208,22 +232,36 @@ async function fetchList() {
} finally { loading.value = false; }
}
onMounted(() => { fetchList(); });
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 / pinduoduo / jingdong / douyin / local' },
{ 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: '本地手动调用' },
];
@ -256,17 +294,13 @@ const errorResp = `// 无可用卡密
{ "code": 400, "msg": "缺少参数 type来源平台" }`;
function copyText(text) {
navigator.clipboard.writeText(text).then(() => { ElMessage.success('已复制'); });
copyToClipboard(text);
}
function copyCardInfo(row) {
const parts = [];
if (row.account) parts.push(row.account);
if (row.password) parts.push(row.password);
if (row.token) parts.push(row.token);
if (!parts.length) { ElMessage.warning('无可复制内容'); return; }
navigator.clipboard.writeText(parts.join('\n')).then(() => { ElMessage.success('已复制'); });
copyToClipboard(buildCopyTextByRow(row));
}
</script>
<template>
@ -286,6 +320,7 @@ function copyCardInfo(row) {
<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>
@ -297,27 +332,19 @@ function copyCardInfo(row) {
<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">
<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="160" align="center">
<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 prop="account" label="账号" min-width="180" show-overflow-tooltip :tooltip-options="tooltipOpts" />
<el-table-column prop="password" label="密码" min-width="160" show-overflow-tooltip :tooltip-options="tooltipOpts">
<template #default="{ row }">{{ row.password || '-' }}</template>
</el-table-column>
<el-table-column label="Token" min-width="200" show-overflow-tooltip :tooltip-options="tooltipOpts">
<template #default="{ row }">{{ row.token || '-' }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip :tooltip-options="tooltipOpts" />
<el-table-column label="提取状态" width="100">
<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 prop="extractedAt" label="提取时间" width="180" />
<el-table-column label="提取平台" width="110">
<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) }}
@ -325,21 +352,24 @@ function copyCardInfo(row) {
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<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>
</el-table>
</div>
<div class="pagination-wrap">
<div class="pager">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
background
layout="total, prev, pager, next, jumper"
:layout="isMobile ? 'prev, pager, next' : 'total, prev, pager, next, jumper'"
:page-sizes="[30, 50, 100]"
:total="total"
/>
@ -367,6 +397,14 @@ function copyCardInfo(row) {
@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">
@ -438,7 +476,23 @@ function copyCardInfo(row) {
.w-260 { width: 260px; }
.w-140 { width: 140px; }
.type-tabs { margin-bottom: 12px; }
.pagination-wrap { display: flex; justify-content: flex-end; margin-top: 14px; }
.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; }
@ -450,11 +504,3 @@ function copyCardInfo(row) {
.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>
<style>
.pool-tooltip.el-popper {
max-width: 600px;
white-space: pre-wrap;
word-break: break-all;
}
</style>