增强和平小店账号管理
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import QRCode from 'qrcode'
|
||||
import { useAccounts } from '@/composables/useAccounts'
|
||||
import { useProducts } from '@/composables/useProducts'
|
||||
import type { Product } from '@/composables/useProducts'
|
||||
@@ -13,6 +14,11 @@ const {
|
||||
toggleAll,
|
||||
getScore,
|
||||
getXiaodianAssets,
|
||||
getXiaodianRole,
|
||||
deleteAccount,
|
||||
xiaodianQRCode,
|
||||
bindXiaodianRole,
|
||||
closeXiaodianQRCode,
|
||||
getAccountProduct,
|
||||
setAccountProduct
|
||||
} = useAccounts()
|
||||
@@ -21,6 +27,22 @@ const { products, selectedProductName } = useProducts()
|
||||
const { currentActivity } = useActivity()
|
||||
const isXiaodian = computed(() => currentActivity.value?.id === 'xiaodian')
|
||||
const productFilterMap = ref<Record<string, string>>({})
|
||||
const qrImage = ref('')
|
||||
|
||||
watch(xiaodianQRCode, async (value) => {
|
||||
qrImage.value = ''
|
||||
if (value?.url) {
|
||||
try {
|
||||
qrImage.value = await QRCode.toDataURL(value.url, { width: 240, margin: 2 })
|
||||
} catch {}
|
||||
}
|
||||
})
|
||||
|
||||
const roleChannel = (type?: string) => type === 'wx' ? '微信' : type === 'qq' ? '手Q' : (type || '')
|
||||
|
||||
const handleBind = async () => {
|
||||
await bindXiaodianRole()
|
||||
}
|
||||
|
||||
const allSelected = computed(() =>
|
||||
accounts.value.length > 0 && selectedIndices.value.size === accounts.value.length
|
||||
@@ -102,39 +124,44 @@ const getResultMessage = (name: string) => {
|
||||
<span class="panel-title">账号作战表</span>
|
||||
<span class="panel-sub">{{ accounts.length }} 个账号,已选 {{ selectedIndices.size }} 个</span>
|
||||
</div>
|
||||
<label class="stat-pill">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allSelected"
|
||||
@change="toggleAll"
|
||||
>
|
||||
全选
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="select-cell"></th>
|
||||
<th class="index-cell">序号</th>
|
||||
<th class="select-cell">
|
||||
<label class="select-all">
|
||||
<span>全选</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allSelected"
|
||||
@change="toggleAll"
|
||||
>
|
||||
</label>
|
||||
</th>
|
||||
<th>账号</th>
|
||||
<th>凭据</th>
|
||||
<th>商品</th>
|
||||
<th>{{ isXiaodian ? '点券' : '积分' }}</th>
|
||||
<th v-if="isXiaodian">扭蛋碎片</th>
|
||||
<th v-if="isXiaodian">角色信息</th>
|
||||
<th class="status-col">状态</th>
|
||||
<th>最后结果</th>
|
||||
<th class="account-action-col">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="accounts.length === 0">
|
||||
<td :colspan="isXiaodian ? 8 : 7" class="empty-state">暂无账号,请加载账号</td>
|
||||
<td :colspan="isXiaodian ? 11 : 9" class="empty-state">暂无账号,请加载账号</td>
|
||||
</tr>
|
||||
<template v-else>
|
||||
<tr
|
||||
v-for="(account, index) in accounts"
|
||||
:key="account.name"
|
||||
>
|
||||
<td class="index-cell">{{ index + 1 }}</td>
|
||||
<td class="select-cell">
|
||||
<label class="account-check" @click="toggleAccount(index)">
|
||||
<div class="check-box" :class="{ checked: selectedIndices.has(index) }">
|
||||
@@ -200,6 +227,18 @@ const getResultMessage = (name: string) => {
|
||||
<td v-if="isXiaodian">
|
||||
{{ getXiaodianAssets(account.name)?.success ? getXiaodianAssets(account.name)?.fragments : '-' }}
|
||||
</td>
|
||||
<td v-if="isXiaodian" class="role-cell">
|
||||
<template v-if="getXiaodianRole(account.name)?.success">
|
||||
<div class="role-name">{{ getXiaodianRole(account.name)?.roleName || '-' }}</div>
|
||||
<div class="account-sub">
|
||||
{{ roleChannel(getXiaodianRole(account.name)?.type) }}
|
||||
<span v-if="getXiaodianRole(account.name)?.areaId"> · 大区{{ getXiaodianRole(account.name)?.areaId }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else class="account-sub" :title="getXiaodianRole(account.name)?.error || ''">
|
||||
{{ getXiaodianRole(account.name)?.error || '-' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="status-td">
|
||||
<span
|
||||
class="status-badge"
|
||||
@@ -213,12 +252,43 @@ const getResultMessage = (name: string) => {
|
||||
{{ getResultMessage(account.name) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="account-action-cell">
|
||||
<button
|
||||
class="btn btn-compact btn-account-delete"
|
||||
title="删除账号及其 CK"
|
||||
@click.stop="deleteAccount(account.name)"
|
||||
>
|
||||
删除账号
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="xiaodianQRCode" class="qr-backdrop" @click.self="closeXiaodianQRCode">
|
||||
<section class="qr-modal" role="dialog" aria-modal="true" aria-label="和平小店绑定二维码">
|
||||
<div class="qr-modal-head">
|
||||
<div>
|
||||
<div class="panel-title">绑定和平小店角色</div>
|
||||
<div class="panel-sub">账号:{{ xiaodianQRCode.accountName }}</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost icon-btn" title="关闭" aria-label="关闭" @click="closeXiaodianQRCode">×</button>
|
||||
</div>
|
||||
<div class="qr-modal-body">
|
||||
<img v-if="qrImage" :src="qrImage" alt="和平小店绑定二维码" class="qr-image">
|
||||
<div v-else class="qr-loading">二维码生成中...</div>
|
||||
<p class="qr-hint">请使用微信扫码选择并绑定游戏角色,完成后点击“绑定”。</p>
|
||||
<a v-if="xiaodianQRCode.gameRoleBindUrl" :href="xiaodianQRCode.gameRoleBindUrl" target="_blank" rel="noreferrer" class="qr-link">打开绑定页面</a>
|
||||
</div>
|
||||
<div class="qr-modal-foot">
|
||||
<button class="btn" @click="closeXiaodianQRCode">取消</button>
|
||||
<button class="btn btn-primary" @click="handleBind">绑定</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -257,15 +327,6 @@ const getResultMessage = (name: string) => {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.panel-head label.stat-pill {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.panel-head label.stat-pill input {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
@@ -317,15 +378,131 @@ tbody tr:hover {
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.select-all {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
cursor: pointer;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.select-all input {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.index-cell {
|
||||
width: 48px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.status-col {
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
.account-action-col,
|
||||
.account-action-cell {
|
||||
width: 92px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-account-delete {
|
||||
color: var(--danger);
|
||||
border-color: var(--danger);
|
||||
background: var(--danger-soft);
|
||||
}
|
||||
|
||||
.account-name {
|
||||
font-weight: 500;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.role-cell {
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
color: var(--text-strong);
|
||||
font-weight: 500;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.qr-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, .58);
|
||||
}
|
||||
|
||||
.qr-modal {
|
||||
width: min(380px, 100%);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.qr-modal-head,
|
||||
.qr-modal-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.qr-modal-head {
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.qr-modal-foot {
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.qr-modal-body {
|
||||
padding: 20px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-image {
|
||||
display: block;
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
margin: 0 auto 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.qr-loading {
|
||||
height: 240px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.qr-hint {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.qr-link {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.account-sub {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
|
||||
@@ -300,6 +300,7 @@ const handleOrganize = async () => {
|
||||
<table class="managed-account-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="managed-index">序号</th>
|
||||
<th class="managed-check"></th>
|
||||
<th>账号</th>
|
||||
<th>CK</th>
|
||||
@@ -308,10 +309,11 @@ const handleOrganize = async () => {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="accounts.length === 0">
|
||||
<td colspan="4" class="managed-empty">暂无账号</td>
|
||||
<td colspan="5" class="managed-empty">暂无账号</td>
|
||||
</tr>
|
||||
<template v-else>
|
||||
<tr v-for="account in accounts" :key="account.name">
|
||||
<tr v-for="(account, index) in accounts" :key="account.name">
|
||||
<td class="managed-index">{{ index + 1 }}</td>
|
||||
<td class="managed-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -618,6 +620,13 @@ const handleOrganize = async () => {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.managed-index {
|
||||
width: 48px;
|
||||
text-align: center !important;
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.managed-name {
|
||||
overflow: hidden;
|
||||
color: var(--text-strong);
|
||||
|
||||
@@ -7,7 +7,15 @@ import { useProducts } from '@/composables/useProducts'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
|
||||
const { scheduleActive, seckillRunning, startNow, stopSeckill, startFixed, startSchedule, cancelSchedule, restoreSchedule, subscribeSeckillState, unsubscribeSchedulerEvents } = useScheduler()
|
||||
const { reloadAccounts, queryAllScore, queryAllXiaodianAssets } = useAccounts()
|
||||
const {
|
||||
accounts,
|
||||
selectedIndices,
|
||||
reloadAccounts,
|
||||
queryAllScore,
|
||||
queryAllXiaodianAssets,
|
||||
queryAllXiaodianRoles,
|
||||
getXiaodianBindQRCode
|
||||
} = useAccounts()
|
||||
const { currentActivity } = useActivity()
|
||||
const isXiaodian = computed(() => currentActivity.value?.id === 'xiaodian')
|
||||
const { products, selectedProductId, loadProducts, selectProduct } = useProducts()
|
||||
@@ -50,6 +58,20 @@ const queryAssets = () => {
|
||||
queryAllScore()
|
||||
}
|
||||
|
||||
const queryRoles = () => {
|
||||
if (isXiaodian.value) queryAllXiaodianRoles()
|
||||
}
|
||||
|
||||
const openBindQRCode = async () => {
|
||||
if (selectedIndices.value.size !== 1) {
|
||||
showToast('获取绑定二维码', '请勾选一个账号后再操作', 'warn')
|
||||
return
|
||||
}
|
||||
const index = [...selectedIndices.value][0]
|
||||
const account = accounts.value[index]
|
||||
if (account) await getXiaodianBindQRCode(account.name)
|
||||
}
|
||||
|
||||
const saveFixedConfig = () => {
|
||||
localStorage.setItem('xiaodianFixedKeyword', fixedKeyword.value.trim())
|
||||
localStorage.setItem('xiaodianWatchKeyword', fixedKeyword.value.trim())
|
||||
@@ -177,6 +199,8 @@ onUnmounted(() => {
|
||||
</button>
|
||||
<button class="btn" @click="reloadAccounts">加载账号</button>
|
||||
<button class="btn" @click="queryAssets">{{ isXiaodian ? '查询点券/碎片' : '查询积分' }}</button>
|
||||
<button v-if="isXiaodian" class="btn" @click="queryRoles">查询角色</button>
|
||||
<button v-if="isXiaodian" class="btn btn-primary" @click="openBindQRCode">获取绑定二维码</button>
|
||||
<button v-if="isXiaodian" class="btn btn-fixed" @click="startFixedBuy">固定抢购</button>
|
||||
</div>
|
||||
<div class="schedule-item target-item">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useScheduler } from '@/composables/useScheduler'
|
||||
import StatusGrid from '@/components/console/StatusGrid.vue'
|
||||
@@ -19,10 +19,70 @@ const { runState, countdownSeconds, formatRemain } = useScheduler()
|
||||
|
||||
const settingsModalRef = ref<InstanceType<typeof SettingsModal> | null>(null)
|
||||
const currentPage = ref<'console' | 'ck' | 'exchange'>('console')
|
||||
const workspaceRef = ref<HTMLElement | null>(null)
|
||||
const resizingWorkspace = ref(false)
|
||||
const compactViewport = ref(false)
|
||||
const storedWorkspaceRatio = Number(localStorage.getItem('accountPanelRatio'))
|
||||
const accountPanelRatio = ref(
|
||||
Number.isFinite(storedWorkspaceRatio) && storedWorkspaceRatio >= 0.55 && storedWorkspaceRatio <= 0.84
|
||||
? storedWorkspaceRatio
|
||||
: 0.76
|
||||
)
|
||||
|
||||
const workspaceStyle = computed(() => ({
|
||||
gridTemplateColumns: compactViewport.value
|
||||
? '1fr'
|
||||
: `minmax(0, ${accountPanelRatio.value}fr) 8px minmax(260px, ${1 - accountPanelRatio.value}fr)`
|
||||
}))
|
||||
|
||||
const openSettings = () => {
|
||||
settingsModalRef.value?.open()
|
||||
}
|
||||
|
||||
const updateWorkspaceRatio = (clientX: number) => {
|
||||
const workspace = workspaceRef.value
|
||||
if (!workspace) return
|
||||
const rect = workspace.getBoundingClientRect()
|
||||
const next = (clientX - rect.left) / rect.width
|
||||
accountPanelRatio.value = Math.max(0.55, Math.min(0.84, next))
|
||||
}
|
||||
|
||||
const startWorkspaceResize = (event: PointerEvent) => {
|
||||
if (window.matchMedia('(max-width: 1180px)').matches) return
|
||||
resizingWorkspace.value = true
|
||||
updateWorkspaceRatio(event.clientX)
|
||||
document.body.classList.add('resizing-columns')
|
||||
window.addEventListener('pointermove', handleWorkspacePointerMove)
|
||||
window.addEventListener('pointerup', stopWorkspaceResize, { once: true })
|
||||
}
|
||||
|
||||
const handleWorkspacePointerMove = (event: PointerEvent) => {
|
||||
if (resizingWorkspace.value) updateWorkspaceRatio(event.clientX)
|
||||
}
|
||||
|
||||
const stopWorkspaceResize = () => {
|
||||
if (!resizingWorkspace.value) return
|
||||
resizingWorkspace.value = false
|
||||
localStorage.setItem('accountPanelRatio', String(accountPanelRatio.value))
|
||||
document.body.classList.remove('resizing-columns')
|
||||
window.removeEventListener('pointermove', handleWorkspacePointerMove)
|
||||
}
|
||||
|
||||
const updateCompactViewport = () => {
|
||||
compactViewport.value = window.matchMedia('(max-width: 1180px)').matches
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateCompactViewport()
|
||||
window.addEventListener('resize', updateCompactViewport)
|
||||
window.addEventListener('pointerup', stopWorkspaceResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopWorkspaceResize()
|
||||
window.removeEventListener('resize', updateCompactViewport)
|
||||
window.removeEventListener('pointerup', stopWorkspaceResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -87,8 +147,14 @@ const openSettings = () => {
|
||||
<ControlStrip />
|
||||
|
||||
<!-- 工作区 -->
|
||||
<section class="workspace">
|
||||
<section ref="workspaceRef" class="workspace" :class="{ resizing: resizingWorkspace }" :style="workspaceStyle">
|
||||
<AccountTable />
|
||||
<div
|
||||
class="workspace-resizer"
|
||||
role="separator"
|
||||
aria-label="调整账号表和日志宽度"
|
||||
@pointerdown="startWorkspaceResize"
|
||||
></div>
|
||||
<aside class="side-stack">
|
||||
<LogPanel />
|
||||
</aside>
|
||||
@@ -220,7 +286,7 @@ const openSettings = () => {
|
||||
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 380px;
|
||||
grid-template-columns: minmax(0, 1fr) 8px minmax(260px, 1fr);
|
||||
gap: 14px;
|
||||
padding: 14px 20px 20px;
|
||||
align-items: stretch;
|
||||
@@ -229,6 +295,25 @@ const openSettings = () => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workspace-resizer {
|
||||
width: 8px;
|
||||
min-height: 100%;
|
||||
border-radius: 4px;
|
||||
cursor: col-resize;
|
||||
transition: background-color .12s ease;
|
||||
}
|
||||
|
||||
.workspace-resizer:hover,
|
||||
.workspace.resizing .workspace-resizer {
|
||||
background: var(--accent-border);
|
||||
}
|
||||
|
||||
:global(body.resizing-columns),
|
||||
:global(body.resizing-columns *) {
|
||||
cursor: col-resize !important;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.side-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -254,5 +339,9 @@ const openSettings = () => {
|
||||
padding: 14px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.workspace-resizer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user