修复补号功能

This commit is contained in:
扫地僧 2026-05-05 17:56:29 +08:00
parent 3ee4b2e9a8
commit cf7c94c7e7
12 changed files with 446 additions and 83 deletions

13
src/api/home.js Normal file
View File

@ -0,0 +1,13 @@
import request from '@/utils/request';
/**
* 按天统计号池已提取售卖数量依据 extracted_time
* @param {{ days?: number }} params days 默认 14最大 90
*/
export function getAccountPoolDailyExtract(params) {
return request({
url: '/platform/home/accountPoolDailyExtract',
method: 'get',
params,
});
}

View File

@ -54,6 +54,8 @@ function normalizeRow(raw) {
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())}`;
};
const st = Number(pick('is_extracted', 'isExtracted', 'IsExtracted'));
const extractStatus = Number.isFinite(st) ? st : 0;
return {
id: pick('id', 'Id', 'ID'),
type: pick('data_type', 'dataType', 'type'),
@ -61,7 +63,8 @@ function normalizeRow(raw) {
password: pick('password', 'Password'),
token: pick('token', 'Token'),
remark: pick('remark', 'Remark'),
extracted: Number(pick('is_extracted', 'isExtracted', 'IsExtracted')) === 1,
extractStatus,
extracted: extractStatus !== 0,
extractedAt: formatTime(pickNullable('extracted_time', 'extractedAt')),
extractedPlatform: pickNullable('extracted_platform', 'extractedPlatform'),
createdAt: formatTime(pick('create_time', 'createdAt')),

View File

@ -100,7 +100,9 @@ function copyToken() {
<span class="token-text">{{ row.token || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="提取状态">
{{ row.extracted ? '已提取' : '未提取' }}
{{
row.extractStatus === 2 ? '补号' : row.extracted ? '已提取' : '未提取'
}}
</el-descriptions-item>
<el-descriptions-item label="提取时间">{{ row.extractedAt || '-' }}</el-descriptions-item>
<el-descriptions-item label="提取平台">

View File

@ -20,13 +20,23 @@ const props = defineProps({
type: String,
default: '',
},
replenish: {
type: Boolean,
default: false,
},
platformMap: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['update:modelValue', 'update:platform', 'update:remark', 'confirm']);
const emit = defineEmits([
'update:modelValue',
'update:platform',
'update:remark',
'update:replenish',
'confirm',
]);
function typeText(type) {
if (type === 'account') return '账号密码';
@ -47,6 +57,14 @@ function typeText(type) {
<el-form-item label="提取类型">
<el-input :model-value="typeText(type)" disabled />
</el-form-item>
<el-form-item label="是否补号">
<el-switch
:model-value="replenish"
active-text="是"
inactive-text="否"
@update:model-value="(v) => emit('update:replenish', v)"
/>
</el-form-item>
<el-form-item label="提取平台">
<el-select
:model-value="platform"

View File

@ -1,5 +1,5 @@
<script setup>
import { computed, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { ElMessage } from "element-plus";
import Edit from "./components/edit.vue";
import DetailDialog from "./components/detail.vue";
@ -40,6 +40,7 @@ const extractForm = reactive({
platform: "local",
type: "account",
remark: "",
replenish: false,
});
const tableData = ref([]);
@ -54,6 +55,9 @@ const pagination = reactive({
pageSize: 20,
});
/** 跳转未提取末页时跳过 watcher避免先被重置到第 1 页 */
const skipWatchFetchDuringUnusedJump = ref(false);
const pagedList = computed(() => tableData.value);
function resetQuery() {
@ -73,6 +77,7 @@ const typeTabs = computed(() => {
watch(
() => [query.keyword, query.status, activeTypeTab.value],
() => {
if (skipWatchFetchDuringUnusedJump.value) return;
pagination.page = 1;
fetchList();
},
@ -81,6 +86,7 @@ watch(
watch(
() => [pagination.page, pagination.pageSize],
() => {
if (skipWatchFetchDuringUnusedJump.value) return;
fetchList();
},
);
@ -142,6 +148,7 @@ function openExtractDialog() {
extractTargetRow.value = null;
extractForm.platform = "local";
extractForm.type = "account";
extractForm.replenish = false;
extractVisible.value = true;
}
@ -150,6 +157,7 @@ function openExtractByRow(row) {
extractForm.platform = "local";
extractForm.type = row.type;
extractForm.remark = row.remark || "";
extractForm.replenish = false;
extractVisible.value = true;
}
@ -165,6 +173,14 @@ function buildCopyTextByRow(row) {
return parts.join('\n');
}
function rowToText(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(' / ');
}
async function copyToClipboard(text) {
if (!text) {
ElMessage.warning('无可复制内容');
@ -193,6 +209,7 @@ async function handleExtract() {
type: target.type,
platform: extractForm.platform,
remark: extractForm.remark || "",
replenish: !!extractForm.replenish,
});
if (res?.code !== 200) {
ElMessage.error(res?.msg || "提取失败");
@ -270,12 +287,46 @@ async function handleBatchExtract() {
}
}
async function handleReplenish() {
loading.value = true;
try {
const res = await replenishAccountPool(moduleKey, {
type: replenishForm.type,
platform: replenishForm.platform,
remark: replenishForm.remark || "",
});
if (res?.code !== 200) {
ElMessage.error(res?.msg || "补号失败");
return;
}
ElMessage.success("补号成功,已复制到剪贴板");
replenishVisible.value = false;
const row = normalizeRow(res.data || {});
navigator.clipboard.writeText(rowToText(row)).catch(() => {});
await fetchList();
} finally {
loading.value = false;
}
}
function typeText(type) {
if (type === "account") return "账号密码";
if (type === "account_tk") return "账号密码+Token";
return "Token";
}
function extractStatusLabel(row) {
if (row?.extractStatus === 2) return "补号";
if (row?.extracted) return "已提取";
return "未提取";
}
function extractStatusTagType(row) {
if (row?.extractStatus === 2) return "warning";
if (row?.extracted) return "success";
return "info";
}
const tooltipOpts = {
popperClass: 'pool-tooltip',
popperStyle: { maxWidth: '600px', wordBreak: 'break-all', whiteSpace: 'pre-wrap' },
@ -318,6 +369,8 @@ function normalizeRow(raw) {
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())}`;
};
const st = Number(pick("is_extracted", "isExtracted", "IsExtracted"));
const extractStatus = Number.isFinite(st) ? st : 0;
return {
id: pick("id", "Id", "ID"),
type: pick("data_type", "dataType", "type"),
@ -325,7 +378,8 @@ function normalizeRow(raw) {
password: pick("password", "Password"),
token: pick("token", "Token"),
remark: pick("remark", "Remark"),
extracted: Number(pick("is_extracted", "isExtracted", "IsExtracted")) === 1,
extractStatus,
extracted: extractStatus !== 0,
extractedAt: formatTime(pickNullable("extracted_time", "extractedAt")),
extractedPlatform: pickNullable("extracted_platform", "extractedPlatform"),
createdAt: formatTime(pick("create_time", "createdAt")),
@ -354,6 +408,35 @@ async function fetchList() {
}
}
/** 筛选「未提取」并翻到该条件下的最后一页(当前关键词、账号类型 tab 不变) */
async function jumpToLastUnusedPage() {
const type = activeTypeTab.value === "all" ? undefined : activeTypeTab.value;
const res = await getAccountPoolList(moduleKey, {
page: 1,
pageSize: pagination.pageSize,
keyword: query.keyword || undefined,
status: "unused",
type,
});
if (res?.code !== 200) {
ElMessage.error(res?.msg || "获取列表失败");
return;
}
const cnt = Number(res?.data?.total || 0);
if (cnt === 0) {
ElMessage.warning("暂无未提取数据");
return;
}
const lastPage = Math.max(1, Math.ceil(cnt / pagination.pageSize));
skipWatchFetchDuringUnusedJump.value = true;
pagination.page = lastPage;
query.status = "unused";
await nextTick();
skipWatchFetchDuringUnusedJump.value = false;
await fetchList();
ElMessage.success(`已跳转未提取第 ${lastPage} 页(共 ${cnt} 条)`);
}
function updateDeviceType() {
isMobile.value = window.innerWidth <= 768;
}
@ -470,6 +553,14 @@ function copyCardInfo(row) {
<el-option label="未提取" value="unused" />
<el-option label="已提取" value="extracted" />
</el-select>
<el-button
type="primary"
plain
title="按当前搜索与账号类型,筛选未提取并跳到最后一页"
@click="jumpToLastUnusedPage"
>
未提取末页
</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<div class="toolbar-right">
@ -517,8 +608,8 @@ function copyCardInfo(row) {
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip :tooltip-options="tooltipOpts" />
<el-table-column label="提取状态" width="100">
<template #default="{ row }">
<el-tag :type="row.extracted ? 'success' : 'info'">
{{ row.extracted ? "已提取" : "未提取" }}
<el-tag :type="extractStatusTagType(row)">
{{ extractStatusLabel(row) }}
</el-tag>
</template>
</el-table-column>
@ -541,9 +632,9 @@ function copyCardInfo(row) {
>详情</el-button
>
<el-button
v-if="!row.extractedAt && !row.extracted"
link
type="warning"
:disabled="row.extracted"
@click="openExtractByRow(row)"
>提取</el-button
>
@ -586,9 +677,11 @@ function copyCardInfo(row) {
:type="extractForm.type"
:platform="extractForm.platform"
:remark="extractForm.remark"
:replenish="extractForm.replenish"
:platform-map="PLATFORM_MAP"
@update:platform="(v) => (extractForm.platform = v)"
@update:remark="(v) => (extractForm.remark = v)"
@update:replenish="(v) => (extractForm.replenish = v)"
@confirm="handleExtract"
/>

View File

@ -90,7 +90,9 @@ function copyToken() {
<el-descriptions-item label="Token">
<span class="token-text">{{ row.token || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="提取状态">{{ row.extracted ? '已提取' : '未提取' }}</el-descriptions-item>
<el-descriptions-item label="提取状态">{{
row.extractStatus === 2 ? '补号' : row.extracted ? '已提取' : '未提取'
}}</el-descriptions-item>
<el-descriptions-item label="提取时间">{{ row.extractedAt || '-' }}</el-descriptions-item>
<el-descriptions-item label="提取平台">
<el-tag v-if="row.extractedPlatform" :type="platformType" size="small">{{ platformLabel }}</el-tag>

View File

@ -5,10 +5,17 @@ const props = defineProps({
type: { type: String, default: 'account' },
platform: { type: String, default: 'local' },
remark: { type: String, default: '' },
replenish: { type: Boolean, default: false },
platformMap: { type: Object, default: () => ({}) },
});
const emit = defineEmits(['update:modelValue', 'update:platform', 'update:remark', 'confirm']);
const emit = defineEmits([
'update:modelValue',
'update:platform',
'update:remark',
'update:replenish',
'confirm',
]);
function typeText(type) {
if (type === 'account') return '账号密码';
@ -29,6 +36,14 @@ function typeText(type) {
<el-form-item label="提取类型">
<el-input :model-value="typeText(type)" disabled />
</el-form-item>
<el-form-item label="是否补号">
<el-switch
:model-value="replenish"
active-text="是"
inactive-text="否"
@update:model-value="(v) => emit('update:replenish', v)"
/>
</el-form-item>
<el-form-item label="提取平台">
<el-select
:model-value="platform"

View File

@ -1,5 +1,5 @@
<script setup>
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import Edit from './components/edit.vue';
import DetailDialog from './components/detail.vue';
@ -34,7 +34,7 @@ const patchVisible = ref(false);
const query = reactive({ keyword: "", status: "" });
const activeTypeTab = ref("all");
const extractForm = reactive({ platform: 'local', type: 'account', remark: '' });
const extractForm = reactive({ platform: 'local', type: 'account', remark: '', replenish: false });
const tableData = ref([]);
const total = ref(0);
@ -44,6 +44,8 @@ const detailRemarkSaving = ref(false);
const isMobile = ref(false);
const pagination = reactive({ page: 1, pageSize: 30 });
const skipWatchFetchDuringUnusedJump = ref(false);
const pagedList = computed(() => tableData.value);
function resetQuery() {
@ -61,6 +63,7 @@ const typeTabs = computed(() => [
watch(
() => [query.keyword, query.status, activeTypeTab.value],
() => {
if (skipWatchFetchDuringUnusedJump.value) return;
pagination.page = 1;
fetchList();
},
@ -68,6 +71,7 @@ watch(
watch(
() => [pagination.page, pagination.pageSize],
() => {
if (skipWatchFetchDuringUnusedJump.value) return;
fetchList();
},
);
@ -124,6 +128,7 @@ function openExtractByRow(row) {
extractForm.platform = "local";
extractForm.type = row.type;
extractForm.remark = row.remark || '';
extractForm.replenish = false;
extractVisible.value = true;
}
@ -157,7 +162,11 @@ async function handleExtract() {
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 || '',
id: target.id,
type: target.type,
platform: extractForm.platform,
remark: extractForm.remark || '',
replenish: !!extractForm.replenish,
});
if (res?.code !== 200) { ElMessage.error(res?.msg || "提取失败"); return; }
ElMessage.success("提取成功");
@ -294,6 +303,8 @@ function normalizeRow(raw) {
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())}`;
};
const st = Number(pick("is_extracted", "isExtracted", "IsExtracted"));
const extractStatus = Number.isFinite(st) ? st : 0;
return {
id: pick("id", "Id", "ID"),
type: pick("data_type", "dataType", "type"),
@ -301,13 +312,26 @@ function normalizeRow(raw) {
password: pick("password", "Password"),
token: pick("token", "Token"),
remark: pick("remark", "Remark"),
extracted: Number(pick("is_extracted", "isExtracted", "IsExtracted")) === 1,
extractStatus,
extracted: extractStatus !== 0,
extractedAt: formatTime(pickNullable("extracted_time", "extractedAt")),
extractedPlatform: pickNullable("extracted_platform", "extractedPlatform"),
createdAt: formatTime(pick("create_time", "createdAt")),
};
}
function extractStatusLabel(row) {
if (row?.extractStatus === 2) return "补号";
if (row?.extracted) return "已提取";
return "未提取";
}
function extractStatusTagType(row) {
if (row?.extractStatus === 2) return "warning";
if (row?.extracted) return "success";
return "info";
}
async function fetchList() {
loading.value = true;
try {
@ -330,6 +354,34 @@ async function fetchList() {
}
}
async function jumpToLastUnusedPage() {
const type = activeTypeTab.value === "all" ? undefined : activeTypeTab.value;
const res = await getAccountPoolList(moduleKey, {
page: 1,
pageSize: pagination.pageSize,
keyword: query.keyword || undefined,
status: "unused",
type,
});
if (res?.code !== 200) {
ElMessage.error(res?.msg || "获取列表失败");
return;
}
const cnt = Number(res?.data?.total || 0);
if (cnt === 0) {
ElMessage.warning("暂无未提取数据");
return;
}
const lastPage = Math.max(1, Math.ceil(cnt / pagination.pageSize));
skipWatchFetchDuringUnusedJump.value = true;
pagination.page = lastPage;
query.status = "unused";
await nextTick();
skipWatchFetchDuringUnusedJump.value = false;
await fetchList();
ElMessage.success(`已跳转未提取第 ${lastPage} 页(共 ${cnt} 条)`);
}
onMounted(() => { fetchList(); });
// ---- ----
@ -428,6 +480,14 @@ function copyCardInfo(row) {
<el-option label="未提取" value="unused" />
<el-option label="已提取" value="extracted" />
</el-select>
<el-button
type="primary"
plain
title="按当前搜索与账号类型,筛选未提取并跳到最后一页"
@click="jumpToLastUnusedPage"
>
未提取末页
</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<div class="toolbar-right">
@ -465,8 +525,8 @@ function copyCardInfo(row) {
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip :tooltip-options="tooltipOpts" />
<el-table-column label="提取状态" width="100">
<template #default="{ row }">
<el-tag :type="row.extracted ? 'success' : 'info'">{{
row.extracted ? "已提取" : "未提取"
<el-tag :type="extractStatusTagType(row)">{{
extractStatusLabel(row)
}}</el-tag>
</template>
</el-table-column>
@ -489,9 +549,9 @@ function copyCardInfo(row) {
>详情</el-button
>
<el-button
v-if="!row.extractedAt && !row.extracted"
link
type="warning"
:disabled="row.extracted"
@click="openExtractByRow(row)"
>提取</el-button
>
@ -534,9 +594,11 @@ function copyCardInfo(row) {
:type="extractForm.type"
:platform="extractForm.platform"
:remark="extractForm.remark"
:replenish="extractForm.replenish"
:platform-map="PLATFORM_MAP"
@update:platform="(v) => (extractForm.platform = v)"
@update:remark="(v) => (extractForm.remark = v)"
@update:replenish="(v) => (extractForm.replenish = v)"
@confirm="handleExtract"
/>

View File

@ -90,7 +90,9 @@ function copyToken() {
<el-descriptions-item label="Token">
<span class="token-text">{{ row.token || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="提取状态">{{ row.extracted ? '已提取' : '未提取' }}</el-descriptions-item>
<el-descriptions-item label="提取状态">{{
row.extractStatus === 2 ? '补号' : row.extracted ? '已提取' : '未提取'
}}</el-descriptions-item>
<el-descriptions-item label="提取时间">{{ row.extractedAt || '-' }}</el-descriptions-item>
<el-descriptions-item label="提取平台">
<el-tag v-if="row.extractedPlatform" :type="platformType" size="small">{{ platformLabel }}</el-tag>

View File

@ -5,10 +5,17 @@ const props = defineProps({
type: { type: String, default: 'account' },
platform: { type: String, default: 'local' },
remark: { type: String, default: '' },
replenish: { type: Boolean, default: false },
platformMap: { type: Object, default: () => ({}) },
});
const emit = defineEmits(['update:modelValue', 'update:platform', 'update:remark', 'confirm']);
const emit = defineEmits([
'update:modelValue',
'update:platform',
'update:remark',
'update:replenish',
'confirm',
]);
function typeText(type) {
if (type === 'account') return '账号密码';
@ -29,6 +36,14 @@ function typeText(type) {
<el-form-item label="提取类型">
<el-input :model-value="typeText(type)" disabled />
</el-form-item>
<el-form-item label="是否补号">
<el-switch
:model-value="replenish"
active-text="是"
inactive-text="否"
@update:model-value="(v) => emit('update:replenish', v)"
/>
</el-form-item>
<el-form-item label="提取平台">
<el-select
:model-value="platform"

View File

@ -1,5 +1,5 @@
<script setup>
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import Edit from './components/edit.vue';
import DetailDialog from './components/detail.vue';
@ -33,7 +33,7 @@ const patchVisible = ref(false);
const query = reactive({ keyword: "", status: "" });
const activeTypeTab = ref("all");
const extractForm = reactive({ platform: 'local', type: 'account', remark: '' });
const extractForm = reactive({ platform: 'local', type: 'account', remark: '', replenish: false });
const tableData = ref([]);
const total = ref(0);
@ -43,6 +43,8 @@ const detailRemarkSaving = ref(false);
const isMobile = ref(false);
const pagination = reactive({ page: 1, pageSize: 30 });
const skipWatchFetchDuringUnusedJump = ref(false);
const pagedList = computed(() => tableData.value);
function resetQuery() {
@ -60,6 +62,7 @@ const typeTabs = computed(() => [
watch(
() => [query.keyword, query.status, activeTypeTab.value],
() => {
if (skipWatchFetchDuringUnusedJump.value) return;
pagination.page = 1;
fetchList();
},
@ -67,6 +70,7 @@ watch(
watch(
() => [pagination.page, pagination.pageSize],
() => {
if (skipWatchFetchDuringUnusedJump.value) return;
fetchList();
},
);
@ -123,6 +127,7 @@ function openExtractByRow(row) {
extractForm.platform = "local";
extractForm.type = row.type;
extractForm.remark = row.remark || '';
extractForm.replenish = false;
extractVisible.value = true;
}
@ -156,7 +161,11 @@ async function handleExtract() {
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 || '',
id: target.id,
type: target.type,
platform: extractForm.platform,
remark: extractForm.remark || '',
replenish: !!extractForm.replenish,
});
if (res?.code !== 200) { ElMessage.error(res?.msg || "提取失败"); return; }
ElMessage.success("提取成功");
@ -293,6 +302,8 @@ function normalizeRow(raw) {
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())}`;
};
const st = Number(pick("is_extracted", "isExtracted", "IsExtracted"));
const extractStatus = Number.isFinite(st) ? st : 0;
return {
id: pick("id", "Id", "ID"),
type: pick("data_type", "dataType", "type"),
@ -300,13 +311,26 @@ function normalizeRow(raw) {
password: pick("password", "Password"),
token: pick("token", "Token"),
remark: pick("remark", "Remark"),
extracted: Number(pick("is_extracted", "isExtracted", "IsExtracted")) === 1,
extractStatus,
extracted: extractStatus !== 0,
extractedAt: formatTime(pickNullable("extracted_time", "extractedAt")),
extractedPlatform: pickNullable("extracted_platform", "extractedPlatform"),
createdAt: formatTime(pick("create_time", "createdAt")),
};
}
function extractStatusLabel(row) {
if (row?.extractStatus === 2) return "补号";
if (row?.extracted) return "已提取";
return "未提取";
}
function extractStatusTagType(row) {
if (row?.extractStatus === 2) return "warning";
if (row?.extracted) return "success";
return "info";
}
async function fetchList() {
loading.value = true;
try {
@ -329,6 +353,34 @@ async function fetchList() {
}
}
async function jumpToLastUnusedPage() {
const type = activeTypeTab.value === 'all' ? undefined : activeTypeTab.value;
const res = await getAccountPoolList(moduleKey, {
page: 1,
pageSize: pagination.pageSize,
keyword: query.keyword || undefined,
status: 'unused',
type,
});
if (res?.code !== 200) {
ElMessage.error(res?.msg || '获取列表失败');
return;
}
const cnt = Number(res?.data?.total || 0);
if (cnt === 0) {
ElMessage.warning('暂无未提取数据');
return;
}
const lastPage = Math.max(1, Math.ceil(cnt / pagination.pageSize));
skipWatchFetchDuringUnusedJump.value = true;
pagination.page = lastPage;
query.status = 'unused';
await nextTick();
skipWatchFetchDuringUnusedJump.value = false;
await fetchList();
ElMessage.success(`已跳转未提取第 ${lastPage} 页(共 ${cnt} 条)`);
}
onMounted(() => { fetchList(); });
// ---- ----
@ -427,6 +479,14 @@ function copyCardInfo(row) {
<el-option label="未提取" value="unused" />
<el-option label="已提取" value="extracted" />
</el-select>
<el-button
type="primary"
plain
title="按当前搜索与账号类型,筛选未提取并跳到最后一页"
@click="jumpToLastUnusedPage"
>
未提取末页
</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<div class="toolbar-right">
@ -464,8 +524,8 @@ function copyCardInfo(row) {
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip :tooltip-options="tooltipOpts" />
<el-table-column label="提取状态" width="100">
<template #default="{ row }">
<el-tag :type="row.extracted ? 'success' : 'info'">{{
row.extracted ? "已提取" : "未提取"
<el-tag :type="extractStatusTagType(row)">{{
extractStatusLabel(row)
}}</el-tag>
</template>
</el-table-column>
@ -488,9 +548,9 @@ function copyCardInfo(row) {
>详情</el-button
>
<el-button
v-if="!row.extractedAt && !row.extracted"
link
type="warning"
:disabled="row.extracted"
@click="openExtractByRow(row)"
>提取</el-button
>
@ -533,9 +593,11 @@ function copyCardInfo(row) {
:type="extractForm.type"
:platform="extractForm.platform"
:remark="extractForm.remark"
:replenish="extractForm.replenish"
:platform-map="PLATFORM_MAP"
@update:platform="(v) => (extractForm.platform = v)"
@update:remark="(v) => (extractForm.remark = v)"
@update:replenish="(v) => (extractForm.replenish = v)"
@confirm="handleExtract"
/>

View File

@ -22,7 +22,7 @@
<el-row :gutter="20" class="charts-row">
<el-col :xs="24" :sm="24" :md="16">
<el-card shadow="hover" header="用户增长趋势">
<el-card shadow="hover" header="Token售卖统计">
<div ref="lineChartRef" class="chart-box"></div>
</el-card>
</el-col>
@ -36,9 +36,11 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, shallowRef } from 'vue';
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
import * as echarts from 'echarts';
import { ElMessage } from 'element-plus';
import { User, Pointer, Connection, Histogram } from '@element-plus/icons-vue';
import { getAccountPoolDailyExtract } from '@/api/home';
// --- ---
interface SummaryItem {
@ -63,35 +65,94 @@ const summaryData = ref<SummaryItem[]>([
{ title: '留存率', value: 85, icon: Histogram, color: '#F56C6C', percentage: 1, isUp: true },
]);
// --- ---
const initCharts = () => {
// 线
if (lineChartRef.value) {
lineChartInstance.value = echarts.init(lineChartRef.value);
lineChartInstance.value.setOption({
tooltip: { trigger: 'axis' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
function buildSalesLineOption(
days: string[],
cursor: number[],
kiro: number[],
windsurf: number[],
) {
const showSymbol = days.length <= 31;
return {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
},
legend: {
data: ['Cursor', 'Kiro', 'Windsurf'],
top: 4,
},
grid: { left: '3%', right: '4%', bottom: '3%', top: 52, containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
data: days,
},
yAxis: {
type: 'value',
minInterval: 1,
},
yAxis: { type: 'value' },
series: [
{
name: '新增用户',
name: 'Cursor',
type: 'line',
smooth: true,
data: [120, 132, 101, 134, 90, 230, 210],
areaStyle: { opacity: 0.3 },
itemStyle: { color: '#3973FF' }
}
]
});
showSymbol,
data: cursor,
areaStyle: { opacity: 0.08 },
lineStyle: { width: 2 },
itemStyle: { color: '#3973FF' },
},
{
name: 'Kiro',
type: 'line',
smooth: true,
showSymbol,
data: kiro,
areaStyle: { opacity: 0.08 },
lineStyle: { width: 2 },
itemStyle: { color: '#67C23A' },
},
{
name: 'Windsurf',
type: 'line',
smooth: true,
showSymbol,
data: windsurf,
areaStyle: { opacity: 0.08 },
lineStyle: { width: 2 },
itemStyle: { color: '#E6A23C' },
},
],
};
}
//
if (pieChartRef.value) {
async function loadAccountPoolDailyExtract() {
await nextTick();
if (!lineChartRef.value) return;
if (!lineChartInstance.value) {
lineChartInstance.value = echarts.init(lineChartRef.value);
}
try {
const res = await getAccountPoolDailyExtract({ days: 14 });
if (res?.code !== 200) {
ElMessage.error((res as { msg?: string })?.msg || '加载售卖数据失败');
return;
}
const d = (res as { data?: Record<string, unknown> }).data || {};
const days = Array.isArray(d.days) ? (d.days as string[]) : [];
const cursor = Array.isArray(d.cursor) ? (d.cursor as number[]) : [];
const kiro = Array.isArray(d.kiro) ? (d.kiro as number[]) : [];
const windsurf = Array.isArray(d.windsurf) ? (d.windsurf as number[]) : [];
lineChartInstance.value.setOption(buildSalesLineOption(days, cursor, kiro, windsurf), {
notMerge: true,
});
} catch {
ElMessage.error('加载售卖数据失败');
}
}
const initPieChart = () => {
if (!pieChartRef.value) return;
pieChartInstance.value = echarts.init(pieChartRef.value);
pieChartInstance.value.setOption({
tooltip: { trigger: 'item' },
@ -113,12 +174,20 @@ const initCharts = () => {
{ value: 1048, name: '普通用户' },
{ value: 735, name: 'VIP会员' },
{ value: 580, name: '超级管理员' },
{ value: 484, name: '运营人员' }
]
}
]
{ value: 484, name: '运营人员' },
],
},
],
});
}
};
const initLineChartShell = () => {
if (!lineChartRef.value) return;
lineChartInstance.value = echarts.init(lineChartRef.value);
lineChartInstance.value.setOption(
buildSalesLineOption([], [], [], []),
{ notMerge: true },
);
};
// --- ---
@ -127,13 +196,20 @@ const handleResize = () => {
pieChartInstance.value?.resize();
};
onMounted(() => {
initCharts();
onMounted(async () => {
await nextTick();
initLineChartShell();
initPieChart();
void loadAccountPoolDailyExtract();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
lineChartInstance.value?.dispose();
lineChartInstance.value = null;
pieChartInstance.value?.dispose();
pieChartInstance.value = null;
});
</script>