新增和平小店兑换记录查询
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useProducts } from '@/composables/useProducts'
|
||||
import { useAccounts } from '@/composables/useAccounts'
|
||||
import { useActivity } from '@/composables/useActivity'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { QueryXiaodianExchangeRecords } from '@wails/go/app/App'
|
||||
import AccountTable from '@/components/account/AccountTable.vue'
|
||||
|
||||
const { selectedProductId, selectedProductName } = useProducts()
|
||||
const { accounts } = useAccounts()
|
||||
const { currentActivity } = useActivity()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const loading = ref(false)
|
||||
const queryResult = ref<any>(null)
|
||||
|
||||
const isXiaodian = computed(() => currentActivity.value?.id === 'xiaodian')
|
||||
const rows = computed(() => {
|
||||
const results = queryResult.value?.results || {}
|
||||
return accounts.value.map(account => ({
|
||||
name: account.name,
|
||||
...(results[account.name] || { success: false, error: '尚未查询' })
|
||||
}))
|
||||
})
|
||||
const matchedCount = computed(() => rows.value.filter(row => row.success && row.matched).length)
|
||||
const noMatchCount = computed(() => rows.value.filter(row => row.success && !row.matched).length)
|
||||
const failedCount = computed(() => rows.value.filter(row => !row.success).length)
|
||||
|
||||
const queryRecords = async () => {
|
||||
if (!isXiaodian.value) {
|
||||
showToast('暂不支持', '兑换记录查询目前仅支持和平小店', 'warn')
|
||||
return
|
||||
}
|
||||
if (!selectedProductId.value) {
|
||||
showToast('提示', '请先在左侧商品列表选择商品', 'warn')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await QueryXiaodianExchangeRecords(
|
||||
selectedProductId.value,
|
||||
selectedProductName.value || ''
|
||||
)
|
||||
queryResult.value = result
|
||||
if (!result.success) {
|
||||
showToast('查询失败', result.error || '兑换记录接口未返回成功', 'error')
|
||||
return
|
||||
}
|
||||
const total = Object.keys(result.results || {}).length
|
||||
showToast('查询完成', `已查询 ${total} 个账号,当前商品命中 ${matchedCount.value} 个`, 'success')
|
||||
} catch (e: any) {
|
||||
showToast('查询失败', e?.message || '', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (value: string) => {
|
||||
if (!value) return '-'
|
||||
const numeric = Number(value)
|
||||
if (Number.isFinite(numeric) && numeric > 0) {
|
||||
return new Date(numeric < 10_000_000_000 ? numeric * 1000 : numeric).toLocaleString('zh-CN')
|
||||
}
|
||||
return value.replace('T', ' ')
|
||||
}
|
||||
|
||||
watch(selectedProductId, () => {
|
||||
queryResult.value = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="exchange-page">
|
||||
<div class="exchange-head">
|
||||
<div>
|
||||
<h2>兑换记录</h2>
|
||||
<p>查询每个账号的和平小店订单,并核对当前商品</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" :disabled="loading || !selectedProductId || !isXiaodian" @click="queryRecords">
|
||||
{{ loading ? '查询中...' : '查询兑换记录' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="selected-product-bar">
|
||||
<span class="bar-label">当前商品</span>
|
||||
<span v-if="selectedProductId" class="bar-product">{{ selectedProductName || selectedProductId }}</span>
|
||||
<span v-else class="bar-empty">请先在左侧商品列表选择商品</span>
|
||||
<span v-if="selectedProductId" class="bar-id">ID {{ selectedProductId }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="queryResult?.error" class="exchange-error">{{ queryResult.error }}</div>
|
||||
|
||||
<template v-if="queryResult?.success">
|
||||
<div class="exchange-stats">
|
||||
<div class="exchange-stat matched">
|
||||
<span>已兑换当前商品</span><strong>{{ matchedCount }}</strong>
|
||||
</div>
|
||||
<div class="exchange-stat no-match">
|
||||
<span>未兑换当前商品</span><strong>{{ noMatchCount }}</strong>
|
||||
</div>
|
||||
<div class="exchange-stat failed">
|
||||
<span>查询失败</span><strong>{{ failedCount }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="record-table-wrap">
|
||||
<table class="record-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>账号</th>
|
||||
<th class="status-column">当前商品</th>
|
||||
<th class="count-column">记录</th>
|
||||
<th>兑换明细</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="rows.length === 0">
|
||||
<td colspan="4" class="record-empty">暂无账号</td>
|
||||
</tr>
|
||||
<tr v-for="row in rows" :key="row.name">
|
||||
<td class="record-account">{{ row.name }}</td>
|
||||
<td class="status-column">
|
||||
<span v-if="!row.success" class="record-status error">查询失败</span>
|
||||
<span v-else-if="row.matched" class="record-status matched">已兑换</span>
|
||||
<span v-else class="record-status no-match">未兑换</span>
|
||||
</td>
|
||||
<td class="count-column">{{ row.success ? row.records?.length || 0 : '-' }}</td>
|
||||
<td>
|
||||
<div v-if="!row.success" class="record-error">{{ row.error }}</div>
|
||||
<div v-else-if="!row.records?.length" class="record-muted">暂无记录</div>
|
||||
<div v-else class="record-details">
|
||||
<div v-for="(record, index) in row.records" :key="`${row.name}-${index}`" class="record-detail">
|
||||
<span class="detail-name">{{ record.commodityName || record.commodityId }}</span>
|
||||
<span class="detail-quantity">×{{ record.quantity || 1 }}</span>
|
||||
<span class="detail-time">{{ formatTime(record.buyTime) }}</span>
|
||||
<span class="detail-status">状态 {{ record.status || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="!queryResult" class="record-placeholder">
|
||||
选择商品后点击查询,结果会显示所有账号的订单记录。
|
||||
</div>
|
||||
|
||||
<section class="exchange-account-section">
|
||||
<div class="account-section-head">
|
||||
<span>账号表格</span>
|
||||
<span class="account-section-meta">可查看账号选择、凭据和资产状态</span>
|
||||
</div>
|
||||
<AccountTable />
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.exchange-page {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 22px 24px 28px;
|
||||
}
|
||||
|
||||
.exchange-head,
|
||||
.selected-product-bar,
|
||||
.exchange-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.exchange-head {
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.exchange-head h2 {
|
||||
color: var(--text-strong);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.exchange-head p {
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.selected-product-bar {
|
||||
min-height: 42px;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bar-product {
|
||||
overflow: hidden;
|
||||
color: var(--text-strong);
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bar-id,
|
||||
.bar-empty {
|
||||
color: var(--muted-2);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.bar-id {
|
||||
margin-left: auto;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
.exchange-error {
|
||||
margin-top: 14px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--danger);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--danger);
|
||||
background: var(--danger-soft);
|
||||
}
|
||||
|
||||
.exchange-stats {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.exchange-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
min-width: 160px;
|
||||
padding: 9px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface-2);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.exchange-stat strong {
|
||||
font-size: 18px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.exchange-stat.matched strong { color: var(--success); }
|
||||
.exchange-stat.no-match strong { color: var(--warning); }
|
||||
.exchange-stat.failed strong { color: var(--danger); }
|
||||
|
||||
.record-table-wrap {
|
||||
overflow: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.record-table {
|
||||
width: 100%;
|
||||
min-width: 700px;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.record-table th,
|
||||
.record-table td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.record-table th {
|
||||
background: var(--surface-2);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.record-table tbody tr:last-child td { border-bottom: 0; }
|
||||
.record-table tbody tr:hover { background: var(--surface-2); }
|
||||
|
||||
.status-column { width: 116px; }
|
||||
.count-column { width: 64px; text-align: center !important; }
|
||||
.record-account { color: var(--text-strong); font-weight: 500; }
|
||||
|
||||
.record-status {
|
||||
display: inline-flex;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.record-status.matched { color: var(--success); background: var(--success-soft); }
|
||||
.record-status.no-match { color: var(--warning); background: var(--warning-soft); }
|
||||
.record-status.error { color: var(--danger); background: var(--danger-soft); }
|
||||
|
||||
.record-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.record-detail {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(130px, 1fr) 34px 145px 58px;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
overflow: hidden;
|
||||
color: var(--text);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-quantity,
|
||||
.detail-status { color: var(--muted-2); }
|
||||
.detail-time { font-variant-numeric: tabular-nums; }
|
||||
.record-muted,
|
||||
.record-error,
|
||||
.record-placeholder,
|
||||
.record-empty { color: var(--muted); font-size: 12px; }
|
||||
.record-error { color: var(--danger); }
|
||||
.record-empty { height: 100px; text-align: center !important; vertical-align: middle !important; }
|
||||
|
||||
.record-placeholder {
|
||||
padding: 54px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.exchange-account-section {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.account-section-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-strong);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.account-section-meta {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.exchange-account-section :deep(.panel) {
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.exchange-page { padding: 16px; }
|
||||
.exchange-head { align-items: flex-start; flex-direction: column; }
|
||||
.exchange-head .btn { width: 100%; }
|
||||
.record-detail { grid-template-columns: minmax(120px, 1fr) 34px; }
|
||||
.detail-time, .detail-status { grid-column: span 2; }
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,7 @@ import AccountTable from '@/components/account/AccountTable.vue'
|
||||
import LogPanel from '@/components/log/LogPanel.vue'
|
||||
import SettingsModal from '@/components/modal/SettingsModal.vue'
|
||||
import CKManager from '@/components/ck/CKManager.vue'
|
||||
import ExchangeRecords from '@/components/exchange/ExchangeRecords.vue'
|
||||
|
||||
defineProps<{
|
||||
serverTime: string
|
||||
@@ -17,7 +18,7 @@ const { theme, toggleTheme } = useTheme()
|
||||
const { runState, countdownSeconds, formatRemain } = useScheduler()
|
||||
|
||||
const settingsModalRef = ref<InstanceType<typeof SettingsModal> | null>(null)
|
||||
const currentPage = ref<'console' | 'ck'>('console')
|
||||
const currentPage = ref<'console' | 'ck' | 'exchange'>('console')
|
||||
|
||||
const openSettings = () => {
|
||||
settingsModalRef.value?.open()
|
||||
@@ -44,6 +45,13 @@ const openSettings = () => {
|
||||
>
|
||||
CK 管理
|
||||
</button>
|
||||
<button
|
||||
class="nav-tab"
|
||||
:class="{ active: currentPage === 'exchange' }"
|
||||
@click="currentPage = 'exchange'"
|
||||
>
|
||||
兑换记录
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
v-if="currentPage === 'console'"
|
||||
@@ -93,6 +101,11 @@ const openSettings = () => {
|
||||
<CKManager />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<!-- 兑换记录页面 -->
|
||||
<template v-else-if="currentPage === 'exchange'">
|
||||
<ExchangeRecords />
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<SettingsModal ref="settingsModalRef" />
|
||||
|
||||
Reference in New Issue
Block a user