7 Commits

Author SHA1 Message Date
cbefb964d4 每周更新 2025-12-05 16:24:20 +08:00
485226d9dc 最新需求修改中 2025-11-28 15:30:36 +08:00
868632400a 新版本更改中 2025-11-21 16:23:46 +08:00
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
38 changed files with 6514 additions and 1335 deletions

View File

@@ -7,9 +7,9 @@ NODE_ENV = production
ENV = 'staging'
# 测试环境
VUE_APP_BASE_API = 'http://10.168.2.220:8888'
# VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
VUE_APP_BASE_URL = 'https://test.m2pool.com/'
# VUE_APP_BASE_API = 'http://10.168.2.220:8888'
VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
VUE_APP_BASE_URL = 'https://test.m2pool.com/'
# 路由懒加载

View File

@@ -12,51 +12,73 @@ export function addSingleOrBatchMachine(data) {
//根据矿机id 删除商品矿机
export function deleteMachine(data) {
return request({
url: `/lease/product/machine/delete`,
method: 'post',
data
})
}
return request({
url: `/lease/product/machine/delete`,
method: 'post',
data
})
}
//根据挖矿账户获取矿机列表
//根据挖矿账户获取矿机列表
export function getUserMachineList(data) {
return request({
url: `/lease/product/machine/getUserMachineList`,
method: 'post',
data
})
}
return request({
url: `/lease/product/machine/getUserMachineList`,
method: 'post',
data
})
}
//根据 登录账户 获取挖矿账户及挖矿币种集合
//根据 登录账户 获取挖矿账户及挖矿币种集合
export function getUserMinersList(data) {
return request({
url: `/lease/product/machine/getUserMinersList`,
method: 'post',
data
})
}
return request({
url: `/lease/product/machine/getUserMinersList`,
method: 'post',
data
})
}
//编辑矿机 + 矿机上下架
//编辑矿机 + 矿机上下架
export function updateMachine(data) {
return request({
url: `/lease/product/machine/updateMachine`,
method: 'post',
data
})
}
return request({
url: `/lease/product/machine/updateMachine`,
method: 'post',
data
})
}
//获取矿机列表
//获取矿机列表
export function getMachineListForUpdate(data) {
return request({
url: `/lease/product/machine/getMachineListForUpdate`,
method: 'post',
data
})
}
return request({
url: `/lease/product/machine/getMachineListForUpdate`,
method: 'post',
data
})
}
//GPU下载客户端
export function downloadClient() {
return request({
url: `/lease/user/downloadClient`,
method: 'get',
responseType: 'blob' // 关键:必须设置为 blob 才能正确下载二进制文件
})
}
//卖家页面---新增ASIC矿机
export function addAsicMachine(data) {
return request({
url: `/lease/v2/product/machine/addAsicMachine`,
method: 'post',
data
})
}

View File

