结算逻辑修改完成,起付额判定待处理

This commit is contained in:
2025-11-14 16:17:36 +08:00
parent bea1aa8e4c
commit 50e5ce8d08
15 changed files with 1053 additions and 220 deletions

View File

@@ -0,0 +1,38 @@
// 金额截断显示工具不补0、不四舍五入
// 规则:
// - USDT: 最多6位小数
// - ETH: 最多8位小数
// - 其他币种: 最多6位小数
// 返回 { text, truncated, full }
export function getMaxDecimalsByCoin(coin) {
const c = (coin || '').toString().toUpperCase();
if (c === 'ETH') return 8;
// USDT 与其他币种都按 6 位
return 6;
}
export function truncateAmountRaw(value, maxDecimals) {
if (value === null || value === undefined) {
return { text: '0', truncated: false, full: '0' };
}
const raw = String(value);
if (!raw) return { text: '0', truncated: false, full: '0' };
// 非数字字符串直接返回原值
if (!/^-?\d+(\.\d+)?$/.test(raw)) {
return { text: raw, truncated: false, full: raw };
}
const isNegative = raw.startsWith('-');
const abs = isNegative ? raw.slice(1) : raw;
const [intPart, decPart = ''] = abs.split('.');
const keep = decPart.slice(0, Math.max(0, maxDecimals));
const truncated = decPart.length > maxDecimals;
const text = (isNegative ? '-' : '') + (keep ? `${intPart}.${keep}` : intPart);
return { text, truncated, full: raw };
}
export function truncateAmountByCoin(value, coin) {
const max = getMaxDecimalsByCoin(coin);
return truncateAmountRaw(value, max);
}

View File

