Files
dygou_go/frontend/src/components/exchange/ExchangeRecords.vue
T

383 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>