每周更新

This commit is contained in:
2025-12-05 16:24:20 +08:00
parent 485226d9dc
commit cbefb964d4
21 changed files with 2546 additions and 700 deletions

View File

@@ -7,8 +7,8 @@ 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_API = 'http://10.168.2.220:8888'
VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
VUE_APP_BASE_URL = 'https://test.m2pool.com/'

View File

@@ -67,7 +67,7 @@ export function downloadClient() {
return request({
url: `/lease/user/downloadClient`,
method: 'get',
responseType: 'blob' // 关键:必须设置为 blob 才能正确下载二进制文件
})
}

View File

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

View File

@@ -37,6 +37,39 @@ export function deleteBatchGoodsForIsDelete(data) {
})
}
//购物车列表V2
export function getGoodsListV2(data) {
return request({
url: `/lease/v2/shopping/cart/getGoodsListV2`,
method: 'post',
data
})
}
//批量删除购物车中已下架商品
export function deleteBatchGoodsForIsDeleteV2(data) {
return request({
url: `/lease/v2/shopping/cart/deleteBatchGoodsForIsDeleteV2`,
method: 'post',
data
})
}
//批批量删除购物车中商品
export function deleteBatchGoodsV2(data) {
return request({
url: `/lease/v2/shopping/cart/deleteBatchGoodsV2`,
method: 'post',
data
})
}

View File

@@ -102,6 +102,16 @@ export function getChainAndCoin(data) {
}
// 卖家绑定钱包明细
export function getShopConfigV2(data) {
return request({
url: `/lease/v2/shop/getShopConfigV2`,
method: 'post',
data
})
}

View File