@@ -10,7 +10,23 @@
<el-table-column prop="payCoin" label="币种" min-width="100" />
<el-table-column prop="address" label="收款地址" min-width="240" />
<el-table-column prop="leaseTime" label="租赁天数" min-width="100" />
<el-table-column prop="price" label="售价(USDT)" min-width="240" />
<el-table-column prop="price" label="售价(USDT)" min-width="240">
<template #default="scope">
<span class="value strong">
<el-tooltip
v-if="formatAmount(scope.row.price, scope.row.payCoin || 'USDT').truncated"
:content="formatAmount(scope.row.price, scope.row.payCoin || 'USDT').full"
placement="top"
>
<span>
{{ formatAmount(scope.row.price, scope.row.payCoin || 'USDT').text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(scope.row.price, scope.row.payCoin || 'USDT').text }}</span>
</span>
</template>
</el-table-column>
</el-table>
</template>
</el-table-column>
@@ -26,7 +42,21 @@
<template #default="scope">{{ Array.isArray(scope.row && scope.row.orderItemDtoList) ? scope.row.orderItemDtoList.length : 0 }}</template>
</el-table-column>
<el-table-column label="总金额(USDT)" min-width="140">
<template #default="scope"><span class="value strong">{{ (scope.row && scope.row.totalPrice) != null ? scope.row.totalPrice : '—' }}</span></template>
<template #default="scope">
<span class="value strong">
<el-tooltip
v-if="formatAmount(scope.row && scope.row.totalPrice, 'USDT').truncated"
:content="formatAmount(scope.row && scope.row.totalPrice, 'USDT').full"
placement="top"
>
<span>
{{ formatAmount(scope.row && scope.row.totalPrice, 'USDT').text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(scope.row && scope.row.totalPrice, 'USDT').text }}</span>
</span>
</template>
</el-table-column>
<el-table-column min-width="180">
<template #header>
@@ -43,11 +73,37 @@
</el-tooltip>
</template>
<template #default="scope">
<span class="value strong">{{ (scope.row && scope.row.payAmount) != null ? scope.row.payAmount : '—' }}</span>
<span class="value strong">
<el-tooltip
v-if="formatAmount(scope.row && scope.row.payAmount, 'USDT').truncated"
:content="formatAmount(scope.row && scope.row.payAmount, 'USDT').full"
placement="top"
>
<span>
{{ formatAmount(scope.row && scope.row.payAmount, 'USDT').text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(scope.row && scope.row.payAmount, 'USDT').text }}</span>
</span>
</template>
</el-table-column>
<el-table-column label="待支付金额(USDT)" min-width="140">
<template #default="scope"><span class="value strong">{{ (scope.row && scope.row.noPayAmount) != null ? scope.row.noPayAmount : '—' }}</span></template>
<template #default="scope">
<span class="value strong">
<el-tooltip
v-if="formatAmount(scope.row && scope.row.noPayAmount, 'USDT').truncated"
:content="formatAmount(scope.row && scope.row.noPayAmount, 'USDT').full"
placement="top"
>
<span>
{{ formatAmount(scope.row && scope.row.noPayAmount, 'USDT').text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(scope.row && scope.row.noPayAmount, 'USDT').text }}</span>
</span>
</template>
</el-table-column>
<el-table-column label="操作" min-width="280" fixed="right">
<template #default="scope">
@@ -79,7 +135,21 @@
<el-dialog :visible.sync="dialogVisible" width="520px" title="请扫码支付">
<div style="text-align:left; margin-bottom:12px; color:#666;">
<div style="margin-bottom:6px;">总金额(USDT)<b>{{ paymentDialog.totalPrice }}</b></div>
<div style="margin-bottom:6px;">总金额(USDT)
<b>
<el-tooltip
v-if="formatAmount(paymentDialog.totalPrice, 'USDT').truncated"
:content="formatAmount(paymentDialog.totalPrice, 'USDT').full"
placement="top"
>
<span>
{{ formatAmount(paymentDialog.totalPrice, 'USDT').text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(paymentDialog.totalPrice, 'USDT').text }}</span>
</b>
</div>
<div style="margin-bottom:6px;display:flex;align-items:center;gap:6px;">
<el-tooltip placement="top" effect="dark">
<div slot="content">
@@ -90,9 +160,35 @@
<i class="el-icon-question" style="color:#909399;" aria-label="说明" role="img"></i>
</el-tooltip>
<span>已支付金额(USDT)</span>
<b class="value strong">{{ paymentDialog.payAmount }}</b>
<b class="value strong">
<el-tooltip
v-if="formatAmount(paymentDialog.payAmount, 'USDT').truncated"
:content="formatAmount(paymentDialog.payAmount, 'USDT').full"
placement="top"
>
<span>
{{ formatAmount(paymentDialog.payAmount, 'USDT').text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(paymentDialog.payAmount, 'USDT').text }}</span>
</b>
</div>
<div style="margin-bottom:6px;">待支付金额(USDT)
<b class="value strong">
<el-tooltip
v-if="formatAmount(paymentDialog.noPayAmount, 'USDT').truncated"
:content="formatAmount(paymentDialog.noPayAmount, 'USDT').full"
placement="top"
>
<span>
{{ formatAmount(paymentDialog.noPayAmount, 'USDT').text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(paymentDialog.noPayAmount, 'USDT').text }}</span>
</b>
</div>
<div style="margin-bottom:6px;">待支付金额(USDT)<b class="value strong">{{ paymentDialog.noPayAmount }}</b></div>
<!-- <div style="word-break:break-all;">收款地址<code>{{ orderDialog.address }}</code></div> -->
</div>
<div style="text-align:center;">
@@ -111,6 +207,7 @@
<script>
import { addOrders } from '../../api/order'
import { truncateAmountByCoin } from '../../utils/amount'
export default {
name: 'OrderList',
props: {
@@ -133,6 +230,9 @@ export default {
}
},
methods: {
formatAmount(value, coin) {
return truncateAmountByCoin(value, coin)
},
buildQrSrc(img) {
if (!img) return ''
try { const s = String(img).trim(); return s.startsWith('data:') ? s : `data:image/png;base64,${s}` } catch (e) { return '' }
@@ -244,6 +344,7 @@ export default {
.empty { color: #888; padding: 24px; text-align: center; }
.value.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; word-break: break-all; }
.value.strong { font-weight: 700; color: #e74c3c; }
.amount-more { font-size: 12px; color: #94a3b8; margin-left: 4px; }
</style>

View File

@@ -13,7 +13,21 @@
<div v-for="(row, idx) in rechargeRows" :key="getRowKey(row, idx)" class="record-item" :class="statusClass(row.status)" @click="toggleExpand('recharge', row, idx)">
<div class="item-main">
<div class="item-left">
<div class="amount">+ {{ formatDec6(row.amount) }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}</div>
<div class="amount">
<el-tooltip
v-if="formatAmount(row.amount, row.fromSymbol).truncated"
:content="`${formatAmount(row.amount, row.fromSymbol).full} ${(row.fromSymbol || 'USDT').toUpperCase()}`"
placement="top"
>
<span>
+ {{ formatAmount(row.amount, row.fromSymbol).text }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>
+ {{ formatAmount(row.amount, row.fromSymbol).text }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}
</span>
</div>
<div class="chain">{{ formatChain(row.fromChain) }}</div>
</div>
<div class="item-right">
@@ -54,7 +68,21 @@
<div v-for="(row, idx) in withdrawRows" :key="getRowKey(row, idx)" class="record-item" :class="statusClass(row.status)" @click="toggleExpand('withdraw', row, idx)">
<div class="item-main">
<div class="item-left">
<div class="amount">- {{ formatDec6(row.amount) }} {{ (row.toSymbol || 'USDT').toUpperCase() }}</div>
<div class="amount">
<el-tooltip
v-if="formatAmount(row.amount, row.toSymbol).truncated"
:content="`${formatAmount(row.amount, row.toSymbol).full} ${(row.toSymbol || 'USDT').toUpperCase()}`"
placement="top"
>
<span>
- {{ formatAmount(row.amount, row.toSymbol).text }} {{ (row.toSymbol || 'USDT').toUpperCase() }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>
- {{ formatAmount(row.amount, row.toSymbol).text }} {{ (row.toSymbol || 'USDT').toUpperCase() }}
</span>
</div>
<div class="chain">{{ formatChain(row.toChain) }}</div>
</div>
<div class="item-right">
@@ -95,7 +123,21 @@
<div v-for="(row, idx) in consumeRows" :key="getRowKey(row, idx)" class="record-item" :class="statusClass(row.status)" @click="toggleExpand('consume', row, idx)">
<div class="item-main">
<div class="item-left">
<div class="amount">- {{ formatDec6(row.realAmount) }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}</div>
<div class="amount">
<el-tooltip
v-if="formatAmount(row.realAmount, row.fromSymbol).truncated"
:content="`${formatAmount(row.realAmount, row.fromSymbol).full} ${(row.fromSymbol || 'USDT').toUpperCase()}`"
placement="top"
>
<span>
- {{ formatAmount(row.realAmount, row.fromSymbol).text }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>
- {{ formatAmount(row.realAmount, row.fromSymbol).text }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}
</span>
</div>
<div class="chain">{{ formatChain(row.fromChain) }}</div>
</div>
<div class="item-right">
@@ -140,6 +182,7 @@
<script>
import { transactionRecord } from '../../api/wallet'
import { truncateAmountByCoin } from '../../utils/amount'
export default {
name: 'AccountFundsFlow',
@@ -254,6 +297,12 @@ export default {
this.loadList()
},
methods: {
/**
* 金额格式化不补0、不四舍五入
*/
formatAmount(value, coin) {
return truncateAmountByCoin(value, coin)
},
/**
* 处理 Tab 切换:清空展开状态,确保手风琴行为
* @param {any} pane - 当前 paneElement UI 传入)
@@ -399,22 +448,7 @@ export default {
* @param {number|string} value
* @returns {string}
*/
formatDec6(value) {
if (value === null || value === undefined || value === '') return '0'
let s = String(value)
// 展开科学计数法为普通小数,避免 1e-7 之类展示
if (/e/i.test(s)) {
const n = Number(value)
if (!Number.isFinite(n)) return '0'
s = n.toFixed(20).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
}
const m = s.match(/^(-?)(\d+)(?:\.(\d+))?$/)
if (!m) return s
let intPart = m[2]
let decPart = m[3] || ''
if (decPart.length > 6) decPart = decPart.slice(0, 6)
return decPart ? `${intPart}.${decPart}` : intPart
},
// 删除旧的 formatDec6,统一使用 formatAmount
handleSizeChange(val) {
console.log(`每页 ${val}`);
this.pagination.pageSize = val;
@@ -517,6 +551,7 @@ export default {
.mono { font-family: "Monaco", "Menlo", monospace; }
.mono-ellipsis { font-family: "Monaco", "Menlo", monospace; max-width: 480px; display: inline-block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty { text-align: center; color: #999; padding: 20px 0; }
.amount-more { font-size: 12px; color: #94a3b8; margin-left: 4px; }
</style>

View File

@@ -756,12 +756,13 @@ export default {
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
}
const endsWithDot = v.endsWith('.')
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (intPart.length > 6) intPart = intPart.slice(0, 6)
if (decPart) decPart = decPart.slice(0, 4)
v = decPart.length ? `${intPart}.${decPart}` : intPart
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
this.$set(this.selectedMachineRows[index], 'powerDissipation', v)
},
/**
@@ -785,12 +786,13 @@ export default {
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
}
const endsWithDot = v.endsWith('.')
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (intPart.length > 6) intPart = intPart.slice(0, 6)
if (decPart) decPart = decPart.slice(0, 4)
v = decPart.length ? `${intPart}.${decPart}` : intPart
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
this.$set(this.selectedMachineRows[index], 'theoryPower', v)
},
/**

View File

@@ -58,13 +58,45 @@
<el-table-column
prop="estimatedEndIncome"
label="预计总收益"
min-width="120"
/>
min-width="140"
>
<template #default="scope">
<span class="value strong">
<el-tooltip
v-if="formatAmount(scope.row.estimatedEndIncome, scope.row.coin || 'USDT').truncated"
:content="formatAmount(scope.row.estimatedEndIncome, scope.row.coin || 'USDT').full"
placement="top"
>
<span>
{{ formatAmount(scope.row.estimatedEndIncome, scope.row.coin || 'USDT').text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(scope.row.estimatedEndIncome, scope.row.coin || 'USDT').text }}</span>
</span>
</template>
</el-table-column>
<el-table-column
prop="estimatedEndUsdtIncome"
label="预计USDT总收益"
min-width="160"
/>
>
<template #default="scope">
<span class="value strong">
<el-tooltip
v-if="formatAmount(scope.row.estimatedEndUsdtIncome, 'USDT').truncated"
:content="formatAmount(scope.row.estimatedEndUsdtIncome, 'USDT').full"
placement="top"
>
<span>
{{ formatAmount(scope.row.estimatedEndUsdtIncome, 'USDT').text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(scope.row.estimatedEndUsdtIncome, 'USDT').text }}</span>
</span>
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" min-width="160" >
<template #default="scope">
<span>{{ formatDateTime(scope.row.startTime) }}</span>
@@ -109,6 +141,7 @@
<script>
import { getOwnedList } from "../../api/products";
import { coinList } from "../../utils/coinList";
import { truncateAmountByCoin } from "../../utils/amount";
export default {
name: "AccountPurchased",
@@ -131,6 +164,9 @@ export default {
this.fetchTableData(this.pagination);
},
methods: {
formatAmount(value, coin) {
return truncateAmountByCoin(value, coin);
},
async fetchTableData(params) {
this.loading = true;
try {
@@ -230,5 +266,6 @@ export default {
justify-content: flex-end;
margin-top: 12px;
}
.amount-more { font-size: 12px; color: #94a3b8; margin-left: 4px; }
</style>

View File

@@ -79,7 +79,21 @@
</el-table-column>
<el-table-column label="收款金额(USDT)" min-width="160" align="right">
<template #default="scope">
<span class="amount-green">+{{ formatTrunc(scope.row.realAmount, 2) }}</span>
<span class="amount-green">
<el-tooltip
v-if="formatAmount(scope.row.realAmount, scope.row.coin || scope.row.toSymbol || 'USDT').truncated"
:content="`+${formatAmount(scope.row.realAmount, scope.row.coin || scope.row.toSymbol || 'USDT').full}`"
placement="top"
>
<span>
+{{ formatAmount(scope.row.realAmount, scope.row.coin || scope.row.toSymbol || 'USDT').text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>
+{{ formatAmount(scope.row.realAmount, scope.row.coin || scope.row.toSymbol || 'USDT').text }}
</span>
</span>
</template>
</el-table-column>
<el-table-column label="收款链" min-width="120">
@@ -132,6 +146,7 @@
<script>
import { sellerReceiptList } from '../../api/wallet'
import { truncateAmountByCoin } from '../../utils/amount'
export default {
name: 'AccountReceiptRecord',
@@ -180,6 +195,9 @@ export default {
this.rows = this.withKeys(this.rows)
},
methods: {
formatAmount(value, coin) {
return truncateAmountByCoin(value, coin)
},
withKeys(list) {
const arr = Array.isArray(list) ? list : []
return arr.map((it, idx) => ({
@@ -299,6 +317,7 @@ export default {
.empty-icon { font-size: 48px; margin-bottom: 8px; }
.amount-green { color: #16a34a; font-weight: 700; }
.amount-red { color: #ef4444; font-weight: 700; }
.amount-more { font-size: 12px; color: #94a3b8; margin-left: 4px; }
.type-green { color: #16a34a; }
.type-red { color: #ef4444; }
.pagination { display: flex; justify-content: flex-end; margin-top: 8px; }

View File

@@ -17,6 +17,7 @@
</div>
<div v-else class="cart-content">
<p style="color:#9E44F1;font-size: 14px;margin-bottom: 10px;">注意各店铺支持多种支付方式请选择店铺支付方式后提交订单结算</p>
<!-- 外层店铺列表 -->
<el-table
ref="shopTable"
@@ -27,9 +28,9 @@
:expand-row-keys="expandedShopKeys"
:header-cell-style="{ textAlign: 'left' }"
:cell-style="{ textAlign: 'left' }"
@expand-change="handleShopExpandChange"
@expand-change="handleGuardExpand"
>
<el-table-column type="expand" width="46">
<el-table-column type="expand" width="46" :expandable="() => false">
<template #default="shopScope">
<!-- 机器列表直接挂在店铺下 -->
<el-table :data="shopScope.row.productMachineDtoList || []" size="small" border style="width: 100%"
@@ -37,7 +38,7 @@
:ref="'innerTable-' + shopScope.row.id"
@selection-change="sels => handleShopInnerSelectionChange(shopScope.row, sels)"
:header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table-column type="selection" width="46" :selectable="isRowSelectable" />
<el-table-column type="selection" width="46" :selectable="row => isRowSelectableByShop(shopScope.row, row)" />
<el-table-column prop="name" label="商品名称" />
<el-table-column prop="miner" label="机器编号" />
<el-table-column prop="algorithm" label="算法" />
@@ -60,9 +61,27 @@
</el-tag>
</template>
</el-table-column> -->
<el-table-column prop="price" label="单价(USDT)" width="100">
<el-table-column prop="price" width="120">
<template #header>单价({{ getSelectedCoinSymbolForShop(shopScope.row) || 'USDT' }}</template>
<template #default="scope">
<span class="price-strong">{{ formatTrunc(scope.row.price, 2) }}</span>
<template v-if="getMachineUnitPriceBySelection(shopScope.row, scope.row) != null">
<span class="price-strong">
<el-tooltip
v-if="formatAmount(getMachineUnitPriceBySelection(shopScope.row, scope.row), getSelectedCoinSymbolForShop(shopScope.row)).truncated"
:content="formatAmount(getMachineUnitPriceBySelection(shopScope.row, scope.row), getSelectedCoinSymbolForShop(shopScope.row)).full"
placement="top"
>
<span>
{{ formatAmount(getMachineUnitPriceBySelection(shopScope.row, scope.row), getSelectedCoinSymbolForShop(shopScope.row)).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>
{{ formatAmount(getMachineUnitPriceBySelection(shopScope.row, scope.row), getSelectedCoinSymbolForShop(shopScope.row)).text }}
</span>
</span>
</template>
<template v-else>-</template>
</template>
</el-table-column>
<el-table-column label="租赁天数" width="145">
@@ -91,34 +110,91 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="机器总价(USDT)" min-width="100">
<template #default="scope"><span class="price-strong">{{ formatTrunc(Number(scope.row.price || 0) * Number(scope.row.leaseTime || 1), 2) }}</span></template>
<el-table-column min-width="120">
<template #header>机器总价({{ getSelectedCoinSymbolForShop(shopScope.row) || 'USDT' }}</template>
<template #default="scope">
<template v-if="getMachineUnitPriceBySelection(shopScope.row, scope.row) != null">
<span class="price-strong">
<el-tooltip
v-if="formatAmount(getMachineUnitPriceBySelection(shopScope.row, scope.row) * Number(scope.row.leaseTime || 1), getSelectedCoinSymbolForShop(shopScope.row)).truncated"
:content="formatAmount(getMachineUnitPriceBySelection(shopScope.row, scope.row) * Number(scope.row.leaseTime || 1), getSelectedCoinSymbolForShop(shopScope.row)).full"
placement="top"
>
<span>
{{ formatAmount(getMachineUnitPriceBySelection(shopScope.row, scope.row) * Number(scope.row.leaseTime || 1), getSelectedCoinSymbolForShop(shopScope.row)).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>
{{ formatAmount(getMachineUnitPriceBySelection(shopScope.row, scope.row) * Number(scope.row.leaseTime || 1), getSelectedCoinSymbolForShop(shopScope.row)).text }}
</span>
</span>
</template>
<template v-else>-</template>
</template>
</el-table-column>
</el-table>
</template>
</el-table-column>
<el-table-column prop="name" label="店铺名称" />
<el-table-column prop="totalMachine" label="机器总数" />
<!-- <el-table-column label="机器总数" min-width="120">
<template #default="scope">{{ countMachines(scope.row) }}</template>
</el-table-column> -->
<el-table-column prop="totalPrice" label="总价(USDT)">
<el-table-column prop="totalPrice">
<template #header>
总价({{ getSelectedCoinSymbolForShopHeader() }}
</template>
<template #default="scope">
<span class="price-strong">{{ computeShopTotalDisplay(scope.row) }}</span>
<span class="price-strong">
<el-tooltip
v-if="formatAmount(displayShopTotalBySelection(scope.row), getSelectedCoinSymbolForShop(scope.row)).truncated"
:content="formatAmount(displayShopTotalBySelection(scope.row), getSelectedCoinSymbolForShop(scope.row)).full"
placement="top"
>
<span>
{{ formatAmount(displayShopTotalBySelection(scope.row), getSelectedCoinSymbolForShop(scope.row)).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>
{{ formatAmount(displayShopTotalBySelection(scope.row), getSelectedCoinSymbolForShop(scope.row)).text }}
</span>
</span>
</template>
</el-table-column>
<el-table-column label="支付方式">
<template #default="scope">
<img v-for="(item,index ) in scope.row.payConfigList" :key="index" :src="item.payCoinImage" :alt="item.payChain" :title="formatPayTooltip(item)" style="width: 20px; height: 20px; margin-right: 10px;" />
</template>
</el-table-column>
<el-table-column label="操作" >
<template #default="scope">
<el-button type="primary" size="mini" :loading="creatingOrder" :disabled="creatingOrder" @click="handleCheckoutShop(scope.row)">结算该店铺订单</el-button>
<el-select
v-model="paySelectionMap[scope.row.id]"
placeholder="请选择"
size="mini"
style="min-width: 180px;"
@change="val => handleShopPayChange(scope.row, val)"
>
<template #prefix>
<img
v-if="getSelectedPayIcon(scope.row)"
:src="getSelectedPayIcon(scope.row)"
:alt="getSelectedCoinSymbolForShop(scope.row)"
style="width:16px;height:16px;margin-right:6px;border-radius:3px;"
/>
</template>
<el-option
v-for="(opt, idx) in getShopPayOptions(scope.row)"
:key="idx"
:value="opt.value"
:label="opt.label"
>
<div style="display:flex;align-items:center;gap:8px;">
<img :src="opt.icon" :alt="opt.label" style="width:18px;height:18px;border-radius:3px;" />
<span>{{ opt.label }}</span>
</div>
</el-option>
</el-select>
</template>
</el-table-column>
<!-- 操作列移除单店铺结算按钮,统一使用底部“结算选中机器” -->
</el-table>
<div class="summary-actions" style="margin-top:16px;display:flex;gap:12px;justify-content:flex-end;">
<div class="summary-inline" style="color:#666;">
@@ -128,28 +204,86 @@
<div class="actions-inline" style="display:flex;gap:12px;">
<el-button type="danger" :disabled="!selectedMachineCount" @click="handleRemoveSelectedMachines">删除所选机器</el-button>
<el-button type="warning" plain :loading="clearOffLoading" @click="handleClearOffShelf">清除已下架商品</el-button>
<el-button type="primary" :disabled="!selectedMachineCount" @click="handleCheckoutSelected">结算选中机器</el-button>
</div>
</div>
<el-dialog :visible.sync="confirmDialog.visible" width="80vw" :close-on-click-modal="false" :title="`确认结算该店铺订单(共 ${confirmDialog.count} 台机器)`">
<el-dialog :visible.sync="confirmDialog.visible" width="80vw" :close-on-click-modal="false" :title="`确认结算(共 ${confirmDialog.count} 台机器)`">
<div>
<el-table :data="confirmDialog.items" height="360" border stripe
:header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table-column prop="product" label="商品" min-width="160" />
<el-table-column prop="coin" label="币种" min-width="100" />
<el-table-column prop="user" label="账户" min-width="120" />
<el-table-column prop="miner" label="机器编号" min-width="160" />
<el-table-column prop="unitPrice" min-width="140">
<template #header>单价({{ payCoinSymbol || 'USDT' }}</template>
<template #default="scope"><span class="price-strong">{{ formatTrunc(scope.row.unitPrice, 2) }}</span></template>
</el-table-column>
<el-table-column prop="leaseTime" label="租赁天数" min-width="120" />
<el-table-column prop="subtotal" min-width="140">
<template #header>小计({{ payCoinSymbol || 'USDT' }}</template>
<template #default="scope"><span class="price-strong">{{ formatTrunc(scope.row.subtotal, 2) }}</span></template>
</el-table-column>
</el-table>
<div style="margin-top:12px;text-align:right;">总金额({{ payCoinSymbol || 'USDT' }}<span class="price-strong">{{ formatTrunc(confirmDialog.total, 2) }}</span></div>
<div v-for="grp in confirmDialog.shops" :key="grp.shopId" style="margin-bottom: 18px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin:8px 0 6px 0;">
<div style="font-weight:600;color:#2c3e50;">
店铺:{{ grp.shopName || grp.shopId }}
<span style="margin-left:12px;color:#666;font-weight:400;">支付方式:{{ grp.payLabel }}</span>
</div>
</div>
<el-table :data="grp.items" max-height="260" border stripe
:header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table-column prop="product" label="商品" min-width="160" />
<el-table-column prop="coin" label="币种" min-width="100" />
<el-table-column prop="user" label="账户" min-width="120" />
<el-table-column prop="miner" label="机器编号" min-width="160" />
<el-table-column prop="unitPrice" min-width="140">
<template #header>单价({{ grp.coinSymbol || 'USDT' }}</template>
<template #default="scope">
<span class="price-strong">
<el-tooltip
v-if="formatAmount(scope.row.unitPrice, grp.coinSymbol).truncated"
:content="formatAmount(scope.row.unitPrice, grp.coinSymbol).full"
placement="top"
>
<span>
{{ formatAmount(scope.row.unitPrice, grp.coinSymbol).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(scope.row.unitPrice, grp.coinSymbol).text }}</span>
</span>
</template>
</el-table-column>
<el-table-column prop="leaseTime" label="租赁天数" min-width="120" />
<el-table-column prop="subtotal" min-width="140">
<template #header>小计({{ grp.coinSymbol || 'USDT' }}</template>
<template #default="scope">
<span class="price-strong">
<el-tooltip
v-if="formatAmount(scope.row.subtotal, grp.coinSymbol).truncated"
:content="formatAmount(scope.row.subtotal, grp.coinSymbol).full"
placement="top"
>
<span>
{{ formatAmount(scope.row.subtotal, grp.coinSymbol).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(scope.row.subtotal, grp.coinSymbol).text }}</span>
</span>
</template>
</el-table-column>
</el-table>
</div>
<div style="margin-top:12px;text-align:right;">
<span style="margin-right:8px;">总金额:</span>
<template v-if="Object.keys(confirmDialog.totalsByCoin || {}).length">
<span v-for="(amt, coin) in confirmDialog.totalsByCoin" :key="coin" style="margin-left:12px;">
{{ coin }}
<span class="price-strong">
<el-tooltip
v-if="formatAmount(amt, coin).truncated"
:content="formatAmount(amt, coin).full"
placement="top"
>
<span>
{{ formatAmount(amt, coin).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(amt, coin).text }}</span>
</span>
</span>
</template>
<template v-else>-</template>
</div>
</div>
<template #footer>
<el-button @click="confirmDialog.visible=false">取消</el-button>
@@ -157,24 +291,7 @@
</template>
</el-dialog>
<!-- 支付链/币种选择弹窗 -->
<el-dialog :visible.sync="payDialog.visible" width="520px" title="选择支付链/币种" :close-on-click-modal="false">
<el-form label-width="120px">
<el-form-item label="/币种">
<el-cascader
v-model="payDialog.value"
:options="options"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="payDialog.visible=false">取消</el-button>
<el-button type="primary" :loading="payDialog.loading" @click="handlePayConfirm">下一步</el-button>
</template>
</el-dialog>
<!-- 购物须知(测试版) - 必须勾选并等待5秒后才可关闭 -->
<el-dialog :visible.sync="noticeDialog.visible" width="680px" title="下单须知" :show-close="false" :close-on-click-modal="false" :close-on-press-escape="false">
@@ -268,6 +385,7 @@
<script>
import { getGoodsList, deleteBatchGoods as apiDeleteBatchGoods ,deleteBatchGoodsForIsDelete} from '../../api/shoppingCart'
import { addOrders,cancelOrder,getOrdersByIds,getOrdersByStatus , getChainAndListForSeller,getCoinPrice } from '../../api/order'
import { truncateAmountByCoin } from '../../utils/amount'
export default {
name: 'Cart',
@@ -279,7 +397,7 @@ export default {
selectedGroups: [],
// 新结构:按店铺选择机器 { shopId: Set(machineId) }
selectedMachinesMap: {},
confirmDialog: { visible: false, items: [], count: 0, total: 0 },
confirmDialog: { visible: false, shops: [], count: 0, totalsByCoin: {} },
expandedGroupKeys: [],
expandedShopKeys: [],
creatingOrder: false,
@@ -289,6 +407,8 @@ export default {
noticeTimer: null,
// 待结算的店铺信息
pendingCheckoutShop: null,
// 多店铺统一结算的临时选择
pendingCheckoutAll: null,
// 谷歌验证码弹窗
googleCodeDialog: {
visible: false,
@@ -301,7 +421,9 @@ export default {
payDialog: { visible: false, value: [], loading: false },
selectedChain: '',
selectedCoin: '',
selectedPrice: 0
selectedPrice: 0,
// 每个店铺的支付方式选择map<shopId, 'chain|coin'>
paySelectionMap: {}
,clearOffLoading: false,
settlementSuccessfulVisible: false
}
@@ -379,6 +501,15 @@ export default {
this.noticeTimer = null
},
methods: {
/**
* 金额格式化不补0、不四舍五入根据币种限定小数位
* @param {number|string} value
* @param {string} coin
* @returns {{text:string,truncated:boolean,full:string}}
*/
formatAmount(value, coin) {
return truncateAmountByCoin(value, coin)
},
/**
* 将金额转为整分整数,避免浮点误差
* @param {number|string} v
@@ -476,6 +607,146 @@ export default {
},
// 获取所有商品分组(兼容保留,现直接返回空,因已移除中间商品层)
getAllGroups() { return [] },
// 生成店铺的支付方式选项,基于 totalPriceList
getShopPayOptions(shop) {
const list = Array.isArray(shop && shop.totalPriceList) ? shop.totalPriceList : []
// 优先使用 payConfigList 提供的 icon以链+币种去匹配
const icons = new Map()
const cfg = Array.isArray(shop && shop.payConfigList) ? shop.payConfigList : []
cfg.forEach(c => {
const key = `${c.payChain || ''}|${c.payCoin || ''}`
icons.set(key, c.payCoinImage || '')
})
return list.map(it => {
const chain = (it && it.chain) || ''
const coin = (it && it.coin) || ''
const key = `${chain}|${coin}`
return {
label: `${chain} - ${this.toUpperText(coin)}`,
value: key,
icon: icons.get(key) || ''
}
})
},
// 判断某机器在当前店铺选择的支付方式下是否有可用单价
hasMachinePriceForSelection(shop, machine) {
if (!shop || !machine) return false
const key = this.paySelectionMap[shop.id] || ''
const [chain, coin] = String(key).split('|')
const list = Array.isArray(machine.priceList) ? machine.priceList : []
return list.some(it => String(it.chain) === chain && String(it.coin) === coin)
},
// 获取店铺当前选择的币种符号(大写)
getSelectedCoinSymbolForShop(shop) {
const key = this.paySelectionMap[shop ? shop.id : undefined]
if (!key) return ''
const parts = String(key).split('|')
return this.toUpperText(parts[1])
},
// 外层表头:取第一个店铺的币种显示在“总价”标题后面
getSelectedCoinSymbolForShopHeader() {
const first = Array.isArray(this.shops) && this.shops.length ? this.shops[0] : null
if (!first) return ''
// 确保默认已设置
this.ensureDefaultPaySelection(first)
return this.getSelectedCoinSymbolForShop(first)
},
// 获取当前店铺选中的支付方式图标(用于 select prefix
getSelectedPayIcon(shop) {
if (!shop) return ''
this.ensureDefaultPaySelection(shop)
const key = this.paySelectionMap[shop.id] || ''
const [chain, coin] = String(key).split('|')
const cfg = Array.isArray(shop && shop.payConfigList) ? shop.payConfigList : []
const hit = cfg.find(c => String(c.payChain) === chain && String(c.payCoin) === coin)
return hit && hit.payCoinImage ? hit.payCoinImage : ''
},
// 默认设置店铺支付方式为第一个选项
ensureDefaultPaySelection(shop) {
if (!shop) return
const list = this.getShopPayOptions(shop)
if (list.length && !this.paySelectionMap[shop.id]) {
this.$set(this.paySelectionMap, shop.id, list[0].value)
}
},
// 处理店铺支付方式变化
handleShopPayChange(shop, val) {
if (!shop) return
this.$set(this.paySelectionMap, shop.id, val)
// 清理已选择但在当前支付方式下无价格的机器,并刷新视觉勾选
const set = this.selectedMachinesMap[shop.id]
if (set && set.size) {
const list = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
list.forEach(m => {
if (set.has(m.id) && !this.hasMachinePriceForSelection(shop, m)) {
set.delete(m.id)
}
})
// 重新应用到表格勾选
this.$nextTick(() => this.applyInnerSelectionFromSet(shop))
}
},
// 根据店铺选择展示其总价(来自 totalPriceList
displayShopTotalBySelection(shop) {
if (!shop) return 0
// 确保有默认选择
this.ensureDefaultPaySelection(shop)
// 若用户修改过任意机器的租赁天数,则按当前租期+所选币种实时汇总
if (this.isShopLeaseChanged(shop)) {
try {
const machines = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
let totalCents = 0
machines.forEach(m => {
const unit = this.getMachineUnitPriceBySelection(shop, m)
if (unit != null) {
const days = Math.max(1, Math.floor(Number(m.leaseTime || 1)))
totalCents += this.toCents(unit) * days
}
})
return totalCents / 100
} catch (e) {
/* noop fallthrough to backend value */
}
}
// 未修改租期:优先使用后端 totalPriceList 当前币种价格
const key = this.paySelectionMap[shop.id] || ''
const [chain, coin] = key.split('|')
const list = Array.isArray(shop.totalPriceList) ? shop.totalPriceList : []
const hit = list.find(it => String(it.chain) === chain && String(it.coin) === coin)
if (hit && hit.price != null) return Number(hit.price || 0)
// 兜底若没命中返回总价或0
return Number(shop.totalPrice || 0)
},
// 是否有任意机器的租期被用户修改
isShopLeaseChanged(shop) {
try {
const list = Array.isArray(shop && shop.productMachineDtoList) ? shop.productMachineDtoList : []
return list.some(m => {
const orig = (m && m._origLeaseTime != null) ? Number(m._origLeaseTime) : Number(m && m.leaseTime)
const cur = Math.max(1, Math.floor(Number(m && m.leaseTime) || 1))
return orig !== cur
})
} catch (e) {
return false
}
},
// 根据店铺选择获取机器的单价(来自 priceList
getMachineUnitPriceBySelection(shop, machine) {
if (!shop || !machine) return Number(machine.price || 0)
this.ensureDefaultPaySelection(shop)
const key = this.paySelectionMap[shop.id] || ''
const [chain, coin] = key.split('|')
const list = Array.isArray(machine.priceList) ? machine.priceList : []
const hit = list.find(it => String(it.chain) === chain && String(it.coin) === coin)
if (hit && hit.price != null) return Number(hit.price || 0)
// 查不到则返回 null用于界面展示和禁用
return null
},
// 对应店铺下勾选列是否可点:同时判断上下架与是否有价格
isRowSelectableByShop(shop, row) {
if (!this.isOnShelf(row)) return false
return this.hasMachinePriceForSelection(shop, row)
},
// 店铺总价(精确到分):优先用后端 totalPrice若用户改动租期则实时按分计算
computeShopTotal(shop) {
if (!shop) return 0
@@ -530,9 +801,6 @@ export default {
// 按照新的传参结构:{code: 谷歌验证码, orderInfoVoList: [之前传参的数组]}
const payload = {
code: googleCode,
chain: this.selectedChain,
coin: this.selectedCoin,
price: this.selectedPrice,
orderInfoVoList: orderInfoVoList
}
const res = await addOrders(payload)
@@ -631,6 +899,10 @@ export default {
...shop,
id: shop.id != null ? String(shop.id) : `shop-${sIdx}`
}))
// 为每个店铺设置默认支付方式
try {
withShopKeys.forEach(sp => this.ensureDefaultPaySelection(sp))
} catch (e) { /* noop */ }
// 记录每台机器的原始租期,便于判断是否被修改
try {
withShopKeys.forEach(sp => {
@@ -641,6 +913,10 @@ export default {
this.shops = withShopKeys
this.groups = []
this.expandedGroupKeys = []
// 默认展开所有店铺
try {
this.expandedShopKeys = withShopKeys.map(sp => String(sp.id))
} catch (e) { this.expandedShopKeys = [] }
const count = withShopKeys.reduce((s, shop) => s + ((Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList.length : 0)), 0)
window.dispatchEvent(new CustomEvent('cart-updated', { detail: { count } }))
return
@@ -812,12 +1088,36 @@ export default {
},
// 实际执行结算的方法
async executeCheckout(googleCode) {
if (!this.pendingCheckoutShop) return
if (!this.pendingCheckoutShop && !this.pendingCheckoutAll) return
const { shop, payload } = this.pendingCheckoutShop
// 聚合 payload支持单店铺或多店铺
let payloadAll = []
if (this.pendingCheckoutAll && this.pendingCheckoutAll.length) {
this.pendingCheckoutAll.forEach(({ shop, items }) => {
(items || []).forEach(m => {
const sel = this.paySelectionMap[shop.id] || ''
const [chain, coin] = String(sel).split('|')
payloadAll.push({
leaseTime: Number(m.leaseTime || 1),
machineId: m.id,
productId: m.productId,
shopId: shop.id,
chain,
coin
})
})
})
} else if (this.pendingCheckoutShop) {
const { payload } = this.pendingCheckoutShop
payloadAll = (Array.isArray(payload) ? payload : []).map(p => {
const sel = this.paySelectionMap[p.shopId] || this.paySelectionMap[this.pendingCheckoutShop.shop.id] || ''
const [chain, coin] = String(sel).split('|')
return { ...p, chain, coin }
})
}
this.creatingOrder = true
try {
const res = await this.fetchAddOrders(payload, googleCode)
const res = await this.fetchAddOrders(payloadAll, googleCode)
let ok = false
if (res && Number(res.code) === 200) {
const dataStr = String(res.data || '')
@@ -845,41 +1145,32 @@ export default {
} finally {
this.creatingOrder = false
this.pendingCheckoutShop = null
this.pendingCheckoutAll = null
}
},
handleCheckoutSelected() {
// 若有选中机器,则以选中机器为准;否则当做选中整个商品分组
let items = []
if (this.selectedMachineCount) {
(this.shops || []).forEach(shop => {
const set = this.selectedMachinesMap[shop.id]
if (!set || set.size === 0) return
const list = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
list.forEach(m => {
if (set.has(m.id)) {
items.push({
product: shop.name || '',
coin: this.toUpperText(m.coin),
machineId: m.id,
user: m.user,
miner: m.miner,
price: Number(m.price || 0)
})
}
})
})
} else {
this.$message({
message: '请先选择商品或机器',
type: 'warning',
showClose: true
})
if (!this.selectedMachineCount) {
this.$message({ message: '请先勾选要结算的机器', type: 'warning', showClose: true })
return
}
this.confirmDialog.items = items
this.confirmDialog.count = items.length
this.confirmDialog.total = items.reduce((s, i) => s + i.price, 0)
this.confirmDialog.visible = true
const shops = Array.isArray(this.shops) ? this.shops : []
const picked = []
shops.forEach(shop => {
const set = this.selectedMachinesMap[shop.id]
if (!set || !set.size) return
const list = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
const items = list.filter(m => set.has(m.id) && this.isOnShelf(m))
if (items.length) picked.push({ shop, items })
})
if (!picked.length) {
this.$message({ message: '未找到可结算的上架机器', type: 'warning', showClose: true })
return
}
this.pendingCheckoutAll = picked
// 打开须知弹窗
this.noticeDialog.visible = true
this.noticeDialog.checked = false
this.startNoticeCountdown()
},
handleRemoveSelectedMachines() {
const payload = this.buildDeletePayload()
@@ -959,49 +1250,35 @@ export default {
this.noticeDialog.visible = false
// 进入下一步前,确保一次恢复(避免刚关闭后消失)
this.$nextTick(() => this.reapplySelectionsForPendingShop())
// 用户确认须知后,先选择支付链/币种
this.openPaySelectDialog()
// 若是统一结算,构建全量确认信息;否则构建单店铺确认
if (this.pendingCheckoutAll && this.pendingCheckoutAll.length) {
this.showConfirmDialogAll()
} else {
// 用户确认须知后,直接按购物车中的店铺支付方式进行结算确认
try {
const shop = this.pendingCheckoutShop && this.pendingCheckoutShop.shop
if (shop) {
const key = this.paySelectionMap[shop.id] || ''
const [chain, coin] = String(key).split('|')
this.selectedChain = chain || ''
this.selectedCoin = coin || ''
} else {
this.selectedChain = ''
this.selectedCoin = ''
}
} catch (e) {
this.selectedChain = ''
this.selectedCoin = ''
}
this.showConfirmDialog()
}
},
openPaySelectDialog() {
this.payDialog.visible = true
// 打开支付选择时再恢复一次(背景表格常被重渲染)
this.$nextTick(() => this.reapplySelectionsForPendingShop())
if (!Array.isArray(this.options) || !this.options.length) {
this.fetchChainAndListForSeller()
}
// 已取消支付方式选择步骤,此函数保留空实现以兼容旧调用
return
},
async handlePayConfirm() {
const val = this.payDialog.value || []
if (!Array.isArray(val) || val.length < 2) {
this.$message.warning('请选择支付链和币种')
return
}
// 关闭前先恢复一次(避免选择时表格刷新导致视觉丢勾)
this.$nextTick(() => this.reapplySelectionsForPendingShop())
this.selectedChain = val[0]
this.selectedCoin = val[1]
// USDT 不需要请求实时币价,也不需要换算
if (String(this.selectedCoin).toUpperCase() === 'USDT') {
this.selectedPrice = 0
} else {
this.payDialog.loading = true
try {
const res = await getCoinPrice({ coin: this.selectedCoin })
const price = (res && (res.data && (res.data.price || res.data))) || res.price || 0
this.selectedPrice = Number(price || 0)
} catch (e) {
this.selectedPrice = 0
} finally {
this.payDialog.loading = false
}
}
this.payDialog.visible = false
// 关闭后也再恢复一次
this.$nextTick(() => this.reapplySelectionsForPendingShop())
// 选择完成,进入确认明细
this.showConfirmDialog()
},
// 显示确认结算弹窗
// 显示确认结算弹窗(支持多店铺结构,这里按当前店铺构建一个分组)
showConfirmDialog() {
if (!this.pendingCheckoutShop) return
@@ -1017,25 +1294,15 @@ export default {
const items = []
list.forEach(m => {
if (selectedIds.has(m.id) && this.isOnShelf(m)) {
// 单价同币种显示若非USDT按实时价换算 用现在的价格除以后端返回的this.selectedPrice
const baseUnit = Number(m.price || 0)
// 使用购物车中已选支付方式对应的单价
const baseUnit = this.getMachineUnitPriceBySelection(shop, m)
if (baseUnit == null) return
const leaseDays = Math.max(1, Math.floor(Number(m.leaseTime || 1)))
const isUSDT = String(this.selectedCoin).toUpperCase() === 'USDT'
console.log('baseUnit', baseUnit)
console.log('selectedPrice', this.selectedPrice)
const unitPrice = !isUSDT && this.selectedPrice > 0 ? (baseUnit / this.selectedPrice) : baseUnit
console.log('baseUnit / this.selectedPrice', baseUnit / this.selectedPrice);
console.log('unitPrice', unitPrice ,'leaseDays', leaseDays)
console.log(`unitPrice * leaseDays`,unitPrice * leaseDays);
console.log(unitPrice*this.selectedPrice,"客服付款");
const subtotal = unitPrice * leaseDays
const unitPrice = Number(baseUnit || 0)
const subtotal = Number(unitPrice) * leaseDays
items.push({
product: shop.name || '',
coin: this.toUpperText(m.coin),
coin: this.toUpperText(this.selectedCoin || ''),
user: m.user,
miner: m.miner,
unitPrice: Number(unitPrice || 0),
@@ -1044,10 +1311,84 @@ export default {
})
}
})
this.confirmDialog.items = items
// 构建对话框分组数据
const key = this.paySelectionMap[shop.id] || ''
const [chain, coin] = String(key).split('|')
const coinSymbol = this.toUpperText(coin || '')
const payLabel = `${chain} - ${coinSymbol}`
const grp = {
shopId: shop.id,
shopName: shop.name || '',
coinSymbol,
payLabel,
items
}
this.confirmDialog.shops = [grp]
this.confirmDialog.count = items.length
this.confirmDialog.total = items.reduce((s, i) => s + i.subtotal, 0)
// 汇总各币种合计
const totals = {}
const centsAdd = (acc, v) => (acc + this.toCents(v))
if (coinSymbol) {
const tCents = items.reduce((acc, it) => centsAdd(acc, it.subtotal || 0), 0)
totals[coinSymbol] = Number(this.centsToText(tCents))
}
this.confirmDialog.totalsByCoin = totals
this.confirmDialog.visible = true
},
// 多店铺:生成确认明细与多币种总额
showConfirmDialogAll() {
// 以当前勾选状态实时构建,避免中途数据变化造成空白
const groups = []
const totalsCentsByCoin = new Map()
let count = 0
const shops = Array.isArray(this.shops) ? this.shops : []
shops.forEach(shop => {
const set = this.selectedMachinesMap[shop.id]
if (!set || !set.size) return
const key = this.paySelectionMap[shop.id] || ''
const [chain, coin] = String(key).split('|')
const coinSymbol = this.toUpperText(coin || '')
const payLabel = `${chain} - ${coinSymbol}`
const rows = []
const list = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
list.forEach(m => {
if (!set.has(m.id) || !this.isOnShelf(m)) return
const baseUnit = this.getMachineUnitPriceBySelection(shop, m)
if (baseUnit == null) return
const leaseDays = Math.max(1, Math.floor(Number(m.leaseTime || 1)))
const unitPrice = Number(baseUnit || 0)
const subtotal = Number(unitPrice) * leaseDays
rows.push({
product: shop.name || '',
coin: coinSymbol,
user: m.user,
miner: m.miner,
unitPrice,
leaseTime: leaseDays,
subtotal
})
const prev = totalsCentsByCoin.get(coinSymbol) || 0
totalsCentsByCoin.set(coinSymbol, prev + this.toCents(subtotal))
count += 1
})
if (rows.length) {
groups.push({
shopId: shop.id,
shopName: shop.name || '',
coinSymbol,
payLabel,
items: rows
})
}
})
const totalsObj = {}
totalsCentsByCoin.forEach((val, coin) => {
totalsObj[coin] = Number(this.centsToText(val))
})
this.confirmDialog.shops = groups
this.confirmDialog.count = count
this.confirmDialog.totalsByCoin = totalsObj
this.confirmDialog.visible = true
},
// 显示谷歌验证码输入框
@@ -1533,4 +1874,22 @@ export default {
.dialog-footer {
text-align: center;
}
.amount-more {
font-size: 12px;
color: #94a3b8; /* slate-400 */
margin-left: 4px;
}
::v-deep .el-input__prefix, .el-input__suffix{
top:24%;
}
::v-deep .el-input--mini .el-input__icon{
line-height: 0px;
}
/* 禁用展开箭头的点击(彻底防止用户折叠) */
::v-deep .el-table .el-table__expand-icon {
pointer-events: none;
}
</style>

View File

@@ -3,6 +3,7 @@ import { getProductById } from '../../utils/productService'
import { addToCart } from '../../utils/cartManager'
import { getMachineInfo, getPayTypes } from '../../api/products'
import { addCart, getGoodsList } from '../../api/shoppingCart'
import { truncateAmountByCoin } from '../../utils/amount'
export default {
name: 'ProductDetail',
@@ -28,9 +29,21 @@ export default {
minPowerDissipation: null,
maxPowerDissipation: null
},
// 排序状态true 升序false 降序
sortStates: {
priceSort: true,
powerSort: true,
powerDissipationSort: true
},
// 当前激活的排序字段(仅当用户点击后才会传参)
activeSortField: '',
// 首次进入时是否已按价格币种设置过支付方式筛选默认值
payFilterDefaultApplied: false,
params: {
id: "",
pageNum: 1,
pageSize: 10,
},
confirmAddDialog: {
@@ -120,13 +133,17 @@ export default {
// number:2001,
// cost:"1000",//价格
// },
],
productDetailLoading:false
productDetailLoading: false,
pageSizes: [10, 20, 50],
currentPage: 1,
total: 0,
}
},
mounted() {
console.log(this.$route.params.id, "i叫哦附加费")
if (this.$route.params.id) {
this.params.id = this.$route.params.id
this.product = true
@@ -134,7 +151,7 @@ export default {
if (this.productListData && this.productListData.length) {
this.expandedRowKeys = [this.productListData[0].id]
}
this.fetchGetMachineInfo(this.params)
this.fetchGetMachineInfo(this.params)//priceSort 价格powerSort 算力功耗powerDissipationSort 布尔类型true 升序false降序
this.fetchPayTypes()
} else {
this.$message.error('商品不存在')
@@ -143,9 +160,64 @@ export default {
this.fetchGetGoodsList()
},
methods: {
// 行币种:优先行内 payCoin > coin其次取全局表头币种
getRowCoin(row) {
try {
const c = (row && (row.payCoin || row.coin)) || this.getPriceCoinSymbol() || ''
return String(c).toUpperCase()
} catch (e) { return '' }
},
// 金额格式化不补0、不四舍五入返回 {text,truncated,full}
formatAmount(value, coin) {
return truncateAmountByCoin(value, coin)
},
/**
* 首次加载时,将“支付方式筛选”的默认选中值设为与价格列币种一致,
* 并同步 filters.chain/filters.coin仅执行一次不触发额外查询。
*/
ensureDefaultPayFilterSelection() {
try {
if (this.payFilterDefaultApplied) return
const payList = Array.isArray(this.paymentMethodList) ? this.paymentMethodList : []
if (!payList.length) return
const coinSymbol = (this.getPriceCoinSymbol && this.getPriceCoinSymbol()) || ''
if (!coinSymbol) return
const hit = payList.find(it => String(it && it.payCoin).toUpperCase() === String(coinSymbol).toUpperCase())
if (!hit) return
const key = `${hit.payChain || ''}|${hit.payCoin || ''}`
this.selectedPayKey = key
this.filters.chain = String(hit.payChain || '').trim()
this.filters.coin = String(hit.payCoin || '').trim()
this.payFilterDefaultApplied = true
} catch (e) { /* noop */ }
},
// 切换排序field in ['priceSort','powerSort','powerDissipationSort']
handleToggleSort(field) {
try {
if (!this.sortStates) this.sortStates = {}
if (this.activeSortField !== field) {
// 切换到新的字段默认从升序开始true
// 先将其它字段复位为升序(▲)
Object.keys(this.sortStates).forEach(k => { this.sortStates[k] = true })
this.activeSortField = field
// 后端默认升序,首次点击应为降序
this.sortStates[field] = false
} else {
// 同一字段:升降序切换
this.sortStates[field] = !this.sortStates[field]
}
const params = this.buildQueryParams()
this.fetchGetMachineInfo(params)
} catch (e) { /* noop */ }
},
// 组合查询参数(带上商品 id 与筛选条件)
buildQueryParams() {
const q = { id: this.params.id }
// 分页参数始终透传
try {
if (this.params && this.params.pageNum != null) q.pageNum = this.params.pageNum
if (this.params && this.params.pageSize != null) q.pageSize = this.params.pageSize
} catch (e) { /* noop */ }
// 仅当用户真实填写(>0时才传参默认/空值不传
const addNum = (obj, key, name) => {
const raw = obj[key]
@@ -162,6 +234,13 @@ export default {
addNum(this.filters, 'maxPower', 'maxPower')
addNum(this.filters, 'minPowerDissipation', 'minPowerDissipation')
addNum(this.filters, 'maxPowerDissipation', 'maxPowerDissipation')
// 排序参数:仅在用户点击某一列后传当前列
try {
if (this.activeSortField) {
const s = this.sortStates || {}
q[this.activeSortField] = !!s[this.activeSortField]
}
} catch (e) { /* noop */ }
return q
},
// 拉取支付方式
@@ -172,6 +251,8 @@ export default {
if (res && (res.code === 0 || res.code === 200)) {
const list = Array.isArray(res.data) ? res.data : []
this.paymentMethodList = list
// 支付方式加载后尝试设置默认筛选
this.ensureDefaultPayFilterSelection()
}
} catch (e) {
// 忽略错误,保持页面可用
@@ -185,6 +266,7 @@ export default {
console.log(res)
if (res && res.code === 200) {
console.log(res.data, 'res.rows');
this.total = res.total||0;
// 新数据结构:机器为扁平 rows 列表;仅当后端返回有效支付方式时才覆盖,避免清空 getPayTypes 的结果
try {
const payList = res && res.data && res.data.payConfigList
@@ -203,6 +285,8 @@ export default {
// 清空旧的两层结构数据,避免误用
this.productListData = []
this.expandedRowKeys = []
// 机器加载后尝试设置默认筛选
this.ensureDefaultPayFilterSelection()
this.$nextTick(() => {
this.machinesLoaded = true
})
@@ -221,17 +305,17 @@ export default {
if (!this.product) {
this.$message({
message: '商品不存在',
type: 'error',
showClose: true
message: '商品不存在',
type: 'error',
showClose: true
})
}
} catch (error) {
console.error('加载商品详情失败:', error)
this.$message({
message: '加载商品详情失败,请稍后重试',
type: 'error',
showClose: true
message: '加载商品详情失败,请稍后重试',
type: 'error',
showClose: true
})
} finally {
this.loading = false
@@ -240,7 +324,7 @@ export default {
//加入购物车
async fetchAddCart(params) {
const res = await addCart(params)
return res
},
//查询购物车列表
@@ -342,7 +426,7 @@ export default {
},
// 已取消对比购物车的自动勾选/禁用逻辑
autoSelectAndDisable() {},
autoSelectAndDisable() { },
// 选择器可选控制:已在购物车中的机器不可再选
isSelectable(row, index) {
@@ -433,7 +517,7 @@ export default {
variant.quantity = 1
} catch (error) {
console.error('添加到购物车失败:', error)
}
},
// 统一加入购物车
@@ -457,7 +541,7 @@ export default {
this.selectedMap = {}
} catch (e) {
console.error('统一加入购物车失败', e)
}
},
// 打开确认弹窗:以当前界面勾选(_selected)为准,并在打开后清空左侧勾选状态
@@ -520,14 +604,14 @@ export default {
})
this.$nextTick(() => this.autoSelectAndDisable())
} catch (e) { /* noop */ }
this.$message({
message: `已加入 ${allSelected.length} 台矿机到购物车`,
type: 'success',
duration: 3000,
showClose: true,
});
});
this.confirmAddDialog.visible = false
// 清空选中映射,然后重新加载数据(数据加载时会自动设置 _selected: false
this.selectedMap = {}
@@ -636,6 +720,21 @@ export default {
console.error('添加到购物车失败:', error)
this.$message.error('添加到购物车失败,请稍后重试')
}
}
},
handleSizeChange(val) {
console.log(`每页 ${val}`);
this.params.pageSize = val;
this.params.pageNum = 1;
this.currentPage = 1;
// 携带当前激活的排序字段
this.fetchGetMachineInfo(this.buildQueryParams());
},
handleCurrentChange(val) {
console.log(`当前页: ${val}`);
this.params.pageNum = val;
// 携带当前激活的排序字段
this.fetchGetMachineInfo(this.buildQueryParams());
},
}
}

View File

@@ -48,6 +48,14 @@
class="filter-control"
@change="handlePayFilterChange"
>
<template #prefix>
<img
v-if="getSelectedPayIcon()"
:src="getSelectedPayIcon()"
alt=""
style="width:16px;height:16px;border-radius:3px;margin-right:6px;"
/>
</template>
<el-option
v-for="(opt, i) in paymentMethodList"
:key="i"
@@ -124,30 +132,43 @@
<el-table-column prop="theoryPower" label="理论算力" min-width="160" header-align="left" align="left" show-overflow-tooltip>
<el-table-column prop="theoryPower" label="理论算力" header-align="left" align="left" show-overflow-tooltip>
<template #default="scope">{{ scope.row.theoryPower }} {{ scope.row.unit }}</template>
</el-table-column>
<el-table-column label="实际算力" min-width="160" header-align="left" align="left" show-overflow-tooltip>
<el-table-column header-align="left" align="left" show-overflow-tooltip>
<template #header>
<span class="sortable" :class="{ active: activeSortField==='powerSort' }" @click="handleToggleSort('powerSort')">
实际算力
<i class="sort-arrow" :class="[(sortStates && sortStates.powerSort) ? 'asc' : 'desc', activeSortField==='powerSort' ? 'active' : '']"></i>
</span>
</template>
<template #default="scope">{{ scope.row.computingPower }} {{ scope.row.unit }}</template>
</el-table-column>
<el-table-column prop="powerDissipation" label="功耗(kw/h)" min-width="140" header-align="left" align="left" />
<el-table-column prop="algorithm" label="算法" min-width="120" header-align="left" align="left" />
<el-table-column prop="powerDissipation" header-align="left" align="left">
<template #header>
<span class="sortable" :class="{ active: activeSortField==='powerDissipationSort' }" @click="handleToggleSort('powerDissipationSort')">
功耗(kw/h)
<i class="sort-arrow" :class="[(sortStates && sortStates.powerDissipationSort) ? 'asc' : 'desc', activeSortField==='powerDissipationSort' ? 'active' : '']"></i>
</span>
</template>
</el-table-column>
<el-table-column prop="algorithm" label="算法" header-align="left" align="left" />
<el-table-column prop="theoryIncome" width="120" header-align="left" align="left" show-overflow-tooltip>
<el-table-column prop="theoryIncome" header-align="left" align="left" show-overflow-tooltip>
<template #header>
单机理论收入(每日)
<span v-if="getFirstCoinSymbol()">{{ getFirstCoinSymbol() }}</span>
</template>
</el-table-column>
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)" width="120" header-align="left" align="left" />
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)" header-align="left" align="left" />
<el-table-column prop="type" label="矿机型号" header-align="left" align="left" min-width="120" />
<el-table-column label="最大可租赁(天)" min-width="140" header-align="left" align="left">
<el-table-column prop="type" label="矿机型号" header-align="left" align="left" />
<el-table-column label="最大可租赁(天)" header-align="left" align="left">
<template #default="scope">{{ getRowMaxLeaseDays(scope.row) }}</template>
</el-table-column>
<el-table-column label="租赁天数(天)" min-width="150" header-align="left" align="left">
<el-table-column label="租赁天数(天)" header-align="left" align="left">
<template #default="scope">
<el-input-number
v-model="scope.row.leaseTime"
@@ -161,14 +182,31 @@
@change="val => handleLeaseDaysChange(scope.row, val)"
/>
</template>
</el-table-column>
<el-table-column prop="price" header-align="left" align="left" min-width="120">
</el-table-column >
<el-table-column prop="price" header-align="left" align="center" >
<template #header>
单价 <span v-if="getPriceCoinSymbol()">{{ getPriceCoinSymbol() }}</span>
<span class="sortable" :class="{ active: activeSortField==='priceSort' }" @click="handleToggleSort('priceSort')">
单价 <span v-if="getPriceCoinSymbol()">{{ getPriceCoinSymbol() }}</span>
<i class="sort-arrow" :class="[(sortStates && sortStates.priceSort) ? 'asc' : 'desc', activeSortField==='priceSort' ? 'active' : '']"></i>
</span>
</template>
<template #default="scope">
<span class="price-strong">
<el-tooltip
v-if="formatAmount(scope.row.price, getRowCoin(scope.row)).truncated"
:content="formatAmount(scope.row.price, getRowCoin(scope.row)).full"
placement="top"
>
<span>
{{ formatAmount(scope.row.price, getRowCoin(scope.row)).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(scope.row.price, getRowCoin(scope.row)).text }}</span>
</span>
</template>
<template #default="scope"><span class="price-strong">{{ scope.row.price }}</span></template>
</el-table-column>
<el-table-column prop="saleState" label="售出状态" header-align="left" align="left" min-width="110">
<el-table-column prop="saleState" label="售出状态" width="110" header-align="left" align="left" >
<template #default="scope">
<el-tag :type="scope.row.saleState === 0 ? 'info' : (scope.row.saleState === 1 ? 'danger' : 'warning')">
{{ scope.row.saleState === 0 ? '未售出' : (scope.row.saleState === 1 ? '已售出' : '售出中') }}
@@ -201,7 +239,21 @@
<template #header>
单价 <span v-if="getPriceCoinSymbol()">{{ getPriceCoinSymbol() }}</span>
</template>
<template #default="scope"><span class="price-strong">{{ scope.row.price }}</span></template>
<template #default="scope">
<span class="price-strong">
<el-tooltip
v-if="formatAmount(scope.row.price, getRowCoin(scope.row)).truncated"
:content="formatAmount(scope.row.price, getRowCoin(scope.row)).full"
placement="top"
>
<span>
{{ formatAmount(scope.row.price, getRowCoin(scope.row)).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(scope.row.price, getRowCoin(scope.row)).text }}</span>
</span>
</template>
</el-table-column>
</el-table>
@@ -211,7 +263,22 @@
<el-button type="primary" @click="handleConfirmAddSelectedToCart">确认加入</el-button>
</template>
</el-dialog>
<el-row style="margin-bottom: 20px;">
<el-col :span="24" style="display: flex; justify-content: center">
<el-pagination
style="margin: 0 auto; margin-top: 10px"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
:page-sizes="pageSizes"
:page-size="params.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
</el-col>
</el-row>
</div>
<div v-else class="not-found">
@@ -253,6 +320,23 @@ export default {
return ''
}
},
/**
* 当前下拉框选中值对应的图标(用于 el-select 前缀展示)
* @returns {string}
*/
getSelectedPayIcon() {
try {
const key = this.selectedPayKey
if (!key) return ''
const [chain, coin] = String(key).split('|')
const list = Array.isArray(this.paymentMethodList) ? this.paymentMethodList : []
const hit = list.find(
it => String(it && it.payChain).toUpperCase() === String(chain).toUpperCase()
&& String(it && it.payCoin).toUpperCase() === String(coin).toUpperCase()
)
return this.getPayImageUrl(hit)
} catch (e) { return '' }
},
/**
* 支付方式下拉变更:选择/清空即触发请求
* @param {{payChain?: string, payCoin?: string}|null} val
@@ -805,4 +889,46 @@ export default {
font-size: 16px;
}
}
/* 排序表头视觉样式 */
.sortable {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
color: #334155; /* slate-700 */
}
.sortable:hover {
color: #1e293b; /* slate-800 */
}
.sort-arrow {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
.sort-arrow.asc {
border-bottom: 7px solid #64748b; /* slate-500 */
}
.sort-arrow.desc {
border-top: 7px solid #64748b;
}
.sortable.active { color: #2563eb; } /* 蓝色高亮 */
.sort-arrow.active.sort-arrow.asc { border-bottom-color: #2563eb; }
.sort-arrow.active.sort-arrow.desc { border-top-color: #2563eb; }
.amount-more {
font-size: 12px;
color: #94a3b8; /* slate-400 */
margin-left: 4px;
}
::v-deep .el-input__prefix, .el-input__suffix{
top:24%;
}
::v-deep .el-input--mini .el-input__icon{
line-height: 0px;
}
</style>

View File

@@ -72,7 +72,10 @@
<img :src="pt.image" :alt="formatPayType(pt)" class="paytype-icon" />
</el-tooltip>
</div>
<span class="product-sold" aria-label="已售数量">已售{{ product && product.saleNumber != null ? product.saleNumber : 0 }}</span>
<div class="right-meta">
<span class="product-sold" aria-label="已售数量">已售{{ product && product.saleNumber != null ? product.saleNumber : 0 }}</span>
<span class="shop-name">店铺{{ product && (product.shopName || product.name) }}</span>
</div>
</div>
</div>
</div>
@@ -205,6 +208,16 @@ export default {
align-items: center;
margin-top: 8px;
}
.right-meta{
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.shop-name{
color: #64748b;
font-size: 12px; /* 与已售一致 */
}
.product-price {
color: #e53e3e;
font-weight: bold;

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>power_leasing</title><script defer="defer" src="/js/chunk-vendors.f4da7ffe.js"></script><script defer="defer" src="/js/app.c7605e06.js"></script><link href="/css/chunk-vendors.10dd4e95.css" rel="stylesheet"><link href="/css/app.4475c0cd.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but power_leasing doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>power_leasing</title><script defer="defer" src="/js/chunk-vendors.f4da7ffe.js"></script><script defer="defer" src="/js/app.d22501a6.js"></script><link href="/css/chunk-vendors.10dd4e95.css" rel="stylesheet"><link href="/css/app.c82f27e9.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but power_leasing doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long