周五定时更新

This commit is contained in:
2025-10-31 14:09:58 +08:00
parent a60603acd0
commit a2fc94b402
28 changed files with 830 additions and 265 deletions

View File

@@ -7,6 +7,7 @@ ENV = 'development'
#开发环境 #开发环境
VUE_APP_BASE_API = 'https://test.m2pool.com/api/' VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
# VUE_APP_BASE_API = 'http://18.183.240.108:8080/api/' # VUE_APP_BASE_API = 'http://18.183.240.108:8080/api/'
# VUE_APP_BASE_API = 'http://10.168.2.220:8888'
VUE_APP_BASE_URL = 'https://test.m2pool.com/' VUE_APP_BASE_URL = 'https://test.m2pool.com/'
# 路由懒加载 # 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true VUE_CLI_BABEL_TRANSPILE_MODULES = true

View File

@@ -5,7 +5,8 @@ VUE_APP_TITLE = m2pool
ENV = 'production' ENV = 'production'
# 生产环境 # 生产环境
VUE_APP_BASE_API = 'https://m2pool.com/api/' # VUE_APP_BASE_API = 'https://m2pool.com/api/'
VUE_APP_BASE_API = 'http://10.168.2.220:8888'
VUE_APP_BASE_URL = 'https://m2pool.com/' VUE_APP_BASE_URL = 'https://m2pool.com/'
# 路由懒加载 # 路由懒加载

View File

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

View File