@@ -125,6 +125,39 @@ export function updateProductListForShopWalletConfig(data) {
})
}
// 卖家绑定钱包明细
export function getShopConfigV2(data) {
return request({
url: `/lease/v2/shop/getShopConfigV2`,
method: 'post',
data
})
}
// 卖家提现
export function withdrawBalanceForSeller(data) {
return request({
url: `/lease/v2/shop/withdrawBalanceForSeller`,
method: 'post',
data
})
}
// 修改钱包配置
export function balanceWithdrawListV2(data) {
return request({
url: `/lease/v2/shop/balanceWithdrawList`,
method: 'post',
data
})
}

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@
必须添加出售机器否则买家无法下单买家点击某个商品后会看到该商品下的机器明细并进行选购
</li>
</ol>
<div class="guide-note">提示建议先创建店铺 完成钱包绑定 创建商品 添加出售机器的顺序避免漏配导致无法收款或无法下单</div>
<div class="guide-note">提示建议先创建店铺 完成钱包绑定 创建商品的顺序避免漏配导致无法收款或无法下单</div>
</div>
</el-card>
@@ -61,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">
@@ -89,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>
@@ -108,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">
@@ -139,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="请输入钱包地址" />
@@ -193,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 {
@@ -227,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: {
@@ -261,12 +324,199 @@ 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空值显示为 '-'
*/
@@ -362,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
@@ -430,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('请输入钱包地址')
@@ -446,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()
@@ -690,6 +938,14 @@ export default {
.guide-note { margin-top: 10px; color: #6b7280; font-size: 13px; background: #f9fafb; border: 1px dashed #e5e7eb; padding: 8px 10px; border-radius: 8px; }
.coin-list { display: flex; align-items: center; gap: 8px; }
.coin-img { width: 20px; height: 20px; border-radius: 4px; display: inline-block; }
/* 提现弹窗样式(与钱包页统一) */
.balance-info { font-size: 12px; color: #666; margin-top: 4px; text-align: left; }
.fee-info { font-size: 12px; color: #e6a23c; margin-top: 4px; text-align: left; }
.actual-amount-info { font-size: 12px; color: #67c23a; margin-top: 4px; text-align: left; font-weight: 500; }
.address-tip { font-size: 12px; color: #f56c6c; margin-top: 4px; line-height: 1.4; text-align: left; }
/* 余额数字红色显示 */
.balance-num { color: #ff4d4f; font-weight: 600; }
.balance-unit { color: #606266; }
</style>
<style>

View File

@@ -22,48 +22,74 @@
<el-radio label="GPU">GPU</el-radio>
</el-radio-group>
</el-form-item>
<!-- ASIC币种与算法支持多个逗号分隔 -->
<el-form-item label="币种(多个用逗号隔开)" prop="coinsInput" :required="form.machineCategory === 'ASIC'">
<el-input
v-model="form.coinsInput"
placeholder="例如USDT, BTC, ETH"
style="width: 50%;"
@input="handleCoinsInput"
/>
<!-- ASIC币种/算法/理论算力/单位 同行支持多行动态增删 -->
<el-form-item
v-if="form.machineCategory === 'ASIC'"
label="币种/算法/算力/单位"
prop="coinAndAlgoList"
:required="true"
>
<div class="coin-algo-rows">
<div
class="coin-algo-line"
v-for="(row, idx) in form.coinAndAlgoList"
:key="idx"
>
<el-input
v-model="row.coin"
placeholder="币种"
class="coin-input"
@input="handleCoinInput(idx)"
/>
<el-input
v-model="row.algorithm"
placeholder="算法"
class="algo-input"
@input="handleAlgorithmInput(idx)"
/>
<el-input
v-model="row.theoryPower"
placeholder="理论算力"
inputmode="decimal"
class="power-input"
@input="handleCoinRowTheoryInput(idx)"
/>
<el-select
v-model="row.unit"
placeholder="单位"
class="unit-select"
@change="handleCoinRowUnitChange(idx, $event)"
>
<el-option label="KH/S" value="KH/S" />
<el-option label="MH/S" value="MH/S" />
<el-option label="GH/S" value="GH/S" />
<el-option label="TH/S" value="TH/S" />
<el-option label="PH/S" value="PH/S" />
</el-select>
<el-button
class="op-btn"
type="primary"
icon="el-icon-plus"
circle
@click="handleAddCoinAlgoRow"
:aria-label="'新增一行'"
/>
<el-button
v-if="form.coinAndAlgoList.length > 1"
class="op-btn"
icon="el-icon-minus"
circle
@click="handleRemoveCoinAlgoRow(idx)"
:aria-label="'删除该行'"
/>
</div>
</div>
</el-form-item>
<el-form-item label="算法(多个用逗号隔开)" prop="algorithmsInput" :required="form.machineCategory === 'ASIC'">
<el-input
v-model="form.algorithmsInput"
placeholder="例如SHA-256, ETHASH"
style="width: 50%;"
@input="handleAlgorithmsInput"
/>
</el-form-item>
<div style="text-align:left; color:#909399; font-size:12px; margin:-6px 0 10px 160px;">
输入多个用逗号隔开
</div>
<el-form-item label="矿机型号" prop="type" :required="true">
<el-input style="width: 50%;" v-model="form.type" placeholder="示例:龍珠" :maxlength="20" @input="handleTypeInput" />
</el-form-item>
<el-form-item label="理论算力" prop="theoryPower">
<el-input
v-model="form.theoryPower"
placeholder="请输入单机理论算力"
inputmode="decimal"
@input="handleNumeric('theoryPower')"
style="width: 50%;"
/>
</el-form-item>
<el-form-item label="算力单位" prop="unit">
<el-select v-model="form.unit" placeholder="请选择算力单位">
<el-option label="KH/S" value="KH/S" />
<el-option label="MH/S" value="MH/S" />
<el-option label="GH/S" value="GH/S" />
<el-option label="TH/S" value="TH/S" />
<el-option label="PH/S" value="PH/S" />
</el-select>
</el-form-item>
<!-- 理论算力与单位已合并到上面的同行多行输入 -->
<el-form-item label="最大租赁天数" prop="maxLeaseDays">
<el-input
v-model="form.maxLeaseDays"
@@ -197,6 +223,7 @@
<script>
import { addSingleOrBatchMachine ,downloadClient,addAsicMachine} from '../../api/machine'
import { getPayTypes } from '../../api/products'
import request from '../../utils/request'
export default {
name: 'AccountProductMachineAdd',
data() {
@@ -209,13 +236,12 @@ export default {
machineCategory: 'ASIC',
/** 出售机器数量(仅 ASIC 模式使用) */
sellCount: '',
/** ASIC 模式下币种/算法输入(逗号分隔的原始文本 */
coinsInput: '',
algorithmsInput: '',
/** ASIC币种/算法/理论算力/单位 行编辑最多10行 */
coinAndAlgoList: [
{ coin: '', algorithm: '', theoryPower: '', unit: 'TH/S' }
],
powerDissipation: null,
theoryPower: null,
type: '',
unit: 'TH/S',
cost: '',
costMap: {}, // { 'CHAIN-COIN': '123.45' }
maxLeaseDays: ''
@@ -245,48 +271,8 @@ export default {
trigger: 'blur'
}
],
coinsInput: [
{
validator: (rule, value, callback) => {
if (String(this.form.machineCategory).toUpperCase() !== 'ASIC') { callback(); return }
const s = String(value || '').trim()
if (!s) { callback(new Error('请输入币种,多个用逗号隔开')); return }
// 禁止汉字,且仅允许英数字与逗号/空格/连字符
if (/[\u4e00-\u9fa5]/.test(s)) { callback(new Error('币种不允许输入汉字')); return }
// 逐项校验英文、数字长度1-10
const tokens = s.split(/[,\s、]+/).map(i => i.trim()).filter(Boolean)
const pattern = /^[A-Za-z0-9]{1,10}$/
for (let i = 0; i < tokens.length; i += 1) {
if (!pattern.test(tokens[i])) {
callback(new Error(`币种“${tokens[i]}”格式非法,仅允许字母或数字`))
return
}
}
callback()
},
trigger: 'blur'
}
],
algorithmsInput: [
{
validator: (rule, value, callback) => {
if (String(this.form.machineCategory).toUpperCase() !== 'ASIC') { callback(); return }
const s = String(value || '').trim()
if (!s) { callback(new Error('请输入算法,多个用逗号隔开')); return }
if (/[\u4e00-\u9fa5]/.test(s)) { callback(new Error('算法不允许输入汉字')); return }
// 逐项校验(字母/数字/连字符2-20
const tokens = s.split(/[,\s、]+/).map(i => i.trim()).filter(Boolean)
const pattern = /^[A-Za-z0-9-]{2,20}$/
for (let i = 0; i < tokens.length; i += 1) {
if (!pattern.test(tokens[i])) {
callback(new Error(`算法“${tokens[i]}”格式非法,仅允许字母、数字或“-”`))
return
}
}
callback()
},
trigger: 'blur'
}
coinAndAlgoList: [
{ validator: (rule, value, callback) => this.validateCoinAlgoRows(rule, value, callback), trigger: 'blur' }
],
sellCount: [
{
@@ -316,21 +302,6 @@ export default {
trigger: 'blur'
}
],
theoryPower: [
{ required: true, message: '理论算力不能为空', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const str = String(value || '')
if (!str) { callback(new Error('理论算力不能为空')); return }
const pattern = /^\d{1,6}(\.\d{1,4})?$/
if (!pattern.test(str)) { callback(new Error('理论算力整数最多6位小数最多4位')); return }
if (Number(str) <= 0) { callback(new Error('理论算力必须大于0')); return }
callback()
},
trigger: 'blur'
}
],
unit: [ { required: true, message: '请选择算力单位', trigger: 'change' } ],
cost: [
{
validator(rule, value, callback) {
@@ -510,6 +481,109 @@ export default {
this.getPayTypes()
},
methods: {
/** ASIC 行校验:币种/算法/理论算力/单位 */
validateCoinAlgoRows(rule, value, callback) {
try {
const rows = Array.isArray(this.form.coinAndAlgoList) ? this.form.coinAndAlgoList : []
if (!rows.length) { callback(new Error('请至少添加一行币种/算法/算力/单位')); return }
const coinPattern = /^[A-Za-z0-9]{1,10}$/
const algoPattern = /^[A-Za-z0-9-]{2,20}$/
const powerPattern = /^\d{1,6}(\.\d{1,4})?$/
for (let i = 0; i < rows.length; i += 1) {
const r = rows[i] || {}
const coin = String(r.coin || '').trim()
const algo = String(r.algorithm || '').trim()
const power = String(r.theoryPower || '').trim()
const unit = String(r.unit || '').trim()
if (!coin) { callback(new Error(`${i + 1} 行:请输入币种`)); return }
if (!coinPattern.test(coin)) { callback(new Error(`${i + 1}币种仅允许字母或数字1-10 位`)); return }
if (!algo) { callback(new Error(`${i + 1} 行:请输入算法`)); return }
if (!algoPattern.test(algo)) { callback(new Error(`${i + 1} 行:算法仅允许字母/数字/“-”2-20 位`)); return }
if (!power || !powerPattern.test(power) || Number(power) <= 0) {
callback(new Error(`${i + 1}理论算力需大于0整数最多6位小数最多4位`)); return
}
if (!unit) { callback(new Error(`${i + 1} 行:请选择算力单位`)); return }
}
callback()
} catch (e) {
callback(new Error('请检查币种/算法/算力/单位填写'))
}
},
/** 行:币种输入过滤(去中文,仅字母数字) */
handleCoinInput(index) {
const r = this.form.coinAlgoRows[index]
let v = String(r.coin || '')
v = v.replace(/[\u4e00-\u9fa5]/g, '').replace(/[^A-Za-z0-9]/g, '')
this.$set(this.form.coinAlgoRows[index], 'coin', v.toUpperCase())
},
/** 行:算法输入过滤(去中文,仅字母数字与- */
handleAlgorithmInput(index) {
const r = this.form.coinAlgoRows[index]
let v = String(r.algorithm || '')
v = v.replace(/[\u4e00-\u9fa5]/g, '').replace(/[^A-Za-z0-9-]/g, '')
this.$set(this.form.coinAlgoRows[index], 'algorithm', v.toUpperCase())
},
/** 行理论算力输入限制6整数4小数 */
handleCoinRowTheoryInput(index) {
let v = String(this.form.coinAndAlgoList[index].theoryPower ?? '')
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
}
const endsWithDot = v.endsWith('.')
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (intPart.length > 6) intPart = intPart.slice(0, 6)
if (decPart) decPart = decPart.slice(0, 4)
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
this.$set(this.form.coinAndAlgoList[index], 'theoryPower', v)
},
/** 行:单位变更 */
handleCoinRowUnitChange(index, value) {
this.$set(this.form.coinAndAlgoList[index], 'unit', value)
},
/** 新增一行 */
handleAddCoinAlgoRow() {
if (this.form.coinAndAlgoList.length >= 10) {
this.$message.warning('最多添加 10 行')
return
}
const last = (this.form.coinAndAlgoList[this.form.coinAndAlgoList.length - 1]) || { unit: 'TH/S' }
this.form.coinAndAlgoList.push({ coin: '', algorithm: '', theoryPower: '', unit: last.unit || 'TH/S' })
},
/** 删除一行 */
handleRemoveCoinAlgoRow(index) {
if (this.form.coinAndAlgoList.length <= 1) return
this.form.coinAndAlgoList.splice(index, 1)
},
/** 从多行聚合 coin CSV */
buildCoinCsvFromRows() {
const set = new Set()
const rows = Array.isArray(this.form.coinAndAlgoList) ? this.form.coinAndAlgoList : []
rows.forEach(r => {
const token = String(r.coin || '')
.split(/[,\s、]+/)
.map(s => s.trim().toUpperCase())
.filter(Boolean)
token.forEach(t => set.add(t))
})
return Array.from(set).join(',')
},
/** 从多行聚合 algorithm CSV */
buildAlgoCsvFromRows() {
const set = new Set()
const rows = Array.isArray(this.form.coinAndAlgoList) ? this.form.coinAndAlgoList : []
rows.forEach(r => {
const token = String(r.algorithm || '')
.split(/[,\s、]+/)
.map(s => s.trim().toUpperCase())
.filter(Boolean)
token.forEach(t => set.add(t))
})
return Array.from(set).join(',')
},
/** 实时过滤币种输入中的中文字符(仅保留英文/数字/分隔符) */
handleCoinsInput() {
let v = String(this.form.coinsInput || '')
@@ -629,27 +703,31 @@ export default {
*/
handleDownloadClient() {
// 走后端接口下载客户端程序
downloadClient()
.then((res) => {
// 处理 blob 下载(兼容封装返回 data 或直接返回 Blob
const data = (res && res.data) ? res.data : res
const blob = data instanceof Blob ? data : new Blob([data], { type: 'application/octet-stream' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.style.display = 'none'
a.href = url
// 默认文件名(可由后端 Content-Disposition 提供,这里简单兜底)
a.download = 'gpu-client.zip'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
this.$message.success('客户端下载已开始')
this.hasDownloadedClient = true
})
.catch(() => {
this.$message.error('下载失败,请稍后重试')
let userEmail =JSON.parse(localStorage.getItem('leasEmail'))
if (!userEmail) {
// 弹出确认框,用户确认后跳转到外部网站
this.$confirm('获取用户信息失败,请重新进入网站?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 用户点击确认,跳转到外部网站
window.open('https://test.m2pool.com/', '_blank')
}).catch(() => {
// 用户点击取消,不执行任何操作
})
return
}
console.log(userEmail,'userEmail');
this.downloadUrl = ` ${request.defaults.baseURL}/lease/user/downloadClient?userEmail=${userEmail}`
let a = document.createElement(`a`)
a.href = this.downloadUrl
a.click()
},
/**
* GPU 客户端已启动:跳转至商品列表
@@ -1109,8 +1187,8 @@ export default {
}
// 统一售价与最大租赁天数已在表单级校验中处理,无需逐机校验
// 组装确认数据并弹框
const coinStr = this.normalizeCsv(this.form.coinsInput || this.form.coin, true)
const algoStr = this.normalizeCsv(this.form.algorithmsInput, true)
const coinStr = this.buildCoinCsvFromRows()
const algoStr = this.buildAlgoCsvFromRows()
this.confirmData = {
coin: coinStr || '-',
algorithm: algoStr || '-',
@@ -1125,15 +1203,18 @@ export default {
this.saving = true
try {
// 统一售卖新增接口参数
const list = (this.form.coinAndAlgoList || []).map(r => ({
coin: String(r.coin || '').toUpperCase().trim(),
algorithm: String(r.algorithm || '').toUpperCase().trim(),
theoryPower: Number(r.theoryPower) || 0,
unit: r.unit
}))
const payload = {
// 逗号分隔(中英文逗号都兼容),统一为英文逗号并大写
coin: this.normalizeCsv(this.form.coinsInput || this.form.coin, true),
algorithm: this.normalizeCsv(this.form.algorithmsInput, true),
coinAndAlgoList: list,
maxLeaseDays: Number(this.form.maxLeaseDays) || 0,
name: this.form.type,
powerDissipation: Number(this.form.powerDissipation) || 0,
theoryPower: Number(this.form.theoryPower) || 0,
unit: this.form.unit,
saleNumbers: Number(this.form.sellCount) || 0,
priceList: this.buildPriceList()
}
@@ -1229,5 +1310,13 @@ export default {
.price-multi { gap: 6px; }
.price-items { gap: 6px; }
.cost-multi { gap: 6px; }
/* ASIC 币种/算法/算力/单位 多行 */
.coin-algo-rows { display: grid; gap: 8px; width: 100%; }
.coin-algo-line { display: flex; align-items: center; gap: 8px; }
.coin-algo-line .coin-input { width: 18%; min-width: 140px; }
.coin-algo-line .algo-input { width: 24%; min-width: 160px; }
.coin-algo-line .power-input { width: 20%; min-width: 140px; }
.coin-algo-line .unit-select { width: 16%; min-width: 120px; }
.coin-algo-line .op-btn { flex: 0 0 auto; }
</style>

View File

@@ -51,7 +51,7 @@
<!-- 售价展示币种选择影响 ASIC 表格售价列展示 -->
<div
v-if="payTypes && payTypes.length"
v-if="listParams.type === 0 && payTypes && payTypes.length"
class="price-select-bar"
style="margin:8px 0 4px; display:flex; justify-content:flex-end; align-items:center;"
>
@@ -103,16 +103,24 @@
</el-tag>
</template>
</el-table-column>
<!-- 币种过长省略hover 显示 -->
<el-table-column prop="coin" label="币种" min-width="100" show-overflow-tooltip />
<!-- 算法过长省略hover 显示 -->
<el-table-column prop="algorithm" label="算法" min-width="100" show-overflow-tooltip />
<!-- 币种来自 coinAndAlgoList多项用逗号拼接兜底 row.coin -->
<el-table-column label="币种" min-width="140" show-overflow-tooltip>
<template #default="scope">
<div class="ellipsis-cell">{{ getRowCoinText(scope.row) }}</div>
</template>
</el-table-column>
<!-- 算法来自 coinAndAlgoList多项用逗号拼接兜底 row.algorithm -->
<el-table-column label="算法" min-width="160" show-overflow-tooltip>
<template #default="scope">
<div class="ellipsis-cell">{{ getRowAlgorithmText(scope.row) }}</div>
</template>
</el-table-column>
<!-- 矿机型号 -->
<el-table-column prop="name" label="矿机型号" />
<!-- 理论算力附带单位 -->
<el-table-column label="理论算力">
<el-table-column label="理论算力" min-width="170" show-overflow-tooltip>
<template #default="scope">
<span>{{ getTheoryText(scope.row) }}</span>
<div class="ellipsis-cell">{{ getTheoryText(scope.row) }}</div>
</template>
</el-table-column>
<!-- 功耗(kw/h) -->
@@ -272,30 +280,76 @@
<el-dialog
:visible.sync="editDialog.visible"
:close-on-click-modal="false"
width="720px"
width="70VW"
:title="'编辑商品 - ' + ((editDialog.form && editDialog.form.name) ? editDialog.form.name : '')"
>
<el-form
v-if="editDialog.form"
:model="editDialog.form"
label-width="120px"
label-width="160px"
ref="editForm"
class="edit-form"
>
<el-form-item label="矿机型号">
<el-input v-model.trim="editDialog.form.name" maxlength="60" />
</el-form-item>
<el-form-item label="币种(逗号隔开)">
<el-input
v-model.trim="editDialog.form.coin"
placeholder="例如USDT,ETH"
/>
</el-form-item>
<el-form-item label="算法(逗号隔开)">
<el-input
v-model.trim="editDialog.form.algorithm"
placeholder="例如SHA256,ETHASH"
/>
<!-- 编辑币种/算法/理论算力/单位可增删最多10行 -->
<el-form-item label="币种/算法/算力/单位">
<div class="coin-algo-rows">
<div
class="coin-algo-line"
v-for="(row, idx) in editDialog.form.coinAndAlgoList"
:key="'edit-ca-' + idx"
>
<el-input
v-model="row.coin"
placeholder="币种"
class="coin-input"
@input="editHandleCoinInput(idx)"
/>
<el-input
v-model="row.algorithm"
placeholder="算法"
class="algo-input"
@input="editHandleAlgorithmInput(idx)"
/>
<el-input
v-model="row.theoryPower"
placeholder="理论算力"
inputmode="decimal"
class="power-input"
@input="editHandleRowTheoryInput(idx)"
/>
<el-select
v-model="row.unit"
placeholder="单位"
class="unit-select"
@change="editHandleRowUnitChange(idx, $event)"
>
<el-option label="KH/S" value="KH/S" />
<el-option label="MH/S" value="MH/S" />
<el-option label="GH/S" value="GH/S" />
<el-option label="TH/S" value="TH/S" />
<el-option label="PH/S" value="PH/S" />
</el-select>
<el-button
class="op-btn"
type="primary"
icon="el-icon-plus"
circle
@click="editHandleAddRow"
:aria-label="'新增一行'"
/>
<el-button
v-if="(editDialog.form.coinAndAlgoList || []).length > 1"
class="op-btn"
icon="el-icon-minus"
circle
@click="editHandleRemoveRow(idx)"
:aria-label="'删除该行'"
/>
</div>
</div>
</el-form-item>
<el-form-item label="最大租赁天数">
<el-input
@@ -319,24 +373,7 @@
style="width: 200px"
/>
</el-form-item>
<el-form-item label="理论算力">
<div style="display:flex; gap:12px; align-items:center;">
<el-input
v-model="editDialog.form.theoryPower"
placeholder="理论算力"
@input="editDialog.form.theoryPower = (String(editDialog.form.theoryPower||'').replace(/[^\d.]/g,''))"
style="width: 220px"
/>
<el-select v-model="editDialog.form.unit" placeholder="单位" style="width: 120px">
<el-option label="H/S" value="H/S" />
<el-option label="KH/S" value="KH/S" />
<el-option label="MH/S" value="MH/S" />
<el-option label="GH/S" value="GH/S" />
<el-option label="TH/S" value="TH/S" />
<el-option label="PH/S" value="PH/S" />
</el-select>
</div>
</el-form-item>
<!-- 理论算力与单位已合并到上方多行编辑 -->
<el-form-item label="出售数量(台)">
<el-input
v-model="editDialog.form.saleNumbers"
@@ -500,6 +537,89 @@ export default {
}
},
methods: {
/** 编辑弹窗:币种输入过滤(仅字母数字,转大写) */
editHandleCoinInput(index) {
const r = this.editDialog.form.coinAndAlgoList[index]
let v = String(r.coin || '')
v = v.replace(/[\u4e00-\u9fa5]/g, '').replace(/[^A-Za-z0-9]/g, '')
this.$set(this.editDialog.form.coinAndAlgoList[index], 'coin', v.toUpperCase())
},
/** 编辑弹窗:算法输入过滤(仅字母数字和-,转大写) */
editHandleAlgorithmInput(index) {
const r = this.editDialog.form.coinAndAlgoList[index]
let v = String(r.algorithm || '')
v = v.replace(/[\u4e00-\u9fa5]/g, '').replace(/[^A-Za-z0-9-]/g, '')
this.$set(this.editDialog.form.coinAndAlgoList[index], 'algorithm', v.toUpperCase())
},
/** 编辑弹窗理论算力限制6整数+4小数 */
editHandleRowTheoryInput(index) {
let v = String(this.editDialog.form.coinAndAlgoList[index].theoryPower ?? '')
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
}
const endsWithDot = v.endsWith('.')
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (intPart.length > 6) intPart = intPart.slice(0, 6)
if (decPart) decPart = decPart.slice(0, 4)
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
this.$set(this.editDialog.form.coinAndAlgoList[index], 'theoryPower', v)
},
/** 编辑弹窗:单位变更 */
editHandleRowUnitChange(index, value) {
this.$set(this.editDialog.form.coinAndAlgoList[index], 'unit', value)
},
/** 编辑弹窗新增一行最多10行 */
editHandleAddRow() {
const list = this.editDialog.form.coinAndAlgoList || []
if (list.length >= 10) {
this.$message.warning('最多添加 10 行')
return
}
const last = list[list.length - 1] || { unit: 'TH/S' }
list.push({ coin: '', algorithm: '', theoryPower: '', unit: last.unit || 'TH/S', coinAndPowerId: null })
this.$set(this.editDialog.form, 'coinAndAlgoList', list)
},
/** 编辑弹窗删除一行至少保留1行 */
editHandleRemoveRow(index) {
const list = this.editDialog.form.coinAndAlgoList || []
if (list.length <= 1) return
list.splice(index, 1)
this.$set(this.editDialog.form, 'coinAndAlgoList', list)
},
/** 行:币种(来自 coinAndAlgoList去重后用中文逗号拼接兜底 row.coin */
getRowCoinText(row) {
try {
const list = Array.isArray(row && row.coinAndAlgoList) ? row.coinAndAlgoList : []
if (list.length) {
const coins = list.map(i => String(i && i.coin ? i.coin : '').trim()).filter(Boolean)
const uniq = Array.from(new Set(coins))
if (uniq.length) return uniq.join('')
}
const fallback = String(row && row.coin ? row.coin : '').trim()
return fallback || '-'
} catch (e) {
return String(row && row.coin ? row.coin : '').trim() || '-'
}
},
/** 行:算法(来自 coinAndAlgoList去重后用中文逗号拼接兜底 row.algorithm */
getRowAlgorithmText(row) {
try {
const list = Array.isArray(row && row.coinAndAlgoList) ? row.coinAndAlgoList : []
if (list.length) {
const algos = list.map(i => String(i && i.slogithm ? i.slogithm : (i && i.algorithm ? i.algorithm : '')).trim()).filter(Boolean)
const uniq = Array.from(new Set(algos))
if (uniq.length) return uniq.join('')
}
const fallback = String(row && row.algorithm ? row.algorithm : '').trim()
return fallback || '-'
} catch (e) {
return String(row && row.algorithm ? row.algorithm : '').trim() || '-'
}
},
/** 从首行 priceList 推断表头单位(优先按 selectedPayKey 匹配) */
computeUnitFromFirstRow() {
try {
@@ -655,12 +775,29 @@ export default {
return '-'
}
},
/** 展示理论算力(带单位 */
/** 展示理论算力(来自 coinAndAlgoList每项“值 单位”,多项以中文逗号拼接;兜底 row.theoryPower + row.unit */
getTheoryText(row) {
const val = row && row.theoryPower != null ? String(row.theoryPower) : ''
if (!val) return '-'
const unit = (row && row.unit ? String(row.unit) : '').trim().toUpperCase()
return unit ? `${val} ${unit}` : val
try {
const list = Array.isArray(row && row.coinAndAlgoList) ? row.coinAndAlgoList : []
if (list.length) {
const parts = list.map(i => {
const power = (i && i.theoryPower != null) ? String(i.theoryPower) : ''
const unit = (i && (i.unit || i.Unit)) ? String(i.unit || i.Unit).trim().toUpperCase() : ''
const text = power ? (unit ? `${power} ${unit}` : power) : ''
return text
}).filter(Boolean)
if (parts.length) return parts.join(' ')
}
const val = row && row.theoryPower != null ? String(row.theoryPower) : ''
if (!val) return '-'
const unit = (row && row.unit ? String(row.unit) : '').trim().toUpperCase()
return unit ? `${val} ${unit}` : val
} catch (e) {
const val = row && row.theoryPower != null ? String(row.theoryPower) : ''
if (!val) return '-'
const unit = (row && row.unit ? String(row.unit) : '').trim().toUpperCase()
return unit ? `${val} ${unit}` : val
}
},
/** 展示功耗kw/h */
getPowerDissText(row) {
@@ -1014,27 +1151,41 @@ export default {
/** 编辑 */
handleEdit(row) {
// coinAndAlgoList从后端 coinAndAlgoList 转换;若没有则用旧字段构造单行
const srcList = Array.isArray(row.coinAndAlgoList) ? row.coinAndAlgoList : []
const coinAndAlgoList = srcList.length
? srcList.map(it => ({
coin: String(it && (it.coin || '')).trim(),
algorithm: String(it && (it.slogithm || it.algorithm || '')).trim(),
theoryPower: (it && it.theoryPower != null) ? String(it.theoryPower) : '',
unit: String(it && (it.unit || '')).trim() || 'TH/S',
coinAndPowerId: it && (it.coinAndPowerId != null) ? it.coinAndPowerId : null
}))
: [{
coin: String(row.coin || '').trim(),
algorithm: String(row.algorithm || '').trim(),
theoryPower: (row && row.theoryPower != null) ? String(row.theoryPower) : '',
unit: String(row.unit || 'TH/S').trim(),
coinAndPowerId: null
}]
const form = {
id: row.id,
name: row.name || '',
coin: row.coin || '',
algorithm: row.algorithm || '',
coinAndAlgoList,
maxLeaseDays: row.maxLeaseDays || '',
powerDissipation: row.powerDissipation || row.powerDissipation || '',
saleNumbers: row.saleNumbers || '',
theoryPower: row.theoryPower || '',
unit: row.unit || 'GH/S',
state: (row && (row.state === 0 || row.state === 1)) ? row.state : 0
}
// 构建可编辑的价格列表:以支持的支付方式为模板
const srcList = Array.isArray(row.priceList) ? row.priceList : []
const priceSrc = Array.isArray(row.priceList) ? row.priceList : []
const template = (this.payTypes || []).map(pt => ({
chain: (pt.payChain || pt.chain || '').toString(),
coin: (pt.payCoin || pt.coin || '').toString(),
payTypeId: pt.payTypeId || pt.id || 0
}))
this.editDialog.priceList = template.map(t => {
const hit = srcList.find(p =>
const hit = priceSrc.find(p =>
String(p.chain || p.payChain || '') === t.chain &&
String(p.coin || p.payCoin || '') === t.coin
)
@@ -1062,16 +1213,40 @@ export default {
const f = this.editDialog.form
// 基础校验
if (!String(f.name || '').trim()) { this.$message.warning('矿机型号不能为空'); return }
if (!String(f.coin || '').trim()) { this.$message.warning('币种不能为空'); return }
if (!String(f.algorithm || '').trim()) { this.$message.warning('算法不能为空'); return }
// 校验 coinAndAlgoList
const list = Array.isArray(f.coinAndAlgoList) ? f.coinAndAlgoList : []
if (!list.length) { this.$message.warning('请至少添加一行币种/算法/算力/单位'); return }
const coinPattern = /^[A-Za-z0-9]{1,10}$/
const algoPattern = /^[A-Za-z0-9-]{2,20}$/
const powerPattern = /^\d{1,6}(\.\d{1,4})?$/
for (let i = 0; i < list.length; i += 1) {
const r = list[i] || {}
const coin = String(r.coin || '').trim()
const algo = String(r.algorithm || '').trim()
const power = String(r.theoryPower || '').trim()
const unit = String(r.unit || '').trim()
if (!coin) { this.$message.warning(`${i + 1} 行:请输入币种`); return }
if (!coinPattern.test(coin)) { this.$message.warning(`${i + 1}币种仅允许字母或数字1-10 位`); return }
if (!algo) { this.$message.warning(`${i + 1} 行:请输入算法`); return }
if (!algoPattern.test(algo)) { this.$message.warning(`${i + 1} 行:算法仅允许字母/数字/“-”2-20 位`); return }
if (!power || !powerPattern.test(power) || Number(power) <= 0) {
this.$message.warning(`${i + 1}理论算力需大于0整数最多6位小数最多4位`); return
}
if (!unit) { this.$message.warning(`${i + 1} 行:请选择算力单位`); return }
}
const days = parseInt(String(f.maxLeaseDays || '').replace(/[^\d]/g, ''), 10)
if (!(Number.isInteger(days) && days >= 1 && days <= 365)) { this.$message.warning('最大租赁天数需为1-365的整数'); return }
const sale = parseInt(String(f.saleNumbers || '').replace(/[^\d]/g, ''), 10)
if (!Number.isInteger(sale) || sale < 0) { this.$message.warning('出售数量应为非负整数'); return }
const payload = {
const payload = {
id: f.id,
algorithm: String(f.algorithm || '').trim(),
coin: String(f.coin || '').trim(),
coinAndAlgoList: (f.coinAndAlgoList || []).map(it => ({
coin: String(it.coin || '').trim().toUpperCase(),
algorithm: String(it.algorithm || '').trim().toUpperCase(),
theoryPower: Number(it.theoryPower) || 0,
unit: it.unit,
coinAndPowerId: it.coinAndPowerId || null
})),
maxLeaseDays: days,
name: String(f.name || '').trim(),
powerDissipation: Number(String(f.powerDissipation || '0').replace(/[^\d.]/g, '')) || 0,
@@ -1083,8 +1258,6 @@ export default {
productMachineId: p.productMachineId || f.id || 0
})),
saleNumbers: sale,
theoryPower: Number(String(f.theoryPower || '0').replace(/[^\d.]/g, '')) || 0,
unit: String(f.unit || '').trim() || 'GH/S',
state: (f && (f.credentials ? f.state : f.state)) ?? 0
}
// 价格至少有一个填写
@@ -1247,6 +1420,13 @@ export default {
padding-left: 12px; /* 对齐到上方 el-input 的内边距视觉效果 */
}
/* 编辑弹窗:左侧标签不换行(长文本保持单行,可溢出隐藏省略) */
.edit-form :deep(.el-form-item__label) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
::v-deep .el-form-item__content{
text-align: left;
}
@@ -1294,6 +1474,24 @@ export default {
font-weight: 700;
}
/* 表格单元格文本溢出省略hover 显示完整(依赖列的 show-overflow-tooltip */
.ellipsis-cell {
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 编辑弹窗:币种/算法/算力/单位 多行布局 */
.coin-algo-rows { display: grid; gap: 8px; width: 100%; }
.coin-algo-line { display: flex; align-items: center; gap: 8px; }
.coin-algo-line .coin-input { width: 18%; min-width: 140px; }
.coin-algo-line .algo-input { width: 24%; min-width: 160px; }
.coin-algo-line .power-input { width: 20%; min-width: 140px; }
.coin-algo-line .unit-select { width: 16%; min-width: 120px; }
.coin-algo-line .op-btn { flex: 0 0 auto; }
::v-deep .el-input__prefix, .el-input__suffix{
top:24%;
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
import { getProductById } from '../../utils/productService'
import { getMachineInfo, getPayTypes,getShopMachineList,addGoodsV2 } from '../../api/products'
import { truncateAmountByCoin, truncateTo6 } from '../../utils/amount'
import { getGoodsListV2 } from '../../api/shoppingCart'
export default {
name: 'ProductDetail',
@@ -162,218 +163,7 @@ export default {
this.machineType = savedType
}
}catch(e){/* noop */}
let arr={
"meta": {
"pageNum": 1, // 当前页码(后端分页用,可选)
"pageSize": 10, // 每页条数(后端分页用,可选)
"total": 123 // 总条数(后端分页用,可选)
},
"columns": [
{
"key": "model", // 列字段名:与 rows 中同名字段映射
"label": "型号", // 表头显示文本
"type": "text", // 列类型text/amount/hashrate/days
"fixed": "left", // 是否左固定列left/right/不传
"width": 100 // 列宽(可选)
},
{
"key": "price", // 价格列字段名
"label": "价格", // 表头显示文本
"type": "amount", // 金额类型:仅渲染数值;单位来自行级 priceList.coin
"width": 100 // 列宽(可选)
},
// 动态币种/算法算力列(示例:仅第一列带注释,其余同结构)
{
"key": "XTM", // 币种/算法代码作为列 key
"label": "XTM", // 表头显示名
"type": "hashrate", // 列类型:算力
"unit": "MH/s", // 单元格单位(表头不显示单位)
"icon": "https://cdn.xxx/coin/xtm.png", // 表头图标(可选)
},
{
"key": "NXNA",
"label": "NXNA",
"type": "hashrate",
"unit": "MH/s",
"icon": "https://cdn.xxx/coin/nxna.png"
},
{
"key": "CLORE",
"label": "CLORE",
"type": "hashrate",
"unit": "MH/s",
"icon": "https://cdn.xxx/coin/clore.png"
},
{
"key": "CFX",
"label": "CFX",
"type": "hashrate",
"unit": "MH/s",
"icon": "https://cdn.xxx/coin/cfx.png"
},
{
"key": "IRON",
"label": "IRON",
"type": "hashrate",
"unit": "MH/s",
"icon": "https://cdn.xxx/coin/iron.png"
},
{
"key": "NEXA",
"label": "NEXA",
"type": "hashrate",
"unit": "MH/s",
"icon": "https://cdn.xxx/coin/nexa.png"
},
{
"key": "KLS",
"label": "KLS",
"type": "hashrate",
"unit": "MH/s",
"icon": "https://cdn.xxx/coin/kls.png"
},
{
"key": "RVN",
"label": "RVN",
"type": "hashrate",
"unit": "MH/s",
"icon": "https://cdn.xxx/coin/rvn.png"
},
{
"key": "ERG",
"label": "ERG",
"type": "hashrate",
"unit": "MH/s",
"icon": "https://cdn.xxx/coin/erg.png"
},
{
"key": "XEL",
"label": "XEL",
"type": "hashrate",
"unit": "kH/s",
"icon": "https://cdn.xxx/coin/xel.png"
},
// 尾部汇总列
{
"key": "monthIncome",
"label": "最大月收益",
"type": "amount",
"currency": "USDT",//固定显示单位
"period": "month",
"width": 110
},
{
"key": "maxLeaseDays",
"label": "最大租赁天数",
"type": "days",
"width": 80
}
],
"rows": [
{
"id": "gpu_100_hbm3", // 唯一标识(加入购物车传 id
"model": "H100 80GB HBM3", // 型号,对应 columns.key = model
"price": 142160.54, // 行价格基准值(当无 priceList 时展示)
"priceList": [ // 价格列表:随支付方式变更价格与单位
{
"chain": "TRON", // 支付链
"coin": "USDT", // 币种(价格单位来源)
"price": 142160.54 // 该支付方式下的单价
},
{ "chain": "TRON", "coin": "NEXA", "price": 142050.00 },
{ "chain": "ETH", "coin": "USDT", "price": 142100.00 },
{ "chain": "ETH", "coin": "ETH", "price": 142000.00 }
],
"saleNumbers": 120, // 总机器数(仅 ASIC 展示,后端只读返回)
"saleOutNumbers": 18, // 已售数量(仅 ASIC 展示,后端只读返回)
"XTM": 255.0, // 对应列 key 的算力数值(可为 null
"NXNA": 255.0,
"CLORE": 343.0,
"CFX": null,
"IRON": null,
"NEXA": null,
"KLS": null,
"RVN": 255.0,
"ERG": 900.0,
"XEL": null,
"monthIncome": 425.01, // 最大月收益(列定义 currency=USDT固定以 USDT 展示)
"maxLeaseDays": 10368 // 最大租赁天数(单位:天)
},
{
"id": "gpu_rtx_5090",
"model": "RTX 5090",
"price": 14216.05,
"priceList": [
{ "chain": "TRON", "coin": "USDT", "price": 14216.05 },
{ "chain": "TRON", "coin": "NEXA", "price": 14199.00 },
{ "chain": "ETH", "coin": "USDT", "price": 14180.00 },
{ "chain": "ETH", "coin": "ETH", "price": 14150.00 }
],
"saleNumbers": 80,
"saleOutNumbers": 22,
"XTM": 28.7,
"NXNA": 100.5,
"CLORE": 100.5,
"CFX": 210.0,
"IRON": 133.5,
"NEXA": 450.0,
"KLS": 133.5,
"RVN": 100.5,
"ERG": 575.0,
"XEL": 120.0,
"monthIncome": 267.84,
"maxLeaseDays": 1645
},
{
"id": "gpu_rtx_4090",
"model": "RTX 4090",
"price": 12083.65,
"priceList": [
{ "chain": "TRON", "coin": "USDT", "price": 12083.65 },
{ "chain": "TRON", "coin": "NEXA", "price": 12050.00 },
{ "chain": "ETH", "coin": "USDT", "price": 12020.00 },
{ "chain": "ETH", "coin": "ETH", "price": 11999.00 }
],
"saleNumbers": 64,
"saleOutNumbers": 9,
"XTM": 16.6,
"NXNA": 65.0,
"CLORE": 65.0,
"CFX": 130.0,
"IRON": 82.5,
"NEXA": 320.0,
"KLS": 82.5,
"RVN": 65.0,
"ERG": 265.0,
"XEL": 40.6,
"monthIncome": 155.00,
"maxLeaseDays": 2418
}
]
}
// 模拟数据写入动态表格(仅演示)
try{
this.dynamicMeta = arr.meta || {}
this.dynamicColumns = Array.isArray(arr.columns) ? arr.columns : []
this.dynamicRows = (Array.isArray(arr.rows) ? arr.rows : []).map(r => ({
saleNumbers: 0,
saleOutNumbers: 0,
leaseTime: 1,
purchaseQuantity: 0,
...r
}))
// 根据价格列表设置默认支付方式,确保筛选框与价格展示一致
this.ensureDefaultPayFilterFromPrices()
}catch(e){/* noop */}
// 不再使用本地模拟数据,动态表格完全依赖后端返回的 columns/rows
// 仅当路由携带 shopId 时,才发起店铺商品请求
const routeShopId =
(this.$route && this.$route.params && (this.$route.params.shopId || this.$route.params.id)) ||
@@ -569,7 +359,17 @@ export default {
},
// 切换矿机种类0-ASIC1-GPU
handleMachineTypeChange(){
// 变更类型后,重新请求数据
// 切换前清空所有已勾选状态与确认弹窗
try {
if (Array.isArray(this.dynamicRows)) {
this.dynamicRows.forEach(r => { if (r) this.$set(r, '_selected', false) })
}
if (this.confirmAddDialog) {
this.confirmAddDialog.items = []
this.confirmAddDialog.visible = false
}
} catch (e) { /* noop */ }
// 变更类型后,重新请求数据与支付方式
this.fetchGetMachineInfo(this.buildQueryParams())
this.fetchPayTypes()
// 本地记住用户选择
@@ -690,19 +490,32 @@ export default {
this.productDetailLoading = true
// 改为使用店铺机器列表接口
const res = await getShopMachineList(params)
console.log(res)
if (res && res.code === 200) {
console.log(res.data, 'res.rows');
this.total = res.total||0;
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 = res && res.data && res.data.payConfigList
const payList = root && root.payConfigList
if (Array.isArray(payList) && payList.length) {
this.paymentMethodList = payList
this.ensureDefaultPayFilterSelection()
}
} catch (e) { /* noop */ }
// 动态表格数据(如后端有对应 rows/columns可在此接入
// 此处保留现有动态表格的模拟/替换逻辑,不再维护旧表格数据结构
}
this.productDetailLoading = false
@@ -742,7 +555,7 @@ export default {
},
//查询购物车列表
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)
@@ -862,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)
@@ -968,7 +791,7 @@ export default {
this.confirmAddDialog.items = picked.map(r => ({
...r,
leaseTime: Number(r.leaseTime || 1),
purchaseQuantity: Number(r.purchaseQuantity || 0)
purchaseQuantity: Number(r.purchaseQuantity || 1)
}))
this.confirmAddDialog.visible = true
},
@@ -992,11 +815,15 @@ export default {
return obj
})
const res = await addGoodsV2(payload)
if (!res || !(res.code === 0 || res.code === 200)) {
this.$message.error('部分商品加入购物车失败,请重试')
} else {
this.$message.success(`已加入 ${items.length} 台矿机到购物车`)
if (res && (res.code === 0 || res.code === 200)) {
this.$message({
message: `已加入 ${items.length} 台矿机到购物车`,
type: 'success',
duration: 3000,
showClose: true
})
}
this.confirmAddDialog.visible = false
// 清空勾选
try {

View File

@@ -116,7 +116,8 @@
<template #default="{ row }">
<el-checkbox
v-model="row._selected"
:title="'选择该矿机'"
:title="isRowDisabled(row) ? (row && (row.saleState === 1 || row.saleState === 2) ? '该机器已售出或售出中,无法选择' : '该机器暂无价格,无法选择') : '选择该矿机'"
:disabled="isRowDisabled(row)"
@change="checked => handleManualSelectFlat(row, checked)"
/>
</template>
@@ -143,7 +144,7 @@
</div>
</template>
<template #default="{ row }">
<span class="num-strong">
<span :class="getCellClass(col)">
<el-tooltip
v-if="formatDynamicCell(row, col).truncated"
:content="formatDynamicCell(row, col).full"
@@ -170,13 +171,40 @@
<!-- 租赁天数始终显示用户手动填写 -->
<el-table-column prop="leaseTime" label="租赁天数(天)">
<template #default="scope">
<el-input-number class="input-full" v-model="scope.row.leaseTime" :min="1" :precision="0" :step="1" :controls="false" size="mini" />
<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>
<!-- 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="0" :precision="0" :step="1" :controls="false" size="mini" />
<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>
@@ -187,7 +215,7 @@
:visible.sync="dynamicSearch.visible"
width="420px"
>
<div style="display:flex;gap:10px;align-items:center;">
<div class="dynamic-search-bar" style="display:flex;gap:10px;align-items:center;">
<el-input
v-model="dynamicSearch.keyword"
placeholder="输入币种代码或算法关键词"
@@ -196,10 +224,6 @@
/>
<el-button type="primary" @click="handleConfirmDynamicSearch">搜索</el-button>
</div>
<template #footer>
<el-button @click="dynamicSearch.visible=false">取消</el-button>
<el-button type="primary" @click="handleConfirmDynamicSearch">确定</el-button>
</template>
</el-dialog>
</section>
@@ -209,7 +233,7 @@
</div>
<!-- 确认加入购物车弹窗 -->
<el-dialog :visible.sync="confirmAddDialog.visible" width="50vw" :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="model" label="型号" header-align="left" align="left" />
@@ -233,6 +257,11 @@
<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>
@@ -273,6 +302,157 @@ export default {
name: 'ProductDetail',
mixins: [Index],
methods: {
/**
* 判断列是否为价格/金额相关列,以控制样式为红色
* - 优先依据 col.type === 'amount'
* - 其次依据 key 命名包含 'price' / 'amount'
* - 再次依据 label 文案包含 '价' / '金额'
* @param {Object} col
* @returns {string} CSS 类名
*/
getCellClass(col) {
try {
if (!col) return 'num-strong'
if (String(col.type).toLowerCase() === 'amount') return 'price-strong'
const key = String(col.key || '').toLowerCase()
if (key.includes('price') || key.includes('amount')) return 'price-strong'
const label = String(col.label || '')
if (label.includes('价') || label.includes('金额')) return 'price-strong'
return 'num-strong'
} catch (e) {
return 'num-strong'
}
},
/**
* 纯字符串方式相乘,避免浮点误差
* @param {Array<string|number>} values
* @returns {string} 不带单位的结果字符串,精确还原小数位
*/
multiplyAsDecimal(values) {
const toIntScale = (n) => {
const s = String(n || 0).trim()
if (!s.includes('.')) return { int: BigInt(s || '0'), scale: 0 }
const [w, f] = s.split('.')
const frac = (f || '').replace(/[^0-9]/g, '')
const whole = (w || '0').replace(/[^0-9-]/g, '')
const sign = whole.startsWith('-') ? '-' : ''
const intStr = (sign ? whole.slice(1) : whole) + frac
const safe = intStr.replace(/^0+(?=\d)/, '') || '0'
return { int: BigInt(sign + safe), scale: frac.length }
}
let acc = 1n
let scale = 0
for (const v of values) {
const { int, scale: sc } = toIntScale(v)
acc = acc * int
scale += sc
}
const neg = acc < 0n
const absStr = (neg ? (-acc) : acc).toString()
if (scale === 0) return (neg ? '-' : '') + absStr
const pad = scale - absStr.length
const s = pad > 0 ? ('0'.repeat(pad) + absStr) : absStr
const i = s.length - scale
let out = s.slice(0, i) + '.' + s.slice(i)
// 去除多余的前导/尾随零
out = out.replace(/^(-?)0+(?=\d)/, '$1')
out = out.replace(/\.?0+$/, '')
if (out.startsWith('.')) out = '0' + out
if (out === '' || out === '-') out = '0'
return (neg ? '-' : '') + out
},
/**
* 截断到最多6位小数不四舍五入
* @param {string} s
* @param {number} maxDecimals
* @returns {string}
*/
truncateDecimalString(s, maxDecimals = 6) {
const str = String(s || '0')
if (!str.includes('.')) return str
const [w, f] = str.split('.')
if (f.length <= maxDecimals) return str
return `${w}.${f.slice(0, maxDecimals)}`
},
/**
* 确认弹窗“总价”展示(附币种)
* ASIC: price*leaseTime*purchaseQuantityGPU: price*leaseTime
* 采用字符串整数相乘避免精度问题展示最多6位小数截断不四舍五入
*/
formatConfirmTotalText(row) {
try {
const price = this.getDisplayPrice ? this.getDisplayPrice(row) : (row && row.price)
const coin = (this.getDisplayPriceCoin && this.getDisplayPriceCoin(row)) || ''
const lease = Number(row && row.leaseTime) || 1
const nums = [price, lease]
if (this.machineType === 0) {
const qty = Number(row && row.purchaseQuantity) || 1
nums.push(qty)
}
const mul = this.multiplyAsDecimal(nums)
const text = this.truncateDecimalString(mul, 6)
const unit = (coin || '').toString().toUpperCase()
return unit ? `${text} ${unit}` : text
} catch (e) {
return '—'
}
},
/**
* 获取该行可购买的最大数量(<= 总机器数)
* @param {Object} row
* @returns {number}
*/
getRowMaxPurchase(row) {
try {
const n = Number(row && row.saleNumbers)
if (!Number.isFinite(n) || n < 0) return 0
return Math.floor(n)
} catch (e) { return 0 }
},
/**
* 购买数量变更时,强制校验区间 [1, max] 且取整
* @param {Object} row
* @param {number} value
*/
handlePurchaseQuantityChange(row, value) {
try {
const max = this.getRowMaxPurchase(row)
let v = Number(value)
if (!Number.isFinite(v)) v = 1
if (v < 1) v = 1
if (max > 0 && v > max) v = max
v = Math.floor(v)
this.$set(row, 'purchaseQuantity', v)
} catch (e) {
this.$set(row, 'purchaseQuantity', 1)
}
},
/**
* 行是否存在任意价格(用于禁用勾选)
* 规则:
* - 若存在 priceList任一项的 price 非 null/undefined 即视为有价格
* - 否则回退 row.price 是否有效
*/
hasAnyPrice(row) {
try {
if (!row) return false
if (Array.isArray(row.priceList) && row.priceList.length) {
return row.priceList.some(it => it && it.price !== null && it.price !== undefined)
}
const v = row.price
return v !== null && v !== undefined && v !== ''
} catch (e) { return false }
},
/**
* 该行是否应禁用选择:已售出/售出中 或 无价格
*/
isRowDisabled(row) {
try {
if (!row) return true
if (row.saleState === 1 || row.saleState === 2) return true
return !this.hasAnyPrice(row)
} catch (e) { return true }
},
/**
* 获取行的最大可租赁天数
* 规则:优先 row.maxLeaseDays否则回退 365保证区间 [1, 365]
@@ -343,18 +523,9 @@ export default {
* 重置筛选
*/
handleResetFilters() {
this.selectedPayKey = null
this.filters = {
chain: '',
coin: '',
minPrice: null,
maxPrice: null,
minPower: null,
maxPower: null,
minPowerDissipation: null,
maxPowerDissipation: null,
unit: 'GH/S'
}
// 仅重置“单价区间”的值,不影响支付方式筛选及其它条件
this.filters.minPrice = null
this.filters.maxPrice = null
this.handleSearchFilters()
},
/**
@@ -730,6 +901,24 @@ export default {
.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%; }

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long