4 Commits

Author SHA1 Message Date
a02c287715 日常更新 2025-11-20 14:28:57 +08:00
c7cee78798 添加了起付额判定 2025-11-18 14:36:43 +08:00
50e5ce8d08 结算逻辑修改完成,起付额判定待处理 2025-11-14 16:17:36 +08:00
bea1aa8e4c 周五固定更新 2025-11-07 16:30:03 +08:00
24 changed files with 2447 additions and 438 deletions

View File

@@ -87,3 +87,13 @@ export function getMachineInfoById(data) {
}
// 查获取商城商品支持的支付方式
export function getPayTypes(data) {
return request({
url: `/lease/product/getPayTypes`,
method: 'post',
data
})
}

View File

@@ -56,43 +56,43 @@ export function closeShop(id) {
// 根据 店铺id 查询店铺商品配置信息列表
export function getShopConfig(id) {
return request({
url: `/lease/shop/getShopConfig`,
method: 'post',
data: { id }
})
}
return request({
url: `/lease/shop/getShopConfig`,
method: 'post',
data: { id }
})
}
// 新增商铺配置
// 新增商铺配置
export function addShopConfig(data) {
return request({
url: `/lease/shop/addShopConfig`,
method: 'post',
data
})
}
return request({
url: `/lease/shop/addShopConfig`,
method: 'post',
data
})
}
// 根据配置id 修改配置
// 根据配置id 修改配置
export function updateShopConfig(data) {
return request({
url: `/lease/shop/updateShopConfig`,
method: 'post',
data
})
}
return request({
url: `/lease/shop/updateShopConfig`,
method: 'post',
data
})
}
// 根据配置id 删除配置
// 根据配置id 删除配置
export function deleteShopConfig(data) {
return request({
url: `/lease/shop/deleteShopConfig`,
method: 'post',
data
})
}
return request({
url: `/lease/shop/deleteShopConfig`,
method: 'post',
data
})
}
// 钱包配置(用于修改卖家钱包地址)----获取链(一级)和币(二级) 下拉列表(获取本系统支持的链和币种)
// 钱包配置(用于修改卖家钱包地址)----获取链(一级)和币(二级) 下拉列表(获取本系统支持的链和币种)
export function getChainAndCoin(data) {
return request({
url: `/lease/shop/getChainAndCoin`,

View File

@@ -106,6 +106,28 @@ export function getRecentlyTransaction(data) {
})
}
//绑定钱包前查询商品列表
export function getProductListForShopWalletConfig(data) {
return request({
url: `/lease/product/getProductListForShopWalletConfig`,
method: 'post',
data
})
}
//设置之前商品列表的新链的机器价格
export function updateProductListForShopWalletConfig(data) {
return request({
url: `/lease/product/updateProductListForShopWalletConfig`,
method: 'post',
data
})
}

View File

@@ -0,0 +1,41 @@
// 金额截断显示工具不补0、不四舍五入
// 规则:
// - USDT: 最多6位小数
// - ETH: 最多8位小数
// - 其他币种: 最多6位小数
// 返回 { text, truncated, full }
export function getMaxDecimalsByCoin() {
// 全站统一:最多 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);
}
// 默认 6 位截断(非币种语境也可复用)
export function truncateTo6(value) {
return truncateAmountRaw(value, 6);
}

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 '' }
@@ -204,16 +304,7 @@ export default {
});
return
}
try {
const curPath = (this.$route && this.$route.path) || ''
const from = curPath.indexOf('/account/orders') === 0 ? 'buyer' : (curPath.indexOf('/account/seller-orders') === 0 ? 'seller' : '')
try { if (from) sessionStorage.setItem('orderDetailFrom', from) } catch (e) {}
if (from) {
this.$router.push({ path: `/account/order-detail/${id}`, query: { from } })
} else {
this.$router.push(`/account/order-detail/${id}`)
}
} catch (e) {
try { this.$router.push(`/account/order-detail/${id}`) } catch (e) {
this.$message({
message: '无法跳转到详情页',
type: 'error',
@@ -253,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

@@ -37,21 +37,22 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="价格范围">
<!-- <el-form-item label="价格范围">
<el-input :value="product && product.priceRange" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
</el-form-item> -->
<el-form-item label="类型">
<el-input :value="product && (product.type === 1 ? '算力套餐' : '挖矿机器')" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-input :value="product && (product.state === 1 ? '下架' : '上架')" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
</el-col>
<!-- <el-col :span="24">
<el-form-item label="图片">
@@ -64,7 +65,7 @@
<el-col :span="24">
<el-form-item label="描述">
<el-input type="textarea" :rows="3" :value="product && product.description" disabled />
<el-input type="textarea" :rows="3" :value="product && product.description" disabled />
</el-form-item>
</el-col>
</el-row>
@@ -76,9 +77,9 @@
<div slot="header" class="section-title">机器组合</div>
<div v-if="machineList && machineList.length">
<el-table :data="machineList" border stripe style="width: 100%">
<el-table-column prop="user" label="挖矿账户" min-width="80" />
<el-table-column prop="id" label="矿机ID" min-width="60" />
<el-table-column prop="miner" label="机器编号" min-width="100" />
<el-table-column prop="user" label="挖矿账户" />
<el-table-column prop="id" label="矿机ID" />
<el-table-column prop="miner" label="机器编号" />
<el-table-column label="实际算力" width="100">
<template slot="header">
<el-tooltip content="实际算力为该机器在本矿池过去24H的平均算力" effect="dark" placement="top">
@@ -100,11 +101,15 @@
:class="{ 'changed-input': isCellChanged(scope.row, 'theoryPower') }"
style="max-width: 260px;"
>
<template slot="append">{{ scope.row.unit || '' }}</template>
<template slot="append">
<el-select v-model="scope.row.unit" size="mini" :disabled="isRowDisabled(scope.row)" class="append-select append-select--unit" style="width: 90px;">
<el-option v-for="u in unitOptions" :key="u" :label="u" :value="u" />
</el-select>
</template>
</el-input>
</template>
</el-table-column>
<el-table-column label="功耗(kw/h)" min-width="140">
<el-table-column label="功耗(kw/h)" >
<template #default="scope">
<el-input
v-model="scope.row.powerDissipation"
@@ -116,16 +121,16 @@
:class="{ 'changed-input': isCellChanged(scope.row, 'powerDissipation') }"
style="max-width: 260px;"
>
<template slot="append">kw/h</template>
<!-- <template slot="append">kw/h</template> -->
</el-input>
</template>
</el-table-column>
<el-table-column label="型号" min-width="140">
<el-table-column label="型号" >
<template #default="scope">
<el-input
v-model="scope.row.type"
size="small"
placeholder="矿机型号"
:maxlength="20"
:disabled="isRowDisabled(scope.row)"
@input="handleTypeCell(scope.$index)"
@@ -134,7 +139,7 @@
/>
</template>
</el-table-column>
<el-table-column label="售价(USDT)" min-width="140">
<el-table-column label="售价" width="188">
<template slot="header">
<el-tooltip effect="dark" placement="top">
<div slot="content">
@@ -145,11 +150,11 @@
</div>
<i class="el-icon-question label-help" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
<span>售价(USDT)</span>
<span>售价按结算币种</span>
</template>
<template slot-scope="scope">
<el-input
v-model="scope.row.price"
v-model="scope.row._priceEditing"
size="small"
inputmode="decimal"
:disabled="isRowDisabled(scope.row)"
@@ -158,11 +163,20 @@
:class="{ 'changed-input': isCellChanged(scope.row, 'price') }"
style="max-width: 260px;"
>
<template slot="append">USDT</template>
<template slot="append">
<el-select v-model="scope.row._selectedPayIndex" size="mini" @change="handlePayTypeChange(scope.$index)" class="append-select append-select--coin" style="width:120px;">
<el-option
v-for="(pt, i) in (scope.row.priceList || [])"
:key="pt.payTypeId || i"
:label="[String(pt.chain||'').toUpperCase(), String(pt.coin||'').toUpperCase()].filter(Boolean).join('-')"
:value="i"
/>
</el-select>
</template>
</el-input>
</template>
</el-table-column>
<el-table-column label="最大租赁天数(天)" min-width="140">
<el-table-column label="最大租赁天数(天)" width="100">
<template #default="scope">
<el-input
v-model="scope.row.maxLeaseDays"
@@ -248,6 +262,8 @@ export default {
// 可编辑字段快照(用于变更高亮)
fieldSnapshot: {},
updateLoading:false,
// 算力单位选项(与新增出售机器页面保持一致)
unitOptions: ['KH/S','MH/S','GH/S','TH/S','PH/S'],
}
},
@@ -269,6 +285,15 @@ export default {
},
methods: {
/** 结算币种切换时,更新当前编辑价格 */
handlePayTypeChange(index) {
const row = this.machineList && this.machineList[index]
if (!row) return
const sel = Number(row._selectedPayIndex || 0)
const list = Array.isArray(row.priceList) ? row.priceList : []
const target = list[sel] || {}
this.$set(this.machineList, index, { ...row, _priceEditing: String(target.price ?? '') })
},
/**
* 判断行是否不可编辑(已售出则禁用)
* @param {Object} row - 当前行数据
@@ -304,7 +329,13 @@ export default {
if (res && res.code === 200) {
this.machineList =res.rows
const rows = Array.isArray(res.rows) ? res.rows : []
this.machineList = rows.map(r => {
const list = Array.isArray(r.priceList) ? r.priceList : []
const sel = 0
const first = list[sel] || {}
return { ...r, _selectedPayIndex: sel, _priceEditing: String(first.price ?? '') }
})
this.refreshStateSnapshot()
this.refreshFieldSnapshot()
}
@@ -338,11 +369,15 @@ export default {
for (let i = 0; i < list.length; i += 1) {
const row = list[i]
if (!row || typeof row.id === 'undefined') continue
const priceMap = {}
if (Array.isArray(row.priceList)) {
row.priceList.forEach(p => { if (p) priceMap[String(p.payTypeId ?? '')] = String(p.price ?? '') })
}
snapshot[row.id] = {
theoryPower: String(row.theoryPower ?? ''),
powerDissipation: String(row.powerDissipation ?? ''),
type: String(row.type ?? ''),
price: String(row.price ?? ''),
priceMap,
maxLeaseDays: String(row.maxLeaseDays ?? ''),
}
}
@@ -358,6 +393,14 @@ export default {
isCellChanged(row, key) {
if (!row || typeof row.id === 'undefined') return false
const snap = this.fieldSnapshot[row.id] || {}
if (key === 'price') {
const sel = Number(row._selectedPayIndex || 0)
const pt = Array.isArray(row.priceList) && row.priceList[sel] ? row.priceList[sel] : null
const pid = String(pt && pt.payTypeId ? pt.payTypeId : sel)
const cur = String(pt && pt.price != null ? pt.price : '')
const ori = String((snap.priceMap && snap.priceMap[pid]) || '')
return cur !== ori
}
const current = String(row[key] ?? '')
const original = String(snap[key] ?? '')
return current !== original
@@ -438,7 +481,7 @@ export default {
// - 功耗6 位整数 + 4 位小数
// - 价格12 位整数 + 2 位小数
// - 其他保持原逻辑6 位小数)
let v = String(this.machineList[index][key] ?? '')
let v = String(key === 'price' ? (this.machineList[index]._priceEditing ?? '') : (this.machineList[index][key] ?? ''))
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
if (firstDot !== -1) {
@@ -460,22 +503,35 @@ export default {
if (intPart.length > 12) { intPart = intPart.slice(0, 12) }
if (decPart) { decPart = decPart.slice(0, 2) }
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
// 同步到当前选中结算币种的价格
this.$set(this.machineList[index], '_priceEditing', v)
const row = this.machineList[index]
const sel = Number(row._selectedPayIndex || 0)
if (Array.isArray(row.priceList) && row.priceList[sel]) {
this.$set(row.priceList[sel], 'price', v)
}
} else {
if (firstDot !== -1) {
const [i, d] = v.split('.')
v = i + '.' + (d ? d.slice(0, 6) : '')
}
}
const row = { ...this.machineList[index], [key]: v }
this.$set(this.machineList, index, row)
if (key !== 'price') {
const row = { ...this.machineList[index], [key]: v }
this.$set(this.machineList, index, row)
}
},
handlePriceBlur(index) {
const raw = String(this.machineList[index].price ?? '')
const raw = String(this.machineList[index]._priceEditing ?? '')
const pattern = /^\d{1,12}(\.\d{1,2})?$/
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
this.$message.warning('单价必须大于0整数最多12位小数最多2位')
const row = { ...this.machineList[index], price: '' }
this.$set(this.machineList, index, row)
this.$set(this.machineList[index], '_priceEditing', '')
const row = this.machineList[index]
const sel = Number(row._selectedPayIndex || 0)
if (Array.isArray(row.priceList) && row.priceList[sel]) {
this.$set(row.priceList[sel], 'price', '')
}
}
},
handleMaxLeaseDaysInput(index) {
@@ -567,7 +623,7 @@ export default {
const row = this.machineList[i]
const rowLabel = row && (row.miner || row.id || i + 1)
const theoryRaw = String(row.theoryPower ?? '')
const priceRaw = String(row.price ?? '')
const priceRaw = String(row._priceEditing ?? '')
const typeRaw = String(row.type ?? '')
const dissRaw = String(row.powerDissipation ?? '')
const daysRaw = String(row.maxLeaseDays ?? '')
@@ -598,7 +654,7 @@ export default {
const payload = this.machineList.map(m => ({
id: m.id,
powerDissipation: Number(m.powerDissipation ?? 0),
price: Number(m.price ?? 0),
priceList: Array.isArray(m.priceList) ? m.priceList.map(p => ({ ...p, price: Number(p && p.price != null && p.price !== '' ? p.price : 0) })) : [],
state: Number(m.state ?? 0),
theoryPower: Number(m.theoryPower ?? 0),
type: m.type || '',
@@ -641,17 +697,51 @@ export default {
.empty-text { color: #909399; text-align: center; padding: 12px 0; }
.label-help { margin-left: 4px; color: #909399; cursor: help; }
/* ::v-deep .el-form-item__content{
margin-left: 52px !important;
} */
</style>
<style>
.el-input-group__append, .el-input-group__prepend{
padding: 0 5px !important;
}
.account-product-detail .el-table .el-input,
.account-product-detail .el-table .el-textarea{
width: 94% !important; /* 仅限制表格内输入宽度,避免影响上面的基础信息区域 */
}
/* 基础信息表单保持满宽,确保“描述”与上方输入左侧对齐 */
.account-product-detail .detail-form .el-input,
.account-product-detail .detail-form .el-textarea{
width: 100% !important;
}
/* 让追加区裁剪内部元素,避免 el-select 下拉箭头溢出到单元格外 */
.el-input-group__append,
.el-input-group__prepend{
overflow: hidden;
}
/* 追加在输入框右侧的下拉(单位/结算币种)细节优化 */
.append-select .el-input__inner{
/* 预留更多箭头空间,避免被右侧裁剪 */
padding-right: 28px;
height: 30px;
line-height: 30px;
}
.append-select .el-select__caret{
right: 10px; /* 箭头往内侧移动,防止被裁切 */
transform: scale(.85); /* 缩小箭头,保证完全显示 */
}
.append-select .el-input__icon{
line-height: 30px; /* 垂直居中,避免上下被裁切 */
}
/* 变化高亮:为输入框外层添加红色边框,视觉醒目但不改变布局 */
.changed-input .el-input__inner,
.changed-input input.el-input__inner {
.changed-input input.el-input__inner,
/* 带有 append 时,同步高亮右侧追加区的边框,保证整体连贯 */
.changed-input .el-input-group__append {
border-color: #f56c6c !important;
}

View File

@@ -56,8 +56,7 @@
</el-form-item>
<el-form-item label="功耗" prop="powerDissipation">
<el-input
v-model="form.powerDissipation"
placeholder="示例0.01"
v-model="form.powerDissipation"
inputmode="decimal"
@input="handleNumeric('powerDissipation')"
style="width: 50%;"
@@ -79,7 +78,22 @@
<i class="el-icon-question label-help" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
</span>
<!-- 若商品定义了多个结算币种则按链-币种动态生成多个售价输入否则回退为旧的 USDT 单价 -->
<div v-if="payTypeDefs && payTypeDefs.length" class="cost-multi">
<div v-for="pt in payTypeDefs" :key="pt.key" class="cost-item">
<el-input
v-model="form.costMap[pt.key]"
placeholder="请输入价格"
inputmode="decimal"
@input="val => handleCostMapInput(pt.key, val)"
style="width: 50%;"
>
<template slot="append">{{ pt.label }}</template>
</el-input>
</div>
</div>
<el-input
v-else
v-model="form.cost"
placeholder="请输入成本USDT"
inputmode="decimal"
@@ -158,7 +172,7 @@
</div>
</template>
</el-table-column>
<el-table-column label="售价(USDT)" min-width="160">
<el-table-column label="售价(按结算币种)" min-width="220">
<template slot="header">
<el-tooltip effect="dark" placement="top">
<div slot="content">
@@ -169,20 +183,35 @@
</div>
<i class="el-icon-question label-help" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
<span>售价(USDT)</span>
<span>售价按结算币种</span>
</template>
<template slot-scope="scope">
<el-input
v-model="scope.row.price"
placeholder="价格"
inputmode="decimal"
@input="handleRowPriceInput(scope.$index)"
@blur="handleRowPriceBlur(scope.$index)"
style="width: 100%;"
>
<template slot="append">USDT</template>
</el-input>
<div class="price-multi">
<div v-if="payTypeDefs && payTypeDefs.length" class="price-items">
<div v-for="pt in payTypeDefs" :key="pt.key" class="price-item">
<el-input
v-model="scope.row.priceMap[pt.key]"
placeholder="价格"
inputmode="decimal"
@input="() => handleRowPriceMapInput(scope.$index, pt.key)"
@blur="() => handleRowPriceMapBlur(scope.$index, pt.key)"
>
<template slot="append">{{ pt.label }}</template>
</el-input>
</div>
</div>
<el-input
v-else
v-model="scope.row.price"
placeholder="价格"
inputmode="decimal"
@input="handleRowPriceInput(scope.$index)"
@blur="handleRowPriceBlur(scope.$index)"
style="width: 100%;"
>
<template slot="append">USDT</template>
</el-input>
</div>
</template>
</el-table-column>
@@ -265,6 +294,7 @@ export default {
type: '',
unit: 'TH/S',
cost: '',
costMap: {}, // { 'CHAIN-COIN': '123.45' }
maxLeaseDays: ''
},
confirmVisible: false,
@@ -301,15 +331,18 @@ export default {
],
unit: [ { required: true, message: '请选择算力单位', trigger: 'change' } ],
cost: [
{ required: true, message: '请填写机器成本USDT', trigger: 'blur' },
{
validator: (rule, value, callback) => {
validator(rule, value, callback) {
// 若为多结算币种模式,跳过此校验(统一售价由每种币种的输入框承担)
if (Array.isArray(this.payTypeDefs) && this.payTypeDefs.length > 0) {
callback()
return
}
const str = String(value || '')
if (!str) {
callback(new Error('请填写机器成本USDT'))
return
}
// 整数最多12位小数最多2位
const pattern = /^\d{1,12}(\.\d{1,2})?$/
if (!pattern.test(str)) {
callback(new Error('成本整数最多12位小数最多2位'))
@@ -421,6 +454,7 @@ export default {
selectedMachineRows: [],
saving: false,
lastCostBaseline: 0,
lastCostMapBaseline: {}, // { key: number }
lastTypeBaseline: '',
lastMaxLeaseDaysBaseline: 0,
lastPowerDissipationBaseline: 0,
@@ -455,10 +489,58 @@ export default {
}
},
created() {
this.initPayTypesFromRoute()
this.fetchMiners()
this.lastTypeBaseline = this.form.type
// 绑定基于组件实例的校验器,避免 this 丢失
if (this.rules && this.rules.cost) {
this.$set(this.rules, 'cost', [{ validator: this.validateCost, trigger: 'blur' }])
}
},
methods: {
/** 统一售价校验:多结算币种时跳过,单价时按 USDT 校验 */
validateCost(rule, value, callback) {
if (Array.isArray(this.payTypeDefs) && this.payTypeDefs.length > 0) {
callback()
return
}
const str = String(value || '')
if (!str) { callback(new Error('请填写机器成本USDT')); return }
const pattern = /^\d{1,12}(\.\d{1,2})?$/
if (!pattern.test(str)) { callback(new Error('成本整数最多12位小数最多2位')); return }
if (Number(str) <= 0) { callback(new Error('成本必须大于 0')); return }
callback()
},
/** 解析路由参数中的支付方式,生成标准定义 */
initPayTypesFromRoute() {
this.payTypeDefs = []
try {
const raw = this.$route.query.payTypes
if (!raw) return
const arr = JSON.parse(decodeURIComponent(raw))
if (!Array.isArray(arr)) return
const defs = []
arr.forEach(it => {
const chain = String(it && it.chain ? it.chain : '').toUpperCase()
const coin = String(it && it.coin ? it.coin : '').toUpperCase()
if (!chain && !coin) return
const key = [chain, coin].filter(Boolean).join('-')
const label = key
defs.push({ chain, coin, key, label })
})
// 去重
const map = new Map()
defs.forEach(d => { if (!map.has(d.key)) map.set(d.key, d) })
this.payTypeDefs = Array.from(map.values())
// 初始化统一售价映射
const initCostMap = {}
this.payTypeDefs.forEach(d => { initCostMap[d.key] = '' })
this.form.costMap = initCostMap
this.lastCostMapBaseline = { ...initCostMap }
} catch (e) {
this.payTypeDefs = []
}
},
handleBack() {
this.$router.back()
},
@@ -516,6 +598,36 @@ export default {
this.syncCostToRows()
}
},
/** 顶部多结算币种统一售价输入 */
handleCostMapInput(key, val) {
// 价格输入整数最多12位小数最多2位允许尾随小数点
let v = String(val ?? this.form.costMap[key] ?? '')
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
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 > 12) intPart = intPart.slice(0, 12)
if (decPart) decPart = decPart.slice(0, 2)
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
this.$set(this.form.costMap, key, v)
// 同步到行:仅当行对应价格未设置或等于旧基线
const oldBaseline = Number(this.lastCostMapBaseline[key] ?? NaN)
this.selectedMachineRows = this.selectedMachineRows.map(row => {
const cur = Number((row.priceMap && row.priceMap[key]) ?? NaN)
const shouldFollow = !Number.isFinite(cur) || cur === oldBaseline
const nextPriceMap = { ...(row.priceMap || {}) }
if (shouldFollow) nextPriceMap[key] = v
return { ...row, priceMap: nextPriceMap }
})
const num = Number(v)
if (Number.isFinite(num)) this.$set(this.lastCostMapBaseline, key, num)
},
/**
* 顶部矿机型号输入限制20字符
*/
@@ -563,18 +675,24 @@ export default {
if (m) {
// 若已存在,沿用已编辑的价格、型号和状态
const existed = this.selectedMachineRows.find(r => r.miner === minerId)
const existedPriceMap = existed && existed.priceMap ? existed.priceMap : null
const defaultPriceMap = {}
if (this.payTypeDefs && this.payTypeDefs.length) {
this.payTypeDefs.forEach(d => { defaultPriceMap[d.key] = this.form.costMap[d.key] })
}
nextRows.push({
user: m.user,
coin: m.coin,
miner: m.miner,
realPower: m.realPower,
price: existed ? existed.price : this.form.cost,
price: existed ? existed.price : this.form.cost, // 兼容单价模式
powerDissipation: existed && existed.powerDissipation !== undefined ? existed.powerDissipation : this.form.powerDissipation,
theoryPower: existed && existed.theoryPower !== undefined ? existed.theoryPower : this.form.theoryPower,
unit: existed && existed.unit ? existed.unit : this.form.unit,
type: existed ? existed.type : this.form.type,
state: existed ? existed.state : 0, // 默认上架
maxLeaseDays: existed && existed.maxLeaseDays !== undefined ? existed.maxLeaseDays : this.form.maxLeaseDays
maxLeaseDays: existed && existed.maxLeaseDays !== undefined ? existed.maxLeaseDays : this.form.maxLeaseDays,
priceMap: existedPriceMap || defaultPriceMap
})
}
})
@@ -638,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)
},
/**
@@ -667,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)
},
/**
@@ -742,6 +862,38 @@ export default {
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
this.$set(this.selectedMachineRows[index], 'price', v)
},
/** 行内多结算币种价格输入 */
handleRowPriceMapInput(index, key) {
// 价格输入整数最多12位小数最多2位允许尾随小数点
const row = this.selectedMachineRows[index]
const map = { ...(row.priceMap || {}) }
let v = String(map[key] ?? '')
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
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 > 12) intPart = intPart.slice(0, 12)
if (decPart) decPart = decPart.slice(0, 2)
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
map[key] = v
this.$set(this.selectedMachineRows[index], 'priceMap', map)
},
handleRowPriceMapBlur(index, key) {
const row = this.selectedMachineRows[index]
const raw = String((row.priceMap && row.priceMap[key]) ?? '')
const pattern = /^\d{1,12}(\.\d{1,2})?$/
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
this.$message.warning('价格必须大于0整数最多12位小数最多2位')
const map = { ...(row.priceMap || {}) }
map[key] = ''
this.$set(this.selectedMachineRows[index], 'priceMap', map)
}
},
handleRowPriceBlur(index) {
const raw = String(this.selectedMachineRows[index].price ?? '')
const pattern = /^\d{1,12}(\.\d{1,2})?$/
@@ -865,14 +1017,27 @@ export default {
this.$message.warning('存在行的矿机型号全是空格,请修正后再试')
return
}
// 校验:已选择机器的价格必须大于0
// 校验:价格与最大租赁天数
for (let i = 0; i < this.selectedMachineRows.length; i += 1) {
const row = this.selectedMachineRows[i]
const priceNum = Number(row && row.price)
if (!Number.isFinite(priceNum) || priceNum <= 0) {
const label = (row && (row.miner || row.user)) || i + 1
this.$message.warning(`${i + 1}行(机器:${label}) 价格必须大于0`)
return
if (this.payTypeDefs && this.payTypeDefs.length) {
for (let j = 0; j < this.payTypeDefs.length; j += 1) {
const def = this.payTypeDefs[j]
const raw = String(row && row.priceMap ? row.priceMap[def.key] : '')
const num = Number(raw)
if (!/^\d{1,12}(\.\d{1,2})?$/.test(raw) || !Number.isFinite(num) || num <= 0) {
const label = (row && (row.miner || row.user)) || i + 1
this.$message.warning(`${i + 1}行(机器:${label}) 价格(${def.label})必须大于0整数最多12位小数最多2位`)
return
}
}
} else {
const priceNum = Number(row && row.price)
if (!Number.isFinite(priceNum) || priceNum <= 0) {
const label = (row && (row.miner || row.user)) || i + 1
this.$message.warning(`${i + 1}行(机器:${label}) 价格必须大于0`)
return
}
}
// 校验:逐行最大租赁天数 1-365
const rawDays = String((row && row.maxLeaseDays) ?? '')
@@ -891,17 +1056,23 @@ export default {
const [user, coin] = this.selectedMiner.split('|')
this.saving = true
try {
// 若是多结算币种,组装 priceList否则沿用单价字段
const payload = {
productId: this.form.productId,
powerDissipation: this.form.powerDissipation,
theoryPower: this.form.theoryPower,
type: this.form.type,
unit: this.form.unit,
cost: this.form.cost,
cost: this.payTypeDefs && this.payTypeDefs.length ? Number(this.form.costMap && this.form.costMap[this.payTypeDefs[0].key]) || 0 : this.form.cost,
maxLeaseDays: this.form.maxLeaseDays,
productMachineURDVos: this.selectedMachineRows.map(r => ({
miner: r.miner,
price: Number(r.price) || 0,
price: this.payTypeDefs && this.payTypeDefs.length ? undefined : (Number(r.price) || 0),
priceList: this.payTypeDefs && this.payTypeDefs.length ? this.payTypeDefs.map(d => ({
chain: d.chain,
coin: d.coin,
price: Number(r.priceMap && r.priceMap[d.key]) || 0
})) : undefined,
state: r.state || 0,
type: r.type || this.form.type,
user: r.user,
@@ -937,6 +1108,20 @@ export default {
,
watch: {
'form.cost': function() { this.syncCostToRows() },
// 当统一售价映射变化时手动同步(深度监听)
form: {
deep: true,
handler(val, oldVal) {
// 仅在 costMap 深度变化且有多支付类型时,做轻量同步(不覆盖用户已手改的值)
if (!this.payTypeDefs || !this.payTypeDefs.length) return
if (!val || !val.costMap) return
Object.keys(val.costMap).forEach(k => {
if (oldVal && oldVal.costMap && val.costMap[k] !== oldVal.costMap[k]) {
this.handleCostMapInput(k, val.costMap[k])
}
})
}
},
'form.type': function() { this.updateMachineType() },
'form.maxLeaseDays': function() { this.syncMaxLeaseDaysToRows() },
'form.powerDissipation': function() { this.syncPowerDissipationToRows() },
@@ -980,5 +1165,37 @@ export default {
text-align: left;
padding-left: 18px !important;
}
/* 多结算币种价格输入的布局优化 */
.cost-multi { display: grid; gap: 8px; }
.cost-item { display: flex; align-items: center; }
.price-multi { display: grid; gap: 8px; }
.price-items { display: grid; gap: 8px; }
/* 让 链-币种 附加区同宽、居中显示,整体对齐 */
.price-item :deep(.el-input-group__append),
.cost-item :deep(.el-input-group__append){
width: 110px;
min-width: 110px;
text-align: center;
padding: 0 8px;
background: #f8fafc;
color: #606266;
}
/* 缩小输入高度并保持垂直居中 */
.price-item :deep(.el-input__inner),
.cost-item :deep(.el-input__inner){
height: 30px;
line-height: 30px;
}
/* 让组内附加区高度与输入一致 */
.price-item :deep(.el-input-group__append),
.cost-item :deep(.el-input-group__append){
height: 30px;
line-height: 30px;
}
/* 略微收紧间距,让整体更紧凑 */
.price-multi { gap: 6px; }
.price-items { gap: 6px; }
.cost-multi { gap: 6px; }
</style>

View File

@@ -30,18 +30,32 @@
style="width: 100%"
>
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
<el-table-column prop="name" label="名称" min-width="100" />
<el-table-column prop="coin" label="币种" width="100" />
<el-table-column prop="priceRange" label="价格范围" width="150" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="coin" label="币种" />
<el-table-column label="支持结算方式" >
<template #default="scope">
<div class="paytypes">
<el-tooltip
v-for="(pt, idx) in (scope.row.payTypes || [])"
:key="idx"
:content="formatPayType(pt)"
placement="top"
:open-delay="80"
>
<img :src="pt.image" :alt="formatPayType(pt)" class="paytype-icon" />
</el-tooltip>
</div>
</template>
</el-table-column>
<!-- <el-table-column label="算力" min-width="140">
<template #default="scope">
<span>{{ scope.row.power }} {{ scope.row.unit }}</span>
</template>
</el-table-column> -->
<el-table-column prop="algorithm" label="算法" min-width="120" />
<el-table-column prop="algorithm" label="算法" />
<!-- <el-table-column prop="electricityBill" label="电费" width="100" /> -->
<!-- <el-table-column prop="incomeRate" label="收益率" width="100" /> -->
<el-table-column prop="type" label="商品类型" width="130">
<el-table-column prop="type" label="商品类型" >
<template #default="scope">
<el-tag :type="scope.row.type === 1 ? 'success' : 'warning'">
{{ scope.row.type === 1 ? '算力套餐' : '挖矿机器' }}
@@ -49,9 +63,9 @@
</template>
</el-table-column>
<el-table-column prop="saleNumber" label="已售数量" min-width="60" />
<el-table-column prop="totalMachineNumber" label="该商品总机器数量" min-width="60" />
<el-table-column prop="state" label="状态" width="100">
<el-table-column prop="saleNumber" label="已售数量" />
<el-table-column prop="totalMachineNumber" label="该商品总机器数量" />
<el-table-column prop="state" label="状态" >
<template #default="scope">
<el-tag :type="scope.row.state === 1 ? 'info' : 'success'">
{{ scope.row.state === 1 ? '下架' : '上架' }}
@@ -149,6 +163,17 @@ export default {
this.fetchTableData()
},
methods: {
/** 格式化支持结算币种的展示,如 TRON-USDT */
formatPayType(item) {
try {
const chain = (item && item.chain ? String(item.chain) : '').toUpperCase()
const coin = (item && item.coin ? String(item.coin) : '').toUpperCase()
if (chain && coin) return `${chain}-${coin}`
return chain || coin || ''
} catch (e) {
return ''
}
},
/** 初始化筛选选项 */
initOptions() {
try {
@@ -372,7 +397,22 @@ export default {
this.$message.warning('缺少商品ID')
return
}
this.$router.push({ path: '/account/product-machine-add', query: { productId: row.id, coin: row.coin, name: row.name } })
let payTypesParam = ''
try {
const pts = Array.isArray(row.payTypes) ? row.payTypes : []
payTypesParam = encodeURIComponent(JSON.stringify(pts))
} catch (e) {
payTypesParam = ''
}
this.$router.push({
path: '/account/product-machine-add',
query: {
productId: row.id,
coin: row.coin,
name: row.name,
payTypes: payTypesParam
}
})
}
}
}
@@ -416,7 +456,12 @@ export default {
}
::v-deep .el-form-item__content{
text-align: left;
text-align: left;
}
/* 支持结算币种的小图标样式 */
.paytypes { display: inline-flex; align-items: center; gap: 8px; }
.paytype-icon { width: 22px; height: 22px; border-radius: 4px; display: inline-block; }
</style>

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; }
@@ -307,7 +326,7 @@ export default {
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 24px; padding: 8px 4px; }
.detail-item { display: grid; grid-template-columns: 90px 1fr; align-items: center; gap: 8px; }
.detail-item-full { grid-column: 1 / -1; }
.detail-label { color: #666; font-size: 13px; text-align: right; }
.detail-label { color: #666; font-size: 13px; text-align: left; }
.detail-value { color: #333; font-size: 13px; text-align: left; }
.detail-value.address { font-family: "Monaco", "Menlo", monospace; word-break: break-all; }

View File

@@ -58,13 +58,61 @@
</el-form-item>
</el-form>
<!-- 绑定前预检测弹窗若存在关联商品先提示用户再继续绑定 -->
<el-dialog :visible.sync="preCheck.visible" width="80vw" :close-on-click-modal="false" title="检测到关联商品" @close="handlePreCheckClose">
<div style="margin-bottom:10px;">
<el-alert
type="warning"
:closable="false"
show-icon
description="检测到以下商品与本次绑定的链/币相关。继续绑定后,可能需要为这些商品配置该新链下的价格。是否继续?"
/>
</div>
<p style="color: red; font-size: 12px; margin-top: 6px;text-align: right;">* 请填写每个商品对应币种的价格,商品包含机器统一设置价格如需单台修改请在商品列表-详情页操作</p>
<el-table :data="preCheck.rows" height="360" border :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table-column label="商品名称" min-width="160">
<template #default="scope">{{ scope.row.name || scope.row.productName || scope.row.title || scope.row.product || '-' }}</template>
</el-table-column>
<el-table-column label="链" min-width="120">
<template #default> {{ (form.chain || '').toUpperCase() }} </template>
</el-table-column>
<el-table-column label="币种" min-width="120">
<template #default> {{ form.payCoin.split(',').map(s=>s.trim().toUpperCase()).join('') }} </template>
</el-table-column>
<el-table-column label="总矿机数" min-width="100">
<template #default="scope">{{ scope.row.totalMachineNumber != null ? scope.row.totalMachineNumber : (scope.row.total || scope.row.totalMachines || '-') }}</template>
</el-table-column>
<el-table-column label="商品状态" min-width="100">
<template #default="scope">{{ Number(scope.row.state) === 1 ? '下架' : '上架' }}</template>
</el-table-column>
<el-table-column v-for="sym in coinsForBind" :key="'price-'+sym" :label="sym + ' 价格'" min-width="160">
<template #default="scope">
<el-input
v-model="preCheck.rowPrices[getRowKey(scope.row, scope.$index)][sym]"
size="mini"
class="price-input"
placeholder="请输入"
inputmode="decimal"
>
<template #append>{{ sym }}</template>
</el-input>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="preCheck.visible = false">取消</el-button>
<el-button type="primary" :disabled="!canSubmitPreCheck" @click="handleConfirmBindAfterPreview">继续绑定</el-button>
</template>
</el-dialog>
</div>
</div>
</template>
<script>
import { getMyShop } from "@/api/shops";
import { getChainAndList, addWalletShopConfig } from "../../api/wallet";
import { getChainAndList, addWalletShopConfig,getProductListForShopWalletConfig,updateProductListForShopWalletConfig } from "../../api/wallet";
export default {
name: "AccountShopConfig",
@@ -116,6 +164,8 @@ export default {
// },
],
loading: false,
// 绑定前预检测弹窗数据
preCheck: { visible: false, rows: [], prices: {}, rowPrices: {} },
};
},
mounted() {
@@ -282,14 +332,159 @@ export default {
return
}
this.FetchAddWalletShopConfig(this.form);
// 新增步骤:绑定前预检测商品列表
this.preCheckBeforeBind()
},
/**
* 绑定前预检测:若接口返回有关联商品,则弹窗展示;否则直接走绑定流程
*/
async preCheckBeforeBind() {
try {
this.loading = true
const params = { chain: this.form.chain, payCoin: this.form.payCoin }
const res = await getProductListForShopWalletConfig(params)
const rows = Array.isArray(res && res.data) ? res.data : (Array.isArray(res && res.rows) ? res.rows : [])
if (rows && rows.length) {
this.preCheck.rows = rows
// 初始化各币种价格输入
const coins = (this.form.payCoin || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean)
const map = {}
coins.forEach(c => { if (!(c in this.preCheck.prices)) map[c] = '' })
this.preCheck.prices = { ...map, ...this.preCheck.prices }
// 初始化每行的价格容器
this.preCheck.rowPrices = this.preCheck.rowPrices || {}
this.preCheck.rows.forEach((r, idx) => {
const key = this.getRowKey(r, idx)
if (!this.preCheck.rowPrices[key]) this.$set(this.preCheck.rowPrices, key, {})
coins.forEach(c => { if (!(c in this.preCheck.rowPrices[key])) this.$set(this.preCheck.rowPrices[key], c, '') })
})
this.preCheck.visible = true
} else {
// 无关联商品,直接绑定并设置(机器列表为空)
await this.submitBindWithPrice([])
}
} catch (e) {
// 接口异常不阻塞绑定流程
await this.submitBindWithPrice([])
} finally {
this.loading = false
}
},
handleConfirmBindAfterPreview() {
// 校验价格必填
const coins = (this.form.payCoin || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean)
// 逐行校验
for (let i = 0; i < this.preCheck.rows.length; i++) {
const row = this.preCheck.rows[i]
const key = this.getRowKey(row, i)
const priceMap = (this.preCheck.rowPrices && this.preCheck.rowPrices[key]) || {}
for (const c of coins) {
const v = priceMap[c]
if (!v || Number(v) <= 0) {
this.$message.warning(`请填写第 ${i + 1}${c} 的价格`)
return
}
}
}
const groups = this.collectMachineGroups(this.preCheck.rows)
this.preCheck.visible = false
this.submitBindWithPrice(groups)
},
/** 收集每一行对应的机器ID分组兼容不同返回结构 */
collectMachineGroups(rows) {
const groups = []
const pushId = (arr, id) => { if (id != null && id !== '') arr.push(id) };
(rows || []).forEach((r, idx) => {
const ids = []
// 兼容多种返回结构,优先使用接口包含的 machineList每台矿机对象含 productMachineId
if (Array.isArray(r && r.machineList)) r.machineList.forEach(m => pushId(ids, m && (m.productMachineId != null ? m.productMachineId : m.id)))
if (Array.isArray(r && r.productMachineIdList)) r.productMachineIdList.forEach(id => pushId(ids, id))
if (r && r.productMachineId != null) pushId(ids, r.productMachineId)
if (Array.isArray(r && r.productMachineDtoList)) r.productMachineDtoList.forEach(m => pushId(ids, (m && (m.productMachineId != null ? m.productMachineId : m.id))))
if (Array.isArray(r && r.machines)) r.machines.forEach(m => pushId(ids, (m && (m.productMachineId != null ? m.productMachineId : m.id))))
if (Array.isArray(r && r.items)) r.items.forEach(m => pushId(ids, (m && (m.productMachineId != null ? m.productMachineId : m.id))))
const key = this.getRowKey(r, idx)
groups.push({ key, machineIds: ids })
})
return groups
},
/** 生成某一行的 key优先 productId/id */
getRowKey(row, index) {
if (row && row.productId != null) return String(row.productId)
if (row && row.id != null) return `p-${row.id}`
return `idx-${index}`
},
/** 提交绑定:使用 updateProductListForShopWalletConfig 完成绑定与价格设置 */
async submitBindWithPrice(machineGroups) {
try {
this.loading = true
const coins = (this.form.payCoin || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean)
const list = []
if (Array.isArray(machineGroups) && machineGroups.length) {
machineGroups.forEach(g => {
const priceMap = (this.preCheck.rowPrices && this.preCheck.rowPrices[g.key]) || {}
const priceStr = coins.map(c => priceMap[c] || '').join(',');
(g.machineIds || []).forEach(id => { list.push({ productMachineId: id, price: priceStr }) })
})
}
const payload = {
chain: this.form.chain,
symbol: this.form.payCoin,
payAddress: this.form.payAddress,
productMachineForWalletConfigVoList: list
}
const res = await updateProductListForShopWalletConfig(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.preCheck.visible = false
this.resetPreCheckPrices()
this.$message.success('绑定成功')
this.$router.push('/account/shops')
}else{
this.preCheck.visible = true
}
} catch (e) {
// 错误交由全局拦截或简单提示
} finally {
this.loading = false
}
},
handleReset() {
this.form = { chain: "", payAddress: "", payCoin: "" };
this.value = []
},
// 清空预检测中的价格输入
resetPreCheckPrices() {
try {
this.preCheck.prices = {}
this.preCheck.rowPrices = {}
} catch (e) { /* noop */ }
},
// 弹窗关闭时清空价格输入
handlePreCheckClose() {
this.resetPreCheckPrices()
},
},
computed: {
coinsForBind() {
return (this.form.payCoin || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean)
},
canSubmitPreCheck() {
if (!this.preCheck || !this.preCheck.visible) return false
const coins = this.coinsForBind
if (!coins.length) return false
// 所有行都需要填写
for (let i = 0; i < (this.preCheck.rows || []).length; i++) {
const row = this.preCheck.rows[i]
const key = this.getRowKey(row, i)
const priceMap = (this.preCheck.rowPrices && this.preCheck.rowPrices[key]) || {}
for (const c of coins) {
const v = priceMap[c]
if (!v || Number(v) <= 0) return false
}
}
return true
},
/**
* 已选择币种的可读展示(中文顿号分隔)
*/
@@ -353,5 +548,11 @@ export default {
.selected-coins { display: flex; flex-wrap: wrap; gap: 8px; min-height: 32px; align-items: center; margin-left: 79px;}
.selected-coins .el-tag { border-radius: 4px; }
.selected-coins .placeholder { color: #c0c4cc; }
/* 价格输入框获得焦点时高亮为红色,提示用户输入 */
.price-input :deep(.el-input__inner:focus) {
border-color: #f56c6c !important;
box-shadow: 0 0 0 1px #f56c6c inset;
}
</style>

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,22 +38,99 @@
: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="算法" />
<el-table-column prop="powerDissipation" label="功耗(kw/h)" />
<el-table-column prop="theoryPower" label="理论算力" >
<template #default="scope">{{ scope.row.theoryPower }} <span v-show="scope.row.theoryPower">{{ scope.row.unit }}</span></template>
<el-table-column prop="powerDissipation" label="功耗(kw/h)">
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.powerDissipation).truncated"
:content="formatNum6(scope.row.powerDissipation).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.powerDissipation).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.powerDissipation).text }}</span>
</span>
</template>
</el-table-column>
<el-table-column prop="theoryPower" label="理论算力">
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.theoryPower).truncated"
:content="formatNum6(scope.row.theoryPower).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.theoryPower).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.theoryPower).text }}</span>
</span>
<span v-show="scope.row.theoryPower"> {{ scope.row.unit }} </span>
</template>
</el-table-column>
<el-table-column prop="computingPower" label="实际算力">
<template #default="scope">{{ scope.row.computingPower }} <span v-show="scope.row.computingPower">{{ scope.row.unit }}</span></template>
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.computingPower).truncated"
:content="formatNum6(scope.row.computingPower).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.computingPower).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.computingPower).text }}</span>
</span>
<span v-show="scope.row.computingPower"> {{ scope.row.unit }} </span>
</template>
</el-table-column>
<el-table-column prop="theoryIncome" label="单机理论收入(每日/币种)">
<template #default="scope">{{ scope.row.theoryIncome }} <span v-show="scope.row.coin">{{ toUpperText(scope.row.coin) }}</span></template>
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.theoryIncome).truncated"
:content="formatNum6(scope.row.theoryIncome).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.theoryIncome).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.theoryIncome).text }}</span>
</span>
<span v-show="scope.row.coin"> {{ toUpperText(scope.row.coin) }} </span>
</template>
</el-table-column>
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)">
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.theoryUsdtIncome).truncated"
:content="formatNum6(scope.row.theoryUsdtIncome).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.theoryUsdtIncome).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.theoryUsdtIncome).text }}</span>
</span>
</template>
</el-table-column>
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)"/>
<!-- <el-table-column prop="state" label="状态" min-width="100" >
<template #default="scope">
<el-tag :type="scope.row.state === 1 ? 'info' : 'success'">
@@ -60,9 +138,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,65 +187,209 @@
</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;">
已选机器:<b>{{ selectedMachineCount }}</b> 台
<span style="margin-left:12px;">金额合计(USDT)<b>{{ formatTrunc(selectedTotal, 2) }}</b></span>
<span style="margin-left:12px;">金额合计(USDT)</span>
<span class="price-strong">
<el-tooltip
v-if="formatAmount(selectedTotal, 'USDT').truncated"
:content="formatAmount(selectedTotal, 'USDT').full"
placement="top"
>
<span>
{{ formatAmount(selectedTotal, 'USDT').text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(selectedTotal, 'USDT').text }}</span>
</span>
</div>
<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>
<template v-if="grp.coinSymbol">
<span v-if="grp.enough"
style="color:#16a34a;font-weight:600;">
已满足起付额
{{ formatAmount(grp.deductibleAmount || 0, grp.coinSymbol).text }}
</span>
<span v-else
style="color:#ef4444;font-weight:600;">
金额不足最低起付额
{{ formatAmount(grp.deductibleAmount || 0, grp.coinSymbol).text }}
,收取手续费
{{ formatAmount(grp.fee || 0, grp.coinSymbol).text }}
</span>
</template>
</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 +397,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 +491,7 @@
<script>
import { getGoodsList, deleteBatchGoods as apiDeleteBatchGoods ,deleteBatchGoodsForIsDelete} from '../../api/shoppingCart'
import { addOrders,cancelOrder,getOrdersByIds,getOrdersByStatus , getChainAndListForSeller,getCoinPrice } from '../../api/order'
import { truncateAmountByCoin, truncateTo6 } from '../../utils/amount'
export default {
name: 'Cart',
@@ -279,7 +503,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 +513,8 @@ export default {
noticeTimer: null,
// 待结算的店铺信息
pendingCheckoutShop: null,
// 多店铺统一结算的临时选择
pendingCheckoutAll: null,
// 谷歌验证码弹窗
googleCodeDialog: {
visible: false,
@@ -301,7 +527,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 +607,23 @@ 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)
},
/**
* 数值格式化最多6位小数截断不四舍五入不补0
* @param {number|string} value
* @returns {{text:string,truncated:boolean,full:string}}
*/
formatNum6(value) {
return truncateTo6(value)
},
/**
* 将金额转为整分整数,避免浮点误差
* @param {number|string} v
@@ -476,6 +721,140 @@ export default {
},
// 获取所有商品分组(兼容保留,现直接返回空,因已移除中间商品层)
getAllGroups() { return [] },
// 生成店铺的支付方式选项,基于 totalPriceList
getShopPayOptions(shop) {
// 下拉渲染严格以 payConfigList 为准(接口定义的可选支付方式)
const cfg = Array.isArray(shop && shop.payConfigList) ? shop.payConfigList : []
return cfg.map(c => {
const chain = c && c.payChain ? String(c.payChain) : ''
const coin = c && c.payCoin ? String(c.payCoin) : ''
const key = `${chain}|${coin}`
return {
label: `${chain} - ${this.toUpperText(coin)}`,
value: key,
icon: c && c.payCoinImage ? c.payCoinImage : ''
}
})
},
// 判断某机器在当前店铺选择的支付方式下是否有可用单价
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 +909,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 +1007,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 +1021,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 +1196,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 +1253,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 +1358,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,26 +1402,16 @@ 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),
user: m.user,
coin: this.toUpperText(this.selectedCoin || ''),
user: m.user,
miner: m.miner,
unitPrice: Number(unitPrice || 0),
leaseTime: leaseDays,
@@ -1044,10 +1419,115 @@ 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 cfgList = Array.isArray(shop && shop.payConfigList) ? shop.payConfigList : []
const cfgHit = cfgList.find(c => String(c && c.payChain).toUpperCase() === String(chain).toUpperCase()
&& String(c && c.payCoin).toUpperCase() === String(coin).toUpperCase())
const deductibleAmount = Number((cfgHit && cfgHit.deductibleAmount) || 0)
const fee = Number((cfgHit && cfgHit.fee) || 0)
const groupSubtotal = items.reduce((sum, it) => sum + Number(it.subtotal || 0), 0)
const enough = groupSubtotal >= deductibleAmount || deductibleAmount <= 0
const grp = {
shopId: shop.id,
shopName: shop.name || '',
coinSymbol,
payLabel,
items,
deductibleAmount,
fee,
enough,
groupSubtotal
}
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) {
let tCents = items.reduce((acc, it) => centsAdd(acc, it.subtotal || 0), 0)
// 若未满足起付额,合计中加入手续费
if (!enough && fee > 0) tCents = tCents + this.toCents(fee)
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 cfgList = Array.isArray(shop && shop.payConfigList) ? shop.payConfigList : []
const cfgHit = cfgList.find(c => String(c && c.payChain).toUpperCase() === String(chain).toUpperCase()
&& String(c && c.payCoin).toUpperCase() === String(coin).toUpperCase())
const deductibleAmount = Number((cfgHit && cfgHit.deductibleAmount) || 0)
const fee = Number((cfgHit && cfgHit.fee) || 0)
const rows = []
const list = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
let groupSubtotal = 0
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
})
groupSubtotal += subtotal
const prev = totalsCentsByCoin.get(coinSymbol) || 0
totalsCentsByCoin.set(coinSymbol, prev + this.toCents(subtotal))
count += 1
})
if (rows.length) {
const enough = groupSubtotal >= deductibleAmount || deductibleAmount <= 0
// 未满足起付额:总额加上手续费
if (!enough && fee > 0) {
const prev = totalsCentsByCoin.get(coinSymbol) || 0
totalsCentsByCoin.set(coinSymbol, prev + this.toCents(fee))
}
groups.push({
shopId: shop.id,
shopName: shop.name || '',
coinSymbol,
payLabel,
items: rows,
deductibleAmount,
fee,
enough,
groupSubtotal
})
}
})
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 +2013,27 @@ export default {
.dialog-footer {
text-align: center;
}
.amount-more {
font-size: 12px;
color: #94a3b8; /* slate-400 */
margin-left: 4px;
}
.num-strong {
font-weight: inherit;
color: inherit;
}
::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

@@ -1,8 +1,9 @@
import { getProductById } from '../../utils/productService'
import { addToCart } from '../../utils/cartManager'
import { getMachineInfo } from '../../api/products'
import { getMachineInfo, getPayTypes } from '../../api/products'
import { addCart, getGoodsList } from '../../api/shoppingCart'
import { truncateAmountByCoin, truncateTo6 } from '../../utils/amount'
export default {
name: 'ProductDetail',
@@ -13,9 +14,39 @@ export default {
// 默认展开的行keys
expandedRowKeys: [],
selectedMap: {},
// 新接口:单层矿机列表 & 支付方式
machineList: [],
paymentMethodList: [],
// 筛选状态
selectedPayKey: null,
filters: {
chain: '',
coin: '',
minPrice: null,
maxPrice: null,
minPower: null,
maxPower: null,
minPowerDissipation: null,
maxPowerDissipation: null,
unit: 'GH/S'
},
// 实际算力单位选项
powerUnitOptions: ['KH/S', 'MH/S', 'GH/S', 'TH/S', 'PH/S'],
// 排序状态true 升序false 降序
sortStates: {
priceSort: true,
powerSort: true,
powerDissipationSort: true
},
// 当前激活的排序字段(仅当用户点击后才会传参)
activeSortField: '',
// 首次进入时是否已按价格币种设置过支付方式筛选默认值
payFilterDefaultApplied: false,
params: {
id: "",
pageNum: 1,
pageSize: 10,
},
confirmAddDialog: {
@@ -105,13 +136,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
@@ -119,7 +154,8 @@ 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('商品不存在')
this.product = false
@@ -127,6 +163,110 @@ 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)
},
// 数值格式化最多6位小数截断不补0
formatNum6(value) {
return truncateTo6(value)
},
/**
* 首次加载时,将“支付方式筛选”的默认选中值设为与价格列币种一致,
* 并同步 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]
if (raw === null || raw === undefined || raw === '') return
const n = Number(raw)
if (Number.isFinite(n) && n > 0) q[name] = n
}
// 支付方式条件:有值才传
if (this.filters.chain && String(this.filters.chain).trim()) q.chain = String(this.filters.chain).trim()
if (this.filters.coin && String(this.filters.coin).trim()) q.coin = String(this.filters.coin).trim()
if (this.filters.unit && String(this.filters.unit).trim()) q.unit = String(this.filters.unit).trim()
addNum(this.filters, 'minPrice', 'minPrice')
addNum(this.filters, 'maxPrice', 'maxPrice')
addNum(this.filters, 'minPower', 'minPower')
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
},
// 拉取支付方式
async fetchPayTypes() {
try {
const res = await getPayTypes({ productId: this.params.id })
// 接口示例:{ code: 0, data: [ { payChain, payCoin, payCoinImage, shopId } ], msg: '' }
if (res && (res.code === 0 || res.code === 200)) {
const list = Array.isArray(res.data) ? res.data : []
this.paymentMethodList = list
// 支付方式加载后尝试设置默认筛选
this.ensureDefaultPayFilterSelection()
}
} catch (e) {
// 忽略错误,保持页面可用
this.paymentMethodList = []
}
},
async fetchGetMachineInfo(params) {
this.productDetailLoading = true
@@ -134,31 +274,29 @@ export default {
console.log(res)
if (res && res.code === 200) {
console.log(res.data, 'res.rows');
this.paymentMethodList = res.data.payConfigList || []
const list =res.data.machineRangeInfoList || []
const withKeys = list.map((group, idx) => {
const fallbackId = `grp-${idx}`
const groupId = group.id || group.onlyKey || (group.productMachineRangeGroupDto && group.productMachineRangeGroupDto.id)
const firstMachineId = Array.isArray(group.productMachines) && group.productMachines.length > 0 ? group.productMachines[0].id : undefined
// 为机器行设置默认租赁天数为1并确保未选中状态
const normalizedMachines = Array.isArray(group.productMachines)
? group.productMachines.map(m => ({
...m,
leaseTime: (m && m.leaseTime && Number(m.leaseTime) > 0) ? Number(m.leaseTime) : 1,
_selected: false // 确保所有机器行初始状态为未选中
}))
: []
return { ...group, id: groupId || (firstMachineId ? `m-${firstMachineId}` : fallbackId), productMachines: normalizedMachines }
})
this.productListData = withKeys
if (this.productListData.length && (!this.expandedRowKeys || !this.expandedRowKeys.length)) {
this.expandedRowKeys = [this.productListData[0].id]
}
// 产品机器加载完成后,依据购物车集合执行一次本地禁用与勾选
this.total = res.total||0;
// 新数据结构:机器为扁平 rows 列表;仅当后端返回有效支付方式时才覆盖,避免清空 getPayTypes 的结果
try {
const payList = res && res.data && res.data.payConfigList
if (Array.isArray(payList) && payList.length) {
this.paymentMethodList = payList
}
} catch (e) { /* keep existing paymentMethodList */ }
const rows = (res && res.data && (res.data.rows || res.data.list)) || (res && res.rows) || []
const normalized = (Array.isArray(rows) ? rows : []).map((m, idx) => ({
...m,
id: m && (m.id !== undefined && m.id !== null) ? m.id : `m-${idx}`,
leaseTime: (m && m.leaseTime && Number(m.leaseTime) > 0) ? Number(m.leaseTime) : 1,
_selected: false
}))
this.machineList = normalized
// 清空旧的两层结构数据,避免误用
this.productListData = []
this.expandedRowKeys = []
// 机器加载后尝试设置默认筛选
this.ensureDefaultPayFilterSelection()
this.$nextTick(() => {
this.machinesLoaded = true
// 已取消与购物车对比:不再自动禁用或勾选
})
}
@@ -175,17 +313,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
@@ -194,7 +332,7 @@ export default {
//加入购物车
async fetchAddCart(params) {
const res = await addCart(params)
return res
},
//查询购物车列表
@@ -296,7 +434,7 @@ export default {
},
// 已取消对比购物车的自动勾选/禁用逻辑
autoSelectAndDisable() {},
autoSelectAndDisable() { },
// 选择器可选控制:已在购物车中的机器不可再选
isSelectable(row, index) {
@@ -387,7 +525,7 @@ export default {
variant.quantity = 1
} catch (error) {
console.error('添加到购物车失败:', error)
}
},
// 统一加入购物车
@@ -411,7 +549,7 @@ export default {
this.selectedMap = {}
} catch (e) {
console.error('统一加入购物车失败', e)
}
},
// 打开确认弹窗:以当前界面勾选(_selected)为准,并在打开后清空左侧勾选状态
@@ -474,14 +612,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 = {}
@@ -503,9 +641,12 @@ export default {
// 取消所有商品勾选(内层表格的自定义 checkbox
clearAllSelections() {
try {
// 清空选中映射
// 清空选中映射(遗留字段)
this.selectedMap = {}
// 遍历所有系列与机器,复位 _selected
if (Array.isArray(this.machineList) && this.machineList.length) {
this.machineList.forEach(m => { if (m) this.$set(m, '_selected', false) })
return
}
const groups = Array.isArray(this.productListData) ? this.productListData : []
groups.forEach(g => {
const list = Array.isArray(g.productMachines) ? g.productMachines : []
@@ -587,6 +728,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

@@ -16,12 +16,13 @@
class="pay-item"
:aria-label="`支付方式 ${item.payChain}`"
>
<el-tooltip :content="formatPayTooltip(item)" placement="top" :open-delay="80">
<img
class="pay-icon"
:src="item.payCoinImage"
:alt="`${item.payChain} 支付`"
:title="item.payChain"
:src="getPayImageUrl(item)"
:alt="`${(item.payChain || '').toUpperCase()} ${(item.payCoin || '').toUpperCase()}`.trim()"
:title="formatPayTooltip(item)"
tabindex="0"
role="img"
@keydown.enter.prevent="handlePayIconKeyDown(item)"
@@ -31,99 +32,251 @@
</li>
</ul>
</section>
<!-- 筛选栏 -->
<section class="filter-bar" aria-label="筛选条件">
<div class="filter-grid">
<!-- 支付方式筛选选择即触发查询 -->
<div class="filter-cell">
<label class="filter-title" for="payFilter">支付方式筛选</label>
<el-select
id="payFilter"
v-model="selectedPayKey"
placeholder="全部"
clearable
filterable
size="small"
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"
:label="formatPayTooltip(opt)"
:value="`${opt.payChain || ''}|${opt.payCoin || ''}`"
>
<div class="pay-opt">
<img :src="getPayImageUrl(opt)" class="pay-icon" alt="" />
<span>{{ (opt.payChain || '').toUpperCase() }} - {{ (opt.payCoin || '').toUpperCase() }}</span>
</div>
</el-option>
</el-select>
</div>
<!-- 价格区间 -->
<div class="filter-cell center-title">
<label class="filter-title">单价区间<span v-if="getPriceCoinSymbol()">{{ getPriceCoinSymbol() }}</span></label>
<div class="range-controls">
<el-input-number v-model="filters.minPrice" :min="0" :step="1" :precision="0" :controls="false" size="small" class="filter-control" />
<span class="filter-sep">-</span>
<el-input-number v-model="filters.maxPrice" :min="0" :step="1" :precision="0" :controls="false" size="small" class="filter-control" />
</div>
</div>
<!-- 实际算力区间 -->
<div class="filter-cell center-title">
<label class="filter-title">实际算力</label>
<div class="range-controls">
<el-input-number v-model="filters.minPower" :min="0" :step="0.1" :precision="2" :controls="false" size="small" class="filter-control" />
<span class="filter-sep">-</span>
<el-input-number v-model="filters.maxPower" :min="0" :step="0.1" :precision="2" :controls="false" size="small" class="filter-control" />
<el-select v-model="filters.unit" placeholder="单位" size="small" class="filter-control" style="max-width: 140px;">
<el-option v-for="u in powerUnitOptions" :key="u" :label="u" :value="u" />
</el-select>
</div>
</div>
<!-- 功耗区间第二行左侧占两列 -->
<div class="filter-cell filter-cell--span-2 center-title">
<label class="filter-title">功耗(kw/h)</label>
<div class="range-controls">
<el-input-number v-model="filters.minPowerDissipation" :min="0" :step="0.1" :precision="2" :controls="false" size="small" class="filter-control" />
<span class="filter-sep">-</span>
<el-input-number v-model="filters.maxPowerDissipation" :min="0" :step="0.1" :precision="2" :controls="false" size="small" class="filter-control" />
<div class="filter-actions-inline">
<el-button type="primary" size="small" @click="handleSearchFilters" aria-label="执行筛选">筛选查询</el-button>
<el-button size="small" @click="handleResetFilters" aria-label="重置筛选">重置</el-button>
</div>
</div>
</div>
<!-- 操作区已合并到功耗区间后面 -->
</div>
</section>
<section class="productList">
<!-- 产品列表可展开 -->
<!-- 单层产品列表 -->
<el-table
ref="seriesTable"
ref="machineTable"
class="series-table"
:data="productListData"
:data="machineList"
row-key="id"
:expand-row-keys="expandedRowKeys"
@expand-change="handleExpandChange"
@row-click="handleSeriesRowClick"
:row-class-name="handleGetSeriesRowClassName"
:row-class-name="handleGetRowClass"
:header-cell-style="{ textAlign: 'left' }"
:cell-style="{ textAlign: 'left' }"
style="width: 100%"
>
<el-table-column type="expand" width="46">
<template #default="outer">
<!-- 子表格展开后显示该行的多个可选条目来自 productMachines -->
<el-table :data="outer.row.productMachines" size="small" style="width: 100%" :show-header="true" :ref="'innerTable-' + outer.row.id" :row-key="'id'" :reserve-selection="false" :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }" :row-class-name="handleGetInnerRowClass">
<el-table-column width="46">
<template #default="scope">
<el-checkbox
v-model="scope.row._selected"
:disabled="scope.row.saleState === 1 || scope.row.saleState === 2"
:title="(scope.row.saleState === 1 || scope.row.saleState === 2) ? '该机器已售出或售出中,无法选择' : ''"
@change="checked => handleManualSelect(outer.row, scope.row, checked)"
/>
</template>
</el-table-column>
<!-- 列宽精简避免横向滚动 -->
<el-table-column prop="theoryPower" label="理论算力" min-width="160" 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>
<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="theoryIncome" min-width="160" header-align="left" align="left" show-overflow-tooltip>
<template #header>单机理论收入(每日) <span v-show="outer.row.productMachines[0].coin">{{outer.row.productMachines[0].coin.toUpperCase() }}</span></template>
</el-table-column>
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)" min-width="170" 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">
<template #default="scope">{{ getRowMaxLeaseDays(scope.row) }}</template>
</el-table-column>
<el-table-column label="租赁天数(天)" min-width="150" header-align="left" align="left">
<template #default="scope">
<el-input-number
v-model="scope.row.leaseTime"
:min="1"
:max="getRowMaxLeaseDays(scope.row)"
:step="1"
:precision="0"
size="mini"
:disabled="scope.row.saleState === 1 || scope.row.saleState === 2"
controls-position="right"
@change="val => handleLeaseDaysChange(scope.row, val)"
/>
</template>
</el-table-column>
<el-table-column prop="saleState" label="售出状态" header-align="left" align="left" min-width="110">
<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 ? '已售出' : '售出中') }}
</el-tag>
</template>
</el-table-column>
</el-table>
<el-table-column width="46">
<template #default="scope">
<el-checkbox
v-model="scope.row._selected"
:disabled="scope.row.saleState === 1 || scope.row.saleState === 2"
:title="(scope.row.saleState === 1 || scope.row.saleState === 2) ? '该机器已售出或售出中,无法选择' : ''"
@change="checked => handleManualSelectFlat(scope.row, checked)"
/>
</template>
</el-table-column>
<!-- 外层列宽同样收紧避免横向滚动 -->
<el-table-column label="价格 (USDT)" header-align="left" align="left" min-width="120">
<template slot-scope="scope"><span class="price-strong">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.price }}</span></template>
<el-table-column prop="theoryPower" label="理论算力" header-align="left" align="left" show-overflow-tooltip>
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.theoryPower).truncated"
:content="formatNum6(scope.row.theoryPower).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.theoryPower).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.theoryPower).text }}</span>
</span>
{{ scope.row.unit }}
</template>
</el-table-column>
<el-table-column label="理论算力范围" min-width="220" header-align="left" align="left" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.theoryPowerRange }}</template>
<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">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.computingPower).truncated"
:content="formatNum6(scope.row.computingPower).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.computingPower).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.computingPower).text }}</span>
</span>
{{ scope.row.unit }}
</template>
</el-table-column>
<el-table-column label="实际算力范围" min-width="200" header-align="left" align="left" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.computingPowerRange }}</template>
<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 label="功耗范围" min-width="160" header-align="left" align="left">
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.powerRange }}</template>
<el-table-column prop="algorithm" label="算法" header-align="left" align="left" />
<el-table-column prop="theoryIncome" header-align="left" align="left" show-overflow-tooltip>
<template #header>
单机理论收入(每日)
<span v-if="getFirstCoinSymbol()">{{ getFirstCoinSymbol() }}</span>
</template>
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.theoryIncome).truncated"
:content="formatNum6(scope.row.theoryIncome).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.theoryIncome).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.theoryIncome).text }}</span>
</span>
</template>
</el-table-column>
<el-table-column label="数量" min-width="100" header-align="left" align="left">
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.number }}</template>
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)" header-align="left" align="left">
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.theoryUsdtIncome).truncated"
:content="formatNum6(scope.row.theoryUsdtIncome).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.theoryUsdtIncome).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.theoryUsdtIncome).text }}</span>
</span>
</template>
</el-table-column>
<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="租赁天数(天)" header-align="left" align="left">
<template #default="scope">
<el-input-number
v-model="scope.row.leaseTime"
:min="1"
:max="getRowMaxLeaseDays(scope.row)"
:step="1"
:precision="0"
size="mini"
:disabled="scope.row.saleState === 1 || scope.row.saleState === 2"
controls-position="right"
@change="val => handleLeaseDaysChange(scope.row, val)"
/>
</template>
</el-table-column >
<el-table-column prop="price" header-align="left" align="center" >
<template #header>
<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>
</el-table-column>
<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 ? '已售出' : '售出中') }}
</el-tag>
</template>
</el-table-column>
</el-table>
</section>
@@ -136,18 +289,81 @@
<div>
<el-table :data="confirmAddDialog.items" height="360" border stripe :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table-column prop="theoryPower" label="理论算力" header-align="left" align="left">
<template #default="scope">{{ scope.row.theoryPower }} {{ scope.row.unit }}</template>
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.theoryPower).truncated"
:content="formatNum6(scope.row.theoryPower).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.theoryPower).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.theoryPower).text }}</span>
</span>
{{ scope.row.unit }}
</template>
</el-table-column>
<el-table-column label="实际算力" header-align="left" align="left">
<template #default="scope">{{ scope.row.computingPower }} {{ scope.row.unit }}</template>
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.computingPower).truncated"
:content="formatNum6(scope.row.computingPower).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.computingPower).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.computingPower).text }}</span>
</span>
{{ scope.row.unit }}
</template>
</el-table-column>
<el-table-column prop="algorithm" label="算法" width="120" header-align="left" align="left" />
<el-table-column prop="powerDissipation" label="功耗(kw/h)" header-align="left" align="left" />
<el-table-column prop="powerDissipation" label="功耗(kw/h)" header-align="left" align="left">
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.powerDissipation).truncated"
:content="formatNum6(scope.row.powerDissipation).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.powerDissipation).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.powerDissipation).text }}</span>
</span>
</template>
</el-table-column>
<el-table-column label="租赁天数(天)" header-align="left" align="left">
<template #default="scope">{{ Number(scope.row.leaseTime || 1) }}</template>
</el-table-column>
<el-table-column prop="price" label="单价(USDT)" header-align="left" align="left">
<template #default="scope"><span class="price-strong">{{ scope.row.price }}</span></template>
<el-table-column prop="price" header-align="left" align="left">
<template #header>
单价 <span v-if="getPriceCoinSymbol()">{{ getPriceCoinSymbol() }}</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>
@@ -157,7 +373,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">
@@ -187,6 +418,105 @@ export default {
if (n > 365) return 365
return Math.floor(n)
},
/**
* 处理支付方式图片 URL去除服务端可能带入的换行/空白)
* @param {Object} item
*/
getPayImageUrl(item) {
try {
const src = (item && item.payCoinImage) ? String(item.payCoinImage) : ''
return src.trim()
} catch (e) {
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
*/
handlePayFilterChange(val) {
try {
const s = typeof val === 'string' ? val : ''
if (!s) {
this.filters.chain = ''
this.filters.coin = ''
} else {
const [chain, coin] = s.split('|')
this.filters.chain = (chain || '').trim()
this.filters.coin = (coin || '').trim()
}
this.handleSearchFilters()
} catch (e) { /* noop */ }
},
/**
* 组合筛选参数并请求数据
*/
handleSearchFilters() {
const params = this.buildQueryParams()
this.fetchGetMachineInfo(params)
},
/**
* 重置筛选
*/
handleResetFilters() {
this.selectedPayKey = null
this.filters = {
chain: '',
coin: '',
minPrice: null,
maxPrice: null,
minPower: null,
maxPower: null,
minPowerDissipation: null,
maxPowerDissipation: null,
unit: 'GH/S'
}
this.handleSearchFilters()
},
/**
* 获取列表第一个条目的币种,安全返回大写字符串
* 用于表头显示币种,避免空数组时报错
*/
getFirstCoinSymbol() {
try {
const list = Array.isArray(this.machineList) ? this.machineList : []
const coin = list.length && list[0] && list[0].coin ? String(list[0].coin) : ''
return coin ? coin.toUpperCase() : ''
} catch (e) {
return ''
}
},
/**
* 获取价格单位(优先读取每行的 payCoin 字段)
*/
getPriceCoinSymbol() {
try {
const list = Array.isArray(this.machineList) ? this.machineList : []
// 寻找第一个存在 payCoin 的条目
const item = list.find(it => it && it.payCoin)
const unit = item && item.payCoin ? String(item.payCoin) : ''
return unit ? unit.toUpperCase() : ''
} catch (e) {
return ''
}
},
/**
* 限制并校验租赁天数:区间 [1, max],并取整
*/
@@ -232,6 +562,52 @@ export default {
// eslint-disable-next-line no-console
console.error('handlePayIconKeyDown error:', err);
}
},
/**
* 单层:切换勾选
* @param {Object} row - 当前机器行
* @param {boolean} checked - 勾选状态
*/
handleManualSelectFlat(row, checked) {
try {
if (!row) return
if (row.saleState === 1 || row.saleState === 2) {
this.$message.warning('该机器已售出或售出中,无法选择')
this.$set(row, '_selected', false)
return
}
this.$set(row, '_selected', !!checked)
} catch (e) {
// eslint-disable-next-line no-console
console.error('handleManualSelectFlat error:', e)
}
},
/**
* 单层:行样式(售出态高亮)
*/
handleGetRowClass({ row }) {
if (!row) return ''
return (row.saleState === 1 || row.saleState === 2) ? 'sold-row' : ''
},
/**
* 覆盖 mixin 的多层版本:基于单层勾选打开确认弹窗
*/
handleOpenAddToCartDialog() {
const list = Array.isArray(this.machineList) ? this.machineList : []
const pickedAll = list.filter(it => !!it && !!it._selected)
const picked = pickedAll.filter(m => m && (m.saleState === 0 || m.saleState === undefined || m.saleState === null))
if (!picked.length) {
this.$message.warning('请先勾选至少一台矿机')
return
}
if (picked.length < pickedAll.length) {
this.$message.warning('部分机器已售出或售出中,已自动为您排除')
}
this.confirmAddDialog.items = picked.slice()
this.confirmAddDialog.visible = true
this.$nextTick(() => {
try { (this.machineList || []).forEach(m => this.$set(m, '_selected', false)) } catch (e) { /* noop */ }
})
}
}
}
@@ -358,6 +734,11 @@ export default {
color: #e74c3c;
}
.num-strong {
font-weight: inherit;
color: inherit;
}
/* 支付方式区域(视觉更友好 + 可达性) */
.pay-methods {
display: flex;
@@ -400,6 +781,9 @@ export default {
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.pay-item-inner { display: inline-flex; align-items: center; gap: 8px; }
.pay-text { font-size: 12px; color: #2c3e50; }
.pay-icon:hover {
transform: translateY(-1px);
}
@@ -409,6 +793,76 @@ export default {
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.2);
}
/* 筛选栏样式 */
.filter-bar {
background: #ffffff;
border: 1px solid #eef2f7;
border-radius: 8px;
padding: 12px 16px;
margin: 0 10px 16px 10px;
}
.filter-grid {
display: grid;
grid-template-columns: repeat(3, minmax(260px, 1fr));
gap: 14px 18px;
align-items: end;
}
.filter-cell {
display: flex;
flex-direction: column;
align-items: start;
gap: 6px;
}
.filter-cell.center-title .filter-title { text-align: center; }
.filter-title {
font-size: 14px;
color: #34495E;
font-weight: 600;
margin-bottom: 8px;
}
.filter-control {
width: 100%;
max-width: 320px;
}
.range-controls {
display: flex;
align-items: center;
gap: 8px;
}
.range-controls :deep(.el-input-number) { width: 150px; }
.pay-opt { display: inline-flex; align-items: center; gap: 8px; }
.filter-sep {
color: #9aa4b2;
}
.filter-actions {
display: flex;
align-items: center;
gap: 10px;
grid-column: 2 / 3; /* 放到中间这一格 */
}
.filter-actions-inline {
display: inline-flex;
align-items: center;
gap: 10px;
margin-left: 12px;
}
@media (max-width: 1200px) {
.filter-grid { grid-template-columns: repeat(2, minmax(220px, 1fr)); }
.filter-cell--span-2 { grid-column: 1 / span 1; }
.filter-actions { grid-column: 1 / -1; justify-content: flex-end; }
}
@media (max-width: 768px) {
.filter-grid {
grid-template-columns: 1fr;
}
.filter-actions { grid-column: 1 / 2; justify-content: flex-end; }
}
/* 外层系列行:整行可点击 + 视觉增强 */
:deep(.series-clickable-row) {
cursor: pointer;
@@ -551,4 +1005,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

@@ -60,11 +60,22 @@
<h4>商品: {{ product.name }}</h4>
<p style="font-size: 16px;margin-top: 10px;font-weight: bold;">算法: {{ product.algorithm }}</p>
<div class="product-footer">
<div class="price-wrap">
<span class="product-price">价格: {{ formatPriceRange(product.priceRange) }}</span>
<span class="unit">USDT</span>
<div class="paytypes">
<span class="paytypes-label">支付方式</span>
<el-tooltip
v-for="(pt, idx) in (product.payTypes || [])"
:key="idx"
:content="formatPayType(pt)"
placement="top"
:open-delay="80"
>
<img :src="pt.image" :alt="formatPayType(pt)" class="paytype-icon" />
</el-tooltip>
</div>
<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>
<span class="product-sold" aria-label="已售数量">已售{{ product && product.saleNumber != null ? product.saleNumber : 0 }}</span>
</div>
</div>
</div>
@@ -89,6 +100,19 @@ export default {
mounted() {},
methods: {
/**
* 将 payType 显示为 CHAIN-COIN 文本
*/
formatPayType(item) {
try {
const chain = (item && item.chain ? String(item.chain) : '').toUpperCase()
const coin = (item && item.coin ? String(item.coin) : '').toUpperCase()
if (chain && coin) return `${chain}-${coin}`
return chain || coin || ''
} catch (e) {
return ''
}
},
/**
* 处理商品点击 - 跳转到详情页
*/
@@ -184,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;
@@ -195,6 +229,9 @@ export default {
.price-wrap { display: inline-flex; align-items: baseline; gap: 6px; }
.unit { color: #999; font-size: 12px; }
.product-sold { color: #64748b; font-size: 12px; }
.paytypes { display: inline-flex; align-items: center; gap: 8px; }
.paytype-icon { width: 22px; height: 22px; border-radius: 4px; display: inline-block; }
.paytypes-label { color: #64748b; font-size: 12px; }
.add-cart-btn {
background: #42b983;
color: #fff;

Binary file not shown.

File diff suppressed because one or more lines are too long

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.d49ccc2c.js"></script><link href="/css/chunk-vendors.10dd4e95.css" rel="stylesheet"><link href="/css/app.ca4b7f36.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long