增加便捷工具

This commit is contained in:
2026-06-17 17:18:49 +08:00
parent 9e482a12be
commit 673b84a0b3
50 changed files with 3135 additions and 9118 deletions
@@ -0,0 +1,600 @@
<script setup>
import { computed, reactive, ref, watch } from "vue";
import { ElMessage } from "element-plus";
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
row: {
type: Object,
default: null,
},
saveLoading: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:modelValue", "save-remark", "detail-action"]);
const remarkText = ref("");
const remarkDialogVisible = ref(false);
const platformDialogVisible = ref(false);
const unavailableDialogVisible = ref(false);
const unextractDialogVisible = ref(false);
const platformForm = reactive({ platform: "local" });
const TYPE_MAP = {
account: { label: "账号密码", type: "success" },
account_tk: { label: "账号密码+Token", type: "primary" },
tk: { label: "Token", type: "warning" },
};
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 statusInfo = computed(() => {
const status = Number(props.row?.extractStatus || 0);
if (status === 2) return { label: "补号", type: "warning" };
if (status === 3) return { label: "续杯", type: "primary" };
if (props.row?.extracted) return { label: "已提取", type: "success" };
return { label: "未提取", type: "info" };
});
const typeInfo = computed(() => {
return (
TYPE_MAP[props.row?.type] || { label: props.row?.type || "-", type: "info" }
);
});
const platformInfo = computed(() => {
const key = props.row?.extractedPlatform;
if (!key) return { label: "-", type: "info" };
return PLATFORM_MAP[key] || { label: key, type: "info" };
});
const isUsedInfo = computed(() => {
const raw = props.row?.isUsed;
if (raw === null || raw === undefined || raw === "") {
return { label: "未探测", type: "info" };
}
const n = Number(raw);
if (n === 1) return { label: "可用", type: "success" };
if (n === 0) return { label: "已用完", type: "danger" };
return { label: String(raw), type: "info" };
});
const hasAccountPassword = computed(
() => !!(props.row?.account || props.row?.password),
);
const hasToken = computed(() => !!props.row?.token);
watch(
() => props.row,
(row) => {
remarkText.value = row?.remark || "";
platformForm.platform = row?.extractedPlatform || "local";
},
{ immediate: true },
);
function closeDialog() {
emit("update:modelValue", false);
}
function openRemarkDialog() {
remarkText.value = props.row?.remark || "";
remarkDialogVisible.value = true;
}
function onSaveRemark() {
if (!props.row?.id) return;
emit("save-remark", { id: props.row.id, remark: remarkText.value || "" });
remarkDialogVisible.value = false;
}
function onSetUnavailable() {
if (!props.row?.id) return;
emit("detail-action", { action: "unavailable", id: props.row.id });
unavailableDialogVisible.value = false;
}
function onUpdatePlatform() {
if (!props.row?.id) return;
emit("detail-action", {
action: "platform",
id: props.row.id,
platform: platformForm.platform,
});
platformDialogVisible.value = false;
}
function onUnextract() {
if (!props.row?.id) return;
emit("detail-action", { action: "unextract", id: props.row.id });
unextractDialogVisible.value = false;
}
async function copyText(text, successText) {
const val = String(text || "").trim();
if (!val) {
ElMessage.warning("暂无可复制内容");
return;
}
try {
await navigator.clipboard.writeText(val);
ElMessage.success(successText || "已复制");
} catch {
ElMessage.error("复制失败,请检查浏览器权限");
}
}
function copyAccountPassword() {
const parts = [];
if (props.row?.account) parts.push(props.row.account);
if (props.row?.password) parts.push(props.row.password);
copyText(parts.join("\n"), "已复制账号+密码");
}
function copyToken() {
copyText(props.row?.token, "已复制 Token");
}
function copyAll() {
const parts = [];
if (props.row?.account) parts.push(`账号:${props.row.account}`);
if (props.row?.password) parts.push(`密码:${props.row.password}`);
if (props.row?.token) parts.push(`Token${props.row.token}`);
copyText(parts.join("\n"), "已复制完整账号信息");
}
</script>
<template>
<el-dialog
class="pool-detail-dialog"
:model-value="modelValue"
width="760px"
destroy-on-close
:show-close="false"
@update:model-value="(v) => emit('update:modelValue', v)"
>
<template #header>
<div class="detail-header">
<div>
<div class="detail-title">账号详情</div>
<div class="detail-subtitle">
通过弹窗执行账号状态、平台、备注等维护操作
</div>
</div>
<div class="header-actions">
<el-button circle plain @click="closeDialog">×</el-button>
</div>
</div>
</template>
<div v-if="row" class="detail-body">
<div class="info-grid">
<div class="info-card">
<div class="info-label">ID</div>
<div class="info-value">{{ row?.id || "-" }}</div>
</div>
<div class="info-card">
<div class="info-label">账号类型</div>
<div class="info-value">
<el-tag :type="typeInfo.type" round>{{ typeInfo.label }}</el-tag>
</div>
</div>
<div class="info-card">
<div class="info-label">提取状态</div>
<div class="info-value">
<el-tag :type="statusInfo.type" effect="dark" round>
{{ statusInfo.label }}
</el-tag>
</div>
</div>
<div class="info-card">
<div class="info-label">提取平台</div>
<div class="info-value">
<el-tag
v-if="row.extractedPlatform"
:type="platformInfo.type"
size="small"
>
{{ platformInfo.label }}
</el-tag>
<span v-else>-</span>
</div>
</div>
<div class="info-card">
<div class="info-label">提取时间</div>
<div class="info-value">{{ row.extractedAt || "-" }}</div>
</div>
<div class="info-card">
<div class="info-label">可用检测</div>
<div class="info-value">
<el-tag :type="isUsedInfo.type" round>
{{ isUsedInfo.label }}
</el-tag>
</div>
</div>
<div class="info-card">
<div class="info-label">账号</div>
<div class="info-value">{{ row.account || "-" }}</div>
</div>
<div class="info-card">
<div class="info-label">密码</div>
<div class="info-value">{{ row.password || "-" }}</div>
</div>
</div>
<div class="section-card">
<div class="section-head">
<div>
<div class="section-title">Token</div>
<div class="section-subtitle">
长 Token 已做自动换行,便于检查与复制
</div>
</div>
<el-button
size="small"
type="primary"
plain
:disabled="!hasToken"
@click="copyToken"
>
复制 Token
</el-button>
</div>
<pre class="token-box">{{ row.token || "暂无 Token" }}</pre>
</div>
<div class="section-card">
<div class="section-head">
<div>
<div class="section-title">快捷功能</div>
<div class="section-subtitle">按使用场景复制账号信息</div>
</div>
</div>
<div class="copy-actions">
<el-button
:disabled="!hasAccountPassword"
@click="copyAccountPassword"
>
复制账号+密码
</el-button>
<el-button
type="primary"
plain
:disabled="!hasToken"
@click="copyToken"
>
复制 Token
</el-button>
<el-button
type="success"
plain
:disabled="!hasAccountPassword && !hasToken"
@click="copyAll"
>
复制全部
</el-button>
</div>
</div>
<div class="section-card">
<div class="section-head">
<div>
<div class="section-title">维护操作</div>
<div class="section-subtitle">
点击按钮后打开确认/编辑弹窗,再执行对应操作
</div>
</div>
</div>
<div class="copy-actions">
<el-button
type="danger"
plain
@click="unavailableDialogVisible = true"
>
改不可用
</el-button>
<el-button
type="warning"
plain
@click="platformDialogVisible = true"
>
改平台
</el-button>
<el-button type="info" plain @click="unextractDialogVisible = true">
反提取
</el-button>
<el-button type="primary" plain @click="openRemarkDialog">
改备注
</el-button>
</div>
</div>
<div class="section-card">
<div class="section-head">
<div>
<div class="section-title">备注</div>
<div class="section-subtitle">备注改为弹窗编辑,当前仅展示</div>
</div>
</div>
<div class="remark-display">{{ row.remark || "暂无备注" }}</div>
</div>
</div>
</el-dialog>
<el-dialog
v-model="unavailableDialogVisible"
title="改不可用"
width="420px"
append-to-body
>
<el-alert type="warning" :closable="false">
确认将当前账号标记为不可用/已用完?
</el-alert>
<template #footer>
<el-button @click="unavailableDialogVisible = false">取消</el-button>
<el-button type="danger" :loading="saveLoading" @click="onSetUnavailable">
确认改不可用
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="platformDialogVisible"
title="改平台"
width="420px"
append-to-body
>
<el-form label-width="84px">
<el-form-item label="提取平台">
<el-select v-model="platformForm.platform" style="width: 100%">
<el-option
v-for="(v, k) in PLATFORM_MAP"
:key="k"
:label="v.label"
:value="k"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="platformDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saveLoading" @click="onUpdatePlatform">
确认修改
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="unextractDialogVisible"
title="反提取"
width="420px"
append-to-body
>
<el-alert type="warning" :closable="false">
反提取会把账号恢复为未提取,并清空提取时间与提取平台。
</el-alert>
<template #footer>
<el-button @click="unextractDialogVisible = false">取消</el-button>
<el-button type="warning" :loading="saveLoading" @click="onUnextract">
确认反提取
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="remarkDialogVisible"
title="改备注"
width="520px"
append-to-body
>
<el-input
v-model="remarkText"
type="textarea"
:rows="5"
resize="none"
placeholder="请输入备注"
/>
<template #footer>
<el-button @click="remarkDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saveLoading" @click="onSaveRemark">
保存备注
</el-button>
</template>
</el-dialog>
</template>
<style scoped>
:deep(.pool-detail-dialog) {
max-width: calc(100vw - 28px);
border-radius: 18px;
overflow: hidden;
}
:deep(.pool-detail-dialog .el-dialog__header) {
padding: 18px 22px;
margin: 0;
border-bottom: 1px solid #eef0f5;
}
:deep(.pool-detail-dialog .el-dialog__body) {
padding: 18px 22px 22px;
background: #f6f8fb;
}
:deep(.el-tag) {
border-radius: 4px !important;
}
.detail-header,
.header-actions,
.section-head,
.copy-actions {
display: flex;
align-items: center;
}
.detail-header {
justify-content: space-between;
gap: 14px;
}
.detail-title {
font-size: 18px;
font-weight: 700;
color: #1f2937;
}
.detail-subtitle,
.section-subtitle {
margin-top: 4px;
font-size: 12px;
color: #909399;
}
.header-actions {
gap: 10px;
}
.header-actions .el-button {
font-size: 18px;
font-weight: 300;
}
.detail-body {
display: flex;
flex-direction: column;
gap: 14px;
}
.section-card,
.info-card {
background: #fff;
border: 1px solid #edf0f6;
box-shadow: 0 10px 28px rgba(31, 41, 55, 0.06);
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.info-card {
border-radius: 14px;
padding: 14px;
min-width: 0;
}
.info-label {
color: #909399;
font-size: 12px;
margin-bottom: 8px;
}
.info-value {
color: #303133;
font-size: 14px;
font-weight: 600;
word-break: break-all;
min-height: 20px;
}
.section-card {
border-radius: 16px;
padding: 16px;
}
.section-head {
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.section-title {
font-size: 15px;
font-weight: 700;
color: #303133;
}
.token-box {
margin: 0;
padding: 14px;
max-height: 220px;
overflow: auto;
border-radius: 12px;
background: #111827;
color: #d1e7ff;
font-size: 12px;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-all;
}
.copy-actions {
flex-wrap: wrap;
gap: 10px;
}
.copy-actions .el-button {
margin: 0;
}
.remark-display {
padding: 12px;
min-height: 42px;
border-radius: 10px;
background: #f8fafc;
color: #303133;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}
@media (max-width: 768px) {
:deep(.pool-detail-dialog) {
width: calc(100vw - 24px) !important;
margin: 0 auto;
}
:deep(.pool-detail-dialog .el-dialog__header) {
padding: 14px 14px;
}
:deep(.pool-detail-dialog .el-dialog__body) {
padding: 12px;
max-height: 76vh;
overflow-y: auto;
}
.detail-header,
.section-head {
align-items: flex-start;
}
.section-head {
flex-direction: column;
}
.info-grid {
grid-template-columns: 1fr;
}
.copy-actions .el-button,
.section-head .el-button {
width: 100%;
}
}
</style>
@@ -0,0 +1,259 @@
<script setup>
import { computed, reactive, watch } from 'vue';
import { ElMessage } from 'element-plus';
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
mode: {
type: String,
default: 'single', // single | batch
},
});
const emit = defineEmits(['update:modelValue', 'submit']);
const form = reactive({
type: 'account', // account | tk | account_tk
account: '',
password: '',
token: '',
batchText: '',
remark: '',
});
const isBatch = computed(() => props.mode === 'batch');
const dialogTitle = computed(() => {
return isBatch.value ? '批量添加账号' : '添加账号';
});
const formatExample = computed(() => {
if (form.type === 'account') {
return 'account,password';
}
if (form.type === 'account_tk') {
return 'account,password,token';
}
return 'token';
});
watch(
() => props.modelValue,
(visible) => {
if (!visible) return;
resetForm();
}
);
function closeDialog() {
emit('update:modelValue', false);
}
function resetForm() {
form.type = 'account';
form.account = '';
form.password = '';
form.token = '';
form.batchText = '';
form.remark = '';
}
function parseBatchRows() {
const rows = form.batchText
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const parsed = [];
const errors = [];
rows.forEach((line, index) => {
if (form.type === 'account') {
const [account, password] = line.split(',').map((x) => (x || '').trim());
if (!account || !password) {
errors.push(`${index + 1} 行格式错误,应为 account,password`);
return;
}
parsed.push({
type: 'account',
account,
password,
token: '',
remark: form.remark,
});
return;
}
if (form.type === 'account_tk') {
const [account, password, token] = line
.split(',')
.map((x) => (x || '').trim());
if (!account || !token) {
errors.push(`${index + 1} 行格式错误,应为 account,password,tokenpassword 可为空)`);
return;
}
parsed.push({
type: 'account_tk',
account,
password,
token,
remark: form.remark,
});
return;
}
parsed.push({
type: 'tk',
account: '',
password: '',
token: line,
remark: form.remark,
});
});
return { parsed, errors };
}
function handleSubmit() {
if (!isBatch.value) {
if (form.type === 'account') {
if (!form.account || !form.password) {
return;
}
emit('submit', {
mode: 'single',
rows: [
{
type: 'account',
account: form.account.trim(),
password: form.password.trim(),
token: '',
remark: form.remark.trim(),
},
],
});
closeDialog();
return;
}
if (form.type === 'account_tk') {
if (!form.account || !form.token) {
ElMessage.warning('请输入账号和 Token,密码可为空');
return;
}
emit('submit', {
mode: 'single',
rows: [
{
type: 'account_tk',
account: form.account.trim(),
password: form.password.trim(),
token: form.token.trim(),
remark: form.remark.trim(),
},
],
});
closeDialog();
return;
}
if (!form.token) return;
emit('submit', {
mode: 'single',
rows: [
{
type: 'tk',
account: '',
password: '',
token: form.token.trim(),
remark: form.remark.trim(),
},
],
});
closeDialog();
return;
}
const { parsed, errors } = parseBatchRows();
if (errors.length || parsed.length === 0) {
ElMessage.warning(errors[0] || '请填写批量内容');
return;
}
emit('submit', {
mode: 'batch',
rows: parsed,
});
closeDialog();
}
</script>
<template>
<el-dialog
:model-value="modelValue"
:title="dialogTitle"
width="640px"
@close="closeDialog"
@update:model-value="(v) => emit('update:modelValue', v)"
>
<el-form label-width="96px">
<el-form-item label="账号类型">
<el-radio-group v-model="form.type">
<el-radio value="account">账号密码</el-radio>
<el-radio value="account_tk">账号密码+Token</el-radio>
<el-radio value="tk">Token</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="!isBatch">
<template v-if="form.type === 'account' || form.type === 'account_tk'">
<el-form-item label="账号">
<el-input v-model="form.account" placeholder="请输入账号" clearable />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" placeholder="请输入密码" clearable />
</el-form-item>
<el-form-item v-if="form.type === 'account_tk'" label="Token">
<el-input
v-model="form.token"
type="textarea"
:rows="4"
placeholder="请输入 token"
clearable
/>
</el-form-item>
</template>
<el-form-item v-else label="Token">
<el-input
v-model="form.token"
type="textarea"
:rows="4"
placeholder="请输入 token"
clearable
/>
</el-form-item>
</template>
<el-form-item v-else label="批量内容">
<el-input
v-model="form.batchText"
type="textarea"
:rows="12"
:placeholder="`每行一条,格式:${formatExample}`"
clearable
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" placeholder="可选备注" clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="handleSubmit">确认</el-button>
</template>
</el-dialog>
</template>
@@ -0,0 +1,103 @@
<script setup>
const props = defineProps({
modelValue: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
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',
'update:replenish',
'confirm',
]);
function typeText(type) {
if (type === 'account') return '账号密码';
if (type === 'account_tk') return '账号密码+Token';
return 'Token';
}
</script>
<template>
<el-dialog
class="pool-extract-dialog"
:model-value="modelValue"
title="提取账号"
width="90%"
@update:model-value="(v) => emit('update:modelValue', v)"
>
<el-form label-width="84px">
<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"
style="width: 100%"
@update:model-value="(v) => emit('update:platform', v)"
>
<el-option v-for="(v, k) in platformMap" :key="k" :value="k" :label="v.label" />
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input
:model-value="remark"
type="textarea"
:rows="3"
placeholder="提取备注可选"
@update:model-value="(v) => emit('update:remark', v)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="emit('update:modelValue', false)">取消</el-button>
<el-button type="primary" :loading="loading" @click="emit('confirm')">确认提取</el-button>
</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>
@@ -0,0 +1,49 @@
<script setup>
const props = defineProps({
modelValue: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
type: { type: String, default: 'tk' },
platform: { type: String, default: 'local' },
remark: { type: String, default: '' },
platformMap: { type: Object, default: () => ({}) },
});
const emit = defineEmits(['update:modelValue', 'update:type', 'update:platform', 'update:remark', 'confirm']);
</script>
<template>
<el-dialog
:model-value="modelValue"
title="补号"
width="420px"
@update:model-value="(v) => emit('update:modelValue', v)"
>
<el-form label-width="84px">
<el-form-item label="账号类型">
<el-select :model-value="type" style="width: 100%" @update:model-value="(v) => emit('update:type', v)">
<el-option label="Token" value="tk" />
<el-option label="账号密码" value="account" />
<el-option label="账号密码+Token" value="account_tk" />
</el-select>
</el-form-item>
<el-form-item label="提取平台">
<el-select :model-value="platform" style="width: 100%" @update:model-value="(v) => emit('update:platform', v)">
<el-option v-for="(v, k) in platformMap" :key="k" :value="k" :label="v.label" />
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input
:model-value="remark"
type="textarea"
:rows="3"
placeholder="提取备注可选"
@update:model-value="(v) => emit('update:remark', v)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="emit('update:modelValue', false)">取消</el-button>
<el-button type="warning" :loading="loading" @click="emit('confirm')">确认补号</el-button>
</template>
</el-dialog>
</template>
File diff suppressed because it is too large Load Diff