Compare commits
7 Commits
power-leas
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| cbefb964d4 | |||
| 485226d9dc | |||
| 868632400a | |||
| a02c287715 | |||
| c7cee78798 | |||
| 50e5ce8d08 | |||
| bea1aa8e4c |
@@ -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/'
|
||||
|
||||
|
||||
# 路由懒加载
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
41
power_leasing/src/utils/amount.js
Normal file
41
power_leasing/src/utils/amount.js
Normal 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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
// 获取错误信息
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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 - 当前 pane(Element 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>
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
201
power_leasing/src/views/account/withdrawRecord.vue
Normal file
201
power_leasing/src/views/account/withdrawRecord.vue
Normal 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
@@ -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-ASIC,1-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('缺少店铺ID(shopId),无法加载商品列表')
|
||||
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 */ }
|
||||
},
|
||||
/**
|
||||
* 获取行在当前支付方式下的展示价格
|
||||
* 优先匹配 selectedPayKey(chain|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-ASIC,1-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 与 type(0-ASIC,1-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());
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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*purchaseQuantity;GPU: 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>
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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.
1
power_leasing/test/css/app.5ed3e526.css
Normal file
1
power_leasing/test/css/app.5ed3e526.css
Normal file
File diff suppressed because one or more lines are too long
1
power_leasing/test/css/app.c82f27e9.css
Normal file
1
power_leasing/test/css/app.c82f27e9.css
Normal file
File diff suppressed because one or more lines are too long
1
power_leasing/test/css/app.ca4b7f36.css
Normal file
1
power_leasing/test/css/app.ca4b7f36.css
Normal file
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
2
power_leasing/test/js/app.551d07c7.js
Normal file
2
power_leasing/test/js/app.551d07c7.js
Normal file
File diff suppressed because one or more lines are too long
1
power_leasing/test/js/app.551d07c7.js.map
Normal file
1
power_leasing/test/js/app.551d07c7.js.map
Normal file
File diff suppressed because one or more lines are too long
2
power_leasing/test/js/app.d22501a6.js
Normal file
2
power_leasing/test/js/app.d22501a6.js
Normal file
File diff suppressed because one or more lines are too long
1
power_leasing/test/js/app.d22501a6.js.map
Normal file
1
power_leasing/test/js/app.d22501a6.js.map
Normal file
File diff suppressed because one or more lines are too long
2
power_leasing/test/js/app.d49ccc2c.js
Normal file
2
power_leasing/test/js/app.d49ccc2c.js
Normal file
File diff suppressed because one or more lines are too long
1
power_leasing/test/js/app.d49ccc2c.js.map
Normal file
1
power_leasing/test/js/app.d49ccc2c.js.map
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user