需求更改开发中

This commit is contained in:
2025-10-20 10:15:13 +08:00
parent d1b3357a8e
commit a60603acd0
30 changed files with 2863 additions and 704 deletions

View File

@@ -46,4 +46,26 @@ export function getOrdersByStatusForSeller(data) {
method: 'post',
data
})
}
}
//结算前链和币种查询
export function getChainAndListForSeller(data) {
return request({
url: `/lease/shop/getChainAndListForSeller`,
method: 'post',
data
})
}
//获取实时币价
export function getCoinPrice(data) {
return request({
url: `/lease/order/info/getCoinPrice`,
method: 'post',
data
})
}

View File

@@ -27,6 +27,17 @@ export function deleteBatchGoods(data) {
})
}
// 批量删除购物车中已下架商品
export function deleteBatchGoodsForIsDelete(data) {
return request({
url: `/lease/shopping/cart/deleteBatchGoodsForIsDelete`,
method: 'post',
data
})
}

View File

@@ -92,3 +92,18 @@ export function deleteShopConfig(data) {
}
// 批量删除购物车中已下架商品
export function deleteBatchGoodsForIsDelete(data) {
return request({
url: `/lease/shopping/cart/deleteBatchGoodsForIsDelete`,
method: 'post',
data
})
}

View File

@@ -11,30 +11,109 @@ export function getWalletInfo(data) {
//余额提现
export function withdrawBalance(data) {
return request({
url: `/lease/user/withdrawBalance`,
method: 'post',
data
})
}
return request({
url: `/lease/user/withdrawBalance`,
method: 'post',
data
})
}
//余额充值记录
export function balanceRechargeList(data) {
return request({
url: `/lease/user/balanceRechargeList`,
method: 'post',
data
})
}
return request({
url: `/lease/user/balanceRechargeList`,
method: 'post',
data
})
}
//提现记录
//提现记录
export function balanceWithdrawList(data) {
return request({
url: `/lease/user/balanceWithdrawList`,
method: 'post',
data
})
}
return request({
url: `/lease/user/balanceWithdrawList`,
method: 'post',
data
})
}
// 卖家收款记录
export function sellerReceiptList(data) {
return request({
url: `/lease/user/balancePayList`,
method: 'post',
data
})
}
//钱包绑定
export function addWalletShopConfig(data) {
return request({
url: `/lease/shop/addShopConfig`,
method: 'post',
data
})
}
//获取支持的链和币种
export function getChainAndList(data) {
return request({
url: `/lease/shop/getChainAndList`,
method: 'post',
data
})
}
//获取钱包绑定列表
export function getShopConfig(data) {
return request({
url: `/lease/shop/getShopConfig`,
method: 'post',
data
})
}
//创建钱包
export function bindWallet(data) {
return request({
url: `/lease/user/bindWallet`,
method: 'post',
data
})
}
//资金流水
export function transactionRecord(data) {
return request({
url: `/lease/user/transactionRecord`,
method: 'post',
data
})
}
//钱包的最近交易
export function getRecentlyTransaction(data) {
return request({
url: `/lease/user/getRecentlyTransaction`,
method: 'post',
data
})
}

View File

@@ -9,7 +9,7 @@ import './utils/loginInfo.js';
// 全局输入防表情守卫(极简、无侵入)
import { initNoEmojiGuard } from './utils/noEmojiGuard.js';
// console.log = ()=>{} //全局关闭打印
console.log = ()=>{} //全局关闭打印
Vue.config.productionTip = false

View File