@@ -18,6 +18,9 @@ body{
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-align: center; text-align: center;
color: #2c3e50; color: #2c3e50;
box-sizing: border-box;
margin: 0;
padding: 0;
} }
nav { nav {

View File

@@ -25,4 +25,10 @@ body,html,*{
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
.el-main{
// background: palegoldenrod;
box-sizing: border-box;
padding: 0;
// overflow-x: hidden;
}
</style> </style>

View File

@@ -91,12 +91,11 @@ export function deleteShopConfig(data) {
}) })
} }
// 批量删除购物车中已下架商品 // 钱包配置(用于修改卖家钱包地址)----获取链(一级)和币(二级) 下拉列表(获取本系统支持的链和币种)
export function deleteBatchGoodsForIsDelete(data) { export function getChainAndCoin(data) {
return request({ return request({
url: `/lease/shopping/cart/deleteBatchGoodsForIsDelete`, url: `/lease/shop/getChainAndCoin`,
method: 'post', method: 'post',
data data
}) })
@@ -107,3 +106,8 @@ export function deleteBatchGoodsForIsDelete(data) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -124,8 +124,8 @@ export const accountRoutes = [
name: 'accountShopConfig', name: 'accountShopConfig',
component: () => import('../views/account/shopConfig.vue'), component: () => import('../views/account/shopConfig.vue'),
meta: { meta: {
title: '店铺配置', title: '钱包绑定',
description: '配置店铺收款和支付方式', description: '绑定店铺收款钱包',
allAuthority: ['all'] allAuthority: ['all']
} }
}, },
@@ -180,7 +180,7 @@ export const accountRoutes = [
} }
}, },
{ {
path: 'purchased-detail/:orderItemId', path: 'purchased-detail/:id',
name: 'PurchasedDetail', name: 'PurchasedDetail',
component: () => import('../views/account/purchasedDetail.vue'), component: () => import('../views/account/purchasedDetail.vue'),
meta: { meta: {

View File

@@ -46,7 +46,7 @@ function decryptData(encryptedText, secretKey) {
return { return {
// 敏感数据(已解密) // 敏感数据(已解密)
token: sensitiveData?.token || '', token: sensitiveData?.token || '',
userEmail: sensitiveData?.userEmail || '', leasEmail: sensitiveData?.leasEmail || '',
userId: sensitiveData?.userId || '', userId: sensitiveData?.userId || '',
timestamp: sensitiveData?.timestamp || null, timestamp: sensitiveData?.timestamp || null,
@@ -61,8 +61,8 @@ function decryptData(encryptedText, secretKey) {
/** /**
* 执行自动登录 * 执行自动登录
*/ */
function performAutoLogin(token, userId, userEmail) { function performAutoLogin(token, userId, leasEmail) {
console.log('执行自动登录:', { userId, userEmail: userEmail ? '***' : '' }); console.log('执行自动登录:', { userId, leasEmail: leasEmail ? '***' : '' });
// 这里可以添加自动登录的逻辑 // 这里可以添加自动登录的逻辑
// 例如:设置全局状态、跳转页面等 // 例如:设置全局状态、跳转页面等
} }
@@ -83,7 +83,7 @@ function decryptData(encryptedText, secretKey) {
console.log(params.token,"params.token 存入"); console.log(params.token,"params.token 存入");
localStorage.setItem('token', params.token); localStorage.setItem('token', params.token);
localStorage.setItem('userEmail', params.userEmail); localStorage.setItem('leasEmail', params.leasEmail);
localStorage.setItem('userId', params.userId); localStorage.setItem('userId', params.userId);
localStorage.setItem('language', params.language); localStorage.setItem('language', params.language);
localStorage.setItem('username', params.username); localStorage.setItem('username', params.username);
@@ -93,7 +93,7 @@ function decryptData(encryptedText, secretKey) {
console.log('接收到的参数:', { console.log('接收到的参数:', {
userId: params.userId ? '***' : '', userId: params.userId ? '***' : '',
userEmail: params.userEmail ? '***' : '', leasEmail: params.leasEmail ? '***' : '',
token: params.token ? '***' : '', token: params.token ? '***' : '',
language: params.language, language: params.language,
username: params.username, username: params.username,
@@ -103,7 +103,7 @@ function decryptData(encryptedText, secretKey) {
// 根据参数执行相应操作 // 根据参数执行相应操作
if (params.token && params.userId) { if (params.token && params.userId) {
// 执行自动登录 // 执行自动登录
performAutoLogin(params.token, params.userId, params.userEmail); performAutoLogin(params.token, params.userId, params.leasEmail);
} }
if (params.language) { if (params.language) {

View File

@@ -28,8 +28,23 @@
<el-table-column label="总金额(USDT)" min-width="140"> <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">{{ (scope.row && scope.row.totalPrice) != null ? scope.row.totalPrice : '—' }}</span></template>
</el-table-column> </el-table-column>
<el-table-column label="已支付金额(USDT)" min-width="140"> <el-table-column min-width="180">
<template #default="scope"><span class="value strong">{{ (scope.row && scope.row.payAmount) != null ? scope.row.payAmount : '—' }}</span></template> <template #header>
<el-tooltip placement="top" effect="dark">
<div slot="content">
实际支付金额/理论支付金额<br/>
1. 实际支付金额是按照矿机实际算力计算支付金额<br/>
2. 理论支付金额是卖家定义出售价格
</div>
<span style="display:inline-flex;align-items:center;gap:6px;">
<i class="el-icon-question" style="color:#909399;" aria-label="说明" role="img"></i>
已支付金额(USDT)
</span>
</el-tooltip>
</template>
<template #default="scope">
<span class="value strong">{{ (scope.row && scope.row.payAmount) != null ? scope.row.payAmount : '—' }}</span>
</template>
</el-table-column> </el-table-column>
<el-table-column label="待支付金额(USDT)" min-width="140"> <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">{{ (scope.row && scope.row.noPayAmount) != null ? scope.row.noPayAmount : '—' }}</span></template>
@@ -65,7 +80,18 @@
<el-dialog :visible.sync="dialogVisible" width="520px" title="请扫码支付"> <el-dialog :visible.sync="dialogVisible" width="520px" title="请扫码支付">
<div style="text-align:left; margin-bottom:12px; color:#666;"> <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>{{ paymentDialog.totalPrice }}</b></div>
<div style="margin-bottom:6px;">已支付金额(USDT)<b class="value strong">{{ paymentDialog.payAmount }}</b></div> <div style="margin-bottom:6px;display:flex;align-items:center;gap:6px;">
<el-tooltip placement="top" effect="dark">
<div slot="content">
实际支付金额/理论支付金额<br/>
1. 实际支付金额是按照矿机实际算力计算支付金额<br/>
2. 理论支付金额是卖家定义出售价格
</div>
<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>
</div>
<div style="margin-bottom:6px;">待支付金额(USDT)<b class="value strong">{{ paymentDialog.noPayAmount }}</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 style="word-break:break-all;">收款地址<code>{{ orderDialog.address }}</code></div> -->
</div> </div>

View File

@@ -13,7 +13,7 @@
<div v-for="(row, idx) in rechargeRows" :key="getRowKey(row, idx)" class="record-item" :class="statusClass(row.status)" @click="toggleExpand('recharge', row, idx)"> <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-main">
<div class="item-left"> <div class="item-left">
<div class="amount">+ {{ formatTrunc(row.amount, 2) }} {{ row.fromSymbol || 'USDT' }}</div> <div class="amount">+ {{ formatDec6(row.amount) }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}</div>
<div class="chain">{{ formatChain(row.fromChain) }}</div> <div class="chain">{{ formatChain(row.fromChain) }}</div>
</div> </div>
<div class="item-right"> <div class="item-right">
@@ -23,8 +23,20 @@
</div> </div>
<div v-show="isExpanded('recharge', row, idx)" class="expand-panel"> <div v-show="isExpanded('recharge', row, idx)" class="expand-panel">
<div class="expand-grid"> <div class="expand-grid">
<div class="expand-item"><span class="label">充值地址</span><span class="value mono-ellipsis" :title="row.fromAddress">{{ row.fromAddress }}</span></div> <div class="expand-item">
<div class="expand-item" v-if="row.txHash"><span class="label">交易哈希</span><span class="value mono-ellipsis" :title="row.txHash">{{ row.txHash }}</span></div> <span class="label">充值地址</span>
<div class="value value-row">
<span class="mono-ellipsis" :title="row.fromAddress">{{ row.fromAddress }}</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click.stop="handleCopy(row.fromAddress, '充值地址')">复制</el-button>
</div>
</div>
<div class="expand-item" v-if="row.txHash">
<span class="label">交易哈希</span>
<div class="value value-row">
<span class="mono-ellipsis" :title="row.txHash">{{ row.txHash }}</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click.stop="handleCopy(row.txHash, '交易哈希')">复制</el-button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -42,7 +54,7 @@
<div v-for="(row, idx) in withdrawRows" :key="getRowKey(row, idx)" class="record-item" :class="statusClass(row.status)" @click="toggleExpand('withdraw', row, idx)"> <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-main">
<div class="item-left"> <div class="item-left">
<div class="amount">- {{ formatTrunc(row.amount, 2) }} {{ row.toSymbol || 'USDT' }}</div> <div class="amount">- {{ formatDec6(row.amount) }} {{ (row.toSymbol || 'USDT').toUpperCase() }}</div>
<div class="chain">{{ formatChain(row.toChain) }}</div> <div class="chain">{{ formatChain(row.toChain) }}</div>
</div> </div>
<div class="item-right"> <div class="item-right">
@@ -52,8 +64,20 @@
</div> </div>
<div v-show="isExpanded('withdraw', row, idx)" class="expand-panel"> <div v-show="isExpanded('withdraw', row, idx)" class="expand-panel">
<div class="expand-grid"> <div class="expand-grid">
<div class="expand-item"><span class="label">收款地址</span><span class="value mono-ellipsis" :title="row.toAddress">{{ row.toAddress }}</span></div> <div class="expand-item">
<div class="expand-item" v-if="row.txHash"><span class="label">交易哈希</span><span class="value mono-ellipsis" :title="row.txHash">{{ row.txHash }}</span></div> <span class="label">收款地址</span>
<div class="value value-row">
<span class="mono-ellipsis" :title="row.toAddress">{{ row.toAddress }}</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click.stop="handleCopy(row.toAddress, '收款地址')">复制</el-button>
</div>
</div>
<div class="expand-item" v-if="row.txHash">
<span class="label">交易哈希</span>
<div class="value value-row">
<span class="mono-ellipsis" :title="row.txHash">{{ row.txHash }}</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click.stop="handleCopy(row.txHash, '交易哈希')">复制</el-button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -71,7 +95,7 @@
<div v-for="(row, idx) in consumeRows" :key="getRowKey(row, idx)" class="record-item" :class="statusClass(row.status)" @click="toggleExpand('consume', row, idx)"> <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-main">
<div class="item-left"> <div class="item-left">
<div class="amount">- {{ formatTrunc(row.realAmount, 2) }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}</div> <div class="amount">- {{ formatDec6(row.realAmount) }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}</div>
<div class="chain">{{ formatChain(row.fromChain) }}</div> <div class="chain">{{ formatChain(row.fromChain) }}</div>
</div> </div>
<div class="item-right"> <div class="item-right">
@@ -138,7 +162,7 @@ export default {
// }, // },
// { // {
// createTime: '2024-01-15 14:30:25', // createTime: '2024-01-15 14:30:25',
// amount: 100, // amount: 100.656578965,
// fromAddress: 'djdddksfhsfj', // fromAddress: 'djdddksfhsfj',
// fromChain: 'tron', // fromChain: 'tron',
// fromSymbol: 'USDT', // fromSymbol: 'USDT',
@@ -345,9 +369,16 @@ export default {
getPayStatusType(s) { return ({ 0: 'danger', 1: 'success', 2: 'warning', 3: 'danger' })[s] || 'info' }, getPayStatusType(s) { return ({ 0: 'danger', 1: 'success', 2: 'warning', 3: 'danger' })[s] || 'info' },
getPayStatusText(s) { return ({ 0: '支付失败', 1: '支付成功', 2: '待校验', 3: '证书校验失败' })[s] || '未知' }, getPayStatusText(s) { return ({ 0: '支付失败', 1: '支付成功', 2: '待校验', 3: '证书校验失败' })[s] || '未知' },
/**
* 将链名称标准化为大写简称
* @param {string} chain - 后端返回的链标识,如 tron/eth/ethereum/bsc/polygon
* @returns {string} 大写显示,如 TRON/ETH/BSC/POLYGON
*/
formatChain(chain) { formatChain(chain) {
const map = { tron: 'Tron (TRC20)', ethereum: 'Ethereum (ERC20)', bsc: 'BSC (BEP20)', polygon: 'Polygon (MATIC)' } if (!chain) return ''
return map[chain] || chain const key = String(chain).toLowerCase()
const map = { tron: 'TRON', trx: 'TRON', eth: 'ETH', ethereum: 'ETH', bsc: 'BSC', polygon: 'POLYGON', matic: 'POLYGON' }
return (map[key] || String(chain)).toUpperCase()
}, },
formatFullTime(time) { if (!time) return ''; try { return new Date(time).toLocaleString('zh-CN') } catch (e) { return String(time) } }, formatFullTime(time) { if (!time) return ''; try { return new Date(time).toLocaleString('zh-CN') } catch (e) { return String(time) } },
formatTime(time) { return this.formatFullTime(time) }, formatTime(time) { return this.formatFullTime(time) },
@@ -363,6 +394,27 @@ export default {
const padded = decPart.padEnd(d, '0') const padded = decPart.padEnd(d, '0')
return `${intPart}.${padded}` return `${intPart}.${padded}`
}, },
/**
* 金额显示保留最多6位小数直接截断不四舍五入不补尾随0始终返回非负字符串
* @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
},
handleSizeChange(val) { handleSizeChange(val) {
console.log(`每页 ${val}`); console.log(`每页 ${val}`);
this.pagination.pageSize = val; this.pagination.pageSize = val;
@@ -376,6 +428,33 @@ export default {
this.loadList(); this.loadList();
}, },
/**
* 复制文本到剪贴板
* @param {string} text - 需要复制的内容
* @param {string} [label] - 语义标签,用于提示文案
*/
async handleCopy(text, label = '内容') {
try {
const value = String(text || '')
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(value)
} else {
const ta = document.createElement('textarea')
ta.value = value
ta.style.position = 'fixed'
ta.style.left = '-9999px'
document.body.appendChild(ta)
ta.focus()
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
}
this.$message.success(`${label}已复制`)
} catch (e) {
this.$message.error('复制失败,请手动选择复制')
}
},
/** /**
* Tab 名称转状态码 * Tab 名称转状态码
* @param {string} tabName - 'recharge' | 'withdraw' | 'consume' * @param {string} tabName - 'recharge' | 'withdraw' | 'consume'
@@ -434,6 +513,7 @@ export default {
.expand-item { display: grid; grid-template-columns: 80px 1fr; gap: 6px; align-items: center; } .expand-item { display: grid; grid-template-columns: 80px 1fr; gap: 6px; align-items: center; }
.label { color: #666; font-size: 13px; text-align: right; } .label { color: #666; font-size: 13px; text-align: right; }
.value { color: #333; font-size: 13px; text-align: left; } .value { color: #333; font-size: 13px; text-align: left; }
.value-row { display: inline-flex; align-items: center; gap: 6px; }
.mono { font-family: "Monaco", "Menlo", monospace; } .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; } .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; } .empty { text-align: center; color: #999; padding: 20px 0; }

View File

@@ -38,8 +38,7 @@
v-for="item in displayedLinks" v-for="item in displayedLinks"
:key="item.to" :key="item.to"
:to="item.to" :to="item.to"
class="side-link" :class="['side-link', isActiveLink(item.to) ? 'active' : '']"
active-class="active"
>{{ item.label }}</router-link> >{{ item.label }}</router-link>
</nav> </nav>
</aside> </aside>
@@ -104,13 +103,16 @@ export default {
if (raw == null) return null if (raw == null) return null
try { return JSON.parse(raw) } catch (e) { return raw } try { return JSON.parse(raw) } catch (e) { return raw }
} }
const val = getVal('userName') || getVal('userEmail') || '' const val =getVal('leasEmail') || ''
this.userEmail = typeof val === 'string' ? val : String(val) this.userEmail = typeof val === 'string' ? val : String(val)
// 恢复上次选择的导航分组(如无则默认 seller // 恢复上次选择的导航分组(如无则默认 seller
const savedRole = getVal('accountActiveRole') const savedRole = getVal('accountActiveRole')
if (savedRole === 'buyer' || savedRole === 'seller') { if (savedRole === 'buyer' || savedRole === 'seller') {
this.activeRole = savedRole this.activeRole = savedRole
} }
// 根据当前路由自动匹配分组,确保左侧导航高亮正确
this.setActiveRoleByRoute()
}, },
methods: { methods: {
/** /**
@@ -122,8 +124,78 @@ export default {
if (role !== 'buyer' && role !== 'seller') return if (role !== 'buyer' && role !== 'seller') return
this.activeRole = role this.activeRole = role
try { localStorage.setItem('accountActiveRole', JSON.stringify(role)) } catch (e) {} try { localStorage.setItem('accountActiveRole', JSON.stringify(role)) } catch (e) {}
// 切换分组后,立即跳转到该分组的第一个导航页面
try {
const firstPath = role === 'buyer'
? (this.buyerLinks && this.buyerLinks[0] && this.buyerLinks[0].to)
: (this.sellerLinks && this.sellerLinks[0] && this.sellerLinks[0].to)
if (firstPath && this.$route && this.$route.path !== firstPath) {
this.$router.push(firstPath)
}
} catch (e) { /* noop */ }
}, },
/**
* 根据当前路由自动选择导航分组,保证进入“我的店铺”等卖家页面时左侧高亮正确
*/
setActiveRoleByRoute() {
const path = (this.$route && this.$route.path) || ''
// 买家前缀优先匹配,确保“已购详情”等页面归属买家侧
const buyerPrefixes = [
'/account/wallet',
'/account/purchased',
'/account/purchased-detail',
'/account/orders',
'/account/funds-flow'
]
const sellerPrefixes = [
'/account/shops',
'/account/shop-new',
'/account/product-new',
'/account/products',
'/account/product-detail',
'/account/product-machine-add',
'/account/seller-orders',
'/account/order-detail',
'/account/receipt-record',
'/account/shop-config'
]
const shouldBuyer = buyerPrefixes.some(p => path.indexOf(p) === 0)
const shouldSeller = sellerPrefixes.some(p => path.indexOf(p) === 0)
const role = shouldBuyer ? 'buyer' : (shouldSeller ? 'seller' : this.activeRole)
if (this.activeRole !== role) {
this.activeRole = role
try { localStorage.setItem('accountActiveRole', JSON.stringify(role)) } catch (e) {}
}
},
/**
* 判断左侧导航项是否高亮
* - 普通路径完全匹配
* - “已售出订单”需同时匹配详情页 /account/order-detail/:id
*/
isActiveLink(pathLike) {
const current = (this.$route && this.$route.path) || ''
if (!pathLike) return false
// 列表-详情联动高亮映射
const map = {
'/account/seller-orders': ['/account/seller-orders', '/account/order-detail'],
'/account/products': ['/account/products', '/account/product-detail'],
'/account/purchased': ['/account/purchased', '/account/purchased-detail']
}
const prefixes = map[pathLike]
if (Array.isArray(prefixes)) {
return prefixes.some(p => current.indexOf(p) === 0)
}
return current.indexOf(pathLike) === 0
}
}, },
watch: {
'$route.path': {
immediate: true,
handler() {
this.setActiveRoleByRoute()
}
}
}
}; };
</script> </script>
@@ -172,6 +244,7 @@ export default {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-bottom: 8px; margin-bottom: 8px;
margin-top: 18px;
} }
.role-button { .role-button {
appearance: none; appearance: none;

View File

@@ -134,17 +134,17 @@
<el-dialog title="修改配置" :visible.sync="visibleConfigEdit" width="560px"> <el-dialog title="修改配置" :visible.sync="visibleConfigEdit" width="560px">
<div class="row"> <div class="row">
<label class="label">支付链</label> <label class="label">支付链</label>
<el-select v-model="configForm.chain" placeholder="请选择链"> <el-input v-model="configForm.chainLabel" placeholder="-" disabled />
<el-option v-for="c in chainOptions" :key="c.value" :value="c.value" :label="c.label" />
</el-select>
</div> </div>
<div class="row"> <div class="row">
<label class="label">支付币种</label> <label class="label">支付币种</label>
<el-select <el-select
class="input" class="input"
size="middle" size="middle"
ref="screen" v-model="configForm.payCoins"
v-model="configForm.payCoin" multiple
collapse-tags
filterable
placeholder="请选择币种" placeholder="请选择币种"
> >
<el-option <el-option
@@ -152,20 +152,22 @@
:key="item.value" :key="item.value"
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
> />
<div style="display: flex; align-items: center">
<img v-if="item.imgUrl" :src="item.imgUrl" style="float: left; width: 20px" />
<span style="float: left; margin-left: 5px">{{ item.label }}</span>
</div>
</el-option>
</el-select> </el-select>
</div> </div>
<div class="row"> <div class="row">
<label class="label">币种类型</label> <label class="label">已选择币种</label>
<el-radio-group v-model="configForm.payType"> <div class="selected-coin-list">
<el-radio :label="0">虚拟币</el-radio> <el-tag
<el-radio :label="1">稳定币</el-radio> v-for="c in selectedCoinLabels"
</el-radio-group> :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>
<div class="row"> <div class="row">
<label class="label">钱包地址</label> <label class="label">钱包地址</label>
@@ -173,7 +175,7 @@
</div> </div>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="visibleConfigEdit=false">取消</el-button> <el-button @click="visibleConfigEdit=false">取消</el-button>
<el-button type="primary" @click="submitConfigEdit">保存</el-button> <el-button type="primary" @click="submitConfigEdit">确认修改</el-button>
</span> </span>
</el-dialog> </el-dialog>
</div> </div>
@@ -181,7 +183,7 @@
</template> </template>
<script> <script>
import { getMyShop, updateShop, deleteShop, queryShop, closeShop ,updateShopConfig,deleteShopConfig} from '@/api/shops' import { getMyShop, updateShop, deleteShop, queryShop, closeShop ,updateShopConfig,deleteShopConfig,getChainAndCoin} from '@/api/shops'
import { coinList } from '@/utils/coinList' import { coinList } from '@/utils/coinList'
import { getShopConfig } from '@/api/wallet' import { getShopConfig } from '@/api/wallet'
@@ -206,9 +208,10 @@ export default {
// 店铺配置列表 // 店铺配置列表
shopConfigs: [], shopConfigs: [],
visibleConfigEdit: false, visibleConfigEdit: false,
configForm: { id: '', chain: '', payAddress: '', payCoin: '', payType: 0 }, configForm: { id: '', chainLabel: '', chainValue: '', payAddress: '', payCoins: [], payCoin: '' },
productOptions: [], productOptions: [],
coinOptions: coinList || [], coinOptions: coinList || [],
editCoinOptionsApi: [],
// 支付链选项(可与后端接口对齐后替换为动态) // 支付链选项(可与后端接口对齐后替换为动态)
chainOptions: [ chainOptions: [
{ label: 'Tron (TRC20)', value: 'tron' }, { label: 'Tron (TRC20)', value: 'tron' },
@@ -244,14 +247,12 @@ export default {
* 弹窗可选币种:稳定币/虚拟币分流 * 弹窗可选币种:稳定币/虚拟币分流
*/ */
editCoinOptions() { editCoinOptions() {
if (Number(this.configForm.payType) === 1) { if (Array.isArray(this.editCoinOptionsApi) && this.editCoinOptionsApi.length) return this.editCoinOptionsApi
return [
{ label: 'USDT', value: 'usdt' },
{ label: 'USDC', value: 'usdc' },
{ label: 'BUSD', value: 'busd' },
]
}
return this.coinOptions return this.coinOptions
},
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())
} }
}, },
created() { created() {
@@ -338,8 +339,6 @@ export default {
this.$message.success('保存成功') this.$message.success('保存成功')
this.visibleConfigEdit = false this.visibleConfigEdit = false
this.fetchShopConfigs(this.shop.id) this.fetchShopConfigs(this.shop.id)
} else {
this.$message.error(res && res.msg ? res.msg : '保存失败')
} }
}, },
async deleteShopConfig(params) { async deleteShopConfig(params) {
@@ -349,16 +348,41 @@ export default {
this.fetchShopConfigs(this.shop.id) this.fetchShopConfigs(this.shop.id)
} }
}, },
handleEditConfig(row) { async handleEditConfig(row) {
this.configForm = { try {
id: row.id, const res = await getChainAndCoin({ id: row.id })
chain: row.chain || '', if (res && (res.code === 0 || res.code === 200) && res.data) {
payCoin: row.payCoin || '', const d = res.data || {}
payType: typeof row.payType === 'number' ? row.payType : Number(row.payType || 0), const children = Array.isArray(d.children) ? d.children : []
payAddress: row.payAddress || '', this.editCoinOptionsApi = children.map(c => ({ label: c.label, value: c.value }))
const preSelected = children.filter(c => Number(c.hasBind) === 1).map(c => c.value)
this.configForm = {
id: row.id,
chainLabel: d.label || '',
chainValue: d.value || '',
payAddress: d.address || '',
payCoins: preSelected,
payCoin: preSelected.join(',')
}
} else {
// 回退:使用行内已有数据
this.editCoinOptionsApi = []
const chainLabel = row.chain || ''
const payCoinStr = String(row.payCoin || '')
const payCoins = payCoinStr ? payCoinStr.split(',') : []
this.configForm = {
id: row.id,
chainLabel,
chainValue: row.chain || '',
payAddress: row.payAddress || '',
payCoins,
payCoin: payCoins.join(',')
}
}
this.visibleConfigEdit = true
} catch (e) {
this.visibleConfigEdit = true
} }
this.visibleConfigEdit = true
}, },
async handleDeleteConfig(row) { async handleDeleteConfig(row) {
this.deleteShopConfig({id:row.id}) this.deleteShopConfig({id:row.id})
@@ -366,11 +390,11 @@ export default {
submitConfigEdit() { submitConfigEdit() {
// 基础校验 // 基础校验
if (!this.configForm.chain) { if (!this.configForm.chainLabel && !this.configForm.chainValue) {
this.$message.warning('请选择支付链') this.$message.warning('请选择支付链')
return return
} }
if (!this.configForm.payCoin) { if (!this.configForm.payCoins || this.configForm.payCoins.length === 0) {
this.$message.warning('请选择支付币种') this.$message.warning('请选择支付币种')
return return
} }
@@ -379,10 +403,21 @@ export default {
this.$message.warning('请输入钱包地址') this.$message.warning('请输入钱包地址')
return return
} }
const { productId, ...rest } = this.configForm const payload = {
const payload = { ...rest, payType: Number(this.configForm.payType || 0) } id: this.configForm.id,
chain: this.configForm.chainValue || this.configForm.chainLabel,
payCoin: (this.configForm.payCoins || []).join(','),
payAddress: this.configForm.payAddress
}
this.updateShopConfig(payload) this.updateShopConfig(payload)
}, },
removeSelectedCoin(labelUpper) {
const label = String(labelUpper || '').toLowerCase()
const map = new Map((this.editCoinOptions || []).map(o => [String(o.label).toLowerCase(), String(o.value)]))
const value = map.get(label)
if (!value) return
this.configForm.payCoins = (this.configForm.payCoins || []).filter(v => String(v) !== String(value))
},
async handleOpenEdit() { async handleOpenEdit() {
try { try {
// 先打开弹窗,提供更快的视觉反馈 // 先打开弹窗,提供更快的视觉反馈
@@ -627,5 +662,9 @@ export default {
.el-dialog__footer { .el-dialog__footer {
padding-top: 4px; padding-top: 4px;
} }
/* 已选择币种 - 靠左对齐且换行友好 */
.selected-coin-list { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-start; }
.selected-coin-list .el-tag { margin-right: 0; }
</style> </style>

View File

@@ -6,7 +6,7 @@
<el-card class="section"> <el-card class="section">
<div class="row"><span class="label">订单ID</span><span class="value mono">{{ order.id || '—' }}</span></div> <div class="row"><span class="label">订单ID</span><span class="value mono">{{ order.id || '—' }}</span></div>
<div class="row"><span class="label">订单号</span><span class="value mono">{{ order.orderNumber || '—' }}</span></div> <div class="row"><span class="label">订单号</span><span class="value mono">{{ order.orderNumber || '—' }}</span></div>
<div class="row"><span class="label">状态</span><span class="value">{{ order.status }}</span></div> <div class="row"><span class="label">状态</span><span class="value">{{ getOrderStatusText(order.status) }}</span></div>
<div class="row"><span class="label">金额(USDT)</span><span class="value strong">{{ order.totalPrice }}</span></div> <div class="row"><span class="label">金额(USDT)</span><span class="value strong">{{ order.totalPrice }}</span></div>
<div class="row"><span class="label">创建时间</span><span class="value">{{ formatDateTime(order.createTime) }}</span></div> <div class="row"><span class="label">创建时间</span><span class="value">{{ formatDateTime(order.createTime) }}</span></div>
</el-card> </el-card>
@@ -72,8 +72,17 @@ export default {
} finally { } finally {
this.loading = false this.loading = false
} }
} },
, /**
* 订单状态文案映射
* 7: 进行中8: 已完成;其他:原样返回
*/
getOrderStatusText(value) {
const n = Number(value)
if (n === 7) return '进行中'
if (n === 8) return '已完成'
return String(value == null ? '' : value)
},
formatDateTime(value) { formatDateTime(value) {
if (!value) return '—' if (!value) return '—'
try { try {

View File

@@ -916,7 +916,13 @@ export default {
const res = await addSingleOrBatchMachine(payload) const res = await addSingleOrBatchMachine(payload)
if (res && (res.code === 0 || res.code === 200)) { if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('添加成功')
this.$message({
message: '添加成功',
duration: 3000,
showClose: true,
type: 'success'
})
this.confirmVisible = false this.confirmVisible = false
this.$router.back() this.$router.back()
} }

View File

@@ -190,11 +190,24 @@ export default {
} }
} }
try { try {
this.userEmail=JSON.parse(localStorage.getItem('userEmail')) this.userEmail=JSON.parse(localStorage.getItem('leasEmail'))
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
if ( !this.userEmail) {
this.$alert('登录信息异常,请重新返回 m2poll 矿池进入该系统', '提示', {
confirmButtonText: '确认',
center: true,
closeOnClickModal: false,
closeOnPressEscape: false,
showClose: false,
callback: () => {
window.location.href = 'https://m2pool.com'
}
})
return
}
const params = { const params = {
pageNum: this.pagination.pageNum, pageNum: this.pagination.pageNum,
pageSize: this.pagination.pageSize, pageSize: this.pagination.pageSize,

View File

@@ -287,8 +287,9 @@ export default {
</script> </script>
<style scoped> <style scoped>
.receipt-page { padding: 4px; }
.card { background: #fff; border: 1px solid #eee; border-radius: 10px; padding: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.04); } .receipt-page { margin: 0; box-sizing: border-box; overflow-x: hidden; }
.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: auto; }
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .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; } .card-title { margin: 0; font-size: 18px; font-weight: 700; color: #2c3e50; }
.card-actions { display: flex; align-items: center; gap: 8px; } .card-actions { display: flex; align-items: center; gap: 8px; }

View File

@@ -3,16 +3,43 @@
<h2 class="panel-title page-title">钱包绑定</h2> <h2 class="panel-title page-title">钱包绑定</h2>
<div class="panel-body" v-loading="loading"> <div class="panel-body" v-loading="loading">
<el-form :model="form" label-width="120px" class="config-form"> <el-form :model="form" label-width="120px" class="config-form">
<el-form-item label="选择链"> <el-form-item label="选择链/币种">
<el-cascader style="width: 420px;" @change="handleChange" v-model="value" :options="options"> </el-cascader> <el-cascader
style="width: 420px;"
v-model="value"
:options="options"
:props="cascaderProps"
:show-all-levels="false"
clearable
filterable
@change="handleChange"
@expand-change="handleExpandChange"
>
<template slot-scope="{ node, data }">
<span class="custom-node" aria-label="cascader-item" tabindex="0" @click.stop="handleItemClick(node, data)">
<span class="node-label">{{ data.label }}</span>
<span v-if="node.isLeaf && node.checked" class="leaf-checked" aria-hidden="true"></span>
</span>
</template>
</el-cascader>
</el-form-item> </el-form-item>
<el-form-item label="币种类型"> <el-form-item label="已选择币种">
<el-radio-group v-model="form.payType" class="radio-group"> <div class="selected-coins" aria-label="selected-coins" tabindex="0">
<!-- <el-radio :label="0">虚拟币</el-radio> --> <el-tag
<el-radio :label="1">稳定币</el-radio> v-for="coin in selectedCoins"
</el-radio-group> :key="coin"
type="warning"
effect="light"
closable
disable-transitions
@close="handleRemoveSelectedCoin(coin)"
>
{{ coin }}
</el-tag>
<span v-if="selectedCoins.length === 0" class="placeholder">未选择</span>
</div>
</el-form-item> </el-form-item>
@@ -39,32 +66,15 @@
import { getMyShop } from "@/api/shops"; import { getMyShop } from "@/api/shops";
import { getChainAndList, addWalletShopConfig } from "../../api/wallet"; import { getChainAndList, addWalletShopConfig } from "../../api/wallet";
// 币种集合
const VIRTUAL_COINS = [
"nexa",
"rxd",
"dgbo",
"dgbq",
"dgbs",
"alph",
"enx",
"grs",
"mona",
];
const STABLE_COINS = ["usdt", "usdc", "busd"];
export default { export default {
name: "AccountShopConfig", name: "AccountShopConfig",
data() { data() {
return { return {
VIRTUAL_COINS,
STABLE_COINS,
productOptions: [], productOptions: [],
form: { form: {
chain: "", chain: "",
payAddress: "", payAddress: "",
payCoin: "", payCoin: "",
payType: 1, // 0 虚拟币 1 稳定币
}, },
shop: { shop: {
@@ -75,7 +85,10 @@ export default {
del: true, del: true,
state: 0, state: 0,
}, },
value: "", // 级联多选值:形如 [[chain, coin], [chain, coin2]]
value: [],
currentChain: '',
cascaderProps: { multiple: true, checkStrictly: false, emitPath: true, value: 'value', label: 'label', children: 'children' },
options: [ options: [
// { // {
// value: "Tron (TRC20)", // value: "Tron (TRC20)",
@@ -110,6 +123,49 @@ export default {
this.getChainAndList(); this.getChainAndList();
}, },
methods: { methods: {
/**
* 关闭标签 -> 移除对应币种
*/
handleRemoveSelectedCoin(coinUpper) {
const coin = String(coinUpper || '').toLowerCase()
const next = (this.value || []).filter(v => Array.isArray(v) && String(v[1]).toLowerCase() !== coin)
this.handleChange(next)
},
// 允许点击整行触发选择/展开,提升“可点文字即可选”的体验
handleItemClick(node, data) {
if (!node) return
if (node.isLeaf) {
// 叶子:切换选中
const path = node.path.map(n => n.value)
const chain = path[0]
const coin = path[1]
this.currentChain = String(chain || '')
let next = Array.isArray(this.value) ? this.value.slice() : []
// 始终以最后点击的链为准:如果链不同,清空旧链
const last = next.length ? next[next.length - 1] : null
const lastChain = Array.isArray(last) ? last[0] : null
if (lastChain && lastChain !== chain) next = []
const idx = next.findIndex(v => Array.isArray(v) && v[0] === chain && v[1] === coin)
if (idx >= 0) next.splice(idx, 1)
else next.push([chain, coin])
this.handleChange(next)
} else {
// 父级:以最后点击为准,清空旧链选择并切换到当前链
const chain = data && data.value
if (!node.expanded) node.expand()
if (chain) {
this.currentChain = String(chain)
this.value = []
this.form.chain = String(chain)
this.form.payCoin = ''
}
}
},
handleExpandChange(nodes) {
// 记录当前展开的链,供 @change 过滤依据
const chain = Array.isArray(nodes) ? (nodes[0] || '') : ''
if (chain) this.currentChain = String(chain)
},
/** /**
* 根据选择的链校验钱包地址格式(参考钱包页面规则) * 根据选择的链校验钱包地址格式(参考钱包页面规则)
* - tron: 以 T 开头的 34 位字符 * - tron: 以 T 开头的 34 位字符
@@ -123,7 +179,9 @@ export default {
if (c.includes('tron') || c === 'tron') { if (c.includes('tron') || c === 'tron') {
const ok = /^T[A-Za-z1-9]{33}$/.test(addr) const ok = /^T[A-Za-z1-9]{33}$/.test(addr)
return ok ? { ok: true } : { ok: false, message: '请输入正确的收款地址格式TRON' } return ok
? { ok: true }
: { ok: false, message: '请输入正确的 TRON 地址:以 T 开头的 34 位字符' }
} }
if ( if (
@@ -134,7 +192,9 @@ export default {
c.includes('erc') || c.includes('bep') c.includes('erc') || c.includes('bep')
) { ) {
const ok = /^0x[a-fA-F0-9]{40}$/.test(addr) const ok = /^0x[a-fA-F0-9]{40}$/.test(addr)
return ok ? { ok: true } : { ok: false, message: '请输入正确的收款地址格式EVM' } return ok
? { ok: true }
: { ok: false, message: '请输入正确的以太坊/EVM 兼容链地址:以 0x 开头 + 40 位十六进制' }
} }
if (addr.length <= 10) { if (addr.length <= 10) {
@@ -179,17 +239,28 @@ export default {
}, },
handleChange(value) { handleChange(value) {
console.log(value); // 仅允许同一链下多选;无提示,始终以“最后一次点击”的链为准
const selections = Array.isArray(value) ? value : []
this.form.payCoin = value[1]; if (selections.length === 0) {
this.form.chain = value[0]; this.form.chain = ""
this.form.payCoin = ""
this.value = []
return
}
const last = selections[selections.length - 1]
const lastChain = Array.isArray(last) ? last[0] : ''
const targetChain = this.currentChain || lastChain
const filtered = selections.filter(v => Array.isArray(v) && v[0] === targetChain)
this.value = filtered
this.form.chain = targetChain || ""
this.form.payCoin = filtered.map(v => v[1]).filter(Boolean).join(',')
}, },
handleSave() { handleSave() {
// 从多选值同步 chain 与 payCoin英文逗号隔开
this.form.chain =this.value[0] const selections = Array.isArray(this.value) ? this.value : []
this.form.payCoin = this.value[1] this.form.chain = selections.length ? (selections[0] && selections[0][0]) : ''
this.form.payCoin = selections.map(v => v && v[1]).filter(Boolean).join(',')
if (!this.form.chain) { if (!this.form.chain) {
this.$message.warning("请选择链"); this.$message.warning("请选择链");
@@ -214,19 +285,25 @@ export default {
this.FetchAddWalletShopConfig(this.form); this.FetchAddWalletShopConfig(this.form);
}, },
handleReset() { handleReset() {
this.form = { chain: "", payAddress: "", payCoin: "", payType: 0 }; this.form = { chain: "", payAddress: "", payCoin: "" };
this.value = []
}, },
}, },
computed: { computed: {
// 根据币种类型动态展示可选币种 /**
coinOptions() { * 已选择币种的可读展示(中文顿号分隔)
return this.form.payType === 1 ? STABLE_COINS : VIRTUAL_COINS; */
selectedCoinsDisplay() {
const arr = Array.isArray(this.value) ? this.value : []
const coins = arr.map(v => v && v[1]).filter(Boolean).map(s => String(s).toUpperCase())
return coins.join('、')
}, },
}, /**
watch: { * 标签列表数据
"form.payType"(val) { */
// 切换类型时清空已选币种,避免类型与币种不匹配 selectedCoins() {
this.form.payCoin = ""; const arr = Array.isArray(this.value) ? this.value : []
return arr.map(v => v && v[1]).filter(Boolean).map(s => String(s).toUpperCase())
}, },
}, },
}; };
@@ -266,5 +343,15 @@ export default {
font-size: 12px; font-size: 12px;
margin-top: 6px; margin-top: 6px;
} }
/* 自定义级联节点样式:叶子被选中时显示勾选 */
.custom-node { display: inline-flex; align-items: center; gap: 8px; }
.leaf-checked { color: #409EFF; font-weight: 700; }
.node-label { line-height: 20px; }
/* 已选择币种标签样式 */
.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; }
</style> </style>

View File

@@ -26,6 +26,14 @@
<span class="balance-amount">{{ (w.walletBalance || w.balance || 0) }} {{ displaySymbol(w) }}</span> <span class="balance-amount">{{ (w.walletBalance || w.balance || 0) }} {{ displaySymbol(w) }}</span>
</div> </div>
<div class="balance-item"> <div class="balance-item">
<el-tooltip placement="top" effect="dark">
<div slot="content">
冻结金额不能使用或提现以下情况会冻结钱包余额<br/>
1. 下单机器后会冻结订单对应金额<br/>
2. 提交提现后金额正在提现中
</div>
<i class="el-icon-question balance-tip-icon"></i>
</el-tooltip>
<span class="balance-label">冻结余额</span> <span class="balance-label">冻结余额</span>
<span class="balance-amount frozen">{{ (w.blockedBalance || 0) }} {{ displaySymbol(w) }}</span> <span class="balance-amount frozen">{{ (w.blockedBalance || 0) }} {{ displaySymbol(w) }}</span>
</div> </div>
@@ -53,9 +61,16 @@
<div class="transaction-info"> <div class="transaction-info">
<span class="transaction-type">{{ transaction.type }}</span> <span class="transaction-type">{{ transaction.type }}</span>
<span class="transaction-time">{{ transaction.time }}</span> <span class="transaction-time">{{ transaction.time }}</span>
<el-tag
size="mini"
class="transaction-status"
:type="transaction.statusTagType || 'info'"
>
{{ transaction.statusText || '-' }}
</el-tag>
</div> </div>
<div class="transaction-amount" :class="transaction.amount > 0 ? 'positive' : 'negative'"> <div class="transaction-amount" :class="transaction.amount > 0 ? 'positive' : 'negative'">
{{ transaction.amount > 0 ? '+' : '' }}{{ transaction.amount }} USDT {{ transaction.amount > 0 ? '+' : '' }}{{ transaction.amountText }} USDT
</div> </div>
</div> </div>
<div v-if="recentTransactions.length === 0" class="empty-state"> <div v-if="recentTransactions.length === 0" class="empty-state">
@@ -136,8 +151,9 @@
<el-dialog <el-dialog
title="USDT提现" title="USDT提现"
:visible.sync="withdrawDialogVisible" :visible.sync="withdrawDialogVisible"
width="600px" width="720px"
@close="resetWithdrawForm" @close="resetWithdrawForm"
:close-on-click-modal="false" :close-on-press-escape="false"
> >
<el-form :model="withdrawForm" :rules="withdrawRules" ref="withdrawForm" label-width="120px"> <el-form :model="withdrawForm" :rules="withdrawRules" ref="withdrawForm" label-width="120px">
<!-- 提现链只读展示当前钱包链 --> <!-- 提现链只读展示当前钱包链 -->
@@ -170,12 +186,22 @@
<template slot="append">{{ displayWithdrawSymbol }}</template> <template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input> </el-input>
<div class="balance-info"> <div class="balance-info">
<div class="balance-detail"> <div class="balance-total">钱包总余额{{ totalBalance }} {{ displayWithdrawSymbol }}</div>
<span>可用余额{{ (WalletData.walletBalance || WalletData.balance || 0) }} {{ displayWithdrawSymbol }}</span> <div class="balance-row">
</div> <span>可用余额{{ availableWithdrawBalance }} {{ displayWithdrawSymbol }}</span>
<div class="balance-detail frozen-info"> <span class="divider">|</span>
<span>冻结余额{{ (WalletData.blockedBalance || 0) }} {{ displayWithdrawSymbol }}</span> <span class="frozen-info">
<span class="frozen-tip">购买机器下单后冻结不可提现</span> <el-tooltip placement="top" effect="dark">
<div slot="content">
冻结金额不能使用或提现以下情况会冻结钱包余额<br/>
1. 下单机器后会冻结订单对应金额<br/>
2. 提交提现后金额正在提现中
</div>
<i class="el-icon-question frozen-tip-icon"></i>
</el-tooltip>
冻结余额{{ (WalletData.blockedBalance || 0) }} {{ displayWithdrawSymbol }}
<span class="frozen-tip">购买机器下单后冻结不可提现</span>
</span>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
@@ -255,6 +281,7 @@
<el-dialog <el-dialog
title="链上充值" title="链上充值"
:visible.sync="createDialogVisible" :visible.sync="createDialogVisible"
:close-on-click-modal="false" :close-on-press-escape="false"
width="520px" width="520px"
> >
<el-form label-width="120px"> <el-form label-width="120px">
@@ -405,22 +432,29 @@ export default {
* 计算实际到账金额 * 计算实际到账金额
*/ */
actualAmount() { actualAmount() {
// 使用“分”为单位进行整数计算,避免浮点误差 // 使用 10^6 精度进行整数计算,避免浮点误差展示时最多6位小数、去尾零
const amountCents = this.toCents(this.withdrawForm.amount) const amountInt = this.toScaledInt(this.withdrawForm.amount)
const feeCents = this.toCents(this.withdrawForm.fee) const feeInt = this.toScaledInt(this.withdrawForm.fee)
if (!Number.isFinite(amountCents) || !Number.isFinite(feeCents)) return '0.00' if (!Number.isFinite(amountInt) || !Number.isFinite(feeInt)) return '0'
const resultCents = amountCents - feeCents const result = amountInt - feeInt
return resultCents > 0 ? this.centsToAmountString(resultCents) : '0.00' if (result <= 0) return '0'
return this.formatDec6FromInt(result)
}, },
/** /**
* 计算总余额(可用余额 + 冻结余额) * 计算总余额(可用余额 + 冻结余额)
*/ */
totalBalance() { totalBalance() {
const available = parseFloat(this.walletBalance) || 0 const available = parseFloat(this.WalletData.walletBalance || this.WalletData.balance || this.walletBalance || 0) || 0
const blocked = parseFloat(this.blockedBalance) || 0 const blocked = parseFloat(this.WalletData.blockedBalance || this.blockedBalance || 0) || 0
return (available + blocked).toFixed(2) return (available + blocked).toFixed(2)
}, },
/**
* 可用余额(提现对话框时以当前卡片的钱包为准)
*/
availableWithdrawBalance() {
return (this.WalletData.walletBalance || this.WalletData.balance || 0)
},
/** /**
* 提现展示单位(始终大写):优先当前 WalletData 的 fromSymbol/coin * 提现展示单位(始终大写):优先当前 WalletData 的 fromSymbol/coin
*/ */
@@ -511,11 +545,18 @@ export default {
const type = Number(r && r.type) const type = Number(r && r.type)
const signAmt = (type === 1) ? Math.abs(amt) : -Math.abs(amt) // 1 充值为正0 支付/2 提现为负 const signAmt = (type === 1) ? Math.abs(amt) : -Math.abs(amt) // 1 充值为正0 支付/2 提现为负
const typeLabel = type === 1 ? '充值' : (type === 2 ? '提现' : '支付') const typeLabel = type === 1 ? '充值' : (type === 2 ? '提现' : '支付')
const status = Number(r && r.status)
const statusTextMap = { 0: '失败', 1: '成功', 2: '处理中', 3: '校验失败' }
const statusTagTypeMap = { 0: 'danger', 1: 'success', 2: 'warning', 3: 'danger' }
return { return {
id: `${r && r.updateTime || ''}-${idx}`, id: `${r && r.updateTime || ''}-${idx}`,
type: typeLabel, type: typeLabel,
amount: Number(signAmt.toFixed(2)), amount: signAmt,
time: this.formatApiTime(r && r.updateTime) amountText: this.formatDec6(Math.abs(signAmt)),
time: this.formatApiTime(r && r.updateTime),
status,
statusText: statusTextMap[status] || '-',
statusTagType: statusTagTypeMap[status] || 'info'
} }
}) })
this.recentTransactions = mapped this.recentTransactions = mapped
@@ -533,35 +574,69 @@ export default {
if (!s) return '' if (!s) return ''
return s.replace('T', ' ').replace('Z', '') return s.replace('T', ' ').replace('Z', '')
}, },
/**
* 金额显示保留最多6位小数直接截断不四舍五入不补尾随0
*/
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
},
/** /**
* 将金额字符串转换为“分”为单位的整数 * 将金额字符串转换为“分”为单位的整数
*/ */
toCents(amountStr) { /**
* 将金额字符串转为按 10^6 精度的整数
*/
toScaledInt(amountStr, decimals = 6) {
if (amountStr === null || amountStr === undefined) return 0 if (amountStr === null || amountStr === undefined) return 0
const normalized = String(amountStr).trim() const normalized = String(amountStr).trim()
if (normalized === '') return 0 if (normalized === '') return 0
// 使用正则确保是最多两位小数的数字格式 const re = new RegExp(`^\\d+(?:\\.(\\d{0,${decimals}}))?$`)
const match = normalized.match(/^\d+(?:\.(\d{0,2}))?$/) const match = normalized.match(re)
if (!match) { if (!match) {
const n = Number(normalized) const n = Number(normalized)
if (!Number.isFinite(n)) return 0 if (!Number.isFinite(n)) return 0
return Math.round(n * 100) const scale = Math.pow(10, decimals)
return Math.round(n * scale)
} }
const [intPart, decPartRaw] = normalized.split('.') const [intPart, decPartRaw] = normalized.split('.')
const decPart = (decPartRaw || '').padEnd(2, '0').slice(0, 2) const decPart = (decPartRaw || '').padEnd(decimals, '0').slice(0, decimals)
return Number(intPart) * 100 + Number(decPart) const scale = Math.pow(10, decimals)
return Number(intPart) * scale + Number(decPart)
}, },
/** /**
* 将“分”为单位的整数转为字符串金额,固定两位小数 * 将“分”为单位的整数转为字符串金额,固定两位小数
*/ */
centsToAmountString(cents) { /**
const sign = cents < 0 ? '-' : '' * 将按 10^6 精度的整数转为字符串金额,固定六位小数
const abs = Math.abs(cents) */
const intPart = Math.floor(abs / 100) scaledIntToString(intVal, decimals = 6) {
const decPart = String(abs % 100).padStart(2, '0') const sign = intVal < 0 ? '-' : ''
const abs = Math.abs(intVal)
const scale = Math.pow(10, decimals)
const intPart = Math.floor(abs / scale)
const decPart = String(abs % scale).padStart(decimals, '0')
return `${sign}${intPart}.${decPart}` return `${sign}${intPart}.${decPart}`
}, },
/**
* 将按10^6精度的整数转为最多6位小数字符串不补零不四舍五入
*/
formatDec6FromInt(intVal) {
const s = this.scaledIntToString(intVal, 6)
return s.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
},
async fetchWalletInfo(params) { async fetchWalletInfo(params) {
try { try {
const res = await getWalletInfo(params) const res = await getWalletInfo(params)
@@ -786,6 +861,14 @@ export default {
* 根据链更新手续费 * 根据链更新手续费
*/ */
updateFeeByChain() { updateFeeByChain() {
// 优先从当前钱包信息中读取提现手续费后端字段charge
const walletCharge = (this.WalletData && (this.WalletData.charge != null ? this.WalletData.charge : this.WalletData.fee))
if (walletCharge != null && walletCharge !== '') {
const n = Number(walletCharge)
this.withdrawForm.fee = Number.isFinite(n) ? n.toFixed(2) : String(walletCharge)
return
}
// 兜底:按链类型给一个默认手续费
const feeMap = { const feeMap = {
tron: '1.00', // TRC20 手续费较低 tron: '1.00', // TRC20 手续费较低
ethereum: '5.00', // ERC20 手续费较高 ethereum: '5.00', // ERC20 手续费较高
@@ -811,6 +894,7 @@ export default {
toSymbol: (this.WalletData && (this.WalletData.fromSymbol || this.WalletData.coin)) || this.withdrawForm.toSymbol, toSymbol: (this.WalletData && (this.WalletData.fromSymbol || this.WalletData.coin)) || this.withdrawForm.toSymbol,
amount: parseFloat(this.withdrawForm.amount), amount: parseFloat(this.withdrawForm.amount),
toAddress: this.withdrawForm.toAddress, toAddress: this.withdrawForm.toAddress,
fromAddress: (this.WalletData && this.WalletData.fromAddress) || '',
code: this.withdrawForm.googleCode // 添加谷歌验证码 code: this.withdrawForm.googleCode // 添加谷歌验证码
}) })
@@ -872,31 +956,32 @@ export default {
} }
// 使用“分”为单位的整数校验 // 使用“分”为单位的整数校验
const amountCents = this.toCents(value) const amountInt = this.toScaledInt(value)
if (!Number.isFinite(amountCents) || amountCents <= 0) { if (!Number.isFinite(amountInt) || amountInt <= 0) {
callback(new Error('请输入有效的金额')) callback(new Error('请输入有效的金额'))
return return
} }
// 手续费与总需求按相同精度计算
const feeCents = this.toCents(this.withdrawForm.fee) const feeInt = this.toScaledInt(this.withdrawForm.fee)
const totalRequired = amountCents + feeCents const totalRequired = amountInt + feeInt
// 钱包余额转分 // 钱包余额转分
const balanceCents = this.toCents(this.walletBalance) const balanceInt = this.toScaledInt(this.walletBalance)
if (totalRequired > balanceCents) { if (totalRequired > balanceInt) {
const totalText = this.centsToAmountString(totalRequired) const totalText = this.scaledIntToString(totalRequired)
callback(new Error(`提现金额加上手续费(${totalText} USDT)不能超过钱包余额`)) callback(new Error(`提现金额加上手续费(${totalText} USDT)不能超过钱包余额`))
return return
} }
// 检查最小提现金额 // 检查最小提现金额
if (amountCents < 1000) { // 10 USDT = 1000 分 // 最小提现金额 1 USDT
callback(new Error('最小提现金额为10 USDT')) if (amountInt < 1000000) {
callback(new Error('最小提现金额为1 USDT'))
return return
} }
// 检查实际到账金额是否为正数 // 检查实际到账金额是否为正数(提现金额必须大于手续费)
if (amountCents <= feeCents) { if (amountInt <= feeInt) {
callback(new Error('提现金额必须大于手续费')) callback(new Error('提现金额必须大于手续费'))
return return
} }
@@ -924,10 +1009,10 @@ export default {
if (firstDot !== -1) { if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '') v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
} }
// 最多位小数 // 最多位小数
if (firstDot !== -1) { if (firstDot !== -1) {
const [intPart, decPart] = v.split('.') const [intPart, decPart] = v.split('.')
v = intPart + '.' + (decPart ? decPart.slice(0, 2) : '') v = intPart + '.' + (decPart ? decPart.slice(0, 6) : '')
} }
// 限制整数部分长度,避免过大导致显示或后端处理异常,这里限制到 12 位(万亿级) // 限制整数部分长度,避免过大导致显示或后端处理异常,这里限制到 12 位(万亿级)
const parts = v.split('.') const parts = v.split('.')
@@ -1182,26 +1267,30 @@ export default {
} }
.transaction-list { .transaction-list {
max-height: 400px; /* API 仅返回 <=5 条,使用自适应高度,避免滚动条 */
overflow-y: auto; max-height: none;
overflow-y: visible;
display: flex;
flex-direction: column;
gap: 6px;
padding-top: 4px;
padding-bottom: 4px;
} }
.transaction-item { .transaction-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 12px 0; padding: 8px 0;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
} }
.transaction-item:hover { .transaction-item:hover {
background-color: #f8f9fa; background-color: #f8f9fa;
border-radius: 6px; border-radius: 6px;
padding-left: 8px; padding-left: 6px;
padding-right: 8px; padding-right: 6px;
} }
.transaction-item:last-child { .transaction-item:last-child {
@@ -1213,6 +1302,7 @@ export default {
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
} }
.transaction-status { align-self: flex-start; }
.transaction-type { .transaction-type {
font-weight: 500; font-weight: 500;
@@ -1407,13 +1497,17 @@ export default {
text-align: left; text-align: left;
} }
.balance-detail { .balance-total { margin-bottom: 4px; font-weight: 600; }
margin-bottom: 4px; .balance-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
} .divider { color: #ccc; }
.frozen-info { .frozen-info {
color: #e6a23c; color: #e6a23c;
} }
.balance-tip-icon, .frozen-tip-icon {
margin-right: 4px;
color: #ffd666;
cursor: pointer;
}
.frozen-tip { .frozen-tip {
font-size: 11px; font-size: 11px;

View File

@@ -102,9 +102,12 @@
<!-- <el-table-column label="机器总数" min-width="120"> <!-- <el-table-column label="机器总数" min-width="120">
<template #default="scope">{{ countMachines(scope.row) }}</template> <template #default="scope">{{ countMachines(scope.row) }}</template>
</el-table-column> --> </el-table-column> -->
<el-table-column label="总价(USDT)" > <el-table-column prop="totalPrice" label="总价(USDT)">
<template #default="scope"><span class="price-strong">{{ formatTrunc(computeShopTotal(scope.row), 2) }}</span></template> <template #default="scope">
<span class="price-strong">{{ computeShopTotalDisplay(scope.row) }}</span>
</template>
</el-table-column> </el-table-column>
<el-table-column label="支付方式"> <el-table-column label="支付方式">
<template #default="scope"> <template #default="scope">
@@ -128,20 +131,25 @@
</div> </div>
</div> </div>
<el-dialog :visible.sync="confirmDialog.visible" width="720px" :close-on-click-modal="false" :title="`确认结算该店铺订单(共 ${confirmDialog.count} 台机器)`"> <el-dialog :visible.sync="confirmDialog.visible" width="80vw" :close-on-click-modal="false" :title="`确认结算该店铺订单(共 ${confirmDialog.count} 台机器)`">
<div> <div>
<el-table :data="confirmDialog.items" height="360" border stripe <el-table :data="confirmDialog.items" height="360" border stripe
:header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }"> :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table-column prop="product" label="商品" min-width="160" /> <el-table-column prop="product" label="商品" min-width="160" />
<el-table-column prop="coin" label="币种" min-width="100" /> <el-table-column prop="coin" label="币种" min-width="100" />
<el-table-column prop="machineId" label="机器ID" min-width="100" />
<el-table-column prop="user" label="账户" min-width="120" /> <el-table-column prop="user" label="账户" min-width="120" />
<el-table-column prop="miner" label="机器编号" min-width="160" /> <el-table-column prop="miner" label="机器编号" min-width="160" />
<el-table-column prop="price" min-width="120"> <el-table-column prop="unitPrice" min-width="140">
<template #header>单价({{ payCoinSymbol || 'USDT' }}</template> <template #header>单价({{ payCoinSymbol || 'USDT' }}</template>
<template #default="scope"><span class="price-strong">{{ formatTrunc(scope.row.unitPrice, 2) }}</span></template>
</el-table-column>
<el-table-column prop="leaseTime" label="租赁天数" min-width="120" />
<el-table-column prop="subtotal" min-width="140">
<template #header>小计({{ payCoinSymbol || 'USDT' }}</template>
<template #default="scope"><span class="price-strong">{{ formatTrunc(scope.row.subtotal, 2) }}</span></template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div style="margin-top:12px;text-align:right;">总金额({{ payCoinSymbol || 'USDT' }}<b>{{ formatTrunc(confirmDialog.total, 2) }}</b></div> <div style="margin-top:12px;text-align:right;">总金额({{ payCoinSymbol || 'USDT' }}<span class="price-strong">{{ formatTrunc(confirmDialog.total, 2) }}</span></div>
</div> </div>
<template #footer> <template #footer>
<el-button @click="confirmDialog.visible=false">取消</el-button> <el-button @click="confirmDialog.visible=false">取消</el-button>
@@ -166,20 +174,7 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 结算成功提示弹窗 -->
<el-dialog :visible.sync="successDialog.visible" width="480px" :close-on-click-modal="false" :close-on-press-escape="false" @close="handleCloseSuccessDialog">
<div style="text-align:center; padding: 20px 0;">
<div style="font-size: 48px; color: #52c41a; margin-bottom: 16px;">✓</div>
<div style="font-size: 18px; color: #333; margin-bottom: 12px;">请求结算处理成功</div>
<div style="color: #666; line-height: 1.6;">
请在订单列表页面查看结算状态<br>
结算成功会自动更新钱包余额
</div>
</div>
<template #footer>
<el-button type="primary" @click="handleCloseSuccessDialog">已知晓</el-button>
</template>
</el-dialog>
<!-- 购物须知(测试版) - 必须勾选并等待5秒后才可关闭 --> <!-- 购物须知(测试版) - 必须勾选并等待5秒后才可关闭 -->
<el-dialog :visible.sync="noticeDialog.visible" width="680px" title="下单须知" :show-close="false" :close-on-click-modal="false" :close-on-press-escape="false"> <el-dialog :visible.sync="noticeDialog.visible" width="680px" title="下单须知" :show-close="false" :close-on-click-modal="false" :close-on-press-escape="false">
@@ -252,7 +247,22 @@
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
<!-- 结算成功提示弹窗(全局,避免被父级条件渲染影响) -->
<el-dialog :visible.sync="settlementSuccessfulVisible" width="480px" append-to-body :close-on-click-modal="false" :close-on-press-escape="false" @close="handleCloseSuccessDialog">
<div style="text-align:center; padding: 20px 0;">
<div style="font-size: 48px; color: #52c41a; margin-bottom: 16px;">✓</div>
<div style="font-size: 18px; color: #333; margin-bottom: 12px;">请求结算处理成功</div>
<div style="color: #666; line-height: 1.6;">
请在订单列表页面查看结算状态<br>
结算成功会自动更新钱包余额
</div>
</div>
<template #footer>
<el-button type="primary" @click="handleCloseSuccessDialog">已知晓</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
@@ -292,7 +302,8 @@ export default {
selectedChain: '', selectedChain: '',
selectedCoin: '', selectedCoin: '',
selectedPrice: 0 selectedPrice: 0
,clearOffLoading: false ,clearOffLoading: false,
settlementSuccessfulVisible: false
} }
}, },
computed: { computed: {
@@ -368,10 +379,45 @@ export default {
this.noticeTimer = null this.noticeTimer = null
}, },
methods: { methods: {
/**
* 将金额转为整分整数,避免浮点误差
* @param {number|string} v
* @returns {number}
*/
toCents(v) {
// 直接截取两位小数(不四舍五入),并转为“分”的整数
if (v === null || v === undefined) return 0
let s = String(v).trim()
if (s === '') return 0
let sign = 1
if (s[0] === '-') { sign = -1; s = s.slice(1) }
const parts = s.split('.')
const intPart = parseInt(parts[0] || '0', 10) || 0
const decRaw = (parts[1] || '').replace(/[^0-9]/g, '')
const decTwo = (decRaw.length >= 2 ? decRaw.slice(0, 2) : decRaw.padEnd(2, '0'))
const cents = intPart * 100 + (parseInt(decTwo || '0', 10) || 0)
return sign * cents
},
/**
* 整分转字符串,固定两位小数
* @param {number} cents
* @returns {string}
*/
centsToText(cents) {
const sign = cents < 0 ? '-' : ''
const abs = Math.abs(Number(cents) || 0)
const intPart = Math.floor(abs / 100)
const decPart = String(abs % 100).padStart(2, '0')
return `${sign}${intPart}.${decPart}`
},
// 选择框可选逻辑:下架(或删除)机器不可选择 // 选择框可选逻辑:下架(或删除)机器不可选择
isRowSelectable(row, index) { isRowSelectable(row, index) {
return !(Number(row && row.del) === 1 || Number(row && row.state) === 1) return !(Number(row && row.del) === 1 || Number(row && row.state) === 1)
}, },
// 判断机器是否“上架”(用于未展开时的自动筛选)
isOnShelf(row) {
return !(Number(row && row.del) === 1 || Number(row && row.state) === 1)
},
// 获取本地最大可租赁天数,默认 365 // 获取本地最大可租赁天数,默认 365
getRowMaxLeaseDaysLocal(row) { getRowMaxLeaseDaysLocal(row) {
const raw = row && row.maxLeaseDays const raw = row && row.maxLeaseDays
@@ -402,6 +448,11 @@ export default {
}, },
//获取支持的链和币种 //获取支持的链和币种
async fetchChainAndListForSeller(shopId) { async fetchChainAndListForSeller(shopId) {
if (!shopId) {
this.options = []
this.loading = false
return
}
this.loading = true; this.loading = true;
const res = await getChainAndListForSeller({ id: shopId }); const res = await getChainAndListForSeller({ id: shopId });
if (res && (res.code === 0 || res.code === 200) && res.data) { if (res && (res.code === 0 || res.code === 200) && res.data) {
@@ -425,10 +476,42 @@ export default {
}, },
// 获取所有商品分组(兼容保留,现直接返回空,因已移除中间商品层) // 获取所有商品分组(兼容保留,现直接返回空,因已移除中间商品层)
getAllGroups() { return [] }, getAllGroups() { return [] },
// 店铺总价 = 累加其所有商品的 (机器单价 × 租赁天数) // 店铺总价(精确到分):优先用后端 totalPrice若用户改动租期则实时按分计算
computeShopTotal(shop) { computeShopTotal(shop) {
if (!shop) return 0
const list = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
// 若没有子项,直接返回后端值
if (!list.length) return Number(shop.totalPrice || 0)
let totalCents = 0
for (const m of list) {
const priceCents = this.toCents(m && m.price)
const days = Math.max(1, Math.floor(Number(m && m.leaseTime) || 1))
totalCents += priceCents * days
}
return totalCents / 100
},
/**
* 返回展示用字符串:两位小数且无精度丢失
*/
computeShopTotalDisplay(shop) {
const list = Array.isArray(shop && shop.productMachineDtoList) ? shop.productMachineDtoList : [] const list = Array.isArray(shop && shop.productMachineDtoList) ? shop.productMachineDtoList : []
return list.reduce((sum, m) => sum + Number(m.price || 0) * Number(m.leaseTime || 1), 0) const backendVal = Number(shop && shop.totalPrice)
const hasBackend = Number.isFinite(backendVal)
// 若未修改任一机器租期,优先展示后端总价
let modified = false
for (const m of list) {
const orig = (m && m._origLeaseTime != null) ? Number(m._origLeaseTime) : Number(m && m.leaseTime)
const cur = Math.max(1, Math.floor(Number(m && m.leaseTime) || 1))
if (orig !== cur) { modified = true; break }
}
if ((hasBackend && !modified) || (!list.length && hasBackend)) return this.formatTrunc(backendVal, 2)
let totalCents = 0
for (const m of list) {
const priceCents = this.toCents(m && m.price)
const days = Math.max(1, Math.floor(Number(m && m.leaseTime) || 1))
totalCents += priceCents * days
}
return this.centsToText(totalCents)
}, },
// 组装批量删除的请求体:数组 [{ machineId, productId }] // 组装批量删除的请求体:数组 [{ machineId, productId }]
buildDeletePayload() { buildDeletePayload() {
@@ -548,6 +631,13 @@ export default {
...shop, ...shop,
id: shop.id != null ? String(shop.id) : `shop-${sIdx}` id: shop.id != null ? String(shop.id) : `shop-${sIdx}`
})) }))
// 记录每台机器的原始租期,便于判断是否被修改
try {
withShopKeys.forEach(sp => {
const list = Array.isArray(sp.productMachineDtoList) ? sp.productMachineDtoList : []
list.forEach(m => { if (m && m._origLeaseTime == null) m._origLeaseTime = Number(m.leaseTime || 1) })
})
} catch (e) { /* noop */ }
this.shops = withShopKeys this.shops = withShopKeys
this.groups = [] this.groups = []
this.expandedGroupKeys = [] this.expandedGroupKeys = []
@@ -681,7 +771,7 @@ export default {
return return
} }
machines.forEach(m => { machines.forEach(m => {
if (selectedSet.has(m.id)) { if (selectedSet.has(m.id) && this.isOnShelf(m)) {
payload.push({ payload.push({
leaseTime: Number(m.leaseTime || 1), leaseTime: Number(m.leaseTime || 1),
machineId: m.id, machineId: m.id,
@@ -690,9 +780,18 @@ export default {
}) })
} }
}) })
if (!payload.length) {
this.$message({ message: '所选机器均已下架,无法结算', type: 'warning', showClose: true })
return
}
} else { } else {
// 未展开:默认使用全部机器 // 未展开:仅结算“上架”机器
machines.forEach(m => { const onShelfMachines = machines.filter(m => this.isOnShelf(m))
if (!onShelfMachines.length) {
this.$message({ message: '该店铺暂无上架机器可结算', type: 'warning', showClose: true })
return
}
onShelfMachines.forEach(m => {
payload.push({ payload.push({
leaseTime: Number(m.leaseTime || 1), leaseTime: Number(m.leaseTime || 1),
machineId: m.id, machineId: m.id,
@@ -723,11 +822,18 @@ export default {
if (res && Number(res.code) === 200) { if (res && Number(res.code) === 200) {
const dataStr = String(res.data || '') const dataStr = String(res.data || '')
ok = dataStr.includes('成功') ok = dataStr.includes('成功')
this.$message({
message: `结算成功,订单状态请在订单列表中查看`,
type: 'success',
duration: 3000,
showClose: true,
})
this.settlementSuccessfulVisible = true
} }
if (ok) { if (ok) {
// 结算成功后重新获取购物车数据,更新购物车数量 // 结算成功后重新获取购物车数据,更新购物车数量
await this.fetchGetGoodsList() await this.fetchGetGoodsList()
this.successDialog.visible = true // this.settlementSuccessfulVisible = true
} else { } else {
// 失败或未成功,恢复之前的勾选 UI // 失败或未成功,恢复之前的勾选 UI
this.reapplySelectionsForPendingShop() this.reapplySelectionsForPendingShop()
@@ -823,7 +929,7 @@ export default {
}, },
// 关闭成功弹窗:跳转到订单列表的订单进行中状态 // 关闭成功弹窗:跳转到订单列表的订单进行中状态
handleCloseSuccessDialog() { handleCloseSuccessDialog() {
try { this.successDialog.visible = false } catch (e) { /* noop */ } try { this.settlementSuccessfulVisible = false } catch (e) { /* noop */ }
this.$router.push({ path: '/account/orders', query: { status: '7' } }) this.$router.push({ path: '/account/orders', query: { status: '7' } })
}, },
// 注意事项:启动 5 秒倒计时 // 注意事项:启动 5 秒倒计时
@@ -910,24 +1016,28 @@ export default {
const items = [] const items = []
list.forEach(m => { list.forEach(m => {
if (selectedIds.has(m.id)) { if (selectedIds.has(m.id) && this.isOnShelf(m)) {
const usdtPrice = Number(m.price || 0) * Number(m.leaseTime || 1) // 单价同币种显示若非USDT按实时价换算
const baseUnit = Number(m.price || 0)
const leaseDays = Math.max(1, Math.floor(Number(m.leaseTime || 1)))
const isUSDT = String(this.selectedCoin).toUpperCase() === 'USDT' const isUSDT = String(this.selectedCoin).toUpperCase() === 'USDT'
const displayPrice = !isUSDT && this.selectedPrice > 0 ? (usdtPrice / this.selectedPrice) : usdtPrice const unitPrice = !isUSDT && this.selectedPrice > 0 ? (baseUnit / this.selectedPrice) : baseUnit
const subtotal = unitPrice * leaseDays
items.push({ items.push({
product: shop.name || '', product: shop.name || '',
coin: this.toUpperText(m.coin), coin: this.toUpperText(m.coin),
machineId: m.id,
user: m.user, user: m.user,
miner: m.miner, miner: m.miner,
price: Number(displayPrice || 0) unitPrice: Number(unitPrice || 0),
leaseTime: leaseDays,
subtotal: Number(subtotal || 0)
}) })
} }
}) })
this.confirmDialog.items = items this.confirmDialog.items = items
this.confirmDialog.count = items.length this.confirmDialog.count = items.length
this.confirmDialog.total = items.reduce((s, i) => s + i.price, 0) this.confirmDialog.total = items.reduce((s, i) => s + i.subtotal, 0)
this.confirmDialog.visible = true this.confirmDialog.visible = true
}, },
// 显示谷歌验证码输入框 // 显示谷歌验证码输入框

View File

@@ -108,7 +108,7 @@
<!-- 外层列宽同样收紧避免横向滚动 --> <!-- 外层列宽同样收紧避免横向滚动 -->
<el-table-column label="价格 (USDT)" header-align="left" align="left" min-width="120"> <el-table-column label="价格 (USDT)" header-align="left" align="left" min-width="120">
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.price }} </template> <template slot-scope="scope"><span class="price-strong">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.price }}</span></template>
</el-table-column> </el-table-column>
<el-table-column label="理论算力范围" min-width="220" header-align="left" align="left" show-overflow-tooltip> <el-table-column label="理论算力范围" min-width="220" header-align="left" align="left" show-overflow-tooltip>
@@ -132,20 +132,21 @@
</div> </div>
<!-- 确认加入购物车弹窗 --> <!-- 确认加入购物车弹窗 -->
<el-dialog :visible.sync="confirmAddDialog.visible" width="60vw" :title="`确认加入购物车(共 ${confirmAddDialog.items.length} 台)`"> <el-dialog :visible.sync="confirmAddDialog.visible" width="80vw" :title="`确认加入购物车(共 ${confirmAddDialog.items.length} 台)`">
<div> <div>
<el-table :data="confirmAddDialog.items" height="360" border stripe :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }"> <el-table :data="confirmAddDialog.items" height="360" border stripe :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table-column prop="type" label="型号" width="160" header-align="left" align="left" /> <el-table-column prop="theoryPower" label="理论算力" header-align="left" align="left" />
<el-table-column prop="theoryPower" label="理论算力" width="160" header-align="left" align="left" /> <el-table-column label="实际算力" header-align="left" align="left">
<el-table-column label="算力" width="160" header-align="left" align="left">
<template #default="scope">{{ scope.row.computingPower }} {{ scope.row.unit }}</template> <template #default="scope">{{ scope.row.computingPower }} {{ scope.row.unit }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="algorithm" label="算法" width="120" header-align="left" align="left" /> <el-table-column prop="algorithm" label="算法" width="120" header-align="left" align="left" />
<el-table-column prop="powerDissipation" label="功耗(kw/h)" width="160" header-align="left" align="left" /> <el-table-column prop="powerDissipation" label="功耗(kw/h)" header-align="left" align="left" />
<el-table-column label="租赁天数(天)" width="160" header-align="left" align="left"> <el-table-column label="租赁天数(天)" header-align="left" align="left">
<template #default="scope">{{ Number(scope.row.leaseTime || 1) }}</template> <template #default="scope">{{ Number(scope.row.leaseTime || 1) }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="price" label="单价(USDT)" width="160" header-align="left" align="left" /> <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>
</el-table> </el-table>
</div> </div>
@@ -350,6 +351,11 @@ export default {
color: #e74c3c; color: #e74c3c;
} }
.price-strong {
font-weight: 700;
color: #e74c3c;
}
/* 支付方式区域(视觉更友好 + 可达性) */ /* 支付方式区域(视觉更友好 + 可达性) */
.pay-methods { .pay-methods {
display: flex; display: flex;

View File

@@ -35,44 +35,44 @@ export default {
], ],
loading: false, loading: false,
powerList: [ powerList: [
{ // {
value: 1, // value: 1,
label: "NexaPow", // label: "NexaPow",
children: [ // children: [
{ // {
value: 1 - 1, // value: 1 - 1,
label: "挖矿账户1", // label: "挖矿账户1",
}, // },
{ // {
value: 1 - 2, // value: 1 - 2,
label: "挖矿账户2", // label: "挖矿账户2",
}, // },
], // ],
}, // },
{ // {
value: 2, // value: 2,
label: "Grepow", // label: "Grepow",
children: [ // children: [
{ // {
value: 2 - 1, // value: 2 - 1,
label: "挖矿账户1", // label: "挖矿账户1",
}, // },
{ // {
value: 2 - 2, // value: 2 - 2,
label: "挖矿账户2", // label: "挖矿账户2",
}, // },
], // ],
}, // },
{ // {
value: 3, // value: 3,
label: "mofang", // label: "mofang",
children: [ // children: [
{ // {
value: 3 - 1, // value: 3 - 1,
label: "挖矿账户1", // label: "挖矿账户1",
}, // },
], // ],
}, // },
], ],
currencyList: [ currencyList: [
{ {

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="product-list" v-loading="productListLoading"> <div class="product-list" v-loading="productListLoading">
<section class="container"> <section class="container">
<h1 class="page-title">商品列表</h1> <h1 class="page-title">商品列表</h1>
<section class="filter-section"> <section class="filter-section">
<label class="required" style="margin-bottom: 10px">币种选择</label> <label class="required" style="margin-bottom: 10px">币种选择</label>
@@ -54,7 +54,8 @@
tabindex="0" tabindex="0"
aria-label="查看详情" aria-label="查看详情"
> >
<img :src="product.image || 'https://img.yzcdn.cn/vant/apple-1.jpg'" :alt="product.name" class="product-image" /> <!-- <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"> <div class="product-info">
<h4>商品: {{ product.name }}</h4> <h4>商品: {{ product.name }}</h4>
<p style="font-size: 16px;margin-top: 10px;font-weight: bold;">算法: {{ product.algorithm }}</p> <p style="font-size: 16px;margin-top: 10px;font-weight: bold;">算法: {{ product.algorithm }}</p>
@@ -166,10 +167,10 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
height: 35vh; height: 40vh;
} }
.product-image { .product-image {
width: 90%; width: 68%;
height:65%; height:65%;
object-fit: cover; object-fit: cover;
margin-bottom: 12px; margin-bottom: 12px;

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.e19a35c5.js"></script><link href="/css/chunk-vendors.10dd4e95.css" rel="stylesheet"><link href="/css/app.95fce3d4.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.7b715e42.js"></script><link href="/css/chunk-vendors.10dd4e95.css" rel="stylesheet"><link href="/css/app.9d232bce.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