@@ -12,44 +12,44 @@ export function addOrders(data) {
//取消订单
export function cancelOrder(data) {
return request({
url: `/lease/order/info/cancelOrder`,
method: 'post',
data
})
}
return request({
url: `/lease/order/info/cancelOrder`,
method: 'post',
data
})
}
//根据订单id查询订单信息
//根据订单id查询订单信息
export function getOrdersByIds(data) {
return request({
url: `/lease/order/info/getOrdersByIds`,
method: 'post',
data
})
}
return request({
url: `/lease/order/info/getOrdersByIds`,
method: 'post',
data
})
}
//查询订单列表(买家侧)
//查询订单列表(买家侧)
export function getOrdersByStatus(data) {
return request({
url: `/lease/order/info/getOrdersByStatus`,
method: 'post',
data
})
}
return request({
url: `/lease/order/info/getOrdersByStatus`,
method: 'post',
data
})
}
//查询订单列表(卖家侧)
//查询订单列表(卖家侧)
export function getOrdersByStatusForSeller(data) {
return request({
url: `/lease/order/info/getOrdersByStatusForSeller`,
method: 'post',
data
})
}
return request({
url: `/lease/order/info/getOrdersByStatusForSeller`,
method: 'post',
data
})
}
//结算前链和币种查询
//结算前链和币种查询
export function getChainAndListForSeller(data) {
return request({
url: `/lease/shop/getChainAndListForSeller`,
@@ -58,14 +58,43 @@ export function getChainAndListForSeller(data) {
})
}
//获取实时币价
export function getCoinPrice(data) {
return request({
url: `/lease/order/info/getCoinPrice`,
method: 'post',
data
})
}
//获取实时币价
export function getCoinPrice(data) {
return request({
url: `/lease/order/info/getCoinPrice`,
method: 'post',
data
})
}
//获取支持的算法币种
export function getMachineSupportCoinAndAlgorithm(data) {
return request({
url: `/lease/v2/order/info/getMachineSupportCoinAndAlgorithm`,
method: 'post',
data
})
}
//获取支持的矿池 和模型
export function getMachineSupportPool(data) {
return request({
url: `/lease/v2/order/info/getMachineSupportPool`,
method: 'post',
data
})
}
//创建订单
export function addOrdersV2(data) {
return request({
url: `/lease/v2/order/info/addOrdersV2`,
method: 'post',
data
})
}

View File

@@ -87,3 +87,93 @@ export function getMachineInfoById(data) {
}
// 查获取商城商品支持的支付方式
export function getPayTypes(data) {
return request({
url: `/lease/product/getPayTypes`,
method: 'post',
data
})
}
// 卖家页面---矿机列表
export function getShopMachineListForSeller(data) {
return request({
url: `/lease/v2/product/machine/getShopMachineListForSeller`,
method: 'post',
data
})
}
// 更新设置GPU商品列表的信息
export function updateGpuMachine(data) {
return request({
url: `/lease/v2/product/machine/updateGpuMachine`,
method: 'post',
data
})
}
// 修改商品列表ASIC商品信息
export function updateAsicMachine(data) {
return request({
url: `/lease/v2/product/machine/updateAsicMachine`,
method: 'post',
data
})
}
// 删除ASIC 或者GPU
export function deleteMachine(data) {
return request({
url: `/lease/v2/product/machine/deleteMachine`,
method: 'post',
data
})
}
// 获取商场页面的店铺列表
export function getShopList(data) {
return request({
url: `/lease/v2/product/machine/getShopList`,
method: 'post',
data
})
}
// 获取店铺详情
export function getShopMachineList(data) {
return request({
url: `/lease/v2/product/machine/getShopMachineList`,
method: 'post',
data
})
}
// 获取店铺详情
export function addGoodsV2(data) {
return request({
url: `/lease/v2/shopping/cart/addGoodsV2`,
method: 'post',
data
})
}

View File

@@ -37,6 +37,39 @@ export function deleteBatchGoodsForIsDelete(data) {
})
}
//购物车列表V2
export function getGoodsListV2(data) {
return request({
url: `/lease/v2/shopping/cart/getGoodsListV2`,
method: 'post',
data
})
}
//批量删除购物车中已下架商品
export function deleteBatchGoodsForIsDeleteV2(data) {
return request({
url: `/lease/v2/shopping/cart/deleteBatchGoodsForIsDeleteV2`,
method: 'post',
data
})
}
//批批量删除购物车中商品
export function deleteBatchGoodsV2(data) {
return request({
url: `/lease/v2/shopping/cart/deleteBatchGoodsV2`,
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`,
@@ -102,6 +102,16 @@ export function getChainAndCoin(data) {
}
// 卖家绑定钱包明细
export function getShopConfigV2(data) {
return request({
url: `/lease/v2/shop/getShopConfigV2`,
method: 'post',
data
})
}

View File

@@ -106,6 +106,61 @@ 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
})
}
// 卖家绑定钱包明细
export function getShopConfigV2(data) {
return request({
url: `/lease/v2/shop/getShopConfigV2`,
method: 'post',
data
})
}
// 卖家提现
export function withdrawBalanceForSeller(data) {
return request({
url: `/lease/v2/shop/withdrawBalanceForSeller`,
method: 'post',
data
})
}
// 修改钱包配置
export function balanceWithdrawListV2(data) {
return request({
url: `/lease/v2/shop/balanceWithdrawList`,
method: 'post',
data
})
}

View File

@@ -109,6 +109,16 @@ export const accountRoutes = [
allAuthority: ['all']
}
},
{
path: 'withdraw-record',
name: 'accountWithdrawRecord',
component: () => import('../views/account/withdrawRecord.vue'),
meta: {
title: '提现记录',
description: '卖家提现流水记录',
allAuthority: ['all']
}
},
{
path: 'shop-new',
name: 'accountShopNew',

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

@@ -256,6 +256,19 @@ service.interceptors.response.use(res => {
// 请求完成后移除
const requestKey = getRequestKey(res.config);
pendingRequestMap.delete(requestKey);
// 特殊处理:如果是 blob 类型响应(文件下载),直接返回原始响应对象
// 因为 blob 数据不是 JSON不能解析 res.data.code
if (res.config.responseType === 'blob' || res.data instanceof Blob) {
// 检查响应状态码
if (res.status >= 200 && res.status < 300) {
return res // 返回完整响应对象,包含 headers 等信息
} else {
// blob 响应但状态码异常,尝试读取错误信息
return Promise.reject(new Error(`下载失败,状态码: ${res.status}`))
}
}
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息

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

@@ -76,6 +76,7 @@ export default {
{ label: '商品列表', to: '/account/products' },
{ label: '已售出订单', to: '/account/seller-orders' },
{ label: '收款记录', to: '/account/receipt-record' },
{ label: '提现记录', to: '/account/withdraw-record' },
],
}
@@ -170,6 +171,7 @@ export default {
'/account/product-machine-add',
'/account/seller-orders',
'/account/receipt-record',
'/account/withdraw-record',
'/account/shop-config'
]
const shouldBuyer = buyerPrefixes.some(p => path.indexOf(p) === 0)

View File

@@ -22,7 +22,7 @@
必须添加出售机器否则买家无法下单买家点击某个商品后会看到该商品下的机器明细并进行选购
</li>
</ol>
<div class="guide-note">提示建议先创建店铺 完成钱包绑定 创建商品 添加出售机器的顺序避免漏配导致无法收款或无法下单</div>
<div class="guide-note">提示建议先创建店铺 完成钱包绑定 创建商品的顺序避免漏配导致无法收款或无法下单</div>
</div>
</el-card>
@@ -39,10 +39,9 @@
</el-tag>
</div>
<div class="desc">{{ shop.description || '这家店还没有描述~' }}</div>
<!-- <div class="meta">
<span>店铺ID{{ shop.id || '-' }}</span>
<span>可删除{{ shop.del ? '是' : '否' }}</span>
</div> -->
<div class="meta">
<span>手续费率{{ formatFeeRate(shop.feeRate) }}</span>
</div>
<div class="actions">
<el-button size="small" type="primary" @click="handleOpenEdit">修改店铺</el-button>
<el-button size="small" type="warning" @click="handleToggleShop">
@@ -62,8 +61,8 @@
<span>已绑定钱包</span>
</div>
<el-table :data="shopConfigs" border style="width: 100%">
<el-table-column prop="chain" label="链" width="140" />
<el-table-column label="支付币种" >
<el-table-column prop="chain" label="链" width="120" />
<el-table-column label="支付币种" width="120" >
<template slot-scope="scope">
<div class="coin-list">
<template v-if="Array.isArray(scope.row.children) && scope.row.children.length">
@@ -90,10 +89,18 @@
<!-- <el-table-column prop="payType" label="币种类型" width="120">
<template slot-scope="scope">{{ scope.row.payType === 1 ? '稳定币' : '虚拟币' }}</template>
</el-table-column> -->
<el-table-column prop="payAddress" label="收款钱包地址" show-overflow-tooltip />
<el-table-column label="操作" width="180" fixed="right">
<el-table-column label="余额" >
<template slot-scope="scope">
<span class="balance-num">{{ formatAmount(scope.row) }}</span>
<span class="balance-unit"> {{ formatCoin(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<template slot-scope="scope">
<el-button type="text" style="color:#409EFF" @click="handleWithdraw(scope.row)">提现</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" @click="handleEditConfig(scope.row)">修改</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" style="color:#e74c3c" @click="handleDeleteConfig(scope.row)">删除</el-button>
@@ -109,6 +116,85 @@
</div>
<el-empty v-else description="正在加载店铺信息..." />
<!-- 提现对话框仅使用本页行数据 -->
<el-dialog
:title="withdrawDialogTitle"
:visible.sync="withdrawDialogVisible"
width="720px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form :model="withdrawForm" :rules="withdrawRules" ref="withdrawForm" label-width="120px">
<!-- 提现链 -->
<el-form-item label="提现链">
<el-input :value="String((currentWithdrawRow.chain || '')).toUpperCase()" :disabled="true" />
</el-form-item>
<!-- 提现币种 -->
<el-form-item label="提现币种">
<el-input :value="displayWithdrawSymbol" :disabled="true" />
</el-form-item>
<!-- 提现金额 -->
<el-form-item label="提现金额" prop="amount">
<el-input
v-model="withdrawForm.amount"
placeholder="请输入提现金额"
inputmode="decimal"
@input="handleAmountInput"
>
<template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input>
<div class="balance-info">
可用余额 {{ availableWithdrawBalance }} {{ displayWithdrawSymbol }}
</div>
</el-form-item>
<!-- 手续费 -->
<el-form-item label="手续费">
<el-input v-model="withdrawForm.fee" :disabled="true">
<template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input>
<div class="fee-info">网络手续费 {{ withdrawForm.fee || '0' }} {{ displayWithdrawSymbol }}</div>
</el-form-item>
<!-- 实际到账 -->
<el-form-item label="实际到账">
<el-input :value="actualAmount" :disabled="true">
<template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input>
<div class="actual-amount-info">实际到账 {{ actualAmount }} {{ displayWithdrawSymbol }}</div>
</el-form-item>
<!-- 收款地址 -->
<el-form-item label="收款地址" prop="toAddress">
<el-input
v-model="withdrawForm.toAddress"
placeholder="请输入收款钱包地址"
:disabled="!withdrawAddressEditable"
ref="withdrawToAddressInput"
>
<template slot="append">
<el-button type="text" @click="handleEditAddressClick">修改</el-button>
</template>
</el-input>
<div class="address-tip">请确认地址正确错误地址将导致资产丢失</div>
</el-form-item>
<!-- 谷歌验证码 -->
<el-form-item label="谷歌验证码" prop="googleCode">
<el-input
v-model="withdrawForm.googleCode"
placeholder="请输入6位谷歌验证码"
maxlength="6"
@input="handleGoogleCodeInput"
>
<template slot="prepend">
<i class="el-icon-key"></i>
</template>
</el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="withdrawDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="withdrawLoading" @click="confirmWithdraw">确认提现</el-button>
</div>
</el-dialog>
<!-- 修改店铺弹窗 -->
<el-dialog title="修改店铺" :visible.sync="visibleEdit" width="520px">
@@ -124,6 +210,14 @@
<label class="label">店铺描述</label>
<el-input type="textarea" :rows="3" v-model="editForm.description" placeholder="请输入描述" :maxlength="300" show-word-limit />
</div>
<div class="row">
<label class="label">手续费比例</label>
<el-input
v-model="editForm.feeRate"
placeholder="比例区间 0.01 - 0.1 之间最多6位小数"
@input="handleEditFeeRateInput"
/>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="visibleEdit=false">取消</el-button>
@@ -132,43 +226,7 @@
</el-dialog>
<!-- 修改钱包绑定配置弹窗参数保持与列表一致 -->
<el-dialog title="修改配置" :visible.sync="visibleConfigEdit" width="560px">
<div class="row">
<label class="label">支付链</label>
<el-input v-model="configForm.chainLabel" placeholder="-" disabled />
</div>
<div class="row">
<label class="label">支付币种</label>
<el-select
class="input"
size="middle"
v-model="configForm.payCoins"
multiple
collapse-tags
filterable
placeholder="请选择币种"
>
<el-option
v-for="item in editCoinOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="row">
<label class="label">已选择币种</label>
<div class="selected-coin-list">
<el-tag
v-for="c in selectedCoinLabels"
:key="c"
type="warning"
effect="light"
closable
@close="removeSelectedCoin(c)"
>{{ c }}</el-tag>
<span v-if="!selectedCoinLabels.length" style="color:#c0c4cc">未选择</span>
</div>
</div>
<div class="row">
<label class="label">钱包地址</label>
<el-input v-model="configForm.payAddress" placeholder="请输入钱包地址" />
@@ -186,7 +244,7 @@
import { getMyShop, updateShop, deleteShop, queryShop, closeShop ,updateShopConfig,deleteShopConfig,getChainAndCoin} from '@/api/shops'
import { coinList } from '@/utils/coinList'
import { getShopConfig } from '@/api/wallet'
import { getShopConfig,getShopConfigV2 ,withdrawBalanceForSeller,updateShopConfigV2} from '@/api/wallet'
export default {
@@ -200,11 +258,12 @@ export default {
name: '',
image: '',
description: '',
feeRate: '',
del: true,
state: 0
},
visibleEdit: false,
editForm: { id: '', name: '', image: '', description: '' },
editForm: { id: '', name: '', image: '', description: '', feeRate: '' },
// 店铺配置列表
shopConfigs: [],
visibleConfigEdit: false,
@@ -219,7 +278,19 @@ export default {
{ label: 'BSC (BEP20)', value: 'bsc' },
{ label: 'Nexa', value: 'nexa' },
],
shopLoading: false
shopLoading: false,
/* 提现弹窗状态(仅本页使用) */
withdrawDialogVisible: false,
withdrawLoading: false,
currentWithdrawRow: {},
withdrawForm: {
amount: '',
toAddress: '',
fee: '0.00',
googleCode: ''
},
withdrawAddressEditable: false,
withdrawRules: {}
}
},
computed: {
@@ -253,12 +324,231 @@ export default {
selectedCoinLabels() {
const map = new Map((this.editCoinOptions || []).map(o => [String(o.value), String(o.label).toUpperCase()]))
return (this.configForm.payCoins || []).map(v => map.get(String(v)) || String(v).toUpperCase())
},
/* 提现弹窗标题:如 USDT提现 */
withdrawDialogTitle() {
const sym = String((this.currentWithdrawRow && this.currentWithdrawRow.payCoin) || '').toUpperCase() || 'USDT'
return `${sym}提现`
},
/* 提现币种(大写) */
displayWithdrawSymbol() {
return String((this.currentWithdrawRow && this.currentWithdrawRow.payCoin) || '').toUpperCase()
},
/* 可用余额最多6位小数显示 */
availableWithdrawBalance() {
const n = Number((this.currentWithdrawRow && this.currentWithdrawRow.balance) || 0)
return this.formatDec6(n)
},
/* 实际到账金额 = 可用余额 - 手续费(只显示可用余额,不展示总余额/冻结) */
actualAmount() {
const amountInt = this.toScaledInt(this.withdrawForm.amount)
const feeInt = this.toScaledInt(this.withdrawForm.fee)
if (!Number.isFinite(amountInt) || !Number.isFinite(feeInt)) return '0'
const res = amountInt - feeInt
return res > 0 ? this.formatDec6FromInt(res) : '0'
}
},
created() {
this.fetchMyShop()
},
methods: {
/** 余额展示:带币种单位 */
formatBalance(row) {
try {
const num = Number(row && row.balance)
const valid = Number.isFinite(num)
const coin = String(row && row.payCoin ? row.payCoin : '').toUpperCase()
if (!valid) return '-'
const text = String(num)
return coin ? `${text} ${coin}` : text
} catch (e) {
return '-'
}
},
/** 仅数字部分(用于红色显示) */
formatAmount(row) {
try {
const num = Number(row && row.balance)
if (!Number.isFinite(num)) return '-'
return String(num)
} catch (e) {
return '-'
}
},
/** 仅币种单位(大写) */
formatCoin(row) {
return String(row && row.payCoin ? row.payCoin : '').toUpperCase()
},
/** 打开提现对话框(行数据驱动) */
async handleWithdraw(row) {
this.currentWithdrawRow = row || {}
const fee = Number(row && (row.serviceCharge != null ? row.serviceCharge : row.charge))
this.withdrawForm.fee = Number.isFinite(fee) ? this.formatDec6(fee) : '0.00'
this.withdrawForm.amount = ''
// 收款地址默认填充为当前行地址,且禁用编辑
this.withdrawForm.toAddress = row && row.payAddress ? row.payAddress : ''
this.withdrawForm.googleCode = ''
this.withdrawAddressEditable = false
// 初始化校验规则
this.withdrawRules = {
amount: [
{ required: true, message: '请输入提现金额', trigger: 'blur' },
{ validator: this.validateWithdrawAmount, trigger: 'blur' }
],
// 地址为只读已填,不再要求用户输入
googleCode: [
{ required: true, message: '请输入谷歌验证码', trigger: 'blur' },
{ validator: this.validateGoogleCode, trigger: 'blur' }
]
}
this.withdrawDialogVisible = true
},
/* 点击“修改”启用收款地址编辑并聚焦 */
handleEditAddressClick() {
this.withdrawAddressEditable = true
this.$nextTick(() => {
const input = this.$refs.withdrawToAddressInput
if (input && input.focus) input.focus()
})
},
/* 提现金额输入(<=6位小数 */
handleAmountInput(v) {
let s = String(v || '')
s = s.replace(/[^0-9.]/g, '')
const i = s.indexOf('.')
if (i !== -1) {
s = s.slice(0, i + 1) + s.slice(i + 1).replace(/\./g, '')
const [intPart, decPart = ''] = s.split('.')
s = intPart + '.' + decPart.slice(0, 6)
}
this.withdrawForm.amount = s
},
/* 谷歌验证码仅数字 */
handleGoogleCodeInput(v) {
this.withdrawForm.googleCode = String(v || '').replace(/\D/g, '')
},
/* 确认提现 - 调用后端卖家提现接口 */
confirmWithdraw() {
this.$refs.withdrawForm.validate(async (valid) => {
if (!valid) return
this.withdrawLoading = true
try {
const row = this.currentWithdrawRow || {}
const payload = {
toChain: row.chain,
toSymbol: row.payCoin,
amount: Number(this.withdrawForm.amount),
toAddress: this.withdrawForm.toAddress,
fromAddress: row.payAddress || this.withdrawForm.toAddress || '',
code: this.withdrawForm.googleCode,
serviceCharge: Number(this.withdrawForm.fee) || 0
}
const res = await withdrawBalanceForSeller(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('提现申请已提交,请等待处理')
this.withdrawDialogVisible = false
this.fetchShopConfigs(this.shop.id)
}
} catch (e) {
console.error('卖家提现失败', e)
} finally {
this.withdrawLoading = false
}
})
},
/* 工具最多6位小数显示 */
formatDec6(value) {
if (value === null || value === undefined || value === '') return '0'
let s = String(value)
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
},
/* 工具:金额字符串 -> 10^6 精度整数 */
toScaledInt(amountStr, decimals = 6) {
if (amountStr === null || amountStr === undefined) return 0
const normalized = String(amountStr).trim()
if (normalized === '') return 0
const re = new RegExp(`^\\d+(?:\\.(\\d{0,${decimals}}))?$`)
const match = normalized.match(re)
if (!match) {
const n = Number(normalized)
if (!Number.isFinite(n)) return 0
const scale = Math.pow(10, decimals)
return Math.round(n * scale)
}
const [intPart, decPartRaw] = normalized.split('.')
const decPart = (decPartRaw || '').padEnd(decimals, '0').slice(0, decimals)
const scale = Math.pow(10, decimals)
return Number(intPart) * scale + Number(decPart)
},
/* 工具10^6 精度整数 -> 最多6位小数字符串 */
formatDec6FromInt(intVal) {
const sign = intVal < 0 ? '-' : ''
const abs = Math.abs(intVal)
const scale = Math.pow(10, 6)
const intPart = Math.floor(abs / scale)
const decPart = String(abs % scale).padStart(6, '0')
const s = `${sign}${intPart}.${decPart}`
return s.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
},
/* 校验:提现金额 */
validateWithdrawAmount(rule, value, callback) {
const amtInt = this.toScaledInt(value)
if (!Number.isFinite(amtInt) || amtInt <= 0) { callback(new Error('请输入有效的金额')); return }
const feeInt = this.toScaledInt(this.withdrawForm.fee)
const balanceInt = this.toScaledInt((this.currentWithdrawRow && this.currentWithdrawRow.balance) || 0)
if (amtInt >= balanceInt) { callback(new Error('提现金额必须小于可用余额')); return }
if (amtInt <= feeInt) { callback(new Error('提现金额必须大于手续费')); return }
if (amtInt < 1000000) { callback(new Error('最小提现金额为 1')); return }
callback()
},
/* 校验:谷歌验证码 */
validateGoogleCode(rule, value, callback) {
const v = String(value || '')
if (!/^\d{6}$/.test(v)) { callback(new Error('谷歌验证码必须是6位数字')); return }
callback()
},
/**
* 手续费率显示最多6位小数去除多余的0空值显示为 '-'
*/
formatFeeRate(value) {
if (value === null || value === undefined || value === '') return '-'
const num = Number(value)
if (!Number.isFinite(num)) return '-'
const fixed = num.toFixed(6)
return fixed.replace(/\.?0+$/, '')
},
/**
* 修改弹窗 - 手续费输入允许一个小数点最多6位小数允许尾随点
*/
handleEditFeeRateInput(value) {
let v = String(value ?? this.editForm.feeRate ?? '')
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 (decPart.length > 6) decPart = decPart.slice(0, 6)
if (intPart && intPart !== '0') intPart = String(Number(intPart))
if (endsWithDot && firstDot !== -1) {
this.editForm.feeRate = `${intPart || '0'}.`
return
}
this.editForm.feeRate = decPart ? `${intPart || '0'}.${decPart}` : (intPart || '')
},
// 简单的emoji检测覆盖常见表情平面与符号范围
hasEmoji(str) {
if (!str || typeof str !== 'string') return false
@@ -291,6 +581,7 @@ export default {
name: res.data.name,
image: res.data.image,
description: res.data.description,
feeRate: res.data.feeRate,
del: !!res.data.del,
state: Number(res.data.state || 0)
}
@@ -321,7 +612,7 @@ export default {
}
try {
const res = await getShopConfig({id:shopId})
const res = await getShopConfigV2({id:shopId})
if (res && (res.code === 0 || res.code === 200) && Array.isArray(res.data)) {
// 直接使用后端返回的数据children: [{payCoin,image}]
this.shopConfigs = res.data
@@ -389,15 +680,7 @@ export default {
},
submitConfigEdit() {
// 基础校验
if (!this.configForm.chainLabel && !this.configForm.chainValue) {
this.$message.warning('请选择支付链')
return
}
if (!this.configForm.payCoins || this.configForm.payCoins.length === 0) {
this.$message.warning('请选择支付币种')
return
}
// 仅校验钱包地址
const addr = (this.configForm.payAddress || '').trim()
if (!addr) {
this.$message.warning('请输入钱包地址')
@@ -405,11 +688,17 @@ export default {
}
const payload = {
id: this.configForm.id,
chain: this.configForm.chainValue || this.configForm.chainLabel,
payCoin: (this.configForm.payCoins || []).join(','),
chain: this.configForm.chainValue || this.configForm.chainLabel || '',
payAddress: this.configForm.payAddress
}
this.updateShopConfig(payload)
;(async () => {
const res = await updateShopConfigV2(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('保存成功')
this.visibleConfigEdit = false
this.fetchShopConfigs(this.shop.id)
}
})()
},
removeSelectedCoin(labelUpper) {
const label = String(labelUpper || '').toLowerCase()
@@ -429,7 +718,8 @@ export default {
id: res.data.id,
name: res.data.name,
image: res.data.image,
description: res.data.description
description: res.data.description,
feeRate: res.data.feeRate
}
@@ -439,7 +729,8 @@ export default {
id: this.shop.id,
name: this.shop.name,
image: this.shop.image,
description: this.shop.description
description: this.shop.description,
feeRate: this.shop.feeRate
}
this.$message.warning(res && res.msg ? res.msg : '未获取到店铺详情')
}
@@ -449,7 +740,8 @@ export default {
id: this.shop.id,
name: this.shop.name,
image: this.shop.image,
description: this.shop.description
description: this.shop.description,
feeRate: this.shop.feeRate
}
console.error('查询店铺详情失败:', error)
@@ -492,7 +784,19 @@ export default {
this.$message.warning('店铺描述不能超过300个字符')
return
}
// 手续费比例必填、0.01-0.1、最多6位小数
const rateRaw = String(this.editForm.feeRate || '').trim()
if (!rateRaw) {
this.$message.warning('请填写店铺手续费比例0.01 - 0.1最多6位小数')
return
}
const rateNum = Number(rateRaw)
const decOk = rateRaw.includes('.') ? ((rateRaw.split('.')[1] || '').length <= 6) : true
if (!Number.isFinite(rateNum) || rateNum < 0.01 || rateNum > 0.1 || !decOk) {
this.$message.warning('手续费比例需在 0.01 - 0.1 之间且小数位不超过6位')
return
}
this.editForm.feeRate = rateNum.toString()
const payload = { ...this.editForm }
const res = await updateShop(payload)
@@ -585,9 +889,9 @@ export default {
})
return
}
// 跳转到新增商品页面并传递店铺ID
// 直接跳转到“添加出售机器”页面并传递店铺ID(供后续扩展使用)
this.$router.push({
path: '/account/product-new',
path: '/account/product-machine-add',
query: { shopId: this.shop.id }
})
},
@@ -634,6 +938,14 @@ export default {
.guide-note { margin-top: 10px; color: #6b7280; font-size: 13px; background: #f9fafb; border: 1px dashed #e5e7eb; padding: 8px 10px; border-radius: 8px; }
.coin-list { display: flex; align-items: center; gap: 8px; }
.coin-img { width: 20px; height: 20px; border-radius: 4px; display: inline-block; }
/* 提现弹窗样式(与钱包页统一) */
.balance-info { font-size: 12px; color: #666; margin-top: 4px; text-align: left; }
.fee-info { font-size: 12px; color: #e6a23c; margin-top: 4px; text-align: left; }
.actual-amount-info { font-size: 12px; color: #67c23a; margin-top: 4px; text-align: left; font-weight: 500; }
.address-tip { font-size: 12px; color: #f56c6c; margin-top: 4px; line-height: 1.4; text-align: left; }
/* 余额数字红色显示 */
.balance-num { color: #ff4d4f; font-weight: 600; }
.balance-unit { color: #606266; }
</style>
<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;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

@@ -3,7 +3,7 @@
<h2 class="panel-title">新增店铺</h2>
<div class="panel-body">
<div class="row">
<label class="label">店铺名称</label>
<label class="label required">店铺名称</label>
<el-input v-model="form.name" placeholder="请输入店铺名称" :maxlength="30" show-word-limit />
</div>
<!-- <div class="row">
@@ -28,7 +28,23 @@
</div>
</div>
<div class="row">
<el-button type="primary" @click="handleCreate">创建店铺</el-button>
<label class="label required">手续费比例</label>
<el-input
v-model="form.feeRate"
placeholder="比例区间 0.01 - 0.1 之间最多6位小数"
@input="handleFeeRateInput"
/>
</div>
<div class="row" style="margin-top:-6px;">
<div></div>
<div style="color:#909399; font-size:12px; text-align:left;">
为提升您的店铺曝光您可为平台交易设置手续费比例该手续费为商家向平台支付的交易佣金,手续费比例将作为影响店铺排名的关键因素,该比例越高您的店铺排名就越靠前
</div>
</div>
<div class="row" style="margin-top:50px;">
<div class="actions-center">
<el-button class="btn-wide" type="primary" @click="handleCreate">创建店铺</el-button>
</div>
</div>
</div>
</div>
@@ -39,7 +55,7 @@ import { getAddShop } from "@/api/shops";
export default {
data() {
return {
form: { name: "", description: "", image: "" },
form: { name: "", description: "", image: "", feeRate: "" },
};
},
mounted() {},
@@ -50,6 +66,32 @@ export default {
const emojiRegex = /[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{1FA70}-\u{1FAFF}\u2600-\u27BF]/u
return emojiRegex.test(str)
},
handleFeeRateInput(value) {
// 仅允许数字与一个小数点限制小数位最多6位保留尾随小数点便于继续输入
let v = String(value ?? this.form.feeRate ?? '')
// 过滤非法字符
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] || ''
// 小数位最多6位
if (decPart.length > 6) decPart = decPart.slice(0, 6)
// 规整前导0范围本就小于1
if (intPart && intPart !== '0') intPart = String(Number(intPart))
// 允许输入以小数点结尾的临时态(例如 "0."
if (endsWithDot && firstDot !== -1) {
this.form.feeRate = `${intPart || '0'}.`
return
}
// 常规态:有小数部分或仅整数部分
this.form.feeRate = decPart ? `${intPart || '0'}.${decPart}` : (intPart || '')
},
async fetchAddShop() {
const res = await getAddShop(this.form);
if (res && res.code==200) {
@@ -133,7 +175,27 @@ export default {
return
}
// 手续费比例校验必填、0.01-0.1 且最多6位小数
const rateRaw = String(this.form.feeRate || '').trim()
if (!rateRaw) {
this.$message({
message: '请填写店铺手续费比例0.01 - 0.1最多6位小数',
type: 'warning',
showClose: true
})
return
}
const rateNum = Number(rateRaw)
const decOk = rateRaw.includes('.') ? ((rateRaw.split('.')[1] || '').length <= 6) : true
if (!Number.isFinite(rateNum) || rateNum < 0.01 || rateNum > 0.1 || !decOk) {
this.$message({
message: '手续费比例需在 0.01 - 0.1 之间且小数位不超过6位',
type: 'warning',
showClose: true
})
return
}
this.form.feeRate = rateNum.toString()
this.fetchAddShop(this.form)
},
@@ -149,7 +211,7 @@ export default {
}
.row {
display: grid;
grid-template-columns: 100px 1fr;
grid-template-columns: 140px 1fr;
gap: 12px;
align-items: center;
margin-bottom: 12px;
@@ -157,6 +219,21 @@ export default {
.label {
color: #666;
text-align: right;
white-space: nowrap; /* 左侧文字不换行 */
word-break: keep-all;
}
.actions-center {
grid-column: 1 / -1; /* 跨两列,居中显示 */
text-align: center;
}
.btn-wide {
min-width: 200px;
padding: 10px 28px;
}
.label.required::before {
content: '*';
color: #f56c6c;
margin-right: 4px;
}
.textarea-wrapper {

View File

@@ -0,0 +1,201 @@
<template>
<div class="receipt-page">
<div class="card" aria-label="提现记录" tabindex="0">
<div class="card-header">
<h3 class="card-title">提现记录</h3>
</div>
<div v-if="loading" class="loading">
<i class="el-icon-loading" aria-label="加载中" role="img"></i>
加载中...
</div>
<div v-else>
<div class="table-wrap">
<el-table
:data="rows"
border
stripe
size="small"
class="withdraw-table"
:header-cell-style="{ textAlign: 'left' }"
:cell-style="{ textAlign: 'left' }"
>
<el-table-column label="申请时间" width="140">
<template #default="scope">{{ formatFullTime(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="提现金额" width="70" show-overflow-tooltip align="right">
<template #default="scope">
<span class="amount-red">
<el-tooltip
v-if="formatAmount(scope.row.amount, scope.row.coin || scope.row.toSymbol || 'USDT').truncated"
:content="`-${formatAmount(scope.row.amount, scope.row.coin || scope.row.toSymbol || 'USDT').full}`"
placement="top"
>
<span>
-{{ formatAmount(scope.row.amount, 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.amount, scope.row.coin || scope.row.toSymbol || 'USDT').text }}
</span>
</span>
</template>
</el-table-column>
<el-table-column label="手续费" width="70" show-overflow-tooltip align="right">
<template #default="scope">
<span class="mono">{{ formatAmount(scope.row.serviceCharge, scope.row.coin || scope.row.toSymbol || 'USDT').text }}</span>
</template>
</el-table-column>
<el-table-column label="提现链" width="100" show-overflow-tooltip>
<template #default="scope">{{ formatChain(scope.row.toChain || scope.row.chain) }}</template>
</el-table-column>
<el-table-column label="币种" width="80" show-overflow-tooltip>
<template #default="scope">{{ String(scope.row.coin || scope.row.toSymbol || '').toUpperCase() }}</template>
</el-table-column>
<el-table-column label="收款地址" min-width="320" show-overflow-tooltip>
<template #default="scope">
<el-tooltip :content="scope.row.toAddress" placement="top">
<span class="mono-ellipsis">{{ scope.row.toAddress }}</span>
</el-tooltip>
<el-button type="text" size="mini" @click.stop="copy(scope.row.toAddress)">复制</el-button>
</template>
</el-table-column>
<el-table-column label="交易HASH" width="300" show-overflow-tooltip>
<template #default="scope">
<el-tooltip :content="scope.row.txHash" placement="top">
<span class="mono-ellipsis">{{ scope.row.txHash }}</span>
</el-tooltip>
<el-button type="text" size="mini" @click.stop="copy(scope.row.txHash)" v-if="scope.row.txHash">复制</el-button>
</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)" size="small">{{ getStatusText(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态更新时间" width="140">
<template #default="scope">{{ formatFullTime(scope.row.updateTime) }}</template>
</el-table-column>
</el-table>
</div>
<div v-if="!rows.length" class="empty">
<div class="empty-icon">🏧</div>
<div class="empty-text">暂无提现记录</div>
</div>
<div class="pagination">
<el-pagination
background
layout="prev, pager, next, jumper"
:current-page.sync="pageNum"
:page-size="pageSize"
:total="total"
@current-change="fetchList"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { balanceWithdrawListV2 } from '../../api/wallet'
import { truncateAmountByCoin } from '../../utils/amount'
export default {
name: 'AccountWithdrawRecord',
data() {
return {
loading: false,
rows: [],
pageNum: 1,
pageSize: 20,
total: 0
}
},
mounted() {
this.fetchList()
},
methods: {
formatAmount(value, coin) {
return truncateAmountByCoin(value, coin)
},
formatFullTime(time) {
if (!time) return ''
try {
return `${time.split('T')[0]} ${time.split('T')[1].split('.')[0]}`
} catch (e) {
return time
}
},
formatChain(chain) {
const map = { tron: 'Tron (TRC20)', ethereum: 'Ethereum (ERC20)', bsc: 'BSC (BEP20)', polygon: 'Polygon', ETH: 'ETH', TRON: 'TRON' }
const k = typeof chain === 'string' ? chain.toLowerCase() : chain
return map[k] || chain || '-'
},
getStatusType(status) {
const map = { 0: 'danger', 1: 'success', 2: 'warning', 3: 'danger' }
return map[status] || 'info'
},
getStatusText(status) {
const map = { 0: '失败', 1: '成功', 2: '处理中', 3: '校验失败' }
return map[status] || '未知'
},
copy(text) {
if (!text) return
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text)
this.$message.success('已复制')
return
}
} catch (e) {}
const area = document.createElement('textarea')
area.value = text
document.body.appendChild(area)
area.select()
try { document.execCommand('copy'); this.$message.success('已复制') } catch (e) {}
document.body.removeChild(area)
},
async fetchList() {
this.loading = true
try {
const res = await balanceWithdrawListV2({ pageNum: this.pageNum, pageSize: this.pageSize })
const data = res && (res.data || res)
const list = Array.isArray(data && data.rows) ? data.rows : (Array.isArray(data) ? data : [])
this.rows = list
const total = Number(data && (data.total != null ? data.total : res.total))
this.total = Number.isFinite(total) ? total : 0
} catch (e) {
this.rows = []
this.total = 0
} finally {
this.loading = false
}
}
}
}
</script>
<style scoped>
.receipt-page { margin: 0; box-sizing: border-box; overflow-x: hidden; max-width: 100%; }
.card { background: #fff; border: 1px solid #eee; border-radius: 10px; padding: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.04); overflow-x: hidden; }
:deep(.withdraw-table) { width: 100%; }
:deep(.withdraw-table .el-table__header-wrapper table),
:deep(.withdraw-table .el-table__body-wrapper table) { table-layout: fixed; width: 100%; }
.table-wrap { width: 100%; overflow-x: hidden; }
:deep(.withdraw-table .cell) { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.card-title { margin: 0; font-size: 18px; font-weight: 700; color: #2c3e50; }
.loading { text-align: center; color: #666; padding: 40px 0; }
.empty { text-align: center; color: #999; padding: 40px 0; }
.empty-icon { font-size: 48px; margin-bottom: 8px; }
.amount-more { font-size: 12px; color: #94a3b8; margin-left: 4px; }
.amount-red { color: #ef4444; font-weight: 700; }
.mono-ellipsis { font-family: "Monaco", "Menlo", monospace; max-width: 360px; display: inline-block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; }
.pagination { display: flex; justify-content: flex-end; margin-top: 8px; }
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
import { getProductById } from '../../utils/productService'
import { addToCart } from '../../utils/cartManager'
import { getMachineInfo } from '../../api/products'
import { addCart, getGoodsList } from '../../api/shoppingCart'
import { getMachineInfo, getPayTypes,getShopMachineList,addGoodsV2 } from '../../api/products'
import { truncateAmountByCoin, truncateTo6 } from '../../utils/amount'
import { getGoodsListV2 } from '../../api/shoppingCart'
export default {
name: 'ProductDetail',
@@ -13,9 +13,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,61 +135,387 @@ export default {
// number:2001,
// cost:"1000",//价格
// },
],
productDetailLoading:false
productDetailLoading: false,
pageSizes: [10, 20, 50],
currentPage: 1,
total: 0,
// 动态列表(模拟渲染)
dynamicMeta: {},
dynamicColumns: [],
dynamicRows: [],
dynamicSearch: {
visible: false,
keyword: ''
},
// 矿机种类0-ASIC1-GPU默认GPU
machineType: 1,
}
},
mounted() {
console.log(this.$route.params.id, "i叫哦附加费")
if (this.$route.params.id) {
this.params.id = this.$route.params.id
// 读取用户上次选择的矿机种类0: ASIC, 1: GPU
try{
const savedType = Number(window && window.localStorage ? window.localStorage.getItem('pl_machineType') : NaN)
if (savedType === 0 || savedType === 1) {
this.machineType = savedType
}
}catch(e){/* noop */}
// 不再使用本地模拟数据,动态表格完全依赖后端返回的 columns/rows
// 仅当路由携带 shopId 时,才发起店铺商品请求
const routeShopId =
(this.$route && this.$route.params && (this.$route.params.shopId || this.$route.params.id)) ||
(this.$route && this.$route.query && this.$route.query.shopId)
if (routeShopId) {
this.params.id = routeShopId
this.product = true
// 默认展开第一行
if (this.productListData && this.productListData.length) {
this.expandedRowKeys = [this.productListData[0].id]
}
this.fetchGetMachineInfo(this.params)
this.fetchGetMachineInfo(this.buildQueryParams())
this.fetchPayTypes()
} else {
this.$message.error('商品不存在')
this.$message.warning('缺少店铺IDshopId无法加载商品列表')
this.product = false
}
this.fetchGetGoodsList()
},
methods: {
// 动态表格单元格格式化(金额/算力/天数/文本)- 统一最多显示6位小数hover展示完整
formatDynamicCell(row, col) {
try{
let val = row[col.key]
if (val === null || val === undefined || val === '') return { text: '—', full: '—', truncated: false }
if (col.type === 'amount') {
// 价格列:单位取 priceList 里的 coin默认取第一条选择支付方式后按选择展示
if (col.key === 'price') {
if (Array.isArray(row.priceList) && row.priceList.length) {
const pv = this.getDisplayPrice(row)
const pc = this.getDisplayPriceCoin(row)
if (pv !== null && pv !== undefined) val = pv
const nPrice = val
const t = truncateTo6(nPrice)
const coinUnit = (pc || '').toString().toUpperCase()
return {
text: coinUnit ? `${t.text} ${coinUnit}` : t.text,
full: coinUnit ? `${t.full} ${coinUnit}` : t.full,
truncated: t.truncated
}
}
// 无 priceList仅展示数值不附加任何单位
const t = truncateTo6(val)
return { text: t.text, full: t.full, truncated: t.truncated }
}
// 列级优先:若列声明 currency=USDT则固定展示为 "xx.xx USDT"
const colCurrency = (col.currency || '').toString().toUpperCase()
if (colCurrency === 'USDT') {
const t = truncateTo6(val)
return { text: `${t.text} USDT`, full: `${t.full} USDT`, truncated: t.truncated }
}
// 兜底:不再使用 meta 的货币符号,直接返回数值
const t = truncateTo6(val)
return { text: t.text, full: t.full, truncated: t.truncated }
}
if (col.type === 'hashrate') {
const unit = col.unit ? ` ${col.unit}` : ''
const t = truncateTo6(val)
return { text: `${t.text}${unit}`, full: `${t.full}${unit}`, truncated: t.truncated }
}
if (col.type === 'days') {
const n = Number(val)
if (!Number.isFinite(n)) return { text: String(val), full: String(val), truncated: false }
const s = `${Math.floor(n)}`
return { text: s, full: s, truncated: false }
}
const s = String(val)
return { text: s, full: s, truncated: false }
}catch(e){ return { text: '—', full: '—', truncated: false } }
},
/**
* 如果存在 priceList则用第一条的 chain|coin 作为默认筛选值,
* 使“支付方式筛选”与价格列默认展示一致
*/
ensureDefaultPayFilterFromPrices() {
try{
if (this.payFilterDefaultApplied) return
const rows = Array.isArray(this.dynamicRows) ? this.dynamicRows : []
const firstWithPriceList = rows.find(r => Array.isArray(r && r.priceList) && r.priceList.length)
if (!firstWithPriceList) return
const first = firstWithPriceList.priceList[0]
const chain = String(first && first.chain || '').trim()
const coin = String(first && first.coin || '').trim()
if (!chain && !coin) return
this.selectedPayKey = `${chain}|${coin}`
this.filters.chain = chain
this.filters.coin = coin
this.payFilterDefaultApplied = true
}catch(e){ /* noop */ }
},
/**
* 获取行在当前支付方式下的展示价格
* 优先匹配 selectedPayKeychain|coin否则回退 priceList[0];再否则回退 row.price
*/
getDisplayPrice(row){
try{
const list = Array.isArray(row && row.priceList) ? row.priceList : []
if (!list.length) return row && row.price
const key = this.selectedPayKey
if (key) {
const [chainRaw, coinRaw] = String(key).split('|')
const chain = String(chainRaw || '').toUpperCase().trim()
const coin = String(coinRaw || '').toUpperCase().trim()
const hit = list.find(it =>
String(it && it.chain).toUpperCase().trim() === chain &&
String(it && it.coin).toUpperCase().trim() === coin
)
if (hit && hit.price !== undefined && hit.price !== null) return hit.price
}
const first = list[0]
if (first && first.price !== undefined && first.price !== null) return first.price
return row && row.price
}catch(e){ return row && row.price }
},
/**
* 获取行在当前支付方式下价格的币种coin
*/
getDisplayPriceCoin(row){
try{
const list = Array.isArray(row && row.priceList) ? row.priceList : []
if (!list.length) return ''
const key = this.selectedPayKey
if (key) {
const [chainRaw, coinRaw] = String(key).split('|')
const chain = String(chainRaw || '').toUpperCase().trim()
const coin = String(coinRaw || '').toUpperCase().trim()
const hit = list.find(it =>
String(it && it.chain).toUpperCase().trim() === chain &&
String(it && it.coin).toUpperCase().trim() === coin
)
if (hit && hit.coin) return String(hit.coin)
}
const first = list[0]
return first && first.coin ? String(first.coin) : ''
}catch(e){ return '' }
},
_truncate(num, decimals=2){
try{
const f = Math.pow(10, decimals)
return (Math.floor(Number(num)*f)/f).toFixed(decimals)
}catch(e){ return String(num) }
},
// 判断是否为“框出来部分”的最后一列(最后一个 hashrate 列)
isLastHashrateColumn(colIdx){
try{
const cols = this.getRenderedColumns()
for (let i = cols.length - 1; i >= 0; i--) {
if (String(cols[i] && cols[i].type).toLowerCase() === 'hashrate') {
return i === colIdx
}
}
return false
}catch(e){ return false }
},
// 仅渲染前 8 个算力列,后接其它非算力列(如收益、回收期)
getRenderedColumns(){
try{
const cols = Array.isArray(this.dynamicColumns) ? this.dynamicColumns : []
const hashrate = cols.filter(c => String(c && c.type).toLowerCase() === 'hashrate').slice(0, 8)
const others = cols.filter(c => String(c && c.type).toLowerCase() !== 'hashrate')
return [...hashrate, ...others]
}catch(e){ return [] }
},
// 打开动态搜索弹窗
handleOpenDynamicSearch(){
this.dynamicSearch.visible = true
this.dynamicSearch.keyword = ''
},
// 确认搜索:向后端请求新的 columns/rows替换动态表格
async handleConfirmDynamicSearch(){
const keyword = (this.dynamicSearch.keyword || '').trim()
this.dynamicSearch.visible = false
await this.fetchDynamicTable({ shopId: this.params.id, type: 1, keyword })
},
// 拉取动态表格数据(占位实现:如果后端已就绪,直接替换为真实接口)
async fetchDynamicTable(params){
try{
// 这里预留与后端对接:
// 期待返回格式:{ code, data: { meta, columns, rows } }
// 示例中用本地 mock 演示:根据 keyword 过滤/调整列
// 如果没有 keyword就还原初始 mock
if (!params || !params.keyword) {
return
}
// 简单模拟:当 keyword 命中 'ERG',只保留 ERG + 价格/型号/回收期 等少量列
const kw = String(params.keyword).toUpperCase()
const baseCols = (this.dynamicColumns || []).filter(c => ['model','price','maxLeaseDays','monthIncome'].includes(c.key))
const hitCols = (this.dynamicColumns || []).filter(c => String(c.label || c.key).toUpperCase().includes(kw))
const nextCols = [...(baseCols.length?baseCols:[this.dynamicColumns[0]||[]]), ...hitCols]
if (nextCols.length) {
this.dynamicColumns = nextCols
// 行数据无需特别处理(真实环境后端会按列同步返回),这里保留原 rows
}
}catch(e){
// eslint-disable-next-line no-console
console.warn('fetchDynamicTable mock error', e)
}
},
// 切换矿机种类0-ASIC1-GPU
handleMachineTypeChange(){
// 切换前清空所有已勾选状态与确认弹窗
try {
if (Array.isArray(this.dynamicRows)) {
this.dynamicRows.forEach(r => { if (r) this.$set(r, '_selected', false) })
}
if (this.confirmAddDialog) {
this.confirmAddDialog.items = []
this.confirmAddDialog.visible = false
}
} catch (e) { /* noop */ }
// 变更类型后,重新请求数据与支付方式
this.fetchGetMachineInfo(this.buildQueryParams())
this.fetchPayTypes()
// 本地记住用户选择
try{
if (window && window.localStorage) {
window.localStorage.setItem('pl_machineType', String(this.machineType))
}
}catch(e){/* noop */}
},
// 行币种:优先行内 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 */ }
},
// 组合查询参数:店铺入口,必须包含 shopId 与 type0-ASIC1-GPU
buildQueryParams() {
const q = { shopId: this.params.id, type: this.machineType }
// 分页参数始终透传
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
},
// 拉取支付方式(兼容 shopId
async fetchPayTypes() {
try {
// 现规则:商品详情由店铺入口进入,使用 shopId 查询支付方式
// 为兼容后端两种入参,优先传 shopId后端若仍使用 productId 也能兼容处理
const res = await getPayTypes({ shopId: this.params.id, 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
const res = await getMachineInfo(params)
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.$nextTick(() => {
this.machinesLoaded = true
// 已取消与购物车对比:不再自动禁用或勾选
})
// 改为使用店铺机器列表接口
const res = await getShopMachineList(params)
if (res && (res.code === 200 || res.code === 0)) {
const root = (res && res.data) ? res.data : res
const columns = Array.isArray(root.columns) ? root.columns : (Array.isArray(res.columns) ? res.columns : [])
const rows = Array.isArray(root.rows) ? root.rows : (Array.isArray(res.rows) ? res.rows : [])
const total = Number(root.total != null ? root.total : (res.total != null ? res.total : 0))
this.total = Number.isFinite(total) ? total : 0
// 动态表格:列与行
this.dynamicColumns = columns
this.dynamicRows = rows.map(r => ({
saleNumbers: 0,
saleOutNumbers: 0,
leaseTime: 1,
purchaseQuantity: 1,
_selected: false,
...r
}))
// 根据 rows 的 priceList 设置默认支付方式
this.ensureDefaultPayFilterFromPrices()
// 若后端同步返回支付方式,刷新本地支付方式
try {
const payList = root && root.payConfigList
if (Array.isArray(payList) && payList.length) {
this.paymentMethodList = payList
this.ensureDefaultPayFilterSelection()
}
} catch (e) { /* noop */ }
}
this.productDetailLoading = false
@@ -175,17 +531,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,12 +550,12 @@ export default {
//加入购物车
async fetchAddCart(params) {
const res = await addCart(params)
return res
},
//查询购物车列表
async fetchGetGoodsList(params) {
const res = await getGoodsList(params)
const res = await getGoodsListV2(params)
// 统计当前商品在购物车中已有的机器ID用于禁用和默认勾选
try {
const productId = this.params && this.params.id ? Number(this.params.id) : Number(this.$route.params.id)
@@ -296,7 +652,7 @@ export default {
},
// 已取消对比购物车的自动勾选/禁用逻辑
autoSelectAndDisable() {},
autoSelectAndDisable() { },
// 选择器可选控制:已在购物车中的机器不可再选
isSelectable(row, index) {
@@ -319,6 +675,16 @@ export default {
this.$set(row, '_selected', false)
return
}
// 无价格:不可选择
try {
const hasPrice = (Array.isArray(row && row.priceList) && row.priceList.some(it => it && it.price !== null && it.price !== undefined))
|| (row && row.price !== null && row.price !== undefined && row.price !== '')
if (!hasPrice) {
this.$message.warning('该机器暂无价格,无法选择')
this.$set(row, '_selected', false)
return
}
} catch (e) { /* noop */ }
const key = parentRow.id
const list = (this.selectedMap[key] && [...this.selectedMap[key]]) || []
const idx = list.findIndex(it => it && it.id === row.id)
@@ -387,7 +753,7 @@ export default {
variant.quantity = 1
} catch (error) {
console.error('添加到购物车失败:', error)
}
},
// 统一加入购物车
@@ -411,91 +777,63 @@ export default {
this.selectedMap = {}
} catch (e) {
console.error('统一加入购物车失败', e)
}
},
// 打开确认弹窗:以当前界面勾选(_selected)为准,并在打开后清空左侧勾选状态
// 打开确认弹窗(基于动态表格的勾选行)
handleOpenAddToCartDialog() {
// 扫描当前所有系列下被勾选的机器
const groups = Array.isArray(this.productListData) ? this.productListData : []
const pickedAll = groups.flatMap(g => Array.isArray(g.productMachines) ? g.productMachines.filter(m => !!m && !!m._selected) : [])
const picked = pickedAll.filter(m => m && (m.saleState === 0 || m.saleState === undefined || m.saleState === null))
const rows = Array.isArray(this.dynamicRows) ? this.dynamicRows : []
const picked = rows.filter(r => !!r && !!r._selected)
if (!picked.length) {
this.$message.warning('请先勾选至少一台矿机')
return
}
if (picked.length < pickedAll.length) {
this.$message.warning('部分机器已售出或售出中,已自动为您排除')
}
// 使用弹窗中的固定快照,避免后续清空勾选影响弹窗显示
this.confirmAddDialog.items = picked.slice()
this.confirmAddDialog.items = picked.map(r => ({
...r,
leaseTime: Number(r.leaseTime || 1),
purchaseQuantity: Number(r.purchaseQuantity || 1)
}))
this.confirmAddDialog.visible = true
// 打开后立即把左侧复选框清空,避免“勾选了两个但弹窗只有一条”的不一致问题
this.$nextTick(() => {
try { this.clearAllSelections() } catch (e) { /* noop */ }
})
},
// 确认加入:调用后端购物车接口,传入裸数组 [{ productId, productMachineId }]
// 确认加入:调用 addGoodsV2按条提交GPU 不传 numbers
async handleConfirmAddSelectedToCart() {
// 以弹窗中的列表为准,避免与左侧勾选状态不一致
const allSelected = Array.isArray(this.confirmAddDialog.items) ? this.confirmAddDialog.items.filter(Boolean) : []
if (!allSelected.length) {
const items = Array.isArray(this.confirmAddDialog.items) ? this.confirmAddDialog.items.filter(Boolean) : []
if (!items.length) {
this.$message.warning('请先勾选至少一台矿机')
return
}
const productId = this.params && this.params.id ? this.params.id : (this.$route && this.$route.params && this.$route.params.id)
if (!productId) {
this.$message.error('商品ID缺失无法加入购物车')
return
}
// 裸数组,仅包含后端要求的两个字段
const payload = allSelected.map(item => ({
productId: productId,
productMachineId: item.id,
leaseTime: Number(item.leaseTime || 1)
}))
try {
const res = await this.fetchAddCart(payload)
// 若后端返回码存在,这里做一下兜底提示
if (!res || (res.code && Number(res.code) !== 200)) {
this.$message.error(res && res.msg ? res.msg : '加入购物车失败,请稍后重试')
return
}
// 立即本地更新禁用状态把刚加入的机器ID合并进本地集合
try {
allSelected.forEach(item => {
if (item && item.id) this.cartMachineIdSet.add(item.id)
this.$set(item, '_selected', false)
this.$set(item, '_inCart', true)
if (!item.leaseTime || Number(item.leaseTime) <= 0) this.$set(item, 'leaseTime', 1)
// 按接口要求:一次性传数组,每个对象代表一个勾选商品
const payload = items.map(it => {
const obj = {
id: it.id,
leaseTime: Number(it.leaseTime || 1)
}
if (this.machineType === 0) {
obj.numbers = Number(it.purchaseQuantity || 1)
}
return obj
})
const res = await addGoodsV2(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message({
message: `已加入 ${items.length} 台矿机到购物车`,
type: 'success',
duration: 3000,
showClose: true
})
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 = {}
// 重新加载机器信息和购物车数据
this.fetchGetMachineInfo(this.params)
this.fetchGetGoodsList()
// 通知头部刷新服务端购物车数量
// 清空
try {
// 如果没有传数量header 会主动拉取服务端数量
window.dispatchEvent(new CustomEvent('cart-updated'))
(this.dynamicRows || []).forEach(r => { if (r) this.$set(r, '_selected', false) })
} catch (e) { /* noop */ }
// 通知头部刷新
try { window.dispatchEvent(new CustomEvent('cart-updated')) } catch (e) { /* noop */ }
} catch (e) {
console.error('加入购物车失败: ', e)
// eslint-disable-next-line no-console
console.error('addGoodsV2 error:', e)
this.$message.error('加入购物车失败,请稍后重试')
}
},
@@ -503,9 +841,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 +928,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,125 +32,236 @@
</li>
</ul>
</section>
<section class="productList">
<!-- 产品列表可展开 -->
<!-- 筛选栏 -->
<section class="filter-bar" aria-label="筛选条件">
<div class="filter-grid">
<!-- 矿机种类放在支付方式筛选前面 -->
<div class="filter-cell">
<label class="filter-title">矿机种类</label>
<div style="display:inline-flex;align-items:center;">
<el-radio-group v-model="machineType" size="small" @change="handleMachineTypeChange">
<el-radio-button :label="1">GPU</el-radio-button>
<el-radio-button :label="0">ASIC</el-radio-button>
</el-radio-group>
</div>
</div>
<!-- 支付方式筛选选择即触发查询 -->
<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"
style="max-width: 260px;"
>
<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 filter-actions">
<div class="action-row">
<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>
</section>
<!-- 动态表格基于后端返回的 columns/rows 渲染 -->
<section v-if="dynamicColumns && dynamicColumns.length" class="dynamic-hashrate" aria-label="动态收益表">
<el-table
ref="seriesTable"
class="series-table"
:data="productListData"
row-key="id"
:expand-row-keys="expandedRowKeys"
@expand-change="handleExpandChange"
@row-click="handleSeriesRowClick"
:row-class-name="handleGetSeriesRowClassName"
:data="dynamicRows"
border
stripe
size="small"
class="dynamic-table"
: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" fixed="left">
<template #default="{ row }">
<el-checkbox
v-model="row._selected"
:title="isRowDisabled(row) ? (row && (row.saleState === 1 || row.saleState === 2) ? '该机器已售出或售出中,无法选择' : '该机器暂无价格,无法选择') : '选择该矿机'"
:disabled="isRowDisabled(row)"
@change="checked => handleManualSelectFlat(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
v-for="(col, colIdx) in getRenderedColumns()"
:key="col.key || colIdx"
:prop="col.key"
:label="col.label"
:fixed="col.fixed || false"
show-overflow-tooltip
>
<template #header>
<div class="col-header" :title="col.label">
<img v-if="col.icon" :src="col.icon" class="col-icon" alt="" />
<span>{{ col.label }}</span>
<!-- 算力列标题不再展示单位仅展示图标和名称 -->
<el-button
v-if="isLastHashrateColumn(colIdx)"
type="text"
class="more-action"
@click.stop="handleOpenDynamicSearch"
>更多</el-button>
</div>
</template>
<template #default="{ row }">
<span :class="getCellClass(col)">
<el-tooltip
v-if="formatDynamicCell(row, col).truncated"
:content="formatDynamicCell(row, col).full"
placement="top"
>
<span>{{ formatDynamicCell(row, col).text }}</span>
</el-tooltip>
<span v-else>{{ formatDynamicCell(row, col).text }}</span>
</span>
</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>
<!-- 仅在 ASIC 时显示总机器数量已售数量 -->
<el-table-column v-if="machineType === 0" prop="saleNumbers" label="总机器数">
<template #default="scope">
<span>{{ scope.row.saleNumbers != null ? scope.row.saleNumbers : '—' }}</span>
</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 v-if="machineType === 0" prop="saleOutNumbers" label="已售数量">
<template #default="scope">
<span>{{ scope.row.saleOutNumbers != null ? scope.row.saleOutNumbers : '—' }}</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="leaseTime" label="租赁天数(天)">
<template #default="scope">
<el-input-number
class="input-full"
v-model="scope.row.leaseTime"
:min="1"
:max="getRowMaxLeaseDays(scope.row)"
:precision="0"
:step="1"
:controls="false"
size="mini"
@change="val => handleLeaseDaysChange(scope.row, val)"
/>
</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>
<!-- ASIC 专用购买数量用户输入 -->
<el-table-column v-if="machineType === 0" prop="purchaseQuantity" label="购买数量">
<template #default="scope">
<el-input-number
class="input-full"
v-model="scope.row.purchaseQuantity"
:min="1"
:max="getRowMaxPurchase(scope.row)"
:precision="0"
:step="1"
:controls="false"
size="mini"
:disabled="getRowMaxPurchase(scope.row) <= 0"
@change="val => handlePurchaseQuantityChange(scope.row, val)"
/>
</template>
</el-table-column>
<!-- 总价ASIC=价格*天数*购买数量GPU=价格*天数 -->
<el-table-column prop="totalAmount" label="总价" header-align="left" align="left">
<template #default="scope">
<span class="price-strong">{{ formatConfirmTotalText(scope.row) }}</span>
</template>
</el-table-column>
</el-table>
<!-- 动态表格 - 搜索弹窗 -->
<el-dialog
title="搜索币种/算法"
:visible.sync="dynamicSearch.visible"
width="420px"
>
<div class="dynamic-search-bar" style="display:flex;gap:10px;align-items:center;">
<el-input
v-model="dynamicSearch.keyword"
placeholder="输入币种代码或算法关键词"
clearable
@keyup.enter.native="handleConfirmDynamicSearch"
/>
<el-button type="primary" @click="handleConfirmDynamicSearch">搜索</el-button>
</div>
</el-dialog>
</section>
<div style="margin: 18px; text-align: right;">
<el-button type="primary" size="small" @click="handleOpenAddToCartDialog">加入购物车</el-button>
</div>
<!-- 确认加入购物车弹窗 -->
<el-dialog :visible.sync="confirmAddDialog.visible" width="80vw" :title="`确认加入购物车(共 ${confirmAddDialog.items.length} 台)`">
<el-dialog :visible.sync="confirmAddDialog.visible" width="70vw" :title="`确认加入购物车(共 ${confirmAddDialog.items.length} 台)`">
<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>
<el-table-column prop="model" label="型号" header-align="left" align="left" />
<el-table-column prop="price" label="价格" header-align="left" align="left">
<template #default="scope">
<span class="price-strong">
<el-tooltip
v-if="formatDynamicCell(scope.row, { key: 'price', type: 'amount' }).truncated"
:content="formatDynamicCell(scope.row, { key: 'price', type: 'amount' }).full"
placement="top"
>
<span>{{ formatDynamicCell(scope.row, { key: 'price', type: 'amount' }).text }}</span>
</el-tooltip>
<span v-else>{{ formatDynamicCell(scope.row, { key: 'price', type: 'amount' }).text }}</span>
</span>
</template>
</el-table-column>
<el-table-column label="实际算力" header-align="left" align="left">
<template #default="scope">{{ scope.row.computingPower }} {{ 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 label="租赁天数(天)" header-align="left" align="left">
<el-table-column prop="leaseTime" 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 v-if="machineType === 0" prop="purchaseQuantity" label="购买数量" header-align="left" align="left">
<template #default="scope">{{ Number(scope.row.purchaseQuantity || 0) }}</template>
</el-table-column>
<el-table-column prop="totalAmount" label="总价" header-align="left" align="left">
<template #default="scope">
<span class="price-strong">{{ formatConfirmTotalText(scope.row) }}</span>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
@@ -157,7 +269,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">
@@ -175,6 +302,157 @@ export default {
name: 'ProductDetail',
mixins: [Index],
methods: {
/**
* 判断列是否为价格/金额相关列,以控制样式为红色
* - 优先依据 col.type === 'amount'
* - 其次依据 key 命名包含 'price' / 'amount'
* - 再次依据 label 文案包含 '价' / '金额'
* @param {Object} col
* @returns {string} CSS 类名
*/
getCellClass(col) {
try {
if (!col) return 'num-strong'
if (String(col.type).toLowerCase() === 'amount') return 'price-strong'
const key = String(col.key || '').toLowerCase()
if (key.includes('price') || key.includes('amount')) return 'price-strong'
const label = String(col.label || '')
if (label.includes('价') || label.includes('金额')) return 'price-strong'
return 'num-strong'
} catch (e) {
return 'num-strong'
}
},
/**
* 纯字符串方式相乘,避免浮点误差
* @param {Array<string|number>} values
* @returns {string} 不带单位的结果字符串,精确还原小数位
*/
multiplyAsDecimal(values) {
const toIntScale = (n) => {
const s = String(n || 0).trim()
if (!s.includes('.')) return { int: BigInt(s || '0'), scale: 0 }
const [w, f] = s.split('.')
const frac = (f || '').replace(/[^0-9]/g, '')
const whole = (w || '0').replace(/[^0-9-]/g, '')
const sign = whole.startsWith('-') ? '-' : ''
const intStr = (sign ? whole.slice(1) : whole) + frac
const safe = intStr.replace(/^0+(?=\d)/, '') || '0'
return { int: BigInt(sign + safe), scale: frac.length }
}
let acc = 1n
let scale = 0
for (const v of values) {
const { int, scale: sc } = toIntScale(v)
acc = acc * int
scale += sc
}
const neg = acc < 0n
const absStr = (neg ? (-acc) : acc).toString()
if (scale === 0) return (neg ? '-' : '') + absStr
const pad = scale - absStr.length
const s = pad > 0 ? ('0'.repeat(pad) + absStr) : absStr
const i = s.length - scale
let out = s.slice(0, i) + '.' + s.slice(i)
// 去除多余的前导/尾随零
out = out.replace(/^(-?)0+(?=\d)/, '$1')
out = out.replace(/\.?0+$/, '')
if (out.startsWith('.')) out = '0' + out
if (out === '' || out === '-') out = '0'
return (neg ? '-' : '') + out
},
/**
* 截断到最多6位小数不四舍五入
* @param {string} s
* @param {number} maxDecimals
* @returns {string}
*/
truncateDecimalString(s, maxDecimals = 6) {
const str = String(s || '0')
if (!str.includes('.')) return str
const [w, f] = str.split('.')
if (f.length <= maxDecimals) return str
return `${w}.${f.slice(0, maxDecimals)}`
},
/**
* 确认弹窗“总价”展示(附币种)
* ASIC: price*leaseTime*purchaseQuantityGPU: price*leaseTime
* 采用字符串整数相乘避免精度问题展示最多6位小数截断不四舍五入
*/
formatConfirmTotalText(row) {
try {
const price = this.getDisplayPrice ? this.getDisplayPrice(row) : (row && row.price)
const coin = (this.getDisplayPriceCoin && this.getDisplayPriceCoin(row)) || ''
const lease = Number(row && row.leaseTime) || 1
const nums = [price, lease]
if (this.machineType === 0) {
const qty = Number(row && row.purchaseQuantity) || 1
nums.push(qty)
}
const mul = this.multiplyAsDecimal(nums)
const text = this.truncateDecimalString(mul, 6)
const unit = (coin || '').toString().toUpperCase()
return unit ? `${text} ${unit}` : text
} catch (e) {
return '—'
}
},
/**
* 获取该行可购买的最大数量(<= 总机器数)
* @param {Object} row
* @returns {number}
*/
getRowMaxPurchase(row) {
try {
const n = Number(row && row.saleNumbers)
if (!Number.isFinite(n) || n < 0) return 0
return Math.floor(n)
} catch (e) { return 0 }
},
/**
* 购买数量变更时,强制校验区间 [1, max] 且取整
* @param {Object} row
* @param {number} value
*/
handlePurchaseQuantityChange(row, value) {
try {
const max = this.getRowMaxPurchase(row)
let v = Number(value)
if (!Number.isFinite(v)) v = 1
if (v < 1) v = 1
if (max > 0 && v > max) v = max
v = Math.floor(v)
this.$set(row, 'purchaseQuantity', v)
} catch (e) {
this.$set(row, 'purchaseQuantity', 1)
}
},
/**
* 行是否存在任意价格(用于禁用勾选)
* 规则:
* - 若存在 priceList任一项的 price 非 null/undefined 即视为有价格
* - 否则回退 row.price 是否有效
*/
hasAnyPrice(row) {
try {
if (!row) return false
if (Array.isArray(row.priceList) && row.priceList.length) {
return row.priceList.some(it => it && it.price !== null && it.price !== undefined)
}
const v = row.price
return v !== null && v !== undefined && v !== ''
} catch (e) { return false }
},
/**
* 该行是否应禁用选择:已售出/售出中 或 无价格
*/
isRowDisabled(row) {
try {
if (!row) return true
if (row.saleState === 1 || row.saleState === 2) return true
return !this.hasAnyPrice(row)
} catch (e) { return true }
},
/**
* 获取行的最大可租赁天数
* 规则:优先 row.maxLeaseDays否则回退 365保证区间 [1, 365]
@@ -187,6 +465,96 @@ 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.filters.minPrice = null
this.filters.maxPrice = null
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 +600,49 @@ 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.dynamicRows) ? this.dynamicRows : []
const picked = list.filter(it => !!it && !!it._selected)
if (!picked.length) {
this.$message.warning('请先勾选至少一台矿机')
return
}
this.confirmAddDialog.items = picked.map(r => ({
...r,
leaseTime: Number(r.leaseTime || 1),
purchaseQuantity: Number(r.purchaseQuantity || 0)
}))
this.confirmAddDialog.visible = true
}
}
}
@@ -358,6 +769,11 @@ export default {
color: #e74c3c;
}
.num-strong {
font-weight: inherit;
color: inherit;
}
/* 支付方式区域(视觉更友好 + 可达性) */
.pay-methods {
display: flex;
@@ -400,6 +816,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 +828,113 @@ 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: flex;
flex-wrap: wrap;
gap: 10px 16px;
align-items: flex-end;
justify-content: flex-start;
}
.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: inline-flex;
align-items: center;
gap: 10px;
}
.action-row { display: inline-flex; align-items: center; gap: 10px; }
.filter-actions-inline {
display: inline-flex;
align-items: center;
gap: 10px;
margin-left: 12px;
}
/* 动态表格样式 */
.dynamic-hashrate {
margin: 10px;
background: #fff;
border: 1px solid #eef2f7;
border-radius: 8px;
padding: 10px;
}
.dynamic-table :deep(.el-table__header th) {
background: #fafcff;
}
.col-header { display: inline-flex; align-items: center; gap: 6px; }
.col-icon { width: 16px; height: 16px; border-radius: 3px; }
.col-unit { color: #94a3b8; font-size: 12px; }
.more-action { margin-left: 8px; color: #2563eb; font-weight: 600; }
.more-action {
font-size: 12px;
padding: 0 4px;
height: auto;
line-height: 1;
border: none;
}
.more-action:hover {
color: #1d4ed8;
text-decoration: underline;
}
.dynamic-search-bar :deep(.el-input__inner) {
font-size: 12px;
}
.el-dialog__title {
font-size: 16px !important;
font-weight: 600;
}
.input-full { width: 100%; }
:deep(.el-input-number.input-full) { width: 100%; }
@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 +1077,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

@@ -1,4 +1,4 @@
import { getProductList } from '../../api/products'
import { getShopList } from '../../api/products'
export default {
name: 'ProductList',
data() {
@@ -172,134 +172,60 @@ export default {
screenCurrency: "",
searchAlgorithm: "",
params:{
coin: "",
algorithm: ""
pageNum: "1",
pageSize: "10",
keyword:""
},
productListLoading:false,
}
},
mounted() {
this.fetchGetList()
this.fetchShopList()
},
methods: {
/**
* 价格裁剪为两位小数(不四舍五入)
* 兼容区间字符串:"min-max" 或 单值
*/
formatPriceRange(input) {
try {
if (input === null || input === undefined) return '0.00'
const raw = String(input)
if (raw.includes('-')) {
const [lo, hi] = raw.split('-')
return `${this._truncate2(lo)}-${this._truncate2(hi)}`
}
return this._truncate2(raw)
} catch (e) {
return '0.00'
}
},
/**
* 将任意数字字符串截断为 2 位小数(不四舍五入)。
*/
_truncate2(val) {
if (val === null || val === undefined) return '0.00'
const str = String(val).trim()
if (!str) return '0.00'
const [intPart, decPart = ''] = str.split('.')
const two = decPart.slice(0, 2)
return `${intPart}.${two.padEnd(2, '0')}`
},
handleCurrencyChange(val){
try{
// 清空时el-select 的 clear 同时触发 change避免重复请求交由 handleCurrencyClear 处理
if (val === undefined || val === null || val === '') return
// 选择具体币种时,合并算法关键词一起查询
this.params.coin = val
const keyword = (this.searchAlgorithm || '').trim()
const req = keyword ? { coin: val, algorithm: keyword } : { coin: val }
this.fetchGetList(req)
// 可在此发起接口getProductList({ coin: val })
// this.fetchGetList({ coin: val })
}catch(e){
console.error('处理币种变更失败', e)
}
},
async fetchGetList(params) {
this.productListLoading = true
try {
const res = await getProductList(params)
console.log('API响应:', res)
if (res && res.code === 200) {
this.products = res.rows || []
console.log('商品数据:', this.products)
} else {
console.error('API返回错误:', res)
// 获取商场页面的店铺列表
async fetchShopList(params) {
this.productListLoading = true
try{
// 仅允许 pageNum、pageSize、keyword 三个参数
const payload = {
pageNum: this.params.pageNum,
pageSize: this.params.pageSize
}
const kw = params && typeof params === 'object' ? params.keyword : this.params.keyword
if (kw) payload.keyword = kw
const res = await getShopList(payload)
if (res && (res.code === 0 || res.code === 200)) {
// 这里直接将店铺列表赋值到 products用于页面渲染
this.products = Array.isArray(res.rows) ? res.rows : []
} else {
this.products = []
}
}catch(e){
console.error('获取店铺列表失败:', e)
this.products = []
}
} catch (error) {
console.error('获取商品列表失败:', error)
this.products = []
// 添加一些测试数据,避免页面空白
this.products = [
// {
// id: 1,
// name: "测试商品1",
// algorithm: "测试算法1",
// priceRange: "100-200",
// image: "https://img.yzcdn.cn/vant/apple-1.jpg"
// },
// {
// id: 2,
// name: "测试商品2",
// algorithm: "测试算法2",
// priceRange: "200-300",
// image: "https://img.yzcdn.cn/vant/apple-1.jpg"
// }
]
}
this.productListLoading = false
},
this.productListLoading = false
},
// 算法搜索(使用同一接口,传入 algorithm 参数)
handleAlgorithmSearch() {
const keyword = (this.searchAlgorithm || '').trim()
const next = { ...this.params }
if (keyword) {
next.algorithm = keyword
this.params.algorithm = keyword
} else {
delete next.algorithm
this.params.algorithm = ""
}
// 不重置下拉,只根据算法关键词查询
if (next.algorithm) this.fetchGetList({ ...next, coin: this.screenCurrency || undefined })
else this.fetchGetList(this.screenCurrency ? { coin: this.screenCurrency } : undefined)
this.params.keyword = keyword
this.fetchShopList(keyword ? { keyword } : undefined)
},
// 清空下拉时:只清 coin保留算法条件
handleCurrencyClear() {
this.screenCurrency = ""
this.params.coin = ""
const keyword = (this.searchAlgorithm || '').trim()
if (keyword) this.fetchGetList({ algorithm: keyword })
else this.fetchGetList()
},
// 清空算法时:只清 algorithm保留下拉 coin
// 清空搜索关键字
handleAlgorithmClear() {
this.searchAlgorithm = ""
this.params.algorithm = ""
const coin = this.screenCurrency
if (coin) this.fetchGetList({ coin })
else this.fetchGetList()
this.params.keyword = ""
this.fetchShopList()
},
handleProductClick(product) {
if (product.id || product.id == 0) {
this.$router.push(`/product/${product.id}`);
const id = (product && (product.shopId != null ? product.shopId : product.id))
if (id !== undefined && id !== null) {
this.$router.push(`/product/${id}`);
}

View File

@@ -3,41 +3,15 @@
<section class="container">
<h1 class="page-title">商品列表</h1>
<section class="filter-section">
<label class="required" style="margin-bottom: 10px">币种选择</label>
<div class="filter-row">
<!-- 币种下拉 -->
<el-select
class="input"
size="middle"
ref="screen"
v-model="screenCurrency"
placeholder="请选择"
@change="handleCurrencyChange"
@clear="handleCurrencyClear"
clearable
>
<el-option
v-for="item in currencyList"
:key="item.value"
:label="item.label"
:value="item.value"
>
<div style="display: flex; align-items: center">
<img :src="item.imgUrl" style="float: left; width: 20px" />
<span style="float: left; margin-left: 5px">{{ item.label }}</span>
</div>
</el-option>
</el-select>
<!-- 算法搜索框 -->
<el-input
v-model="searchAlgorithm"
size="middle"
placeholder="输入算法关键词"
placeholder="输入币种或算法搜索"
clearable
@clear="handleAlgorithmClear"
@keyup.enter.native="handleAlgorithmSearch"
style="width: 240px;"
class="search-input"
>
<template #append>
<el-button type="primary" @click="handleAlgorithmSearch">搜索</el-button>
@@ -47,8 +21,8 @@
</section>
<div class="product-list-grid">
<div
v-for="product in products"
:key="product.id"
v-for="(product, idx) in products"
:key="product.shopId || product.id || idx"
class="product-item"
@click="handleProductClick(product)"
tabindex="0"
@@ -57,14 +31,38 @@
<!-- <img :src="product.image || 'https://img.yzcdn.cn/vant/apple-1.jpg'" :alt="product.name" class="product-image" /> -->
<img src="../../assets/imgs/commodity.png" :alt="product.name" class="product-image" />
<div class="product-info">
<h4>商品: {{ product.name }}</h4>
<p style="font-size: 16px;margin-top: 10px;font-weight: bold;">算法: {{ product.algorithm }}</p>
<h4 class="title-line">
<span class="label">店铺</span>
<span class="value ellipsis" :title="product.shopName || product.name">{{ product.shopName || product.name }}</span>
</h4>
<p class="info-line coin-line">
<span class="label">币种</span>
<el-tooltip :content="product.coin" placement="top" :open-delay="80">
<span class="value ellipsis" tabindex="0" :aria-label="`币种 ${product.coin}`">{{ product.coin }}</span>
</el-tooltip>
</p>
<p class="info-line algorithm-line">
<span class="label">算法</span>
<el-tooltip :content="product.algorithm" placement="top" :open-delay="80">
<span class="value ellipsis" tabindex="0" :aria-label="`算法 ${product.algorithm}`">{{ product.algorithm }}</span>
</el-tooltip>
</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>
</div>
<span class="product-sold" aria-label="已售数量">已售{{ product && product.saleNumber != null ? product.saleNumber : 0 }}</span>
</div>
</div>
</div>
@@ -79,7 +77,6 @@
</template>
<script>
import { listProducts } from "../../utils/productService";
import { addToCart } from "../../utils/cartManager";
import Index from "./index";
@@ -89,6 +86,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 ''
}
},
/**
* 处理商品点击 - 跳转到详情页
*/
@@ -170,20 +180,80 @@ export default {
height: 40vh;
}
.product-image {
width: 68%;
height:65%;
width: 57%;
height:55%;
object-fit: cover;
margin-bottom: 12px;
}
.product-info {
width: 100%;
}
.title-line{
display: flex;
align-items: baseline;
gap: 6px;
font-size: 14px;
margin: 0 0 4px 0;
font-weight: normal;
}
.title-line .label{
font-weight: 700;
}
.info-line {
display: flex;
align-items: center;
gap: 6px;
}
.info-line .label { color: #334155; font-weight: 700; }
.info-line .value {
flex: 1;
min-width: 0;
}
.ellipsis {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.coin-line { font-size: 14px; margin-top: 8px; }
.algorithm-line { font-size: 14px; margin-top: 6px; }
.algorithm-line .value.bold { font-weight: bold; }
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.search-input{
width: 420px;
}
/* 轻度统一高度与圆角,保留 Element 默认视觉 */
::v-deep .search-input .el-input__inner{
height: 40px;
line-height: 40px;
border-radius: 6px 0 0 6px;
padding: 0 14px;
}
::v-deep .search-input .el-input__inner::placeholder{
color: #9aa4b2; /* 略浅的占位符 */
}
::v-deep .search-input .el-input__inner:focus{
box-shadow: 0 0 0 2px rgba(64,158,255,.12);
}
::v-deep .search-input .el-input-group__append .el-button{
height: 40px;
border-radius: 0 6px 6px 0;
padding: 0 16px;
}
.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 +265,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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long