@@ -62,7 +62,7 @@ export const accountRoutes = [
path: '/account',
name: 'account',
component: () => import('../views/account/index.vue'),
redirect: '/account/wallet',
redirect: '/account/shops',
meta: {
title: '个人中心',
description: '管理个人资料和店铺',
@@ -99,6 +99,16 @@ export const accountRoutes = [
allAuthority: ['all']
}
},
{
path: 'receipt-record',
name: 'accountReceiptRecord',
component: () => import('../views/account/receiptRecord.vue'),
meta: {
title: '收款记录',
description: '卖家收款流水记录',
allAuthority: ['all']
}
},
{
path: 'shop-new',
name: 'accountShopNew',
@@ -159,6 +169,16 @@ export const accountRoutes = [
allAuthority: ['all']
}
},
{
path: 'funds-flow',
name: 'accountFundsFlow',
component: () => import('../views/account/fundsFlow.vue'),
meta: {
title: '资金流水',
description: '充值/提现/消费记录切换查看',
allAuthority: ['all']
}
},
{
path: 'purchased-detail/:orderItemId',
name: 'PurchasedDetail',

View File

@@ -10,7 +10,7 @@
<el-table-column prop="payCoin" label="币种" min-width="100" />
<el-table-column prop="address" label="收款地址" min-width="240" />
<el-table-column prop="leaseTime" label="租赁天数" min-width="100" />
<el-table-column prop="price" label="价(USDT)" min-width="240" />
<el-table-column prop="price" label="价(USDT)" min-width="240" />
</el-table>
</template>
</el-table-column>

View File

@@ -0,0 +1,442 @@
<template>
<div class="funds-page">
<h3 class="title" style="margin-bottom: 18px; text-align: left;">资金流水</h3>
<div class="tabs-card">
<el-tabs v-model="active" @tab-click="handleTab">
<el-tab-pane label="充值记录" name="recharge">
<div class="list-wrap">
<div class="list-header">
<span class="list-title">全部充值 ({{ rechargeRows.length }})</span>
<el-button type="primary" size="small" @click="loadRecharge">刷新</el-button>
</div>
<div class="record-list" v-loading="loading.recharge">
<div v-for="(row, idx) in rechargeRows" :key="getRowKey(row, idx)" class="record-item" :class="statusClass(row.status)" @click="toggleExpand('recharge', row, idx)">
<div class="item-main">
<div class="item-left">
<div class="amount">+ {{ formatTrunc(row.amount, 2) }} {{ row.fromSymbol || 'USDT' }}</div>
<div class="chain">{{ formatChain(row.fromChain) }}</div>
</div>
<div class="item-right">
<div class="status"><el-tag :type="getRechargeStatusType(row.status)" size="small">{{ getRechargeStatusText(row.status) }}</el-tag></div>
<div class="time">{{ formatFullTime(row.createTime) }}</div>
</div>
</div>
<div v-show="isExpanded('recharge', row, idx)" class="expand-panel">
<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" v-if="row.txHash"><span class="label">交易哈希</span><span class="value mono-ellipsis" :title="row.txHash">{{ row.txHash }}</span></div>
</div>
</div>
</div>
<div v-if="!rechargeRows.length" class="empty">暂无充值记录</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="提现记录" name="withdraw">
<div class="list-wrap">
<div class="list-header">
<span class="list-title">全部提现 ({{ withdrawRows.length }})</span>
<el-button type="primary" size="small" @click="loadWithdraw">刷新</el-button>
</div>
<div class="record-list" v-loading="loading.withdraw">
<div v-for="(row, idx) in withdrawRows" :key="getRowKey(row, idx)" class="record-item" :class="statusClass(row.status)" @click="toggleExpand('withdraw', row, idx)">
<div class="item-main">
<div class="item-left">
<div class="amount">- {{ formatTrunc(row.amount, 2) }} {{ row.toSymbol || 'USDT' }}</div>
<div class="chain">{{ formatChain(row.toChain) }}</div>
</div>
<div class="item-right">
<div class="status"><el-tag :type="getWithdrawStatusType(row.status)" size="small">{{ getWithdrawStatusText(row.status) }}</el-tag></div>
<div class="time">{{ formatFullTime(row.createTime) }}</div>
</div>
</div>
<div v-show="isExpanded('withdraw', row, idx)" class="expand-panel">
<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" v-if="row.txHash"><span class="label">交易哈希</span><span class="value mono-ellipsis" :title="row.txHash">{{ row.txHash }}</span></div>
</div>
</div>
</div>
<div v-if="!withdrawRows.length" class="empty">暂无提现记录</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="消费记录" name="consume">
<div class="list-wrap">
<div class="list-header">
<span class="list-title">全部消费 ({{ consumeRows.length }})</span>
<el-button type="primary" size="small" @click="loadConsume">刷新</el-button>
</div>
<div class="record-list" v-loading="loading.consume">
<div v-for="(row, idx) in consumeRows" :key="getRowKey(row, idx)" class="record-item" :class="statusClass(row.status)" @click="toggleExpand('consume', row, idx)">
<div class="item-main">
<div class="item-left">
<div class="amount">- {{ formatTrunc(row.realAmount, 2) }} {{ (row.fromSymbol || 'USDT').toUpperCase() }}</div>
<div class="chain">{{ formatChain(row.fromChain) }}</div>
</div>
<div class="item-right">
<div class="status"><el-tag :type="getPayStatusType(row.status)" size="small">{{ getPayStatusText(row.status) }}</el-tag></div>
<div class="time">{{ formatFullTime(row.createTime || row.time) }}</div>
</div>
</div>
<div v-show="isExpanded('consume', row, idx)" class="expand-panel">
<div class="expand-grid">
<div class="expand-item"><span class="label">订单号</span><span class="value mono">{{ row.orderId || '' }}</span></div>
<div class="expand-item"><span class="label">支付地址</span><span class="value mono-ellipsis" :title="row.fromAddress">{{ row.fromAddress || '' }}</span></div>
<div class="expand-item"><span class="label">收款地址</span><span class="value mono-ellipsis" :title="row.toAddress">{{ row.toAddress || '' }}</span></div>
<div class="expand-item" v-if="row.txHash"><span class="label">交易哈希</span><span class="value mono-ellipsis" :title="row.txHash">{{ row.txHash }}</span></div>
</div>
</div>
</div>
<div v-if="!consumeRows.length" class="empty">暂无消费记录</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
<el-row>
<el-col :span="24" style="display: flex; justify-content: center">
<el-pagination
style="margin: 0 auto; margin-top: 10px"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
:page-sizes="pageSizes"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import { transactionRecord } from '../../api/wallet'
export default {
name: 'AccountFundsFlow',
data() {
return {
active: 'recharge',
loading: { recharge: false, withdraw: false, consume: false },
rechargeRows: [
// {
// createTime: '2024-01-15 14:30:25',
// amount: 100,
// fromAddress: 'djdddksfhsfj',
// fromChain: 'tron',
// fromSymbol: 'USDT',
// status: 2,
// id: 1,
// txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
// updateTime: '2024-01-15 14:30:25',
// },
// {
// createTime: '2024-01-15 14:30:25',
// amount: 100,
// fromAddress: 'djdddksfhsfj',
// fromChain: 'tron',
// fromSymbol: 'USDT',
// status: 2,
// id: 1,
// txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
// updateTime: '2024-01-15 14:30:25',
// },
],
withdrawRows: [
// {
// createTime: '2024-01-15 14:30:25',
// amount: 100,
// toAddress: 'djdddksfhsfj',
// toChain: 'tron',
// toSymbol: 'USDT',
// status: 2,
// id: 1,
// txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
// updateTime: '2024-01-15 14:30:25',
// },
// {
// createTime: '2024-01-15 14:30:25',
// amount: 100,
// toAddress: 'djdddksfhsfj',
// toChain: 'tron',
// toSymbol: 'USDT',
// status: 2,
// id: 1,
// txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
// updateTime: '2024-01-15 14:30:25',
// },
],
consumeRows: [
// {
// orderId: '1234567890',
// fromAddress: 'djdddksfhsfj',
// fromChain: 'tron',
// fromSymbol: 'USDT',
// createTime: '2024-01-15 14:30:25',
// amount: 100,
// toAddress: 'djdddksfhsfj',
// toChain: 'tron',
// toSymbol: 'USDT',
// status: 2,
// id: 1,
// txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
// updateTime: '2024-01-15 14:30:25',
// },
// {
// orderId: '1234567890',
// fromAddress: 'djdddksfhsfj',
// fromChain: 'tron',
// fromSymbol: 'USDT',
// createTime: '2024-01-15 14:30:25',
// realAmount: 100, //后端告知只有消费记录金额用这个字段
// toAddress: 'djdddksfhsfj',
// toChain: 'tron',
// toSymbol: 'USDT',
// status: 2,
// id: 1,
// txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
// updateTime: '2024-01-15 14:30:25',
// },
],
// 手风琴展开集合
expandedKeys: new Set(),
total: 0,
pageSizes: [10, 20, 50],
currentPage: 1,
// 分页和筛选参数
pagination: {
pageNum: 1,
pageSize: 10,
status: 2,
},
}
},
mounted() {
const tab = (this.$route && this.$route.query && this.$route.query.tab) || 'recharge'
if (['recharge', 'withdraw', 'consume'].includes(tab)) this.active = tab
// 初始化按当前 Tab 的状态加载列表
this.pagination.status = this.getStatusByTab(this.active)
this.loadList()
},
methods: {
/**
* 处理 Tab 切换:清空展开状态,确保手风琴行为
* @param {any} pane - 当前 paneElement UI 传入)
* @param {Event} evt - 事件对象
*/
handleTab(pane, evt) {
this.expandedKeys.clear()
// 触发视图刷新
this.expandedKeys = new Set(this.expandedKeys)
// 按 Tab 切换刷新对应数据
const tabName = (pane && pane.name) || this.active
this.pagination.status = this.getStatusByTab(tabName)
this.pagination.pageNum = 1
this.currentPage = 1
this.loadList()
},
/**
* 生成唯一键,优先使用后端提供的稳定字段,其次回退到索引
* @param {Record<string, any>} row - 当前行数据
* @param {number} [idx] - v-for 提供的索引
* @returns {string}
*/
getRowKey(row, idx) {
const indexPart = idx != null ? `#${idx}` : ''
if (!row) return String(idx != null ? idx : '')
const stable = row.__key || row.id || row.txHash || row.orderId || `${row.createTime || ''}-${row.updateTime || ''}`
// 始终附加索引,避免重复 id/txHash 导致多条记录共用同一键
if (stable == null || stable === '') return String(idx != null ? idx : '')
return `${String(stable)}${indexPart}`
},
/**
* 判断当前行是否展开(手风琴:全局仅允许一个展开)
* @param {string} type - 列表类型recharge/withdraw/consume
* @param {Record<string, any>} row - 当前行数据
* @param {number} [idx] - 行索引
* @returns {boolean}
*/
isExpanded(type, row, idx) {
const key = `${type}-${this.getRowKey(row, idx)}`
return this.expandedKeys.has(key)
},
/**
* 切换展开(手风琴):若点击同一行则收起,否则清空后仅展开当前行
* @param {string} type - 列表类型
* @param {Record<string, any>} row - 当前行数据
* @param {number} [idx] - 行索引
*/
toggleExpand(type, row, idx) {
const key = `${type}-${this.getRowKey(row, idx)}`
if (this.expandedKeys.has(key)) {
this.expandedKeys.clear()
} else {
this.expandedKeys.clear()
this.expandedKeys.add(key)
}
// 触发视图刷新
this.expandedKeys = new Set(this.expandedKeys)
},
// 统一加载:根据 pagination.status 请求并填充对应列表
async loadList() {
const status = Number(this.pagination.status)
const typeKey = this.getTypeKeyByStatus(status)
if (!typeKey) return
this.loading[typeKey] = true
try {
const res = await transactionRecord({
pageNum: this.pagination.pageNum,
pageSize: this.pagination.pageSize,
status
})
const rows = res?.rows || res?.data?.rows || []
this.total = res?.total || res?.data?.total || (Array.isArray(rows) ? rows.length : 0)
const mapped = (Array.isArray(rows) ? rows : []).map((r, i) => ({
...r,
__key: r.id || r.txHash || r.orderId || `${i}`
}))
if (status === 2) this.rechargeRows = mapped
else if (status === 1) this.withdrawRows = mapped
else this.consumeRows = mapped
// 刷新时收起展开
this.expandedKeys.clear()
this.expandedKeys = new Set(this.expandedKeys)
} finally {
this.loading[typeKey] = false
}
},
/**
* 包装:根据传入状态加载,并同步 Tab/分页重置
* @param {0|1|2} status - 0 支付1 提现2 充值
*/
loadByStatus(status) {
this.pagination.status = status
this.active = this.getTabByStatus(status)
this.pagination.pageNum = 1
this.currentPage = 1
return this.loadList()
},
// 兼容现有按钮点击
loadRecharge() { return this.loadByStatus(2) },
loadWithdraw() { return this.loadByStatus(1) },
loadConsume() { return this.loadByStatus(0) },
statusClass(status) { return { 0: 'failed', 1: 'success', 2: 'pending' }[status] || 'neutral' },
getRechargeStatusType(s) { return ({ 0: 'danger', 1: 'success', 2: 'warning' })[s] || 'info' },
getRechargeStatusText(s) { return ({ 0: '充值失败', 1: '充值成功', 2: '充值中', 3: '证书校验失败' })[s] || '未知' },
getWithdrawStatusType(s) { return ({ 0: 'danger', 1: 'success', 2: 'warning' })[s] || 'info' },
getWithdrawStatusText(s) { return ({ 0: '提现失败', 1: '提现成功', 2: '提现中', 3: '证书校验失败' })[s] || '未知' },
getPayStatusType(s) { return ({ 0: 'danger', 1: 'success', 2: 'warning', 3: 'danger' })[s] || 'info' },
getPayStatusText(s) { return ({ 0: '支付失败', 1: '支付成功', 2: '待校验', 3: '证书校验失败' })[s] || '未知' },
formatChain(chain) {
const map = { tron: 'Tron (TRC20)', ethereum: 'Ethereum (ERC20)', bsc: 'BSC (BEP20)', polygon: 'Polygon (MATIC)' }
return map[chain] || chain
},
formatFullTime(time) { if (!time) return ''; try { return new Date(time).toLocaleString('zh-CN') } catch (e) { return String(time) } },
formatTime(time) { return this.formatFullTime(time) },
formatTrunc(value, decimals = 2) {
const num = Number(value)
if (!Number.isFinite(num)) return '0'
const d = Math.max(0, Number(decimals) || 0)
const factor = Math.pow(10, d)
const truncated = Math.trunc(num * factor) / factor
const str = String(truncated)
if (d === 0) return str
const [intPart, decPart = ''] = str.split('.')
const padded = decPart.padEnd(d, '0')
return `${intPart}.${padded}`
},
handleSizeChange(val) {
console.log(`每页 ${val}`);
this.pagination.pageSize = val;
this.pagination.pageNum = 1;
this.currentPage = 1;
this.loadList();
},
handleCurrentChange(val) {
console.log(`当前页: ${val}`);
this.pagination.pageNum = val;
this.loadList();
},
/**
* Tab 名称转状态码
* @param {string} tabName - 'recharge' | 'withdraw' | 'consume'
* @returns {0|1|2}
*/
getStatusByTab(tabName) {
if (tabName === 'recharge') return 2
if (tabName === 'withdraw') return 1
return 0
},
/**
* 状态码转 Tab 名称
* @param {number} status - 0|1|2
* @returns {'recharge'|'withdraw'|'consume'}
*/
getTabByStatus(status) {
if (Number(status) === 2) return 'recharge'
if (Number(status) === 1) return 'withdraw'
return 'consume'
},
/**
* 状态码转 loading/列表 key
* @param {number} status - 0|1|2
* @returns {'recharge'|'withdraw'|'consume'|''}
*/
getTypeKeyByStatus(status) {
if (Number(status) === 2) return 'recharge'
if (Number(status) === 1) return 'withdraw'
if (Number(status) === 0) return 'consume'
return ''
},
}
}
</script>
<style scoped>
.funds-page { padding: 8px; }
.tabs-card { background: #fff; border: 1px solid #eee; border-radius: 10px; padding: 12px; }
.list-wrap { padding: 6px 0; }
.list-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #f0f0f0; }
.list-title { font-size: 14px; font-weight: 600; color: #333; }
.record-list { display: flex; flex-direction: column; gap: 10px; max-height: 62vh; overflow-y: auto; }
.record-item { background: #f8f9fa; border-radius: 8px; padding: 12px; border: 1px solid transparent; transition: all .15s ease; }
.record-item:hover { background: #eef2f7; border-color: #409eff; box-shadow: 0 4px 12px rgba(64,158,255,.12); transform: translateY(-1px); }
.record-item.pending { border-left: 4px solid #e6a23c; }
.record-item.success { border-left: 4px solid #67c23a; }
.record-item.failed { border-left: 4px solid #f56c6c; }
.item-main { display: flex; justify-content: space-between; align-items: center; }
.item-left .amount { font-size: 16px; font-weight: 700; color: #111; margin-bottom: 2px; }
.item-left .chain { font-size: 12px; color: #666; }
.item-right { text-align: right; }
.status { margin-bottom: 2px; }
.time { font-size: 12px; color: #999; }
.expand-panel { background: #fff; border: 1px dashed #e5e7eb; border-radius: 8px; padding: 10px; margin-top: 8px; }
.expand-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 24px; }
.expand-item { display: grid; grid-template-columns: 80px 1fr; gap: 6px; align-items: center; }
.label { color: #666; font-size: 13px; text-align: right; }
.value { color: #333; font-size: 13px; text-align: left; }
.mono { font-family: "Monaco", "Menlo", monospace; }
.mono-ellipsis { font-family: "Monaco", "Menlo", monospace; max-width: 480px; display: inline-block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty { text-align: center; color: #999; padding: 20px 0; }
</style>

View File

@@ -13,67 +13,34 @@
<div class="user-email" :title="userEmail || '未登录'">{{ userEmail || '未登录' }}</div>
</div>
</div>
<div class="user-role">
<button>买家相关</button>
<button>卖家相关</button>
<div class="user-role" role="group" aria-label="导航分组切换">
<button
class="role-button"
:class="{ active: activeRole === 'buyer' }"
@click="handleClickRole('buyer')"
@keydown.enter.prevent="handleClickRole('buyer')"
@keydown.space.prevent="handleClickRole('buyer')"
:aria-pressed="activeRole === 'buyer'"
tabindex="0"
>买家相关</button>
<button
class="role-button"
:class="{ active: activeRole === 'seller' }"
@click="handleClickRole('seller')"
@keydown.enter.prevent="handleClickRole('seller')"
@keydown.space.prevent="handleClickRole('seller')"
:aria-pressed="activeRole === 'seller'"
tabindex="0"
>卖家相关</button>
</div>
<router-link
to="/account/wallet"
v-for="item in displayedLinks"
:key="item.to"
:to="item.to"
class="side-link"
active-class="active"
>我的钱包</router-link
>
<!-- <router-link
to="/account/shop-new"
class="side-link"
active-class="active"
>新增店铺</router-link
> -->
<router-link
to="/account/shop-config"
class="side-link"
active-class="active"
>钱包绑定</router-link
>
<router-link
to="/account/shops"
class="side-link"
active-class="active"
>我的店铺</router-link
>
<router-link
to="/account/products"
class="side-link"
active-class="active"
>商品列表</router-link
>
<router-link
to="/account/purchased"
class="side-link"
active-class="active"
>已购商品</router-link
>
<router-link
to="/account/seller-orders"
class="side-link"
active-class="active"
>已售出订单</router-link>
<router-link
to="/account/orders"
class="side-link"
active-class="active"
>已购买订单列表</router-link>
<router-link
to="/account/rechargeRecord"
class="side-link"
active-class="active"
>充值记录</router-link>
<router-link
to="/account/withdrawalHistory"
class="side-link"
active-class="active"
>提现记录</router-link>
>{{ item.label }}</router-link>
</nav>
</aside>
@@ -92,6 +59,26 @@ export default {
return {
activeIndex: '1',
userEmail: '',
// 导航分组buyer(买家) / seller(卖家);默认卖家
activeRole: 'seller',
// 买家侧导航
buyerLinks: [
{ label: '我的钱包', to: '/account/wallet' },
{ label: '已购商品', to: '/account/purchased' },
{ label: '订单列表', to: '/account/orders' },
// { label: '充值记录', to: '/account/rechargeRecord' },
// { label: '提现记录', to: '/account/withdrawalHistory' },
{ label: '资金流水', to: '/account/funds-flow' },
],
// 卖家侧导航
sellerLinks: [
// { label: '我的钱包', to: '/account/wallet' },
{ label: '我的店铺', to: '/account/shops' },
{ label: '商品列表', to: '/account/products' },
{ label: '已售出订单', to: '/account/seller-orders' },
{ label: '收款记录', to: '/account/receipt-record' },
],
}
},
computed: {
@@ -103,6 +90,13 @@ export default {
const email = (this.userEmail || '').trim()
return email ? email[0].toUpperCase() : '?'
},
/**
* 根据当前分组返回展示的导航链接
* @returns {{label:string,to:string}[]}
*/
displayedLinks() {
return this.activeRole === 'buyer' ? this.buyerLinks : this.sellerLinks
},
},
mounted() {
const getVal = (key) => {
@@ -112,6 +106,23 @@ export default {
}
const val = getVal('userName') || getVal('userEmail') || ''
this.userEmail = typeof val === 'string' ? val : String(val)
// 恢复上次选择的导航分组(如无则默认 seller
const savedRole = getVal('accountActiveRole')
if (savedRole === 'buyer' || savedRole === 'seller') {
this.activeRole = savedRole
}
},
methods: {
/**
* 点击切换导航分组
* @param {('buyer'|'seller')} role 要切换到的分组
* @returns {void}
*/
handleClickRole(role) {
if (role !== 'buyer' && role !== 'seller') return
this.activeRole = role
try { localStorage.setItem('accountActiveRole', JSON.stringify(role)) } catch (e) {}
},
},
};
</script>
@@ -156,6 +167,30 @@ export default {
flex-direction: column;
gap: 8px;
}
/* 分组切换按钮样式 */
.user-role {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.role-button {
appearance: none;
background: #f6f8fa;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 6px 10px;
color: #2c3e50;
cursor: pointer;
}
.role-button.active {
background: #42b983;
border-color: #42b983;
color: #fff;
}
.role-button:focus {
outline: 2px solid #42b98333;
outline-offset: 2px;
}
/* 用户信息卡片:置于导航最前,展示邮箱与首字母头像 */
.user-info-card {
display: flex;

View File

@@ -2,6 +2,30 @@
<div class="panel" >
<h2 class="panel-title">我的店铺</h2>
<div class="panel-body">
<!-- 店铺层级说明 -->
<el-card class="guide-card" shadow="never" style="margin-bottom: 16px;">
<div slot="header" class="guide-header">店铺层级说明</div>
<div class="guide-content">
<p class="hierarchy">层级结构店铺 商品 出售机器</p>
<ol class="guide-steps">
<li>
<b>店铺唯一</b>每个用户在平台<strong>仅能创建一个店铺</strong>创建成功后
请在本页点击 <b>钱包绑定</b>配置自己的收款地址支持不同链与币种
</li>
<li>
<b>商品</b>完成钱包绑定后即可在我的店铺页面 <b>创建商品</b>
商品可按 <b>币种</b> 进行分类管理创建的商品会在商城对买家展示
商品可理解为不同算法币种的机器集合分类
</li>
<li>
<b>出售机器</b>创建商品后请进入 <b>商品列表</b> 为该商品 <b>添加出售机器明细</b>
必须添加出售机器否则买家无法下单买家点击某个商品后会看到该商品下的机器明细并进行选购
</li>
</ol>
<div class="guide-note">提示建议先创建店铺 完成钱包绑定 创建商品 添加出售机器的顺序避免漏配导致无法收款或无法下单</div>
</div>
</el-card>
<el-card v-if="loaded && hasShop" class="shop-card" shadow="hover">
<div class="shop-row">
<div class="shop-cover">
@@ -26,26 +50,48 @@
</el-button>
<el-button size="small" type="danger" @click="handleDelete">删除店铺</el-button>
<el-button size="small" type="success" @click="handleAddProduct">新增商品</el-button>
<el-button size="small" type="success" @click="handleWalletBind">钱包绑定</el-button>
</div>
</div>
</div>
</el-card>
<!-- 店铺配置表格 -->
<!-- <el-card v-if="loaded && hasShop" class="shop-config-card" shadow="never" style="margin-top: 16px;">
<el-card v-if="loaded && hasShop" class="shop-config-card" shadow="never" style="margin-top: 16px;">
<div slot="header" class="clearfix">
<span>店铺配置</span>
<span>已绑定钱包</span>
</div>
<el-table :data="shopConfigs" border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="商品" width="120">
<template slot-scope="scope">{{ scope.row.productId === 0 ? '所有商品' : scope.row.productId }}</template>
<el-table-column prop="chain" label="" width="140" />
<el-table-column label="支付币种" >
<template slot-scope="scope">
<div class="coin-list">
<template v-if="Array.isArray(scope.row.children) && scope.row.children.length">
<el-tooltip
v-for="(c, idx) in scope.row.children"
:key="idx"
:content="String(c && c.payCoin ? c.payCoin : '').toUpperCase()"
placement="top"
>
<img
v-if="c && c.image"
class="coin-img"
:src="c.image"
:alt="(c.payCoin || '').toUpperCase()"
/>
</el-tooltip>
</template>
<template v-else>
{{ String(scope.row.payCoin || '').toUpperCase() }}
</template>
</div>
</template>
</el-table-column>
<el-table-column prop="payType" label="币种类型" width="120">
<!-- <el-table-column prop="payType" label="币种类型" width="120">
<template slot-scope="scope">{{ scope.row.payType === 1 ? '稳定币' : '虚拟币' }}</template>
</el-table-column>
<el-table-column prop="payCoin" label="支付币种" width="140" />
<el-table-column prop="payAddress" label="收款钱包地址" />
</el-table-column> -->
<el-table-column prop="payAddress" label="收款钱包地址" show-overflow-tooltip />
<el-table-column label="操作" width="180" fixed="right">
<template slot-scope="scope">
<el-button type="text" @click="handleEditConfig(scope.row)">修改</el-button>
@@ -54,7 +100,7 @@
</template>
</el-table-column>
</el-table>
</el-card> -->
</el-card>
<div v-else-if="loaded && !hasShop" class="no-shop">
<el-empty description="暂无店铺">
@@ -84,18 +130,35 @@
<el-button type="primary" @click="submitEdit">保存</el-button>
</span>
</el-dialog>
<!-- 修改店铺配置弹窗 -->
<!-- 修改钱包绑定配置弹窗参数保持与列表一致 -->
<el-dialog title="修改配置" :visible.sync="visibleConfigEdit" width="560px">
<div class="row">
<label class="label">适用商品</label>
<el-select v-model="configForm.productId" placeholder="请选择商品">
<el-option :value="0" label="所有商品" />
<el-option v-for="p in productOptions" :key="p.id" :value="p.id" :label="`${p.id} - ${p.name}`" />
<label class="label">支付链</label>
<el-select v-model="configForm.chain" placeholder="请选择">
<el-option v-for="c in chainOptions" :key="c.value" :value="c.value" :label="c.label" />
</el-select>
</div>
<div class="row">
<label class="label">收款地址</label>
<el-input v-model="configForm.payAddress" placeholder="请输入钱包地址" />
<label class="label">支付币种</label>
<el-select
class="input"
size="middle"
ref="screen"
v-model="configForm.payCoin"
placeholder="请选择币种"
>
<el-option
v-for="item in editCoinOptions"
:key="item.value"
:label="item.label"
: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>
</div>
<div class="row">
<label class="label">币种类型</label>
@@ -105,29 +168,8 @@
</el-radio-group>
</div>
<div class="row">
<label class="label">支付币种</label>
<el-select
class="input"
size="middle"
ref="screen"
v-model="configForm.payCoin"
placeholder="请选择"
>
<el-option
v-for="item in coinOptions"
:key="item.value"
:label="item.label"
:value="item.value"
>
<div style="display: flex; align-items: center">
<img :src="item.imgUrl" style="float: left; width: 20px" />
<span style="float: left; margin-left: 5px">
{{ item.label }}</span
>
</div>
</el-option>
</el-select>
<label class="label">钱包地址</label>
<el-input v-model="configForm.payAddress" placeholder="请输入钱包地址" />
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="visibleConfigEdit=false">取消</el-button>
@@ -139,9 +181,10 @@
</template>
<script>
import { getMyShop, updateShop, deleteShop, queryShop, closeShop, getShopConfig ,updateShopConfig,deleteShopConfig} from '@/api/shops'
import { getMyShop, updateShop, deleteShop, queryShop, closeShop ,updateShopConfig,deleteShopConfig} from '@/api/shops'
import { coinList } from '@/utils/coinList'
import { getShopConfig } from '@/api/wallet'
export default {
@@ -163,9 +206,16 @@ export default {
// 店铺配置列表
shopConfigs: [],
visibleConfigEdit: false,
configForm: { id: '', payAddress: '', payCoin: '', payType: 0, productId: 0 },
configForm: { id: '', chain: '', payAddress: '', payCoin: '', payType: 0 },
productOptions: [],
coinOptions: coinList || [],
// 支付链选项(可与后端接口对齐后替换为动态)
chainOptions: [
{ label: 'Tron (TRC20)', value: 'tron' },
{ label: 'Ethereum (ERC20)', value: 'ethereum' },
{ label: 'BSC (BEP20)', value: 'bsc' },
{ label: 'Nexa', value: 'nexa' },
],
shopLoading: false
}
},
@@ -189,6 +239,19 @@ export default {
},
canCreateShop() {
return !this.hasShop
},
/**
* 弹窗可选币种:稳定币/虚拟币分流
*/
editCoinOptions() {
if (Number(this.configForm.payType) === 1) {
return [
{ label: 'USDT', value: 'usdt' },
{ label: 'USDC', value: 'usdc' },
{ label: 'BUSD', value: 'busd' },
]
}
return this.coinOptions
}
},
created() {
@@ -230,7 +293,7 @@ export default {
del: !!res.data.del,
state: Number(res.data.state || 0)
}
// 同步加载店铺配置
// 同步加载钱包绑定
this.fetchShopConfigs(res.data.id)
} else {
// 当接口返回错误或没有数据时,重置店铺状态
@@ -257,8 +320,9 @@ export default {
}
try {
const res = await getShopConfig(shopId)
const res = await getShopConfig({id:shopId})
if (res && (res.code === 0 || res.code === 200) && Array.isArray(res.data)) {
// 直接使用后端返回的数据children: [{payCoin,image}]
this.shopConfigs = res.data
} else {
this.shopConfigs = []
@@ -286,7 +350,13 @@ export default {
}
},
handleEditConfig(row) {
this.configForm = { ...row }
this.configForm = {
id: row.id,
chain: row.chain || '',
payCoin: row.payCoin || '',
payType: typeof row.payType === 'number' ? row.payType : Number(row.payType || 0),
payAddress: row.payAddress || '',
}
this.visibleConfigEdit = true
},
@@ -295,8 +365,23 @@ export default {
},
submitConfigEdit() {
this.updateShopConfig(this.configForm)
// 基础校验
if (!this.configForm.chain) {
this.$message.warning('请选择支付链')
return
}
if (!this.configForm.payCoin) {
this.$message.warning('请选择支付币种')
return
}
const addr = (this.configForm.payAddress || '').trim()
if (!addr) {
this.$message.warning('请输入钱包地址')
return
}
const { productId, ...rest } = this.configForm
const payload = { ...rest, payType: Number(this.configForm.payType || 0) }
this.updateShopConfig(payload)
},
async handleOpenEdit() {
try {
@@ -470,7 +555,26 @@ export default {
path: '/account/product-new',
query: { shopId: this.shop.id }
})
},
/**
* 跳转到钱包绑定页面
*/
handleWalletBind() {
if (!this.hasShop) {
this.$message({
message: '请先创建店铺',
type: 'warning',
showClose: true
})
return
}
this.$router.push('/account/shop-config')
}
}
}
</script>
@@ -485,6 +589,16 @@ export default {
.desc { color: #666; }
.meta { color: #999; display: flex; gap: 16px; font-size: 12px; }
.actions { margin-top: 8px; display: flex; gap: 8px; }
.guide-card { border: 1px solid #eef2f7; border-radius: 10px; }
.guide-header { text-align: center; font-weight: 700; color: #2c3e50; background: #f9fafb; border-bottom: 1px solid #eef2f7; padding: 10px 12px; border-radius: 10px 10px 0 0; }
.guide-content { padding: 4px 6px; text-align: left; }
.guide-card .hierarchy { margin: 0 0 8px 0; color: #111827; font-weight: 700; font-size: 14px; }
.guide-steps { margin: 0; padding-left: 18px; color: #374151; }
.guide-steps li { line-height: 1.9; margin: 6px 0; }
.guide-steps b { color: #111827; }
.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; }
</style>
<style>

View File

@@ -79,8 +79,14 @@
<el-table-column prop="user" label="挖矿账户" min-width="80" />
<el-table-column prop="id" label="矿机ID" min-width="60" />
<el-table-column prop="miner" label="机器编号" min-width="100" />
<el-table-column label="实算力">
<template #default="scope">{{ scope.row.computingPower }} {{ scope.row.unit || '' }}</template>
<el-table-column label="实算力" width="100">
<template slot="header">
<el-tooltip content="实际算力为该机器在本矿池过去24H的平均算力" effect="dark" placement="top">
<i class="el-icon-question label-help" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
<span>实际算力</span>
</template>
<template slot-scope="scope">{{ scope.row.computingPower }} {{ scope.row.unit || '' }}</template>
</el-table-column>
<el-table-column label="理论算力" min-width="140">
<template #default="scope">
@@ -128,8 +134,20 @@
/>
</template>
</el-table-column>
<el-table-column label="价(USDT)" min-width="140">
<template #default="scope">
<el-table-column label="价(USDT)" min-width="140">
<template slot="header">
<el-tooltip effect="dark" placement="top">
<div slot="content">
卖家最终收款金额 = 机器售价 × 波动率<br/>
波动率规则<br/>
10% - 5%包含5%波动率 = 1按售价结算<br/>
25%以上波动率 = 实际算力 / 理论算力且不会超过 1,即最终结算时不会超过机器售价
</div>
<i class="el-icon-question label-help" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
<span>售价(USDT)</span>
</template>
<template slot-scope="scope">
<el-input
v-model="scope.row.price"
size="small"
@@ -144,6 +162,22 @@
</el-input>
</template>
</el-table-column>
<el-table-column label="最大租赁天数(天)" min-width="140">
<template #default="scope">
<el-input
v-model="scope.row.maxLeaseDays"
size="small"
inputmode="numeric"
:disabled="isRowDisabled(scope.row)"
@input="handleMaxLeaseDaysInput(scope.$index)"
@blur="handleMaxLeaseDaysBlur(scope.$index)"
:class="{ 'changed-input': isCellChanged(scope.row, 'maxLeaseDays') }"
style="max-width: 260px;"
>
<template slot="append"></template>
</el-input>
</template>
</el-table-column>
<el-table-column label="上下架" min-width="140">
<template #default="scope">
<el-switch
@@ -309,6 +343,7 @@ export default {
powerDissipation: String(row.powerDissipation ?? ''),
type: String(row.type ?? ''),
price: String(row.price ?? ''),
maxLeaseDays: String(row.maxLeaseDays ?? ''),
}
}
this.fieldSnapshot = snapshot
@@ -443,6 +478,30 @@ export default {
this.$set(this.machineList, index, row)
}
},
handleMaxLeaseDaysInput(index) {
const rowItem = this.machineList && this.machineList[index]
if (!rowItem || this.isRowDisabled(rowItem)) return
let v = String(this.machineList[index].maxLeaseDays ?? '')
v = v.replace(/\D/g, '')
if (v.length > 3) v = v.slice(0, 3)
const row = { ...this.machineList[index], maxLeaseDays: v }
this.$set(this.machineList, index, row)
},
handleMaxLeaseDaysBlur(index) {
const raw = String(this.machineList[index].maxLeaseDays ?? '')
if (!/^\d{1,3}$/.test(raw)) {
this.$message.warning('最大租赁天数需为 1-365 的整数')
const row = { ...this.machineList[index], maxLeaseDays: '' }
this.$set(this.machineList, index, row)
return
}
const n = Number(raw)
if (!Number.isInteger(n) || n < 1 || n > 365) {
this.$message.warning('最大租赁天数需为 1-365 的整数')
const row = { ...this.machineList[index], maxLeaseDays: '' }
this.$set(this.machineList, index, row)
}
},
handleTheoryPowerBlur(index) {
const raw = String(this.machineList[index].theoryPower ?? '')
const pattern = /^\d{1,6}(\.\d{1,4})?$/
@@ -511,6 +570,7 @@ export default {
const priceRaw = String(row.price ?? '')
const typeRaw = String(row.type ?? '')
const dissRaw = String(row.powerDissipation ?? '')
const daysRaw = String(row.maxLeaseDays ?? '')
if (!theoryRaw || Number(theoryRaw) <= 0 || !powerPattern.test(theoryRaw)) {
this.$message.warning(`${i + 1}行(机器:${rowLabel}) 理论算力必须大于0整数最多6位小数最多4位`)
@@ -524,6 +584,10 @@ export default {
this.$message.warning(`${i + 1}行(机器:${rowLabel}) 单价必须大于0整数最多12位小数最多2位`)
return
}
if (!/^\d{1,3}$/.test(daysRaw) || !Number.isInteger(Number(daysRaw)) || Number(daysRaw) < 1 || Number(daysRaw) > 365) {
this.$message.warning(`${i + 1}行(机器:${rowLabel}) 最大租赁天数需为 1-365 的整数`)
return
}
// 型号允许为空,但如果填写则不能全空格
if (typeRaw && isOnlySpaces(typeRaw)) {
this.$message.warning(`${i + 1}行(机器:${rowLabel}) 型号不能全是空格`)
@@ -538,6 +602,7 @@ export default {
state: Number(m.state ?? 0),
theoryPower: Number(m.theoryPower ?? 0),
type: m.type || '',
maxLeaseDays: Number(m.maxLeaseDays ?? 0),
unit: m.unit || ''
}))
@@ -575,6 +640,8 @@ export default {
.split { width: 8px; }
.empty-text { color: #909399; text-align: center; padding: 12px 0; }
.label-help { margin-left: 4px; color: #909399; cursor: help; }
</style>
<style>
@@ -587,5 +654,13 @@ export default {
.changed-input input.el-input__inner {
border-color: #f56c6c !important;
}
.el-input.is-disabled .el-input__inner{
color: #000 !important;
}
.el-textarea.is-disabled .el-textarea__inner{
color: #000 !important;
}
</style>

View File

@@ -5,35 +5,25 @@
<h2 class="title">添加出售机器</h2>
</div>
<el-alert
class="notice-alert"
type="warning"
show-icon
:closable="false"
title="新增出售机器必须在 M2pool 有挖矿算力记录才能添加出租"
description="建议稳定在 M2pool 矿池挖矿 24 小时之后,再添加出售该机器"
/>
<el-card shadow="never" class="form-card">
<el-form ref="machineForm" :model="form" :rules="rules" label-width="160px" size="small">
<el-form-item label="商品名称">
<el-input v-model="form.productName" disabled />
</el-form-item>
<el-form-item label="功耗" prop="powerDissipation">
<el-input
v-model="form.powerDissipation"
placeholder="示例0.01"
inputmode="decimal"
@input="handleNumeric('powerDissipation')"
>
<template slot="append">kw/h</template>
</el-input>
</el-form-item>
<el-form-item label="机器成本价格" prop="cost">
<el-input
v-model="form.cost"
placeholder="请输入成本USDT"
inputmode="decimal"
@input="handleNumeric('cost')"
>
<template slot="append">USDT</template>
</el-input>
<el-input v-model="form.productName" disabled style="width: 50%;" />
</el-form-item>
<el-form-item label="矿机型号">
<el-input v-model="form.type" placeholder="示例:龍珠" :maxlength="20" @input="handleTypeInput" />
<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
@@ -41,16 +31,65 @@
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"
placeholder="1-365"
inputmode="numeric"
@input="handleNumeric('maxLeaseDays')"
style="width: 50%;"
>
<template slot="append"></template>
</el-input>
</el-form-item>
<el-form-item label="功耗" prop="powerDissipation">
<el-input
v-model="form.powerDissipation"
placeholder="示例0.01"
inputmode="decimal"
@input="handleNumeric('powerDissipation')"
style="width: 50%;"
>
<template slot="append">kw/h</template>
</el-input>
</el-form-item>
<el-form-item label="统一售价" prop="cost">
<span slot="label">
统一售价
<el-tooltip effect="dark" placement="top">
<div slot="content">
卖家最终收款金额 = 机器售价 × 波动率<br/>
波动率规则<br/>
10% - 5%包含5%波动率 = 1按售价结算<br/>
25%以上波动率 = 实际算力 / 理论算力且不会超过 1,即最终结算时不会超过机器售价
</div>
<i class="el-icon-question label-help" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
</span>
<el-input
v-model="form.cost"
placeholder="请输入成本USDT"
inputmode="decimal"
@input="handleNumeric('cost')"
style="width: 50%;"
>
<template slot="append">USDT</template>
</el-input>
</el-form-item>
<el-form-item label="选择挖矿账户">
<el-select v-model="selectedMiner" filterable clearable placeholder="请选择挖矿账户" @change="handleMinerChange" :loading="minersLoading">
@@ -69,17 +108,77 @@
<el-card shadow="never" class="form-card" v-if="selectedMachineRows.length">
<div slot="header" class="section-title">已选择机器</div>
<el-table :data="selectedMachineRows" border stripe style="width: 100%">
<el-table-column prop="user" label="挖矿账户" min-width="160" />
<el-table-column prop="miner" label="机器编号" min-width="160" />
<el-table-column label="价格(USDT)" min-width="220">
<el-table-column prop="user" label="挖矿账户" />
<el-table-column prop="miner" label="机器编号" />
<el-table-column prop="realPower" label="实际算力(MH/S)">
<template slot="header">
<el-tooltip content="实际算力为该机器在本矿池过去24H的平均算力" effect="dark" placement="top">
<i class="el-icon-question" style="margin-right: 4px; color: #909399;" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
<span>实际算力(MH/S)</span>
</template>
</el-table-column>
<el-table-column label="功耗(kw/h)" min-width="120">
<template #default="scope">
<el-input
v-model="scope.row.powerDissipation"
placeholder="示例0.01"
inputmode="decimal"
@input="handleRowPowerDissipationInput(scope.$index)"
@blur="handleRowPowerDissipationBlur(scope.$index)"
style="width: 100%;"
>
<template slot="append">kw/h</template>
</el-input>
</template>
</el-table-column>
<el-table-column label="理论算力" min-width="160">
<template #default="scope">
<div style="display: flex; align-items: center; gap: 8px;">
<el-input
v-model="scope.row.theoryPower"
placeholder="理论算力"
inputmode="decimal"
@input="handleRowTheoryPowerInput(scope.$index)"
@blur="handleRowTheoryPowerBlur(scope.$index)"
style="width: 100%"
/>
<el-select
v-model="scope.row.unit"
placeholder="单位"
style="width:150px;"
@change="val => handleRowUnitChange(scope.$index, val)"
>
<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>
</template>
</el-table-column>
<el-table-column label="售价(USDT)" min-width="160">
<template slot="header">
<el-tooltip effect="dark" placement="top">
<div slot="content">
卖家最终收款金额 = 机器售价 × 波动率<br/>
波动率规则<br/>
10% - 5%包含5%波动率 = 1按售价结算<br/>
25%以上波动率 = 实际算力 / 理论算力且不会超过 1,即最终结算时不会超过机器售价
</div>
<i class="el-icon-question label-help" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
<span>售价(USDT)</span>
</template>
<template slot-scope="scope">
<el-input
v-model="scope.row.price"
placeholder="价格"
inputmode="decimal"
@input="handleRowPriceInput(scope.$index)"
@blur="handleRowPriceBlur(scope.$index)"
style="width: 70%;"
style="width: 100%;"
>
<template slot="append">USDT</template>
</el-input>
@@ -87,7 +186,21 @@
</template>
</el-table-column>
<el-table-column label="矿机型号" min-width="200">
<el-table-column label="最大租赁天数(天)" min-width="120">
<template #default="scope">
<el-input
v-model="scope.row.maxLeaseDays"
placeholder="1-365"
inputmode="numeric"
@input="handleRowMaxLeaseDaysInput(scope.$index)"
@blur="handleRowMaxLeaseDaysBlur(scope.$index)"
style="width: 100%;"
>
<template slot="append"></template>
</el-input>
</template>
</el-table-column>
<el-table-column label="矿机型号">
<template #default="scope">
<el-input
v-model="scope.row.type"
@@ -95,11 +208,11 @@
@input="handleRowTypeInput(scope.$index)"
@blur="handleRowTypeBlur(scope.$index)"
:maxlength="20"
style="width: 70%;"
style="width: 100%;"
/>
</template>
</el-table-column>
<el-table-column label="上下架状态" min-width="120">
<el-table-column label="上下架状态" width="100">
<template #default="scope">
<el-button
:type="scope.row.state === 0 ? 'success' : 'info'"
@@ -151,7 +264,8 @@ export default {
theoryPower: null,
type: '',
unit: 'TH/S',
cost: ''
cost: '',
maxLeaseDays: ''
},
confirmVisible: false,
rules: {
@@ -210,6 +324,21 @@ export default {
trigger: 'blur'
}
]
,
maxLeaseDays: [
{ required: true, message: '请填写最大租赁天数', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const raw = String(value ?? '')
if (!raw) { callback(new Error('请填写最大租赁天数')); return }
if (!/^\d{1,3}$/.test(raw)) { callback(new Error('仅允许整数,范围 1-365')); return }
const n = Number(raw)
if (!Number.isInteger(n) || n < 1 || n > 365) { callback(new Error('范围需在 1-365 天')); return }
callback()
},
trigger: 'blur'
}
]
},
miners: [
// {
@@ -293,6 +422,10 @@ export default {
saving: false,
lastCostBaseline: 0,
lastTypeBaseline: '',
lastMaxLeaseDaysBaseline: 0,
lastPowerDissipationBaseline: 0,
lastTheoryPowerBaseline: 0,
lastUnitBaseline: 'TH/S',
params:{
cost:353400,
powerDissipation:0.01,
@@ -364,6 +497,13 @@ export default {
decPart = decPart.slice(0, 4)
}
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
} else if (key === 'maxLeaseDays') {
// 最大租赁天数:仅整数,范围 1-365输入阶段限制为最多3位数字
v = v.replace(/\D/g, '')
if (v.length > 3) v = v.slice(0, 3)
this.form[key] = v
this.syncMaxLeaseDaysToRows()
return
} else {
// 其他最多6位小数保持原有逻辑
if (firstDot !== -1) {
@@ -427,14 +567,164 @@ export default {
user: m.user,
coin: m.coin,
miner: m.miner,
realPower: m.realPower,
price: existed ? existed.price : this.form.cost,
powerDissipation: existed && existed.powerDissipation !== undefined ? existed.powerDissipation : this.form.powerDissipation,
theoryPower: existed && existed.theoryPower !== undefined ? existed.theoryPower : this.form.theoryPower,
unit: existed && existed.unit ? existed.unit : this.form.unit,
type: existed ? existed.type : this.form.type,
state: existed ? existed.state : 0 // 默认上架
state: existed ? existed.state : 0, // 默认上架
maxLeaseDays: existed && existed.maxLeaseDays !== undefined ? existed.maxLeaseDays : this.form.maxLeaseDays
})
}
})
this.selectedMachineRows = nextRows
},
/**
* 同步顶部功耗到行(行未自定义或无效则跟随)
*/
syncPowerDissipationToRows() {
const newVal = Number(this.form.powerDissipation)
if (!Number.isFinite(newVal)) return
const oldBaseline = this.lastPowerDissipationBaseline
this.selectedMachineRows = this.selectedMachineRows.map(row => {
const rowNum = Number(row.powerDissipation)
if (!Number.isFinite(rowNum) || rowNum === oldBaseline) {
return { ...row, powerDissipation: newVal }
}
return row
})
this.lastPowerDissipationBaseline = newVal
},
/**
* 同步顶部理论算力到行(行未自定义或无效则跟随)
*/
syncTheoryPowerToRows() {
const newVal = Number(this.form.theoryPower)
if (!Number.isFinite(newVal)) return
const oldBaseline = this.lastTheoryPowerBaseline
this.selectedMachineRows = this.selectedMachineRows.map(row => {
const rowNum = Number(row.theoryPower)
if (!Number.isFinite(rowNum) || rowNum === oldBaseline) {
return { ...row, theoryPower: newVal }
}
return row
})
this.lastTheoryPowerBaseline = newVal
},
/**
* 同步顶部单位到行(行未自定义或等于旧基线时跟随)
*/
syncUnitToRows() {
const newUnit = this.form.unit
if (!newUnit) return
const oldBaseline = this.lastUnitBaseline
this.selectedMachineRows = this.selectedMachineRows.map(row => {
const rowUnit = row.unit
if (!rowUnit || rowUnit === oldBaseline) {
return { ...row, unit: newUnit }
}
return row
})
this.lastUnitBaseline = newUnit
},
/**
* 行内功耗输入限制整数最多6位小数最多4位
*/
handleRowPowerDissipationInput(index) {
let v = String(this.selectedMachineRows[index].powerDissipation ?? '')
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 parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (intPart.length > 6) intPart = intPart.slice(0, 6)
if (decPart) decPart = decPart.slice(0, 4)
v = decPart.length ? `${intPart}.${decPart}` : intPart
this.$set(this.selectedMachineRows[index], 'powerDissipation', v)
},
/**
* 行内功耗校验
*/
handleRowPowerDissipationBlur(index) {
const raw = String(this.selectedMachineRows[index].powerDissipation ?? '')
const pattern = /^\d{1,6}(\.\d{1,4})?$/
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
this.$message.warning('功耗需大于0整数最多6位小数最多4位')
this.$set(this.selectedMachineRows[index], 'powerDissipation', '')
}
},
/**
* 行内理论算力输入限制整数最多6位小数最多4位
*/
handleRowTheoryPowerInput(index) {
let v = String(this.selectedMachineRows[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 parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (intPart.length > 6) intPart = intPart.slice(0, 6)
if (decPart) decPart = decPart.slice(0, 4)
v = decPart.length ? `${intPart}.${decPart}` : intPart
this.$set(this.selectedMachineRows[index], 'theoryPower', v)
},
/**
* 行内理论算力校验
*/
handleRowTheoryPowerBlur(index) {
const raw = String(this.selectedMachineRows[index].theoryPower ?? '')
const pattern = /^\d{1,6}(\.\d{1,4})?$/
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
this.$message.warning('理论算力需大于0整数最多6位小数最多4位')
this.$set(this.selectedMachineRows[index], 'theoryPower', '')
}
},
/**
* 行内单位变更
*/
handleRowUnitChange(index, value) {
this.$set(this.selectedMachineRows[index], 'unit', value)
},
syncMaxLeaseDaysToRows() {
const raw = this.form.maxLeaseDays
const n = Number(raw)
if (!Number.isInteger(n)) return
const oldBaseline = this.lastMaxLeaseDaysBaseline
this.selectedMachineRows = this.selectedMachineRows.map(row => {
const rowNum = Number(row.maxLeaseDays)
if (!Number.isInteger(rowNum) || rowNum === oldBaseline) {
return { ...row, maxLeaseDays: n }
}
return row
})
this.lastMaxLeaseDaysBaseline = n
},
handleRowMaxLeaseDaysInput(index) {
let v = String(this.selectedMachineRows[index].maxLeaseDays ?? '')
v = v.replace(/\D/g, '')
if (v.length > 3) v = v.slice(0, 3)
this.$set(this.selectedMachineRows[index], 'maxLeaseDays', v)
},
handleRowMaxLeaseDaysBlur(index) {
const raw = String(this.selectedMachineRows[index].maxLeaseDays ?? '')
if (!/^\d{1,3}$/.test(raw)) {
this.$message.warning('最大租赁天数需为 1-365 的整数')
this.$set(this.selectedMachineRows[index], 'maxLeaseDays', '')
return
}
const n = Number(raw)
if (!Number.isInteger(n) || n < 1 || n > 365) {
this.$message.warning('最大租赁天数需为 1-365 的整数')
this.$set(this.selectedMachineRows[index], 'maxLeaseDays', '')
}
},
handleRowPriceInput(index) {
// 价格输入整数最多12位小数最多2位允许尾随小数点
let v = String(this.selectedMachineRows[index].price ?? '')
@@ -537,7 +827,7 @@ export default {
console.log('机器列表数据:', this.machineOptions)
} catch (e) {
console.error('获取机器列表失败', e)
this.$message.error('获取机器列表失败,请重试')
} finally {
this.machinesLoading = false
}
@@ -584,6 +874,14 @@ export default {
this.$message.warning(`${i + 1}行(机器:${label}) 价格必须大于0`)
return
}
// 校验:逐行最大租赁天数 1-365
const rawDays = String((row && row.maxLeaseDays) ?? '')
const n = Number(rawDays)
if (!/^\d{1,3}$/.test(rawDays) || !Number.isInteger(n) || n < 1 || n > 365) {
const label = (row && (row.miner || row.user)) || i + 1
this.$message.warning(`${i + 1}行(机器:${label}) 最大租赁天数需为 1-365 的整数`)
return
}
}
// 通过所有预校验后,弹出确认框
this.confirmVisible = true
@@ -600,12 +898,17 @@ export default {
type: this.form.type,
unit: this.form.unit,
cost: this.form.cost,
maxLeaseDays: this.form.maxLeaseDays,
productMachineURDVos: this.selectedMachineRows.map(r => ({
miner: r.miner,
price: Number(r.price) || 0,
state: r.state || 0,
type: r.type || this.form.type,
user: r.user
user: r.user,
maxLeaseDays: Number(r.maxLeaseDays) || Number(this.form.maxLeaseDays) || 0,
powerDissipation: Number(r.powerDissipation) || Number(this.form.powerDissipation) || 0,
theoryPower: Number(r.theoryPower) || Number(this.form.theoryPower) || 0,
unit: r.unit || this.form.unit
}))
}
@@ -616,9 +919,7 @@ export default {
this.$message.success('添加成功')
this.confirmVisible = false
this.$router.back()
} else {
this.$message.error(res?.msg || '添加失败')
}
}
} catch (e) {
console.error('添加出售机器失败', e)
console.log('添加失败')
@@ -631,6 +932,10 @@ export default {
watch: {
'form.cost': function() { this.syncCostToRows() },
'form.type': function() { this.updateMachineType() },
'form.maxLeaseDays': function() { this.syncMaxLeaseDaysToRows() },
'form.powerDissipation': function() { this.syncPowerDissipationToRows() },
'form.theoryPower': function() { this.syncTheoryPowerToRows() },
'form.unit': function() { this.syncUnitToRows() },
selectedMachines() {
this.updateSelectedMachineRows()
}
@@ -642,6 +947,11 @@ export default {
.product-machine-add { padding: 8px; }
.header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.title { margin: 0; font-size: 18px; font-weight: 600; }
.notice-alert { margin-bottom: 12px; }
.notice-alert :deep(.el-alert__content) { text-align: left; }
.notice-alert :deep(.el-alert__title),
.notice-alert :deep(.el-alert__description) { text-align: left; }
.label-help { margin-left: 4px; color: #909399; cursor: help; }
.form-card { margin-bottom: 12px; }
.actions { text-align: right; }
@@ -649,11 +959,11 @@ export default {
.product-machine-add :deep(.el-form-item__content) {
justify-content: flex-start;
}
.product-machine-add :deep(.el-input),
/* .product-machine-add :deep(.el-input),
.product-machine-add :deep(.el-select),
.product-machine-add :deep(.el-textarea) {
width: 50%;
}
} */
.product-machine-add :deep(.el-input-group__append) {
background: #f5f7fa;
color: #606266;

View File

@@ -27,7 +27,7 @@
<el-form-item label="商品类型" prop="type" class="align-like-input">
<el-radio-group v-model="form.type">
<el-radio :label="0">矿机</el-radio>
<el-radio :label="1">算力</el-radio>
<!-- <el-radio :label="1">算力</el-radio> -->
</el-radio-group>
</el-form-item>
@@ -274,7 +274,7 @@ export default {
type: 'success',
showClose: true
})
this.$router.push('/account/shops')
this.$router.push('/account/products')
}else {
this.$message({
message: res && res.msg ? res.msg : '创建失败',

View File

@@ -29,8 +29,8 @@
stripe
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="名称" min-width="160" />
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
<el-table-column prop="name" label="名称" min-width="100" />
<el-table-column prop="coin" label="币种" width="100" />
<el-table-column prop="priceRange" label="价格范围" width="150" />
<!-- <el-table-column label="算力" min-width="140">
@@ -48,6 +48,9 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="saleNumber" label="已售数量" min-width="60" />
<el-table-column prop="totalMachineNumber" label="该商品总机器数量" min-width="60" />
<el-table-column prop="state" label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.state === 1 ? 'info' : 'success'">

View File

@@ -56,7 +56,7 @@
<el-card class="section" style="margin-top: 12px;">
<div class="sub-title">收益信息</div>
<div class="row">
<span class="label">当前实算力</span>
<span class="label">当前实算力</span>
<span class="value strong">{{ detail.currentComputingPower || '0' }}</span>
</div>
<div class="row">

View File

@@ -0,0 +1,326 @@
<template>
<div class="receipt-page">
<div class="card" aria-label="收款记录" tabindex="0">
<div class="card-header">
<h3 class="card-title">收款记录</h3>
<!-- <div class="card-actions">
<el-date-picker
v-model="range"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
size="small"
:clearable="true"
@change="handleRangeChange"
/>
<el-input
v-model="keyword"
size="small"
placeholder="订单号/备注搜索"
clearable
class="search-input"
@keyup.enter.native="fetchList"
/>
<el-button type="primary" size="small" @click="fetchList">查询</el-button>
</div> -->
</div>
<div v-if="loading" class="loading">
<i class="el-icon-loading" aria-label="加载中" role="img"></i>
加载中...
</div>
<div v-else>
<el-table
ref="receiptTable"
:data="rows"
border
stripe
size="small"
style="width: 100%"
:row-key="getRowKey"
:expand-row-keys="expandedRowKeys"
:row-class-name="getRowClassName"
@row-click="handleRowClick"
@expand-change="handleExpandChange"
:header-cell-style="{ textAlign: 'left' }"
:cell-style="{ textAlign: 'left' }"
>
<el-table-column type="expand" width="46">
<template #default="scope">
<div class="detail-panel">
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">订单号</span>
<span class="detail-value mono">{{ scope.row.orderId || '-' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">付款链</span>
<span class="detail-value"><span class="badge">{{ formatChain(scope.row.fromChain) || '-' }}</span></span>
</div>
<div class="detail-item">
<span class="detail-label">付款币种</span>
<span class="detail-value"><span class="badge badge-blue">{{ String((scope.row.fromSymbol || scope.row.coin) || '') .toUpperCase() }}</span></span>
</div>
<div class="detail-item detail-item-full">
<span class="detail-label">付款地址</span>
<span class="detail-value address">
<span class="mono-ellipsis" :title="scope.row.fromAddress">{{ scope.row.fromAddress || '-' }}</span>
<el-button type="text" size="mini" @click.stop="copy(scope.row.fromAddress)" v-if="scope.row.fromAddress">复制</el-button>
</span>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="支付时间" min-width="160">
<template #default="scope">{{ formatFullTime(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="收款金额(USDT)" min-width="160" align="right">
<template #default="scope">
<span class="amount-green">+{{ formatTrunc(scope.row.realAmount, 2) }}</span>
</template>
</el-table-column>
<el-table-column label="收款链" min-width="120">
<template #default="scope">{{ formatChain(scope.row.toChain) }}</template>
</el-table-column>
<el-table-column label="收款币种" min-width="100">
<template #default="scope">{{ String(scope.row.coin || '').toUpperCase() }}</template>
</el-table-column>
<el-table-column label="收款地址" min-width="260">
<template #default="scope">
<span class="mono-ellipsis" :title="scope.row.toAddress">{{ scope.row.toAddress }}</span>
<el-button type="text" size="mini" @click.stop="copy(scope.row.toAddress)">复制</el-button>
</template>
</el-table-column>
<el-table-column label="交易HASH" min-width="260">
<template #default="scope">
<span class="mono-ellipsis" :title="scope.row.txHash">{{ scope.row.txHash }}</span>
<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="支付状态" min-width="120">
<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="状态更新时间" min-width="160">
<template #default="scope">{{ formatFullTime(scope.row.updateTime) }}</template>
</el-table-column>
</el-table>
<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="page"
:page-size="pageSize"
:total="total"
@current-change="fetchList"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { sellerReceiptList } from '../../api/wallet'
export default {
name: 'AccountReceiptRecord',
data() {
return {
loading: false,
rows: [
{
orderId: '1234567890',
fromChain: 'tron',
fromSymbol: 'USDT',
fromAddress: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
toChain: 'tron',
coin: 'USDT',
toAddress: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
status: 2,
updateTime: '2024-01-15 14:30:25',
realAmount: 100,
},
{
orderId: '1234567890',
fromChain: 'tron',
fromSymbol: 'USDT',
fromAddress: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
toChain: 'tron',
coin: 'USDT',
toAddress: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
txHash: 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE',
status: 1,
updateTime: '2024-01-15 14:30:25',
realAmount: 106,
}
],
page: 1,
pageSize: 10,
total: 0,
range: [],
keyword: '',
expandedRowKeys: []
}
},
mounted() {
this.fetchList()
// 为示例数据补充唯一行键,避免展开联动
this.rows = this.withKeys(this.rows)
},
methods: {
withKeys(list) {
const arr = Array.isArray(list) ? list : []
return arr.map((it, idx) => ({
...it,
__rowKey: it && it.__rowKey ? it.__rowKey : `${it && (it.txHash || it.orderId || it.updateTime || '')}_${idx}`
}))
},
getRowKey(row) { return row && row.__rowKey },
handleRowClick(row) {
const key = this.getRowKey(row)
const isOpen = this.expandedRowKeys.includes(key)
// 手风琴:只保留当前点击行
this.expandedRowKeys = isOpen ? [] : [key]
},
handleExpandChange(row, expandedRows) {
// 由内置展开事件触发时,同步 keys确保手风琴
if (!Array.isArray(expandedRows)) {
this.expandedRowKeys = []
return
}
this.expandedRowKeys = expandedRows.length ? [this.getRowKey(expandedRows[expandedRows.length - 1])] : []
},
getRowClassName() { return 'clickable-row' },
/**
* 精确裁剪数字到指定位数(不四舍五入)
*/
formatTrunc(value, decimals = 2) {
const num = Number(value)
if (!Number.isFinite(num)) return '0'
const d = Math.max(0, Number(decimals) || 0)
const factor = Math.pow(10, d)
const truncated = Math.trunc(num * factor) / factor
const str = String(truncated)
if (d === 0) return str
const [intPart, decPart = ''] = str.split('.')
const padded = decPart.padEnd(d, '0')
return `${intPart}.${padded}`
},
formatFullTime(time) {
if (!time) return ''
try {
return `${time.split('T')[0]} ${time.split('T')[1].split('.')[0]}`
} catch (e) {
console.log(e,"时间");
return time
}
},
formatChain(chain) {
const map = { tron: 'Tron (TRC20)', ethereum: 'Ethereum (ERC20)', bsc: 'BSC (BEP20)', polygon: 'Polygon' }
return map[chain] || 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)
},
handleRangeChange() {
this.page = 1
},
async fetchList() {
this.loading = true
try {
const params = {
page: this.page,
pageSize: this.pageSize,
// keyword: this.keyword || undefined,
// startTime: Array.isArray(this.range) && this.range[0] ? new Date(this.range[0]).getTime() : undefined,
// endTime: Array.isArray(this.range) && this.range[1] ? new Date(this.range[1]).getTime() : undefined
}
const res = await sellerReceiptList(params)
const data = res && (res.data || res)
const list = Array.isArray(data && data.rows) ? data.rows : (Array.isArray(data) ? data : [])
this.rows = this.withKeys(list)
this.total = res.total
} catch (e) {
this.rows = []
this.total = 0
} finally {
this.loading = false
}
}
}
}
</script>
<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); }
.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-actions { display: flex; align-items: center; gap: 8px; }
.search-input { width: 220px; }
.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-green { color: #16a34a; font-weight: 700; }
.amount-red { color: #ef4444; font-weight: 700; }
.type-green { color: #16a34a; }
.type-red { color: #ef4444; }
.pagination { display: flex; justify-content: flex-end; margin-top: 8px; }
/* 展开详情样式 */
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 24px; padding: 8px 4px; }
.detail-item { display: grid; grid-template-columns: 90px 1fr; align-items: center; gap: 8px; }
.detail-item-full { grid-column: 1 / -1; }
.detail-label { color: #666; font-size: 13px; text-align: right; }
.detail-value { color: #333; font-size: 13px; text-align: left; }
.detail-value.address { font-family: "Monaco", "Menlo", monospace; word-break: break-all; }
/* 单行等宽省略 */
.mono-ellipsis { font-family: "Monaco", "Menlo", monospace; max-width: 360px; display: inline-block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; }
/* 可点击行的轻交互提示 */
.clickable-row:hover > td { background: #f8fafc !important; cursor: pointer; }
/* 展开面板视觉优化 */
.detail-panel { background: #f9fafb; border: 1px dashed #e5e7eb; border-radius: 8px; padding: 12px; }
.mono { font-family: "Monaco", "Menlo", monospace; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; background: #eef2ff; color: #3b82f6; font-size: 12px; line-height: 18px; }
.badge-blue { background: #eff6ff; color: #2563eb; }
</style>

View File

@@ -306,36 +306,36 @@ export default {
// 充值记录数据
rechargeRecords: [
// {
// address: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
// amount: 100,
// fromSymbol: "USDT",
// fromChain: "tron",
// status: 2,
// createTime: "2024-01-15 14:30:25",
// id: 1,
// txHash: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
// },
// {
// address: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
// amount: 100,
// fromSymbol: "USDT",
// fromChain: "tron",
// status: 2,
// createTime: "2024-01-15 14:30:25",
// id: 2,
// txHash: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
// },
// {
// address: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
// amount: 100,
// fromSymbol: "USDT",
// fromChain: "tron",
// status: 2,
// createTime: "2024-01-15 14:30:25",
// id: 3,
// txHash: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
// },
{
address: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
amount: 100,
fromSymbol: "USDT",
fromChain: "tron",
status: 2,
createTime: "2024-01-15 14:30:25",
id: 1,
txHash: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
},
{
address: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
amount: 100,
fromSymbol: "USDT",
fromChain: "tron",
status: 2,
createTime: "2024-01-15 14:30:25",
id: 2,
txHash: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
},
{
address: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
amount: 100,
fromSymbol: "USDT",
fromChain: "tron",
status: 2,
createTime: "2024-01-15 14:30:25",
id: 3,
txHash: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
},
// {
// address: "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
// amount: 100,

View File

@@ -1,45 +1,34 @@
<template>
<div class="panel">
<h2 class="panel-title page-title">钱包绑定</h2>
<div class="panel-body">
<div class="panel-body" v-loading="loading">
<el-form :model="form" label-width="120px" class="config-form">
<el-form-item label="适用商品">
<el-select v-model="form.productId" placeholder="请选择商品">
<el-option :value="0" label="全部商品" />
<el-option
v-for="p in productOptions"
:key="p.id"
:value="p.id"
:label="`${p.id} - ${p.name}`"
/>
</el-select>
<el-form-item label="选择链">
<el-cascader style="width: 420px;" @change="handleChange" v-model="value" :options="options"> </el-cascader>
</el-form-item>
</el-form-item>
<el-form-item label="收款钱包地址">
<el-input
v-model="form.payAddress"
placeholder="示例nexa:nqtsq5g50jkkmklvjyaflg46c4nwuy46z9gzswqe3l0csc7g"
/>
</el-form-item>
<el-form-item label="币种类型">
<el-radio-group v-model="form.payType" class="radio-group">
<el-radio :label="0">虚拟币</el-radio>
<!-- <el-radio :label="0">虚拟币</el-radio> -->
<el-radio :label="1">稳定币</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="支付币种">
<el-select v-model="form.payCoin" placeholder="请选择支付币种" filterable clearable>
<el-option v-for="c in coinOptions" :key="c" :label="c" :value="c" />
</el-select>
<el-form-item label="收款钱包地址">
<el-input
v-model="form.payAddress"
placeholder="请输入"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSave">保存配置</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button style="width: 200px;" type="primary" @click="handleSave">确认绑定</el-button>
</el-form-item>
</el-form>
</div>
@@ -47,113 +36,222 @@
</template>
<script>
import { addShopConfig ,getMyShop} from '@/api/shops'
import { getMyShop } from "@/api/shops";
import { getChainAndList, addWalletShopConfig } from "../../api/wallet";
// 币种集合
const VIRTUAL_COINS = ['nexa', 'rxd', 'dgbo', 'dgbq', 'dgbs', 'alph', 'enx', 'grs', 'mona']
const STABLE_COINS = ['usdt', 'usdc', 'busd']
const VIRTUAL_COINS = [
"nexa",
"rxd",
"dgbo",
"dgbq",
"dgbs",
"alph",
"enx",
"grs",
"mona",
];
const STABLE_COINS = ["usdt", "usdc", "busd"];
export default {
name: 'AccountShopConfig',
name: "AccountShopConfig",
data() {
return {
VIRTUAL_COINS,
STABLE_COINS,
productOptions: [],
form: {
payAddress: 'nexa:nqtsq5g50jkkmklvjyaflg46c4nwuy46z9gzswqe3l0csc7g',
payCoin: '',
payType: 0, // 0 虚拟币 1 稳定币
productId: 0, // 0 代表所有商品
shopId: 0
chain: "",
payAddress: "",
payCoin: "",
payType: 1, // 0 虚拟币 1 稳定币
},
shop: {
id: 0,
name: '',
image: '',
description: '',
name: "",
image: "",
description: "",
del: true,
state: 0
state: 0,
},
}
value: "",
options: [
// {
// value: "Tron (TRC20)",
// label: "Tron (TRC20)",
// children: [
// {
// value: "USDT",
// label: "USDT (TRC20)",
// },
// {
// value: "币种2",
// label: "币种2",
// },
// ],
// },
// {
// value: "ETH",
// label: "ETH",
// children: [
// {
// value: "USDT",
// label: "USDT (TRC20)",
// },
// ],
// },
],
loading: false,
};
},
mounted() {
this.fetchMyShop()
this.getChainAndList();
},
methods: {
async fetchMyShop() {
try {
const res = await getMyShop()
// 预期格式:{"code":0,"data":{"del":true,"description":"","id":0,"image":"","name":"","state":0},"msg":""}
if (res && (res.code === 0 || res.code === 200) && res.data) {
this.shop = {
id: res.data.id,
name: res.data.name,
image: res.data.image,
description: res.data.description,
del: !!res.data.del,
state: Number(res.data.state || 0)
}
this.form.shopId = this.shop.id
} else {
this.$message.warning(res.msg || '未获取到店铺数据')
}
} catch (error) {
console.error('获取店铺信息失败:', error)
} finally {
this.loaded = true
/**
* 根据选择的链校验钱包地址格式(参考钱包页面规则)
* - tron: 以 T 开头的 34 位字符
* - ethereum/bsc/polygon 等 EVM: 0x 开头 + 40 位十六进制
* - 其他:长度 > 10 的宽松校验
*/
validateAddressByChain(chain, address) {
const c = String(chain || '').toLowerCase()
const addr = String(address || '').trim()
if (!addr) return { ok: false, message: '请输入收款地址' }
if (c.includes('tron') || c === 'tron') {
const ok = /^T[A-Za-z1-9]{33}$/.test(addr)
return ok ? { ok: true } : { ok: false, message: '请输入正确的收款地址格式TRON' }
}
if (
c.includes('ethereum') || c === 'ethereum' ||
c.includes('eth') ||
c.includes('bsc') || c === 'bsc' ||
c.includes('polygon') || c === 'polygon' ||
c.includes('erc') || c.includes('bep')
) {
const ok = /^0x[a-fA-F0-9]{40}$/.test(addr)
return ok ? { ok: true } : { ok: false, message: '请输入正确的收款地址格式EVM' }
}
if (addr.length <= 10) {
return { ok: false, message: '请输入正确的收款地址格式' }
}
return { ok: true }
},
async getChainAndList() {
this.loading = true;
const res = await getChainAndList();
if (res && (res.code === 0 || res.code === 200) && res.data) {
this.options = this.toUpperOptions(res.data);
}
this.loading = false;
},
/**
* 将级联选项的 label 文本统一转为大写(不修改 value
* @param {Array} list
* @returns {Array}
*/
toUpperOptions(list) {
const arr = Array.isArray(list) ? list : []
return arr.map(item => {
const next = { ...item }
const src = (item && (item.label != null ? item.label : item.value)) || ''
next.label = String(src).toUpperCase()
if (Array.isArray(item && item.children)) {
next.children = this.toUpperOptions(item.children)
}
return next
})
},
async FetchAddWalletShopConfig(params) {
this.loading = true;
const res = await addWalletShopConfig(params);
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success("绑定成功");
this.$router.push("/account/shops");
}
this.loading = false;
},
async addShopConfig(params) {
const res = await addShopConfig(params)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('已保存配置(示例)')
} else {
this.$message.error(res.msg || '保存失败')
}
handleChange(value) {
console.log(value);
this.form.payCoin = value[1];
this.form.chain = value[0];
},
handleSave() {
this.form.shopId = this.shop.id
if (!this.form.shopId) {
this.$message.warning(`未查询到店铺信息`)
return
}
this.addShopConfig(this.form)
this.form.chain =this.value[0]
this.form.payCoin = this.value[1]
if (!this.form.chain) {
this.$message.warning("请选择链");
return;
}
if (!this.form.payCoin) {
this.$message.warning("请选择币种");
return;
}
if (!this.form.payAddress) {
this.$message.warning("请输入钱包地址");
return;
}
// 链路格式校验
const { ok, message } = this.validateAddressByChain(this.form.chain, this.form.payAddress)
if (!ok) {
this.$message.warning(message || '钱包地址格式不正确')
return
}
this.FetchAddWalletShopConfig(this.form);
},
handleReset() {
this.form = { payAddress: '', payCoin: '', payType: 0, productId: 0 }
}
this.form = { chain: "", payAddress: "", payCoin: "", payType: 0 };
},
},
computed: {
// 根据币种类型动态展示可选币种
coinOptions() {
return this.form.payType === 1 ? STABLE_COINS : VIRTUAL_COINS
}
return this.form.payType === 1 ? STABLE_COINS : VIRTUAL_COINS;
},
},
watch: {
'form.payType'(val) {
"form.payType"(val) {
// 切换类型时清空已选币种,避免类型与币种不匹配
this.form.payCoin = ''
}
}
}
this.form.payCoin = "";
},
},
};
</script>
<style scoped>
.page-title { text-align: left; margin-bottom: 16px; font-size: 20px; padding-left: 4px; }
.page-title {
text-align: left;
margin-bottom: 16px;
font-size: 20px;
padding-left: 4px;
}
.config-form {
max-width: 720px;
margin: 0;
background: #fff;
padding: 8px 12px;
}
.config-form .el-form-item { margin-bottom: 18px; }
.config-form .el-form-item {
margin-bottom: 18px;
}
.config-form .el-select,
.config-form .el-input { width: 420px; }
.config-form .el-input {
width: 420px;
}
.radio-group {
display: inline-flex;
align-items: center;
@@ -163,6 +261,10 @@ export default {
padding-left: 12px; /* 留出与输入框一致的内边距感 */
box-sizing: border-box;
}
.tip { color: #999; font-size: 12px; margin-top: 6px; }
.tip {
color: #999;
font-size: 12px;
margin-top: 6px;
}
</style>

View File

@@ -1,44 +1,46 @@
<template>
<div class="wallet-container">
<!-- 钱包余额卡片 -->
<div class="wallet-card">
<div class="wallet-toolbar" role="region" aria-label="钱包操作">
<el-button
type="primary"
class="create-wallet-btn"
@click="openCreateWallet"
>
<i class="el-icon-plus" style="margin-right:6px;"></i>充值
</el-button>
</div>
<!-- 多个钱包余额卡片 -->
<section class="wallet-card-section">
<div class="wallet-card" v-for="w in walletList" :key="w.id">
<div class="wallet-header">
<h2 class="wallet-title"> <i class="el-icon-wallet"></i> 我的钱包</h2>
<h2 class="wallet-title">
<i class="el-icon-wallet"></i> 我的钱包
<el-tag size="mini" effect="dark" style="margin-left:8px;">
{{ (w.fromChain || w.chain || '').toUpperCase() }} {{ (w.fromSymbol || w.coin || '').toUpperCase() }}
</el-tag>
</h2>
<div class="wallet-balance">
<div class="balance-item">
<span class="balance-label">可用余额</span>
<span class="balance-amount">{{ walletBalance }} USDT</span>
<span class="balance-amount">{{ (w.walletBalance || w.balance || 0) }} {{ displaySymbol(w) }}</span>
</div>
<div class="balance-item">
<span class="balance-label">冻结余额</span>
<span class="balance-amount frozen">{{ blockedBalance }} USDT</span>
<span class="balance-amount frozen">{{ (w.blockedBalance || 0) }} {{ displaySymbol(w) }}</span>
</div>
</div>
</div>
<!-- 操作按钮区域 -->
<div class="wallet-actions">
<el-button
type="primary"
size="large"
class="action-btn recharge-btn"
@click="handleRecharge"
>
<!-- <i class="el-icon-plus"></i> -->
充值
</el-button>
<el-button
type="success"
size="large"
class="action-btn withdraw-btn"
@click="handleWithdraw"
size="mini"
class="withdraw-inline-btn"
@click="handleWithdraw(w)"
>
<!-- <i class="el-icon-minus"></i> -->
提现
</el-button>
</div>
</div>
</div>
</section>
<!-- 交易记录区域 -->
<div class="transaction-section">
<h3 class="section-title">最近交易</h3>
@@ -73,6 +75,18 @@
<!-- 钱包地址区域 -->
<div class="wallet-address-section">
<h4 class="section-title">钱包地址</h4>
<div class="charge-meta">
<el-tag size="small" effect="dark" type="warning" class="meta-tag">
<i class="el-icon-link"></i>
<span class="meta-title">充值链</span>
<span class="meta-val">{{ (WalletData.fromChain || WalletData.chain || '').toString().toUpperCase() }}</span>
</el-tag>
<el-tag size="small" effect="dark" type="warning" class="meta-tag">
<i class="el-icon-coin"></i>
<span class="meta-title">充值币种</span>
<span class="meta-val">{{ (WalletData.fromSymbol || WalletData.coin || '').toString().toUpperCase() }}</span>
</el-tag>
</div>
<div class="address-container">
<el-input
v-model="WalletData.fromAddress"
@@ -89,7 +103,7 @@
复制
</el-button>
</div>
<p class="address-tip">请向此地址转账USDT到账后余额将自动更新</p>
<p class="address-tip">请向此地址转账{{ displaySymbol(WalletData) }}资产否则资产将无法找回.</p>
</div>
<!-- 二维码区域 -->
@@ -99,7 +113,7 @@
<div class="qr-code" ref="qrCodeRef">
<!-- 二维码将在这里生成 -->
</div>
<p class="qr-tip">使用支持USDT的钱包扫描二维码</p>
<p class="qr-tip">使用支持{{ displaySymbol(WalletData) }}的钱包扫描二维码</p>
</div>
</div>
@@ -107,8 +121,8 @@
<div class="recharge-notice">
<h4 class="section-title">充值说明</h4>
<ul class="notice-list">
<li>暂时仅支持USDT (TRC20) 网络转账</li>
<li>最小充值金额10 USDT</li>
<li>充值后请耐心等待余额更新或在资金流水页面查看最新充值记录</li>
<li>最小充值金额10 {{ displaySymbol(WalletData) }}</li>
</ul>
</div>
</div>
@@ -126,38 +140,22 @@
@close="resetWithdrawForm"
>
<el-form :model="withdrawForm" :rules="withdrawRules" ref="withdrawForm" label-width="120px">
<!-- 选择链 -->
<el-form-item label="选择链" prop="chain">
<el-select
v-model="withdrawForm.toChain"
placeholder="请选择区块链网络"
<!-- 提现链只读展示当前钱包链 -->
<el-form-item label="提现链">
<el-input
:value="(WalletData.fromChain || WalletData.chain || withdrawForm.toChain || '').toString().toUpperCase()"
:disabled="true"
style="width: 100%"
@change="onChainChange"
>
<el-option
v-for="chain in chainOptions"
:key="chain.value"
:label="chain.label"
:value="chain.value"
/>
</el-select>
/>
</el-form-item>
<!-- 选择币种 -->
<el-form-item label="选择币种" prop="token">
<el-select
v-model="withdrawForm.toSymbol"
placeholder="请选择提现币种"
<!-- 提现币种只读展示当前钱包币种 -->
<el-form-item label="提现币种">
<el-input
:value="displayWithdrawSymbol"
:disabled="true"
style="width: 100%"
:disabled="!withdrawForm.toChain"
>
<el-option
v-for="token in availableTokens"
:key="token.value"
:label="token.label"
:value="token.value"
/>
</el-select>
/>
</el-form-item>
<!-- 提现金额 -->
@@ -169,14 +167,14 @@
inputmode="decimal"
@input="handleAmountInput"
>
<template slot="append">{{ withdrawForm.toSymbol || 'USDT' }}</template>
<template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input>
<div class="balance-info">
<div class="balance-detail">
<span>可用余额{{ walletBalance }} USDT</span>
<span>可用余额{{ (WalletData.walletBalance || WalletData.balance || 0) }} {{ displayWithdrawSymbol }}</span>
</div>
<div class="balance-detail frozen-info">
<span>冻结余额{{ blockedBalance }} USDT</span>
<span>冻结余额{{ (WalletData.blockedBalance || 0) }} {{ displayWithdrawSymbol }}</span>
<span class="frozen-tip">购买机器下单后冻结不可提现</span>
</div>
</div>
@@ -190,10 +188,10 @@
style="width: 100%"
:disabled="true"
>
<template slot="append">{{ withdrawForm.toSymbol || 'USDT' }}</template>
<template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input>
<div class="fee-info">
网络手续费{{ withdrawForm.fee || '0.00' }} {{ withdrawForm.toSymbol || 'USDT' }}
网络手续费{{ withdrawForm.fee || '0.00' }} {{ displayWithdrawSymbol }}
</div>
</el-form-item>
@@ -205,10 +203,10 @@
style="width: 100%"
:disabled="true"
>
<template slot="append">{{ withdrawForm.toSymbol || 'USDT' }}</template>
<template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input>
<div class="actual-amount-info">
实际到账{{ actualAmount }} {{ withdrawForm.toSymbol || 'USDT' }}
实际到账{{ actualAmount }} {{ displayWithdrawSymbol }}
</div>
</el-form-item>
@@ -252,18 +250,45 @@
<el-button type="primary" @click="confirmWithdraw" :loading="withdrawLoading">确认提现</el-button>
</div>
</el-dialog>
<!-- 链上充值 对话框 -->
<el-dialog
title="链上充值"
:visible.sync="createDialogVisible"
width="520px"
>
<el-form label-width="120px">
<el-form-item label="选择充值链/币种">
<el-cascader
v-model="createValue"
:options="options"
style="width: 100%"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmCreateWallet" :loading="createLoading">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getWalletInfo, withdrawBalance ,balanceRechargeList,balanceWithdrawList} from '@/api/wallet'
import { getWalletInfo, withdrawBalance ,balanceRechargeList,balanceWithdrawList,getRecentlyTransaction} from '@/api/wallet'
import { getChainAndList,bindWallet } from "../../api/wallet";
export default {
name: 'WalletPage',
data() {
return {
// 钱包余额
// 单钱包旧字段(兼容保留,不再直接渲染)
walletBalance: 0,
blockedBalance: 0, // 冻结余额
// 多钱包列表
walletList: [],
WalletData: {},
// 充值对话框相关
rechargeDialogVisible: false,
@@ -308,7 +333,12 @@ export default {
// { label: 'BSC (BEP20)', value: 'bsc' },
// { label: 'Polygon (MATIC)', value: 'polygon' }
],
options: [],
loading: false,
// 新建钱包弹窗
createDialogVisible: false,
createLoading: false,
createValue: [],
// 币种选项(根据链动态变化)
tokenOptions: {
tron: [
@@ -335,30 +365,30 @@ export default {
// 最近交易记录
recentTransactions: [
{
id: 1,
type: '充值',
amount: 500.00,
time: '2024-01-15 14:30'
},
{
id: 2,
type: '购买商品',
amount: -89.50,
time: '2024-01-14 10:20'
},
{
id: 3,
type: '提现',
amount: -200.00,
time: '2024-01-13 16:45'
},
{
id: 4,
type: '充值',
amount: 1000.00,
time: '2024-01-12 09:15'
}
// {
// id: 1,
// type: '充值',
// amount: 500.00,
// time: '2024-01-15 14:30'
// },
// {
// id: 2,
// type: '购买商品',
// amount: -89.50,
// time: '2024-01-14 10:20'
// },
// {
// id: 3,
// type: '提现',
// amount: -200.00,
// time: '2024-01-13 16:45'
// },
// {
// id: 4,
// type: '充值',
// amount: 1000.00,
// time: '2024-01-12 09:15'
// }
]
}
},
@@ -390,15 +420,119 @@ export default {
const available = parseFloat(this.walletBalance) || 0
const blocked = parseFloat(this.blockedBalance) || 0
return (available + blocked).toFixed(2)
},
/**
* 提现展示单位(始终大写):优先当前 WalletData 的 fromSymbol/coin
*/
displayWithdrawSymbol() {
const sym = (this.WalletData && (this.WalletData.fromSymbol || this.WalletData.coin || this.withdrawForm.toSymbol)) || ''
return String(sym).toUpperCase()
}
},
mounted() {
this.fetchWalletInfo()
// 初始化手续费
this.updateFeeByChain()
this.getChainAndList()
this.fetchRecentlyTransaction()
},
methods: {
/**
* 统一获取钱包展示单位优先级fromSymbol > toSymbol > coin > 'USDT'
*/
displaySymbol(w) {
const sym = (w && (w.fromSymbol || w.toSymbol || w.coin)) || ''
return String(sym).toUpperCase()
},
openCreateWallet() {
this.createDialogVisible = true
if (!Array.isArray(this.options) || this.options.length === 0) {
this.getChainAndList()
}
},
async confirmCreateWallet() {
const val = this.createValue || []
if (!Array.isArray(val) || val.length < 2) {
this.$message.warning('请先选择链与币种')
return
}
const [chain, coin] = val
if (!chain || !coin) {
this.$message.warning('请选择完整的链与币种')
return
}
try {
this.createLoading = true
const res = await bindWallet({ chain, coin })
if (res && (res.code === 0 || res.code === 200)) {
// 后端会返回充值相关信息(地址/二维码等),直接展示充值弹窗
const data = res.data
if (data) {
const walletInfo = Array.isArray(data) ? (data[0] || {}) : data
this.WalletData = walletInfo
this.rechargeDialogVisible = true
this.qrCodeGenerated = false
this.$nextTick(() => { this.generateQRCode() })
}
// 同步刷新钱包列表(异步,不影响弹窗展示)
this.fetchWalletInfo()
// 关闭链/币种选择弹窗
this.createDialogVisible = false
this.createValue = []
}
} catch (e) {
console.error('获取充值信息失败', e)
} finally {
this.createLoading = false
}
},
async getChainAndList() {
this.loading = true;
const res = await getChainAndList();
if (res && (res.code === 0 || res.code === 200) && res.data) {
this.options = res.data;
}
this.loading = false;
},
/**
* 获取最近交易记录(充值/提现/支付)
*/
async fetchRecentlyTransaction() {
try {
const res = await getRecentlyTransaction()
if (res && (res.code === 0 || res.code === 200)) {
const rows = Array.isArray(res.data) ? res.data : []
const mapped = rows.map((r, idx) => {
const rawAmt = Number(r && r.amount)
const amt = Number.isFinite(rawAmt) ? rawAmt : 0
const type = Number(r && r.type)
const signAmt = (type === 1) ? Math.abs(amt) : -Math.abs(amt) // 1 充值为正0 支付/2 提现为负
const typeLabel = type === 1 ? '充值' : (type === 2 ? '提现' : '支付')
return {
id: `${r && r.updateTime || ''}-${idx}`,
type: typeLabel,
amount: Number(signAmt.toFixed(2)),
time: this.formatApiTime(r && r.updateTime)
}
})
this.recentTransactions = mapped
}
} catch (e) {
// 忽略错误,保留本地占位
// console.error('获取最近交易失败', e)
}
},
/**
* 将后端时间格式 2025-10-16T07:57:28 转换为 2025-10-16 07:57:28
*/
formatApiTime(value) {
const s = String(value || '')
if (!s) return ''
return s.replace('T', ' ').replace('Z', '')
},
/**
* 将金额字符串转换为“分”为单位的整数
*/
@@ -433,9 +567,26 @@ export default {
const res = await getWalletInfo(params)
if (res && (res.code === 0 || res.code === 200)) {
this.walletBalance = res.data.balance
this.blockedBalance = res.data.blockedBalance || 0
this.WalletData = res.data
// 兼容两种返回:对象或数组
const data = res.data
if (Array.isArray(data)) {
this.walletList = data
// 兜底设置第一个钱包到旧字段,兼容现有充值二维码逻辑
const first = data[0] || {}
this.walletBalance = first.walletBalance || first.balance || 0
this.blockedBalance = first.blockedBalance || 0
this.WalletData = first
} else if (data && typeof data === 'object') {
this.walletList = [data]
this.walletBalance = data.walletBalance || data.balance || 0
this.blockedBalance = data.blockedBalance || 0
this.WalletData = data
} else {
this.walletList = []
this.walletBalance = 0
this.blockedBalance = 0
this.WalletData = {}
}
}
} catch (error) {
console.error('获取钱包信息失败:', error)
@@ -500,9 +651,13 @@ export default {
/**
* 打开充值对话框
*/
handleRecharge() {
handleRecharge(wallet) {
// 切换到被点击钱包的数据作为当前充值来源
if (wallet && typeof wallet === 'object') {
this.WalletData = wallet
}
this.rechargeDialogVisible = true
this.qrCodeGenerated = false
this.$nextTick(() => {
this.generateQRCode()
})
@@ -511,7 +666,17 @@ export default {
/**
* 打开提现对话框
*/
handleWithdraw() {
handleWithdraw(wallet) {
// 若需要也可根据点击卡片切换默认链/币种
if (wallet) {
// 同步当前选中的钱包,驱动只读展示链与币种
this.WalletData = wallet
const chain = wallet.fromChain || wallet.chain || this.withdrawForm.toChain
const symbol = wallet.fromSymbol || wallet.coin || this.withdrawForm.toSymbol
this.withdrawForm.toChain = chain
this.withdrawForm.toSymbol = symbol
this.updateFeeByChain()
}
this.withdrawDialogVisible = true
},
@@ -642,8 +807,8 @@ export default {
try {
// 调用后台提现API添加谷歌验证码参数
const res = await withdrawBalance({
toChain: this.withdrawForm.toChain,
toSymbol: this.withdrawForm.toSymbol,
toChain: (this.WalletData && (this.WalletData.fromChain || this.WalletData.chain)) || this.withdrawForm.toChain,
toSymbol: (this.WalletData && (this.WalletData.fromSymbol || this.WalletData.coin)) || this.withdrawForm.toSymbol,
amount: parseFloat(this.withdrawForm.amount),
toAddress: this.withdrawForm.toAddress,
code: this.withdrawForm.googleCode // 添加谷歌验证码
@@ -864,12 +1029,37 @@ export default {
padding: 20px;
}
/* 顶部工具栏 */
.wallet-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.create-wallet-btn {
background: linear-gradient(135deg, #409eff 0%, #36cfc9 100%);
border: none;
color: #fff;
font-weight: 600;
border-radius: 8px;
box-shadow: 0 6px 18px rgba(64, 158, 255, 0.25);
}
.create-wallet-btn:hover {
filter: brightness(1.05);
}
.wallet-card-section{
max-height: 600px;
/* height: 600px; */
overflow-y: auto;
padding: 8px;
}
/* 钱包卡片样式 */
.wallet-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
color: white;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
}
@@ -878,11 +1068,11 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
margin-bottom: 12px;
}
.wallet-title {
font-size: 24px;
font-size: 18px;
font-weight: 700;
margin: 0;
}
@@ -890,32 +1080,34 @@ export default {
.wallet-balance {
text-align: right;
display: flex;
flex-direction: column;
gap: 8px;
flex-direction: row;
align-items: center;
gap: 16px;
}
.balance-item {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-direction: row;
align-items: center;
gap: 8px;
}
.balance-label {
display: block;
font-size: 14px;
opacity: 0.8;
margin-bottom: 4px;
display: inline-block;
font-size: 16px;
opacity: 0.85;
margin: 0;
}
.balance-amount {
font-size: 32px;
font-size: 20px;
font-weight: 700;
font-family: 'Monaco', 'Menlo', monospace;
}
.balance-amount.frozen {
font-size: 24px;
opacity: 0.8;
font-size: 20px;
opacity: 0.9;
color: #ffa940;
}
@@ -923,16 +1115,21 @@ export default {
.wallet-actions {
display: flex;
gap: 16px;
justify-content: right;
}
.action-btn {
flex: 1;
height: 48px;
font-size: 16px;
width: 100px;
height: 30px;
font-size: 13px;
font-weight: 600;
border-radius: 8px;
border: none;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.recharge-btn {
@@ -957,6 +1154,17 @@ export default {
transform: translateY(-2px);
}
/* 内联提现按钮(余额行末尾) */
.withdraw-inline-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.withdraw-inline-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
/* 交易记录区域 */
.transaction-section {
background: white;
@@ -1119,6 +1327,18 @@ export default {
background-color: #f8f9fa;
}
/* 充值链/币种信息 */
.charge-meta {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 10px;
flex-wrap: wrap;
}
.meta-tag { border-radius: 14px; }
.meta-title { margin-left: 4px; opacity: .9; }
.meta-val { margin-left: 2px; font-weight: 700; letter-spacing: .3px; }
.copy-btn {
flex-shrink: 0;
}

View File

@@ -31,86 +31,87 @@
>
<el-table-column type="expand" width="46">
<template #default="shopScope">
<!-- 中层商品分组列表店铺 -->
<el-table
:ref="'productTable-' + shopScope.row.id"
:data="shopScope.row.shoppingCartInfoDtoList || []"
border size="small" style="width: 100%"
:row-key="'id'"
:header-cell-style="{ textAlign: 'left' }"
:cell-style="{ textAlign: 'left' }"
@selection-change="sels => handleGroupSelectionChangeForShop(shopScope.row, sels)"
@expand-change="(row, expandedRows) => handleProductExpandChange(shopScope.row, row, expandedRows)"
>
<el-table-column type="selection" width="46" />
<el-table-column type="expand" width="46">
<template #default="outer">
<!-- 内层:机器列表 -->
<el-table :data="outer.row.productMachineDtoList" size="small" border style="width: 100%"
:row-key="'id'"
:ref="'innerTable-' + outer.row.id"
@selection-change="sels => handleInnerSelectionChange(outer.row, sels)"
:header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table-column type="selection" width="46" />
<el-table-column prop="miner" label="机器编号" min-width="160" />
<el-table-column prop="algorithm" label="算法" min-width="140" />
<el-table-column prop="powerDissipation" label="功耗(kw/h)" min-width="140" />
<el-table-column prop="theoryPower" label="理论算力" min-width="140" />
<el-table-column prop="theoryIncome" min-width="200">
<template #header>单机理论收入(每日){{ outer.row.coin || '' }}</template>
</el-table-column>
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)" min-width="200" />
<el-table-column prop="state" label="状态" min-width="100" >
<template #default="scope">
<el-tag :type="scope.row.state === 1 ? 'info' : 'success'">
{{ scope.row.state === 1 ? '下架' : '上架' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="price" label="单价(USDT)" min-width="120" />
<el-table-column label="租赁天数()" min-width="140">
<template #default="scope">
<el-input-number
v-model="scope.row.leaseTime"
:min="1"
:max="365"
:precision="0"
:step="1"
size="mini"
controls-position="right"
@change="handleLeaseTimeChange(scope.row)"
@input="handleLeaseTimeInput(scope.row, $event)"
/>
</template>
</el-table-column>
<el-table-column label="机器总价(USDT)" min-width="160">
<template #default="scope">{{ (Number(scope.row.price || 0) * Number(scope.row.leaseTime || 1)).toFixed(2) }}</template>
</el-table-column>
</el-table>
<!-- 机器列表直接挂在店铺 -->
<el-table :data="shopScope.row.productMachineDtoList || []" size="small" border style="width: 100%"
:row-key="'id'" reserve-selection
:ref="'innerTable-' + shopScope.row.id"
@selection-change="sels => handleShopInnerSelectionChange(shopScope.row, sels)"
:header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table-column type="selection" width="46" :selectable="isRowSelectable" />
<el-table-column prop="name" label="商品名称" />
<el-table-column prop="miner" label="机器编号" />
<el-table-column prop="algorithm" label="算法" />
<el-table-column prop="powerDissipation" label="功耗(kw/h)" />
<el-table-column prop="theoryPower" label="理论算力" >
<template #default="scope">{{ scope.row.theoryPower }} <span v-show="scope.row.theoryPower">{{ scope.row.unit }}</span></template>
</el-table-column>
<el-table-column prop="computingPower" label="实际算力">
<template #default="scope">{{ scope.row.computingPower }} <span v-show="scope.row.computingPower">{{ scope.row.unit }}</span></template>
</el-table-column>
<el-table-column prop="theoryIncome" label="单机理论收入(每日/币种)">
<template #default="scope">{{ scope.row.theoryIncome }} <span v-show="scope.row.coin">{{ toUpperText(scope.row.coin) }}</span></template>
</el-table-column>
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)"/>
<!-- <el-table-column prop="state" label="状态" min-width="100" >
<template #default="scope">
<el-tag :type="scope.row.state === 1 ? 'info' : 'success'">
{{ scope.row.state === 1 ? '下架' : '上架' }}
</el-tag>
</template>
</el-table-column> -->
<el-table-column prop="price" label="单价(USDT)" width="100">
<template #default="scope">
<span class="price-strong">{{ formatTrunc(scope.row.price, 2) }}</span>
</template>
</el-table-column>
<el-table-column prop="name" label="商品名称" min-width="160" />
<el-table-column prop="coin" label="币种" min-width="120" />
<el-table-column label="机器数量" min-width="120">
<template #default="scope">{{ (scope.row.productMachineDtoList || []).length }}</template>
<el-table-column label="租赁天数" width="145">
<template #default="scope">
<el-input-number
v-model="scope.row.leaseTime"
:min="1"
:max="getRowMaxLeaseDaysLocal(scope.row)"
:precision="0"
:step="1"
size="mini"
controls-position="right"
@change="handleLeaseTimeChange(scope.row)"
@input="handleLeaseTimeInput(scope.row, $event)"
/>
</template>
</el-table-column>
<el-table-column label="总价(USDT)" min-width="140">
<template #default="scope"><span class="price-strong">{{ calcGroupTotal(scope.row).toFixed(2) }}</span></template>
<el-table-column label="最大可租()" min-width="60">
<template #default="scope">{{ scope.row.maxLeaseDays != null ? scope.row.maxLeaseDays : '' }}</template>
</el-table-column>
<el-table-column label="机器状态" width="110">
<template #default="scope">
<el-tag :type="(Number(scope.row.del) === 1 || Number(scope.row.state) === 1) ? 'info' : 'success'">
{{ (Number(scope.row.del) === 1 || Number(scope.row.state) === 1) ? '下架' : '上架' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="机器总价(USDT)" min-width="100">
<template #default="scope"><span class="price-strong">{{ formatTrunc(Number(scope.row.price || 0) * Number(scope.row.leaseTime || 1), 2) }}</span></template>
</el-table-column>
</el-table>
</template>
</el-table-column>
<el-table-column prop="name" label="店铺名称" min-width="220" />
<el-table-column label="商品" min-width="120">
<template #default="scope">{{ (scope.row.shoppingCartInfoDtoList || []).length }}</template>
</el-table-column>
<el-table-column label="机器总数" min-width="120">
<el-table-column prop="name" label="店铺名称" />
<el-table-column prop="totalMachine" label="机器总" />
<!-- <el-table-column label="机器总数" min-width="120">
<template #default="scope">{{ countMachines(scope.row) }}</template>
</el-table-column> -->
<el-table-column label="总价(USDT)" >
<template #default="scope"><span class="price-strong">{{ formatTrunc(computeShopTotal(scope.row), 2) }}</span></template>
</el-table-column>
<el-table-column label="总价(USDT)" min-width="140">
<template #default="scope"><span class="price-strong">{{ computeShopTotal(scope.row).toFixed(2) }}</span></template>
<el-table-column label="支付方式">
<template #default="scope">
<img v-for="(item,index ) in scope.row.payConfigList" :key="index" :src="item.payCoinImage" :alt="item.payChain" :title="formatPayTooltip(item)" style="width: 20px; height: 20px; margin-right: 10px;" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="160">
<el-table-column label="操作" >
<template #default="scope">
<el-button type="primary" size="mini" :loading="creatingOrder" :disabled="creatingOrder" @click="handleCheckoutShop(scope.row)">结算该店铺订单</el-button>
</template>
@@ -119,10 +120,11 @@
<div class="summary-actions" style="margin-top:16px;display:flex;gap:12px;justify-content:flex-end;">
<div class="summary-inline" style="color:#666;">
已选机器:<b>{{ selectedMachineCount }}</b> 台
<span style="margin-left:12px;">金额合计(USDT)<b>{{ selectedTotal.toFixed(2) }}</b></span>
<span style="margin-left:12px;">金额合计(USDT)<b>{{ formatTrunc(selectedTotal, 2) }}</b></span>
</div>
<div class="actions-inline" style="display:flex;gap:12px;">
<el-button type="danger" :disabled="!selectedMachineCount" @click="handleRemoveSelectedMachines">删除所选机器</el-button>
<el-button type="warning" plain :loading="clearOffLoading" @click="handleClearOffShelf">清除已下架商品</el-button>
</div>
</div>
@@ -135,9 +137,11 @@
<el-table-column prop="machineId" label="机器ID" min-width="100" />
<el-table-column prop="user" label="账户" min-width="120" />
<el-table-column prop="miner" label="机器编号" min-width="160" />
<el-table-column prop="price" label="单价(USDT)" min-width="120" />
<el-table-column prop="price" min-width="120">
<template #header>单价({{ payCoinSymbol || 'USDT' }}</template>
</el-table-column>
</el-table>
<div style="margin-top:12px;text-align:right;">总金额(USDT)<b>{{ confirmDialog.total.toFixed(2) }}</b></div>
<div style="margin-top:12px;text-align:right;">总金额{{ payCoinSymbol || 'USDT' }}<b>{{ formatTrunc(confirmDialog.total, 2) }}</b></div>
</div>
<template #footer>
<el-button @click="confirmDialog.visible=false">取消</el-button>
@@ -145,6 +149,23 @@
</template>
</el-dialog>
<!-- 支付链/币种选择弹窗 -->
<el-dialog :visible.sync="payDialog.visible" width="520px" title="选择支付链/币种" :close-on-click-modal="false">
<el-form label-width="120px">
<el-form-item label="/币种">
<el-cascader
v-model="payDialog.value"
:options="options"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="payDialog.visible=false">取消</el-button>
<el-button type="primary" :loading="payDialog.loading" @click="handlePayConfirm">下一步</el-button>
</template>
</el-dialog>
<!-- 结算成功提示弹窗 -->
<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;">
@@ -235,8 +256,9 @@
</template>
<script>
import { getGoodsList, deleteBatchGoods as apiDeleteBatchGoods} from '../../api/shoppingCart'
import { addOrders,cancelOrder,getOrdersByIds,getOrdersByStatus } from '../../api/order'
import { getGoodsList, deleteBatchGoods as apiDeleteBatchGoods ,deleteBatchGoodsForIsDelete} from '../../api/shoppingCart'
import { addOrders,cancelOrder,getOrdersByIds,getOrdersByStatus , getChainAndListForSeller,getCoinPrice } from '../../api/order'
export default {
name: 'Cart',
data() {
@@ -245,7 +267,8 @@ export default {
shops: [], // 店铺数组,每个店铺下有 shoppingCartInfoDtoList
groups: [], // 兼容旧结构,保留
selectedGroups: [],
selectedMachinesMap: {}, // { groupId: Set(machineId) }
// 新结构:按店铺选择机器 { shopId: Set(machineId) }
selectedMachinesMap: {},
confirmDialog: { visible: false, items: [], count: 0, total: 0 },
expandedGroupKeys: [],
expandedShopKeys: [],
@@ -262,7 +285,14 @@ export default {
code: '',
error: '',
loading: false
}
},
// 支付选择
options: [],
payDialog: { visible: false, value: [], loading: false },
selectedChain: '',
selectedCoin: '',
selectedPrice: 0
,clearOffLoading: false
}
},
computed: {
@@ -302,6 +332,10 @@ export default {
isGoogleCodeValid() {
const code = this.googleCodeDialog.code
return /^\d{6}$/.test(code)
},
// 支付币种展示用(大写)
payCoinSymbol() {
return (this.selectedCoin || '').toUpperCase()
}
},
mounted() {
@@ -311,10 +345,22 @@ export default {
'noticeDialog.visible'(val) {
if (val) {
this.startNoticeCountdown()
// 弹窗打开时也立即恢复子表勾选(避免视觉上丢失)
this.$nextTick(() => this.reapplySelectionsForPendingShop())
} else if (this.noticeTimer) {
try { clearInterval(this.noticeTimer) } catch (e) { /* noop */ }
this.noticeTimer = null
}
},
'confirmDialog.visible'(val) {
// 打开或关闭任一阶段都尝试恢复一次视觉勾选
this.$nextTick(() => this.reapplySelectionsForPendingShop())
},
'payDialog.visible'(val) {
this.$nextTick(() => this.reapplySelectionsForPendingShop())
},
'googleCodeDialog.visible'(val) {
this.$nextTick(() => this.reapplySelectionsForPendingShop())
}
},
beforeDestroy() {
@@ -322,54 +368,78 @@ export default {
this.noticeTimer = null
},
methods: {
// 获取所有商品分组(兼容:旧结构 this.groups新结构 this.shops 下的 shoppingCartInfoDtoList
getAllGroups() {
if (Array.isArray(this.groups) && this.groups.length) return this.groups
const flat = []
const shops = Array.isArray(this.shops) ? this.shops : []
shops.forEach(shop => {
const arr = Array.isArray(shop.shoppingCartInfoDtoList) ? shop.shoppingCartInfoDtoList : []
arr.forEach(g => flat.push(g))
})
return flat
// 选择框可选逻辑:下架(或删除)机器不可选择
isRowSelectable(row, index) {
return !(Number(row && row.del) === 1 || Number(row && row.state) === 1)
},
// 获取本地最大可租赁天数,默认 365
getRowMaxLeaseDaysLocal(row) {
const raw = row && row.maxLeaseDays
const n = Number(raw)
if (!Number.isFinite(n)) return 365
if (n < 1) return 1
if (n > 365) return 365
return Math.floor(n)
},
/**
* 精确裁剪数字到指定位数(不四舍五入)
* @param {number|string} value - 原始值
* @param {number} decimals - 小数位数默认2
* @returns {string}
*/
formatTrunc(value, decimals = 2) {
const num = Number(value)
if (!Number.isFinite(num)) return '0'
const d = Math.max(0, Number(decimals) || 0)
const factor = Math.pow(10, d)
const truncated = Math.trunc(num * factor) / factor
// 使用 toLocaleString 保留精度但不四舍五入,通过补零对齐位数
const str = String(truncated)
if (d === 0) return str
const [intPart, decPart = ''] = str.split('.')
const padded = decPart.padEnd(d, '0')
return `${intPart}.${padded}`
},
//获取支持的链和币种
async fetchChainAndListForSeller(shopId) {
this.loading = true;
const res = await getChainAndListForSeller({ id: shopId });
if (res && (res.code === 0 || res.code === 200) && res.data) {
this.options = this.toUpperOptions(res.data);
}
this.loading = false;
},
// 将级联选项的显示文本统一转为大写(不修改传给后端的 value
toUpperOptions(list) {
const arr = Array.isArray(list) ? list : []
return arr.map(item => {
const next = { ...item }
const labelSrc = (item && (item.label != null ? item.label : item.value)) || ''
next.label = String(labelSrc).toUpperCase()
if (Array.isArray(item && item.children)) {
next.children = this.toUpperOptions(item.children)
}
return next
})
},
// 获取所有商品分组(兼容保留,现直接返回空,因已移除中间商品层)
getAllGroups() { return [] },
// 店铺总价 = 累加其所有商品的 (机器单价 × 租赁天数)
computeShopTotal(shop) {
const groups = Array.isArray(shop && shop.shoppingCartInfoDtoList) ? shop.shoppingCartInfoDtoList : []
let total = 0
groups.forEach(g => {
const list = Array.isArray(g.productMachineDtoList) ? g.productMachineDtoList : []
list.forEach(m => {
const price = Number(m.price || 0)
const days = Number(m.leaseTime || 1)
total += price * days
})
})
return total
const list = Array.isArray(shop && shop.productMachineDtoList) ? shop.productMachineDtoList : []
return list.reduce((sum, m) => sum + Number(m.price || 0) * Number(m.leaseTime || 1), 0)
},
// 组装批量删除的请求体:数组 [{ machineId, productId }]
buildDeletePayload() {
const payload = []
const groups = this.getAllGroups()
const hasMachineSelection = this.selectedMachineCount > 0
if (hasMachineSelection) {
groups.forEach(g => {
const set = this.selectedMachinesMap[g.id]
if (!set || set.size === 0) return
const list = Array.isArray(g.productMachineDtoList) ? g.productMachineDtoList : []
list.forEach(m => {
if (set.has(m.id)) {
payload.push({ machineId: m.id, productId: g.productId })
}
})
})
} else if (this.selectedGroups && this.selectedGroups.length) {
// 未选机器但选了商品分组:删除该分组下的所有机器
this.selectedGroups.forEach(g => {
const list = Array.isArray(g.productMachineDtoList) ? g.productMachineDtoList : []
list.forEach(m => payload.push({ machineId: m.id, productId: g.productId }))
})
}
const shops = Array.isArray(this.shops) ? this.shops : []
shops.forEach(shop => {
const set = this.selectedMachinesMap[shop.id]
if (!set || set.size === 0) return
const list = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
list.forEach(m => { if (set.has(m.id)) payload.push({ machineId: m.id, productId: m.productId }) })
})
return payload.filter(it => it && (it.machineId != null))
},
async fetchAddOrders(orderInfoVoList, googleCode) {
@@ -377,6 +447,9 @@ export default {
// 按照新的传参结构:{code: 谷歌验证码, orderInfoVoList: [之前传参的数组]}
const payload = {
code: googleCode,
chain: this.selectedChain,
coin: this.selectedCoin,
price: this.selectedPrice,
orderInfoVoList: orderInfoVoList
}
const res = await addOrders(payload)
@@ -394,6 +467,32 @@ export default {
return { code: -1, msg: '网络异常' }
}
},
// 清除已下架/删除商品
async handleClearOffShelf() {
if (this.clearOffLoading) return
this.clearOffLoading = true
try {
const res = await deleteBatchGoodsForIsDelete()
if (res && Number(res.code) === 200) {
this.$message({ message: '已清除下架商品', type: 'success', showClose: true })
await this.fetchGetGoodsList()
} else {
this.$message({ message: (res && res.msg) || '清除失败', type: 'error', showClose: true })
}
} catch (e) {
this.$message({ message: '网络异常', type: 'error', showClose: true })
} finally {
this.clearOffLoading = false
}
},
/**
* 将文本安全地转为大写
* @param {string|number|null|undefined} value 原值
* @returns {string} 大写字符串
*/
toUpperText(value) {
return value == null ? '' : String(value).toUpperCase()
},
// 外层展开/收起同步到 expandedGroupKeys确保点击箭头生效
handleOuterExpandChange(row, expandedRows) {
// ElTable 会把当前已展开行数组给我们,这里只需要记录其 id 列表即可
@@ -412,6 +511,11 @@ export default {
? expandedRows.map(r => r && (r.id != null ? String(r.id) : undefined)).filter(Boolean)
: []
this.expandedShopKeys = keys
// 展开时恢复该店铺下已选中的机器勾选
const isExpanded = keys.includes(String(row.id))
if (isExpanded) {
this.$nextTick(() => this.applyInnerSelectionFromSet(row))
}
} catch (e) {
this.expandedShopKeys = []
}
@@ -439,21 +543,15 @@ export default {
}
// 如果是店铺结构
if (rawRows.length && rawRows[0].shoppingCartInfoDtoList) {
if (rawRows.length && rawRows[0].productMachineDtoList) {
const withShopKeys = rawRows.map((shop, sIdx) => ({
...shop,
id: shop.id != null ? String(shop.id) : `shop-${sIdx}`,
shoppingCartInfoDtoList: (shop.shoppingCartInfoDtoList || []).map((g, gIdx) => ({
...g,
id: g.id != null ? String(g.id) : (g.productId != null ? `p-${g.productId}` : `g-${sIdx}-${gIdx}`)
}))
id: shop.id != null ? String(shop.id) : `shop-${sIdx}`
}))
this.shops = withShopKeys
// 清空旧结构数据
this.groups = []
this.expandedGroupKeys = []
// 同步数量:统计所有店铺下所有机器数量
const count = withShopKeys.reduce((s, shop) => s + (shop.shoppingCartInfoDtoList || []).reduce((ss, g) => ss + ((Array.isArray(g.productMachineDtoList) ? g.productMachineDtoList.length : 0)), 0), 0)
const count = withShopKeys.reduce((s, shop) => s + ((Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList.length : 0)), 0)
window.dispatchEvent(new CustomEvent('cart-updated', { detail: { count } }))
return
}
@@ -481,71 +579,13 @@ export default {
}
},
handleGroupSelectionChange(selection) {
this.selectedGroups = selection
const selectedIdSet = new Set(selection.map(s => s.id))
// 1) 展开被选中的商品,收起取消选择的商品
const nextExpanded = new Set(this.expandedGroupKeys)
this.groups.forEach(g => {
if (selectedIdSet.has(g.id)) nextExpanded.add(g.id)
else nextExpanded.delete(g.id)
})
this.expandedGroupKeys = Array.from(nextExpanded)
// 2) 待子表渲染完成后,勾选/清空子表的机器
this.$nextTick(() => {
this.groups.forEach(g => {
const inner = this.$refs['innerTable-' + g.id]
const list = Array.isArray(g.productMachineDtoList) ? g.productMachineDtoList : []
const shouldSelectAll = selectedIdSet.has(g.id)
// 自定义 ref 可能还未渲染,做空判
if (inner && typeof inner.clearSelection === 'function') {
try { inner.clearSelection() } catch (e) { /* ignore clear selection error */ }
if (shouldSelectAll) {
list.forEach(m => { try { inner.toggleRowSelection(m, true) } catch (e) { /* ignore toggle selection error */ } })
this.$set(this.selectedMachinesMap, g.id, new Set(list.map(m => m.id)))
} else {
this.$set(this.selectedMachinesMap, g.id, new Set())
}
} else {
// 即使未渲染,也同步 map等渲染后由其它交互刷新
this.$set(this.selectedMachinesMap, g.id, shouldSelectAll ? new Set(list.map(m => m.id)) : new Set())
}
})
})
},
handleGroupSelectionChange() { /* 中间层已移除,保留空实现以兼容旧调用 */ },
// 选中某店铺下的商品分组:自动展开并全选其机器;取消则收起并清空
handleGroupSelectionChangeForShop(shop, selection) {
const table = this.$refs['productTable-' + (shop && shop.id)]
const groups = Array.isArray(shop && shop.shoppingCartInfoDtoList) ? shop.shoppingCartInfoDtoList : []
const selectedIdSet = new Set(Array.isArray(selection) ? selection.map(s => s && s.id) : [])
this.$nextTick(() => {
// 1) 展开/收起对应商品行
if (table && typeof table.toggleRowExpansion === 'function') {
try {
groups.forEach(g => {
const shouldExpand = selectedIdSet.has(g.id)
table.toggleRowExpansion(g, shouldExpand)
})
} catch (e) { /* ignore expand error */ }
}
// 2) 勾选/清空子表机器
groups.forEach(g => {
const list = Array.isArray(g.productMachineDtoList) ? g.productMachineDtoList : []
const shouldSelectAll = selectedIdSet.has(g.id)
// 先同步数据集,随后确保 UI 复选框勾选
this.$set(this.selectedMachinesMap, g.id, shouldSelectAll ? new Set(list.map(m => m && m.id)) : new Set())
this.applyInnerSelection(g, shouldSelectAll)
})
})
},
handleGroupSelectionChangeForShop() { /* 已移除商品层,保留空实现 */ },
// 尝试对子表应用选择,若未渲染则重试几次
applyInnerSelection(group, shouldSelectAll, retry = 0) {
const inner = this.$refs['innerTable-' + group.id]
const list = Array.isArray(group.productMachineDtoList) ? group.productMachineDtoList : []
applyInnerSelection(shop, shouldSelectAll, retry = 0) {
const inner = this.$refs['innerTable-' + shop.id]
const list = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
if (inner && typeof inner.clearSelection === 'function') {
try { inner.clearSelection() } catch (e) { /* ignore */ }
if (shouldSelectAll) {
@@ -554,11 +594,40 @@ export default {
return
}
if (retry >= 5) return
this.$nextTick(() => this.applyInnerSelection(group, shouldSelectAll, retry + 1))
this.$nextTick(() => this.applyInnerSelection(shop, shouldSelectAll, retry + 1))
},
handleInnerSelectionChange(group, selections) {
const selIds = new Set(selections.map(s => s.id))
this.$set(this.selectedMachinesMap, group.id, selIds)
/**
* 根据已保存的 selectedMachinesMap 重新应用某店铺子表的勾选(仅勾选之前勾选的机器)
*/
applyInnerSelectionFromSet(shop, retry = 0) {
if (!shop) return
const inner = this.$refs['innerTable-' + shop.id]
const list = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
const set = this.selectedMachinesMap[shop.id]
if (inner && typeof inner.clearSelection === 'function') {
try { inner.clearSelection() } catch (e) { /* ignore */ }
if (set && set.size) {
list.forEach(m => {
if (set.has(m.id)) {
try { inner.toggleRowSelection(m, true) } catch (e) { /* ignore */ }
}
})
}
return
}
if (retry >= 5) return
this.$nextTick(() => this.applyInnerSelectionFromSet(shop, retry + 1))
},
/**
* 在结算流程被用户中断/失败返回时,恢复列表中的勾选 UI 状态
*/
reapplySelectionsForPendingShop() {
const shop = this.pendingCheckoutShop && this.pendingCheckoutShop.shop
if (shop) this.applyInnerSelectionFromSet(shop)
},
handleShopInnerSelectionChange(shop, selections) {
const selIds = new Set((selections || []).map(s => s.id))
this.$set(this.selectedMachinesMap, shop.id, selIds)
},
toggleSelectAll() {
const table = this.$refs.outerTable
@@ -582,35 +651,60 @@ export default {
const groups = Array.isArray(shop && shop.shoppingCartInfoDtoList) ? shop.shoppingCartInfoDtoList : []
return groups.reduce((s, g) => s + ((Array.isArray(g.productMachineDtoList) ? g.productMachineDtoList.length : 0)), 0)
},
/**
* 结算当前店铺:
* - 如果店铺未展开,则默认包含该店铺下的全部机器进入下一步流程;
* - 如果店铺已展开,则仅在勾选了机器的情况下进入下一步,否则给出提示。
* @param {Record<string, any>} shop 当前店铺行
*/
async handleCheckoutShop(shop) {
if (!shop) return
const groups = Array.isArray(shop.shoppingCartInfoDtoList) ? shop.shoppingCartInfoDtoList : []
// 仅根据当前店铺下“已选中的机器”构建下单参数:[{ leaseTime, machineId, productId, shopId }]
const shopId = shop.id
const machines = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
if (machines.length === 0) {
this.$message({ message: '该店铺暂无可结算的机器', type: 'warning', showClose: true })
return
}
// 是否已展开:仅在已展开时校验是否有选中机器
const isExpanded = Array.isArray(this.expandedShopKeys) && this.expandedShopKeys.includes(String(shopId))
const payload = []
groups.forEach(g => {
const list = Array.isArray(g.productMachineDtoList) ? g.productMachineDtoList : []
const selectedSet = this.selectedMachinesMap[g.id]
if (!selectedSet || selectedSet.size === 0) return
list.forEach(m => {
if (isExpanded) {
const selectedSet = this.selectedMachinesMap[shopId]
if (!selectedSet || selectedSet.size === 0) {
this.$message({
message: '请先在该店铺下勾选要结算的机器',
type: 'warning',
showClose: true
})
return
}
machines.forEach(m => {
if (selectedSet.has(m.id)) {
payload.push({
leaseTime: Number(m.leaseTime || 1),
machineId: m.id,
productId: g.productId,
shopId: shop.id
productId: m.productId,
shopId: shopId
})
}
})
})
if (!payload.length) {
this.$message({
message: '请先在该店铺下勾选要结算的机器',
type: 'warning',
showClose: true
} else {
// 未展开:默认使用全部机器
machines.forEach(m => {
payload.push({
leaseTime: Number(m.leaseTime || 1),
machineId: m.id,
productId: m.productId,
shopId: shopId
})
})
return
}
// 先根据当前店铺请求可选支付链/币种
await this.fetchChainAndListForSeller(shopId)
// 保存待结算的店铺信息,弹出购物须知弹窗
this.pendingCheckoutShop = { shop, payload }
this.noticeDialog.visible = true
@@ -625,20 +719,23 @@ export default {
this.creatingOrder = true
try {
const res = await this.fetchAddOrders(payload, googleCode)
if (!res || Number(res.code) !== 200) {
return
let ok = false
if (res && Number(res.code) === 200) {
const dataStr = String(res.data || '')
ok = dataStr.includes('成功')
}
// 检查返回数据是否包含"成功"字样
const dataStr = String(res.data || '')
if (dataStr.includes('成功')) {
if (ok) {
// 结算成功后重新获取购物车数据,更新购物车数量
await this.fetchGetGoodsList()
this.successDialog.visible = true
}
} else {
// 失败或未成功,恢复之前的勾选 UI
this.reapplySelectionsForPendingShop()
}
} catch (e) {
console.log('网络错误,请重试')
// 异常也恢复勾选
this.reapplySelectionsForPendingShop()
} finally {
this.creatingOrder = false
this.pendingCheckoutShop = null
@@ -648,15 +745,15 @@ export default {
// 若有选中机器,则以选中机器为准;否则当做选中整个商品分组
let items = []
if (this.selectedMachineCount) {
this.groups.forEach(g => {
const set = this.selectedMachinesMap[g.id]
(this.shops || []).forEach(shop => {
const set = this.selectedMachinesMap[shop.id]
if (!set || set.size === 0) return
const list = Array.isArray(g.productMachineDtoList) ? g.productMachineDtoList : []
const list = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
list.forEach(m => {
if (set.has(m.id)) {
items.push({
product: g.name || '',
coin: g.coin || '',
product: shop.name || '',
coin: this.toUpperText(m.coin),
machineId: m.id,
user: m.user,
miner: m.miner,
@@ -665,18 +762,6 @@ export default {
}
})
})
} else if (this.selectedGroups.length) {
items = this.selectedGroups.flatMap(g => {
const list = Array.isArray(g.productMachineDtoList) ? g.productMachineDtoList : []
return list.map(m => ({
product: g.name || '',
coin: g.coin || '',
machineId: m.id,
user: m.user,
miner: m.miner,
price: Number(m.price || 0)
}))
})
} else {
this.$message({
message: '请先选择商品或机器',
@@ -766,7 +851,48 @@ export default {
return
}
this.noticeDialog.visible = false
// 用户确认须知后,弹出确认结算弹窗
// 进入下一步前,确保一次恢复(避免刚关闭后消失)
this.$nextTick(() => this.reapplySelectionsForPendingShop())
// 用户确认须知后,先选择支付链/币种
this.openPaySelectDialog()
},
openPaySelectDialog() {
this.payDialog.visible = true
// 打开支付选择时再恢复一次(背景表格常被重渲染)
this.$nextTick(() => this.reapplySelectionsForPendingShop())
if (!Array.isArray(this.options) || !this.options.length) {
this.fetchChainAndListForSeller()
}
},
async handlePayConfirm() {
const val = this.payDialog.value || []
if (!Array.isArray(val) || val.length < 2) {
this.$message.warning('请选择支付链和币种')
return
}
// 关闭前先恢复一次(避免选择时表格刷新导致视觉丢勾)
this.$nextTick(() => this.reapplySelectionsForPendingShop())
this.selectedChain = val[0]
this.selectedCoin = val[1]
// USDT 不需要请求实时币价,也不需要换算
if (String(this.selectedCoin).toUpperCase() === 'USDT') {
this.selectedPrice = 0
} else {
this.payDialog.loading = true
try {
const res = await getCoinPrice({ coin: this.selectedCoin })
const price = (res && (res.data && (res.data.price || res.data))) || res.price || 0
this.selectedPrice = Number(price || 0)
} catch (e) {
this.selectedPrice = 0
} finally {
this.payDialog.loading = false
}
}
this.payDialog.visible = false
// 关闭后也再恢复一次
this.$nextTick(() => this.reapplySelectionsForPendingShop())
// 选择完成,进入确认明细
this.showConfirmDialog()
},
// 显示确认结算弹窗
@@ -774,32 +900,29 @@ export default {
if (!this.pendingCheckoutShop) return
const { shop, payload } = this.pendingCheckoutShop
const groups = Array.isArray(shop.shoppingCartInfoDtoList) ? shop.shoppingCartInfoDtoList : []
// 打开确认前,确保一次恢复
this.$nextTick(() => this.reapplySelectionsForPendingShop())
const list = Array.isArray(shop.productMachineDtoList) ? shop.productMachineDtoList : []
// 通过 payload 中 machineId 来还原确认明细(仅选中的机器)
const selectedByGroupId = new Map()
payload.forEach(p => {
const arr = selectedByGroupId.get(p.productId) || []
arr.push(p.machineId)
selectedByGroupId.set(p.productId, arr)
})
const selectedIds = new Set(payload.map(p => p.machineId))
const items = []
groups.forEach(g => {
const list = Array.isArray(g.productMachineDtoList) ? g.productMachineDtoList : []
const selectedIds = new Set(selectedByGroupId.get(g.productId) || [])
list.forEach(m => {
if (selectedIds.has(m.id)) {
items.push({
product: g.name || '',
coin: g.coin || '',
machineId: m.id,
user: m.user,
miner: m.miner,
price: Number(m.price || 0) * Number(m.leaseTime || 1)
})
}
})
list.forEach(m => {
if (selectedIds.has(m.id)) {
const usdtPrice = Number(m.price || 0) * Number(m.leaseTime || 1)
const isUSDT = String(this.selectedCoin).toUpperCase() === 'USDT'
const displayPrice = !isUSDT && this.selectedPrice > 0 ? (usdtPrice / this.selectedPrice) : usdtPrice
items.push({
product: shop.name || '',
coin: this.toUpperText(m.coin),
machineId: m.id,
user: m.user,
miner: m.miner,
price: Number(displayPrice || 0)
})
}
})
this.confirmDialog.items = items
@@ -857,6 +980,8 @@ export default {
this.googleCodeDialog.error = ''
this.googleCodeDialog.loading = false
// 清除待结算信息
// 先恢复勾选,再清空
this.reapplySelectionsForPendingShop()
this.pendingCheckoutShop = null
},
// 处理租赁天数输入变化
@@ -927,6 +1052,9 @@ export default {
if (!table || !product) return false
const selectedRows = table.selection || []
return Array.isArray(selectedRows) && selectedRows.some(r => r && r.id === product.id)
},
formatPayTooltip(item) {
return `${item.payChain} - ${this.toUpperText(item.payCoin)}`
}
}
}

View File

@@ -134,7 +134,8 @@ export default {
console.log(res)
if (res && res.code === 200) {
console.log(res.data, 'res.rows');
const list = Array.isArray(res.data) ? res.data : []
this.paymentMethodList = res.data.payConfigList || []
const list =res.data.machineRangeInfoList || []
const withKeys = list.map((group, idx) => {
const fallbackId = `grp-${idx}`
const groupId = group.id || group.onlyKey || (group.productMachineRangeGroupDto && group.productMachineRangeGroupDto.id)
@@ -312,6 +313,12 @@ export default {
// 手动切换选择自定义checkbox与 selectedMap 同步),并维护每行的 _selected 状态
handleManualSelect(parentRow, row, checked) {
// 禁用:已售出或售出中的机器不可选择
if (row && (row.saleState === 1 || row.saleState === 2)) {
this.$message.warning('该机器已售出或售出中,无法选择')
this.$set(row, '_selected', false)
return
}
const key = parentRow.id
const list = (this.selectedMap[key] && [...this.selectedMap[key]]) || []
const idx = list.findIndex(it => it && it.id === row.id)
@@ -322,8 +329,9 @@ export default {
},
// 为子表中已在购物车的行添加只读样式,并阻止点击取消
getInnerRowClass() {
return ''
handleGetInnerRowClass({ row }) {
if (!row) return ''
return (row.saleState === 1 || row.saleState === 2) ? 'sold-row' : ''
},
/**
@@ -410,11 +418,15 @@ export default {
handleOpenAddToCartDialog() {
// 扫描当前所有系列下被勾选的机器
const groups = Array.isArray(this.productListData) ? this.productListData : []
const picked = groups.flatMap(g => Array.isArray(g.productMachines) ? g.productMachines.filter(m => !!m && !!m._selected) : [])
const pickedAll = groups.flatMap(g => Array.isArray(g.productMachines) ? g.productMachines.filter(m => !!m && !!m._selected) : [])
const picked = pickedAll.filter(m => m && (m.saleState === 0 || m.saleState === undefined || m.saleState === null))
if (!picked.length) {
this.$message.warning('请先勾选至少一台矿机')
return
}
if (picked.length < pickedAll.length) {
this.$message.warning('部分机器已售出或售出中,已自动为您排除')
}
// 使用弹窗中的固定快照,避免后续清空勾选影响弹窗显示
this.confirmAddDialog.items = picked.slice()
this.confirmAddDialog.visible = true

View File

@@ -7,10 +7,35 @@
<div v-else-if="product" class="detail-container">
<h2 style="margin: 10px; text-align: left;margin-top: 28px;">商品详情-选择矿机</h2>
<section class="pay-methods" aria-label="支付方式">
<div class="pay-label" tabindex="0" aria-label="支付方式标签">支付方式</div>
<ul class="pay-list" role="list" aria-label="支付方式列表">
<li
v-for="(item, index) in paymentMethodList"
:key="index"
class="pay-item"
:aria-label="`支付方式 ${item.payChain}`"
>
<el-tooltip :content="formatPayTooltip(item)" placement="top" :open-delay="80">
<img
class="pay-icon"
:src="item.payCoinImage"
:alt="`${item.payChain} 支付`"
:title="item.payChain"
tabindex="0"
role="img"
@keydown.enter.prevent="handlePayIconKeyDown(item)"
@keydown.space.prevent="handlePayIconKeyDown(item)"
/>
</el-tooltip>
</li>
</ul>
</section>
<section class="productList">
<!-- 产品列表可展开 -->
<el-table
ref="seriesTable"
class="series-table"
:data="productListData"
row-key="id"
:expand-row-keys="expandedRowKeys"
@@ -24,57 +49,78 @@
<el-table-column type="expand" width="46">
<template #default="outer">
<!-- 子表格展开后显示该行的多个可选条目来自 productMachines -->
<el-table :data="outer.row.productMachines" size="small" style="width: 100%" :show-header="true" :ref="'innerTable-' + outer.row.id" :row-key="'id'" :reserve-selection="false" :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table :data="outer.row.productMachines" size="small" style="width: 100%" :show-header="true" :ref="'innerTable-' + outer.row.id" :row-key="'id'" :reserve-selection="false" :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }" :row-class-name="handleGetInnerRowClass">
<el-table-column width="46">
<template #default="scope">
<el-checkbox v-model="scope.row._selected" @change="checked => handleManualSelect(outer.row, scope.row, checked)" />
<el-checkbox
v-model="scope.row._selected"
:disabled="scope.row.saleState === 1 || scope.row.saleState === 2"
:title="(scope.row.saleState === 1 || scope.row.saleState === 2) ? '该机器已售出或售出中,无法选择' : ''"
@change="checked => handleManualSelect(outer.row, scope.row, checked)"
/>
</template>
</el-table-column>
<!-- 调整列顺序使理论算力实际算力与上层对齐并且指定相同宽度 -->
<el-table-column prop="theoryPower" label="理论算力" width="280" header-align="left" align="left" />
<el-table-column label="实际算力" width="230" header-align="left" align="left">
<!-- 列宽精简避免横向滚动 -->
<el-table-column prop="theoryPower" label="理论算力" min-width="160" header-align="left" align="left" show-overflow-tooltip>
<template #default="scope">{{ scope.row.theoryPower }} {{ scope.row.unit }}</template>
</el-table-column>
<el-table-column label="实际算力" min-width="160" header-align="left" align="left" show-overflow-tooltip>
<template #default="scope">{{ scope.row.computingPower }} {{ scope.row.unit }}</template>
</el-table-column>
<el-table-column prop="powerDissipation" label="功耗(kw/h)" width="230" header-align="left" align="left" />
<el-table-column prop="algorithm" label="算法" width="180" header-align="left" align="left" />
<el-table-column prop="powerDissipation" label="功耗(kw/h)" min-width="140" header-align="left" align="left" />
<el-table-column prop="algorithm" label="算法" min-width="120" header-align="left" align="left" />
<el-table-column prop="theoryIncome" width="220" header-align="left" align="left">
<template #header>单机理论收入(每日) <span v-show="outer.row.coin">{{ outer.row.coin || '' }}</span></template>
<el-table-column prop="theoryIncome" min-width="160" header-align="left" align="left" show-overflow-tooltip>
<template #header>单机理论收入(每日) <span v-show="outer.row.productMachines[0].coin">{{outer.row.productMachines[0].coin.toUpperCase() }}</span></template>
</el-table-column>
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)" width="240" header-align="left" align="left" />
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)" min-width="170" header-align="left" align="left" />
<!-- 矿机型号置于最后不影响上层对齐 -->
<el-table-column prop="type" label="矿机型号" header-align="left" align="left" />
<el-table-column label="租赁天数(天)" width="200" header-align="left" align="left">
<el-table-column prop="type" label="矿机型号" header-align="left" align="left" min-width="120" />
<el-table-column label="最大可租赁(天)" min-width="140" header-align="left" align="left">
<template #default="scope">{{ getRowMaxLeaseDays(scope.row) }}</template>
</el-table-column>
<el-table-column label="租赁天数(天)" min-width="150" header-align="left" align="left">
<template #default="scope">
<el-input-number
v-model="scope.row.leaseTime"
:min="1"
:max="365"
:max="getRowMaxLeaseDays(scope.row)"
:step="1"
:precision="0"
size="mini"
:disabled="scope.row.saleState === 1 || scope.row.saleState === 2"
controls-position="right"
@change="val => handleLeaseDaysChange(scope.row, val)"
/>
</template>
</el-table-column>
<el-table-column prop="saleState" label="售出状态" header-align="left" align="left" min-width="110">
<template #default="scope">
<el-tag :type="scope.row.saleState === 0 ? 'info' : (scope.row.saleState === 1 ? 'danger' : 'warning')">
{{ scope.row.saleState === 0 ? '未售出' : (scope.row.saleState === 1 ? '已售出' : '售出中') }}
</el-tag>
</template>
</el-table-column>
</el-table>
</template>
</el-table-column>
<!-- 外层与内层列宽保持一致160/160/160/120/160 -->
<el-table-column label="价格" header-align="left" align="left">
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.price }}</template>
<!-- 外层列宽同样收紧避免横向滚动 -->
<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>
</el-table-column>
<el-table-column label="理论算力范围" width="280" header-align="left" align="left">
<el-table-column label="理论算力范围" min-width="220" header-align="left" align="left" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.theoryPowerRange }}</template>
</el-table-column>
<el-table-column label="实际算力范围" width="230" header-align="left" align="left">
<el-table-column label="实际算力范围" min-width="200" header-align="left" align="left" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.computingPowerRange }}</template>
</el-table-column>
<el-table-column label="功耗范围" width="230" header-align="left" align="left">
<el-table-column label="功耗范围" min-width="160" header-align="left" align="left">
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.powerRange }}</template>
</el-table-column>
<el-table-column label="数量" width="180" header-align="left" align="left">
<el-table-column label="数量" min-width="100" header-align="left" align="left">
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.number }}</template>
</el-table-column>
@@ -125,7 +171,66 @@ import Index from './index'
export default {
name: 'ProductDetail',
mixins: [Index],
methods: {
/**
* 获取行的最大可租赁天数
* 规则:优先 row.maxLeaseDays否则回退 365保证区间 [1, 365]
*/
getRowMaxLeaseDays(row) {
const raw = (row && (row.maxLeaseDays || row.maxLeaseDay || row.max_lease_days)) || 365
const n = Number(raw)
if (!Number.isFinite(n)) return 365
if (n < 1) return 1
if (n > 365) return 365
return Math.floor(n)
},
/**
* 限制并校验租赁天数:区间 [1, max],并取整
*/
handleLeaseDaysChange(row, value) {
const max = this.getRowMaxLeaseDays(row)
let v = Number(value)
if (!Number.isFinite(v)) v = 1
if (v < 1) v = 1
if (v > max) v = max
v = Math.floor(v)
this.$set(row, 'leaseTime', v)
},
/**
* 组合 tooltip 展示内容payChain - payCoin
* @param {Object} item - 支付方式对象
* @returns {string}
*/
formatPayTooltip(item) {
try {
if (!item) return ''
const chain = (item.payChain || '').toString().trim()
const coin = (item.payCoin || '').toString().trim()
if (chain && coin) return `${chain} - ${coin}`
return chain || coin || ''
} catch (err) {
// eslint-disable-next-line no-console
console.error('formatPayTooltip error:', err)
return ''
}
},
/**
* 支付方式图标键盘可达性处理
* @param {Object} item - 支付方式对象
*/
handlePayIconKeyDown(item) {
// 仅用于可达性保障与监控打点,避免无事件时的报错
try {
if (!item) return;
// 简单输出,后续可接入埋点系统
// eslint-disable-next-line no-console
console.debug('[pay-icon-keydown]', item.payChain);
} catch (err) {
// eslint-disable-next-line no-console
console.error('handlePayIconKeyDown error:', err);
}
}
}
}
</script>
@@ -146,6 +251,11 @@ export default {
border-color: #dcdfe6;
}
/* 内层表:已售出/售出中行禁用态高亮 */
:deep(.sold-row) {
background: #fff5f5;
}
.loading {
text-align: center;
@@ -240,10 +350,89 @@ export default {
color: #e74c3c;
}
/* 外层系列行:整行可点击的视觉提示 */
/* 支付方式区域(视觉更友好 + 可达性) */
.pay-methods {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
margin: 8px 10px 16px 10px;
background: #f8fafc;
border: 1px solid #eef2f7;
border-radius: 8px;
}
.pay-label {
color: #34495e;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
}
.pay-list {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px 12px;
margin: 0;
padding: 0;
list-style: none;
}
.pay-item {
display: inline-flex;
align-items: center;
}
.pay-icon {
width: 24px;
height: 24px;
display: block;
border-radius: 4px;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.pay-icon:hover {
transform: translateY(-1px);
}
.pay-icon:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.2);
}
/* 外层系列行:整行可点击 + 视觉增强 */
:deep(.series-clickable-row) {
cursor: pointer;
}
:deep(.series-clickable-row > td) {
background: #f9fbff; /* 淡蓝背景区分层级 */
padding-top: 14px; /* 增加行高 */
padding-bottom: 14px;
border-bottom: 1px solid #eef2f7; /* 更柔和的分割线 */
}
:deep(.series-clickable-row:hover > td) {
background: #f0f6ff; /* 悬浮更亮一些 */
}
/* 展开的内层区域容器样式:与外层形成卡片层级 */
:deep(.el-table__expanded-cell) {
background: #ffffff;
}
:deep(.el-table__expanded-cell .el-table) {
background: #fff;
border: 1px solid #eef2f7;
border-radius: 8px;
/* 优化:表格内容自适应宽度,减少横向滚动 */
width: 100%;
}
/* 列表标题上色:与上方支付方式背景做区分 */
/* 仅给最外层系列表头上色;内层子表不受影响 */
.series-table :deep(.el-table__header th) {
background: #f9fbff;
color: #34495e;
font-weight: 600;
}
.quantity-section {
display: flex;
@@ -251,6 +440,9 @@ export default {
gap: 16px;
}
/* 租赁天数单元格:提示 + 输入并排 */
/* 已移除行内提示样式,保留空位便于将来复用(不影响现有布局) */
.quantity-label {
font-size: 16px;
color: #666;

View File

@@ -59,9 +59,11 @@
<h4>商品: {{ product.name }}</h4>
<p style="font-size: 16px;margin-top: 10px;font-weight: bold;">算法: {{ product.algorithm }}</p>
<div class="product-footer">
<span class="product-price">价格: {{ formatPriceRange(product.priceRange) }}</span> <span style="color: #999;font-size: 12px;">USDT</span>
<div class="price-wrap">
<span class="product-price">价格: {{ formatPriceRange(product.priceRange) }}</span>
<span class="unit">USDT</span>
</div>
<span class="product-sold" aria-label="已售数量">已售{{ product && product.saleNumber != null ? product.saleNumber : 0 }}</span>
</div>
</div>
</div>
@@ -176,9 +178,9 @@ export default {
width: 100%;
}
.product-footer {
// display: flex;
// justify-content: space-between;
// align-items: center;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.product-price {
@@ -189,6 +191,9 @@ export default {
text-overflow: ellipsis;
white-space: nowrap;
}
.price-wrap { display: inline-flex; align-items: baseline; gap: 6px; }
.unit { color: #999; font-size: 12px; }
.product-sold { color: #64748b; font-size: 12px; }
.add-cart-btn {
background: #42b983;
color: #fff;