每周更新

This commit is contained in:
2025-12-19 15:40:53 +08:00
parent 5945ab5588
commit ee5ed42a0d
26 changed files with 1177 additions and 213 deletions

View File

@@ -108,5 +108,17 @@ export function getPurchasedItems(data) {
})
}
//已购商品详情
export function getPurchasedInfoV2(data) {
return request({
url: `/lease/v2/order/info/getPurchasedInfo`,
method: 'post',
data
})
}

View File

@@ -154,6 +154,15 @@ export function balanceWithdrawListV2(data) {
})
}
// 修改店铺钱包配置 V2
export function updateShopConfigV2(data) {
return request({
url: `/lease/v2/shop/updateShopConfigV2`,
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

@@ -100,9 +100,13 @@ export const accountRoutes = [
}
},
{
// 兼容旧入口:收款记录 -> 卖家资金流水收款tab
path: 'receipt-record',
name: 'accountReceiptRecord',
component: () => import('../views/account/receiptRecord.vue'),
redirect: (to) => ({
path: '/account/seller-funds-flow',
query: { ...(to && to.query ? to.query : {}), tab: 'receipt' }
}),
meta: {
title: '收款记录',
description: '卖家收款流水记录',
@@ -110,15 +114,29 @@ export const accountRoutes = [
}
},
{
// 兼容旧入口:提现记录 -> 卖家资金流水提现tab
path: 'withdraw-record',
name: 'accountWithdrawRecord',
component: () => import('../views/account/withdrawRecord.vue'),
redirect: (to) => ({
path: '/account/seller-funds-flow',
query: { ...(to && to.query ? to.query : {}), tab: 'withdraw' }
}),
meta: {
title: '提现记录',
description: '卖家提现流水记录',
allAuthority: ['all']
}
},
{
path: 'seller-funds-flow',
name: 'accountSellerFundsFlow',
component: () => import('../views/account/sellerFundsFlow.vue'),
meta: {
title: '资金流水',
description: '卖家收款/提现记录切换查看',
allAuthority: ['all']
}
},
{
path: 'shop-new',
name: 'accountShopNew',
@@ -169,23 +187,23 @@ export const accountRoutes = [
allAuthority: ['all']
}
},
{
path: 'purchased',
name: 'accountPurchased',
component: () => import('../views/account/purchased.vue'),
meta: {
title: '已购商品',
description: '查看已购买的商品列表',
allAuthority: ['all']
}
},
{
path: 'purchased-machine-config',
name: 'accountPurchasedMachineConfig',
component: () => import('../views/account/purchasedMachineConfig.vue'),
meta: {
title: '已购矿机配置',
description: '查看已购买矿机的配置信息',
title: '已购商品',
description: '查看已购买商品的配置信息',
allAuthority: ['all']
}
},
{
path: 'purchased-machine-detail/:id',
name: 'purchasedMachineDetail',
component: () => import('../views/account/purchasedMachineDetail.vue'),
meta: {
title: '已购商品详情',
description: '查看已购买商品的详细信息',
allAuthority: ['all']
}
},
@@ -199,16 +217,6 @@ export const accountRoutes = [
allAuthority: ['all']
}
},
{
path: 'purchased-detail/:id',
name: 'PurchasedDetail',
component: () => import('../views/account/purchasedDetail.vue'),
meta: {
title: '已购商品详情',
description: '查看已购商品详细信息',
allAuthority: ['all']
}
},
{
path: 'orders',
name: 'accountOrders',

View File

@@ -0,0 +1,101 @@
/**
* RSA 加密工具
* 使用 jsencrypt 库进行 RSA 公钥加密
*/
// 导入 jsencrypt
import JSEncrypt from 'jsencrypt'
// RSA 公钥Base64 格式,不带 BEGIN/END 标记)
const RSA_PUBLIC_KEY_BASE64 = `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsQVIKYozXCfnXUw8+omYLdcdL1pTzmQh35YPsvn22wM4SQJKvMmXmcS6bI5Bu+5zCjL0F56DzfKz0BNZEwb46UshUOO+KFBUr8CxjYE8NOgIsoe5FUn57O6er9/KySaWlkpGZX49K+l3e90R+dFUEfRE/ijYpeZWkLRwcgWZ+2u6HGpl9h/eF6XD0aW9asDjdAbxUQ48TlaWgfP+OHC+Zy2GKGQG16EcDMczrN6a2HbFnwRIUKrFP67UqyRq11BTUziOhXLY8J0MFuwXUk2OY4VpqjrJjHHjlHYADjIL/5K4Io2AhRU9+QSsKFR2wGxi4e8vw2IXDzscrDuah/7YSwIDAQAB`
// RSA 公钥PEM 格式)
const RSA_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
${RSA_PUBLIC_KEY_BASE64}
-----END PUBLIC KEY-----`
/**
* 获取 JSEncrypt 构造函数
* @returns {Function} JSEncrypt 构造函数
*/
function getJSEncrypt() {
// 优先使用已导入的 JSEncrypt
if (JSEncrypt) {
return JSEncrypt
}
// 尝试从全局获取(可能通过 CDN 引入)
if (typeof window !== 'undefined' && window.JSEncrypt) {
return window.JSEncrypt
}
return null
}
/**
* RSA 加密函数(同步版本)
* @param {string} plainText - 要加密的明文
* @returns {string|null} 加密后的密文Base64 编码),失败返回 null
*/
export function rsaEncryptSync(plainText) {
if (!plainText || typeof plainText !== 'string') {
console.error('RSA 加密:输入必须是非空字符串')
return null
}
try {
const Encrypt = getJSEncrypt()
if (!Encrypt) {
console.error('JSEncrypt 未加载')
return null
}
const encrypt = new Encrypt()
encrypt.setPublicKey(RSA_PUBLIC_KEY)
const encrypted = encrypt.encrypt(plainText)
if (!encrypted) {
console.error('RSA 加密失败:返回值为空')
return null
}
return encrypted
} catch (error) {
console.error('RSA 加密异常:', error)
return null
}
}
/**
* RSA 加密函数(异步版本,兼容同步调用)
* @param {string} plainText - 要加密的明文
* @returns {Promise<string|null>} 加密后的密文Base64 编码),失败返回 null
*/
export async function rsaEncrypt(plainText) {
if (!plainText || typeof plainText !== 'string') {
console.error('RSA 加密:输入必须是非空字符串')
return null
}
try {
const Encrypt = getJSEncrypt()
if (!Encrypt) {
console.error('JSEncrypt 未加载')
return null
}
const encrypt = new Encrypt()
encrypt.setPublicKey(RSA_PUBLIC_KEY)
const encrypted = encrypt.encrypt(plainText)
if (!encrypted) {
console.error('RSA 加密失败:返回值为空')
return null
}
return encrypted
} catch (error) {
console.error('RSA 加密异常:', error)
return null
}
}

View File

@@ -63,8 +63,7 @@ export default {
// 买家侧导航
buyerLinks: [
{ label: '我的钱包', to: '/account/wallet' },
{ label: '已购商品', to: '/account/purchased' },
{ label: '已购矿机配置', to: '/account/purchased-machine-config' },
{ label: '已购商品', to: '/account/purchased-machine-config' },
{ label: '订单列表', to: '/account/orders' },
// { label: '充值记录', to: '/account/rechargeRecord' },
// { label: '提现记录', to: '/account/withdrawalHistory' },
@@ -76,8 +75,7 @@ export default {
{ label: '我的店铺', to: '/account/shops' },
{ label: '商品列表', to: '/account/products' },
{ label: '已售出订单', to: '/account/seller-orders' },
{ label: '收款记录', to: '/account/receipt-record' },
{ label: '提现记录', to: '/account/withdraw-record' },
{ label: '资金流水', to: '/account/seller-funds-flow' },
],
}
@@ -158,9 +156,8 @@ export default {
// 买家前缀优先匹配,确保"已购详情"等页面归属买家侧
const buyerPrefixes = [
'/account/wallet',
'/account/purchased',
'/account/purchased-detail',
'/account/purchased-machine-config',
'/account/purchased-machine-detail',
'/account/orders',
'/account/funds-flow'
]
@@ -172,6 +169,8 @@ export default {
'/account/product-detail',
'/account/product-machine-add',
'/account/seller-orders',
'/account/seller-funds-flow',
// 兼容旧路径(会被路由重定向到 seller-funds-flow
'/account/receipt-record',
'/account/withdraw-record',
'/account/shop-config'
@@ -208,16 +207,18 @@ export default {
const map = {
'/account/seller-orders': ['/account/seller-orders'],
'/account/products': ['/account/products', '/account/product-detail'],
'/account/purchased': ['/account/purchased', '/account/purchased-detail'],
'/account/purchased-machine-config': ['/account/purchased-machine-config']
'/account/purchased-machine-config': ['/account/purchased-machine-config', '/account/purchased-machine-detail']
}
const prefixes = map[pathLike]
if (Array.isArray(prefixes)) {
return prefixes.some(p => {
// 精确匹配
if (current === p) return true
// 对于详情页路径,使用前缀匹配(支持 /account/purchased-detail/:id
if (p === '/account/purchased-detail' || p === '/account/product-detail') {
// 对于详情页路径,使用前缀匹配(支持 /account/product-detail/:id
if (p === '/account/product-detail') {
return current.indexOf(p) === 0
}
if (p === '/account/purchased-machine-detail') {
return current.indexOf(p) === 0
}
// 列表页已经在上面精确匹配了,这里不需要额外处理
@@ -225,8 +226,6 @@ export default {
})
}
// 不在 map 中的路径,精确匹配
// 特别注意:避免 /account/purchased 匹配到 /account/purchased-machine-config
// 使用精确匹配而不是前缀匹配
return current === pathLike
}
},

View File

@@ -13,13 +13,17 @@
请在本页点击 <b>钱包绑定</b>配置自己的收款地址支持不同链与币种
</li>
<li>
<b>商品</b>完成钱包绑定后即可在我的店铺页面 <b>创建商品</b>
商品可按 <b>币种</b> 进行分类管理创建的商品会在商城对买家展示
商品可理解为不同算法币种的机器集合分类
</li>
<li>
<b>出售机器</b>创建商品后请进入 <b>商品列表</b> 为该商品 <b>添加出售机器明细</b>
必须添加出售机器否则买家无法下单买家点击某个商品后会看到该商品下的机器明细并进行选购
<b>创建商品</b>完成钱包绑定后即可在我的店铺页面 点击<b>新增商品</b>按钮
<ul class="guide-substeps">
<li>
<b>ASIC 商品创建</b>选择矿机种类为 ASIC填写页面商品信息后创建商品可按 <b>币种</b> 进行分类管理创建的商品会在商城对买家展示
商品可理解为不同算法币种的机器集合分类
</li>
<li>
<b>GPU 商品创建</b>选择矿机种类为 GPU查看页面注意事项并下载对应客户端启动后读取自动创建创建完成请进入 <b>商品列表</b> 为该商品手动配置售价等相关信息并上架
</li>
</ul>
</li>
</ol>
<div class="guide-note">提示建议先创建店铺 完成钱包绑定 创建商品的顺序避免漏配导致无法收款或无法下单</div>
@@ -245,6 +249,7 @@ import { getMyShop, updateShop, deleteShop, queryShop, closeShop ,updateShopConf
import { coinList } from '@/utils/coinList'
import { getShopConfig,getShopConfigV2 ,withdrawBalanceForSeller,updateShopConfigV2} from '@/api/wallet'
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
export default {
@@ -434,12 +439,55 @@ export default {
this.withdrawLoading = true
try {
const row = this.currentWithdrawRow || {}
/**
* 提现地址 RSA 加密(与“钱包绑定”页面保持一致:同步优先,异步兜底)
* - toAddress: 用户输入/默认地址
* - fromAddress: 当前绑定的钱包地址(后端可能用于校验来源)
*/
const toAddressPlain = String(this.withdrawForm.toAddress || '').trim()
const fromAddressPlain = String(row.payAddress || this.withdrawForm.toAddress || '').trim()
/** @type {string} */
let encryptedToAddress = toAddressPlain
if (encryptedToAddress) {
const syncEncrypted = rsaEncryptSync(encryptedToAddress)
if (syncEncrypted) {
encryptedToAddress = syncEncrypted
} else {
const asyncEncrypted = await rsaEncrypt(encryptedToAddress)
if (asyncEncrypted) {
encryptedToAddress = asyncEncrypted
} else {
this.$message.error('钱包地址加密失败,请重试')
return
}
}
}
/** @type {string} */
let encryptedFromAddress = fromAddressPlain
if (encryptedFromAddress) {
const syncEncrypted = rsaEncryptSync(encryptedFromAddress)
if (syncEncrypted) {
encryptedFromAddress = syncEncrypted
} else {
const asyncEncrypted = await rsaEncrypt(encryptedFromAddress)
if (asyncEncrypted) {
encryptedFromAddress = asyncEncrypted
} else {
this.$message.error('钱包地址加密失败,请重试')
return
}
}
}
const payload = {
toChain: row.chain,
toSymbol: row.payCoin,
amount: Number(this.withdrawForm.amount),
toAddress: this.withdrawForm.toAddress,
fromAddress: row.payAddress || this.withdrawForm.toAddress || '',
toAddress: encryptedToAddress,
fromAddress: encryptedFromAddress,
code: this.withdrawForm.googleCode,
serviceCharge: Number(this.withdrawForm.fee) || 0
}
@@ -679,26 +727,53 @@ export default {
this.deleteShopConfig({id:row.id})
},
submitConfigEdit() {
/**
* 提交配置修改
*/
async submitConfigEdit() {
// 仅校验钱包地址
const addr = (this.configForm.payAddress || '').trim()
if (!addr) {
this.$message.warning('请输入钱包地址')
return
}
/**
* 使用 RSA 加密钱包地址(与“钱包绑定”页面保持一致:同步优先,异步兜底)
* @type {string}
*/
let encryptedPayAddress = addr
if (encryptedPayAddress) {
const syncEncrypted = rsaEncryptSync(encryptedPayAddress)
if (syncEncrypted) {
encryptedPayAddress = syncEncrypted
} else {
const asyncEncrypted = await rsaEncrypt(encryptedPayAddress)
if (asyncEncrypted) {
encryptedPayAddress = asyncEncrypted
} else {
this.$message.error('钱包地址加密失败,请重试')
return
}
}
}
const payload = {
id: this.configForm.id,
chain: this.configForm.chainValue || this.configForm.chainLabel || '',
payAddress: this.configForm.payAddress
payAddress: encryptedPayAddress
}
;(async () => {
try {
const res = await updateShopConfigV2(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('保存成功')
this.visibleConfigEdit = false
this.fetchShopConfigs(this.shop.id)
}
})()
}
} catch (e) {
console.error('修改配置失败', e)
this.$message.error('修改配置失败,请重试')
}
},
removeSelectedCoin(labelUpper) {
const label = String(labelUpper || '').toLowerCase()
@@ -935,6 +1010,8 @@ export default {
.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-substeps { margin: 6px 0 0 0; padding-left: 18px; list-style: disc; }
.guide-substeps li { line-height: 1.8; margin: 4px 0; }
.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; }

View File

@@ -2,7 +2,7 @@
<div class="product-machine-add">
<div class="header">
<el-button type="text" @click="handleBack">返回</el-button>
<h2 class="title">添加出售机器</h2>
<h2 class="title">创建商品</h2>
</div>
<!-- <el-alert
@@ -10,8 +10,8 @@
type="warning"
show-icon
:closable="false"
title="新增出售机器必须在 M2pool 有挖矿算力记录才能添加出租"
description="建议稳定在 M2pool 矿池挖矿 24 小时之后,再添加出售该机器"
title="新增商品必须在 M2pool 有挖矿算力记录才能添加出租"
description="建议稳定在 M2pool 矿池挖矿 24 小时之后,再创建该商品"
/> -->
<el-card shadow="never" class="form-card">
@@ -236,7 +236,7 @@
<div v-if="form.machineCategory === 'ASIC'" class="actions">
<el-button @click="handleBack">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSave"
>确认添加</el-button
>确认创建</el-button
>
</div>
@@ -1531,7 +1531,7 @@ export default {
const res = await addAsicMachine(payload);
if (res && (res.code === 0 || res.code === 200)) {
this.$message({
message: "添加成功",
message: "创建成功",
duration: 3000,
showClose: true,
type: "success",
@@ -1540,8 +1540,8 @@ export default {
this.$router.push("/account/products");
}
} catch (e) {
console.error("添加出售机器失败", e);
console.log("添加失败");
console.error("创建商品失败", e);
console.log("创建失败");
} finally {
this.saving = false;
}

View File

@@ -196,7 +196,7 @@
<el-switch
:active-value="0"
:inactive-value="1"
:value="(updateMap[getRowId(scope.row)] && updateMap[getRowId(scope.row)].state) || 0"
:value="(updateMap[getRowId(scope.row)] && updateMap[getRowId(scope.row)].state) != null ? updateMap[getRowId(scope.row)].state : 1"
@change="handleToggleState(scope.row, $event)"
/>
<span
@@ -896,15 +896,31 @@ export default {
const merged = { ...exist }
// 覆盖为后端最新状态/天数(若存在)
if (m && typeof m.maxLeaseDays !== 'undefined') merged.maxLeaseDays = m.maxLeaseDays
if (m && (m.state === 0 || m.state === 1)) merged.state = m.state
// 如果接口有返回 state使用接口返回的值否则保持现有值
if (m && (m.state === 0 || m.state === 1)) {
merged.state = m.state
} else {
// 如果接口没有返回 state默认下架state = 1
merged.state = 1
}
// 保存 effect 字段(从原始数据获取)
if (m && (m.effect === 0 || m.effect === 1)) {
merged.effect = m.effect
} else if (merged.effect === undefined) {
// 如果原始数据没有 effect默认设为 1能上架
merged.effect = 1
}
merged.priceList = normalizePriceList(m && m.priceList, exist.priceList)
nextMap[key] = merged
} else {
// 如果接口没有返回 state默认下架state = 1
const defaultState = (m && (m.state === 0 || m.state === 1)) ? m.state : 1
nextMap[key] = {
id: key,
maxLeaseDays: (m && typeof m.maxLeaseDays !== 'undefined') ? m.maxLeaseDays : '',
priceList: normalizePriceList(m && m.priceList, []),
state: (m && (m.state === 0 || m.state === 1)) ? m.state : 0
state: defaultState,
effect: (m && (m.effect === 0 || m.effect === 1)) ? m.effect : 1 // 保存 effect 字段默认1能上架
}
}
})
@@ -1015,7 +1031,35 @@ export default {
/** 切换状态0 上架 1 下架) */
handleToggleState(row, val) {
const rowId = this.getRowId(row) || row.__key
if (this.updateMap[rowId]) this.updateMap[rowId].state = val
// 如果要上架val === 0需要检查 effect 字段
if (val === 0) {
// 从原始数据或 updateMap 中获取 effect 字段
let effect = 1 // 默认能上架
if (row && (row.effect === 0 || row.effect === 1)) {
effect = row.effect
} else if (this.updateMap[rowId] && (this.updateMap[rowId].effect === 0 || this.updateMap[rowId].effect === 1)) {
effect = this.updateMap[rowId].effect
}
if (effect === 0) {
// effect = 0 表示不能上架,提示用户
this.$message.warning('本网站暂时不支持该矿机币种及算法,暂时不能上架!')
// 恢复为下架状态
if (this.updateMap[rowId]) {
this.updateMap[rowId].state = 1
}
this.updateArr = Object.values(this.updateMap)
return
}
}
// 允许切换状态
if (this.updateMap[rowId]) {
this.updateMap[rowId].state = val
// 确保 effect 字段被保存(从原始数据获取)
if (row && (row.effect === 0 || row.effect === 1)) {
this.updateMap[rowId].effect = row.effect
}
}
this.updateArr = Object.values(this.updateMap)
},
/** 提交 GPU 更新(触发按钮) */

View File

@@ -2,7 +2,7 @@
<div class="account-purchased-machine-config">
<div class="toolbar">
<div class="left-area">
<h2 class="page-title">已购矿机配置</h2>
<h2 class="page-title">已购商品</h2>
</div>
</div>
@@ -15,63 +15,6 @@
:header-cell-style="{ textAlign: 'left' }"
:cell-style="{ textAlign: 'left' }"
>
<el-table-column type="expand" width="46">
<template #default="scope">
<div class="expand-content">
<div class="expand-row" v-if="scope.row.walletAddress">
<div class="expand-label">钱包地址</div>
<div class="expand-value">
<span class="mono-ellipsis" style="font-family: monospace;">{{ scope.row.walletAddress }}</span>
<el-button
type="text"
size="mini"
icon="el-icon-document-copy"
@click="handleCopy(scope.row.walletAddress, '钱包地址')"
class="copy-btn"
>
复制
</el-button>
</div>
</div>
<div class="expand-row" v-if="scope.row.poolUrl">
<div class="expand-label">矿池地址</div>
<div class="expand-value">
<span class="mono-ellipsis">{{ scope.row.poolUrl }}</span>
<el-button
type="text"
size="mini"
icon="el-icon-document-copy"
@click="handleCopy(scope.row.poolUrl, '矿池地址')"
class="copy-btn"
>
复制
</el-button>
</div>
</div>
<div class="expand-row" v-if="scope.row.watchUrl">
<div class="expand-label">挖矿信息页面地址</div>
<div class="expand-value">
<span class="mono-ellipsis">{{ scope.row.watchUrl }}</span>
<el-button
type="text"
size="mini"
icon="el-icon-document-copy"
@click="handleCopy(scope.row.watchUrl, '挖矿信息页面地址')"
class="copy-btn"
>
复制
</el-button>
</div>
</div>
<div v-if="!scope.row.walletAddress && !scope.row.poolUrl && !scope.row.watchUrl" class="expand-empty">
暂无地址信息
</div>
</div>
</template>
</el-table-column>
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
<el-table-column prop="coin" label="币种" width="100">
<template #default="scope">
<span>{{ scope.row.coin || '—' }}</span>
@@ -90,31 +33,53 @@
</template>
</el-table-column>
<el-table-column prop="poolUser" label="矿池用户" min-width="140">
<el-table-column prop="walletAddress" label="钱包地址" min-width="200">
<template #default="scope">
<span>{{ scope.row.poolUser || '—' }}</span>
<div class="address-cell">
<span v-if="scope.row.walletAddress" class="mono-ellipsis" style="font-family: monospace;">{{ scope.row.walletAddress }}</span>
<span v-else></span>
<el-button
v-if="scope.row.walletAddress"
type="text"
size="mini"
icon="el-icon-document-copy"
@click="handleCopy(scope.row.walletAddress, '钱包地址')"
class="copy-btn"
>
复制
</el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" min-width="160">
<el-table-column prop="poolUrl" label="矿池地址" min-width="200">
<template #default="scope">
<span>{{ formatDateTime(scope.row.startTime) }}</span>
<div class="address-cell">
<span v-if="scope.row.poolUrl" class="mono-ellipsis">{{ scope.row.poolUrl }}</span>
<span v-else></span>
<el-button
v-if="scope.row.poolUrl"
type="text"
size="mini"
icon="el-icon-document-copy"
@click="handleCopy(scope.row.poolUrl, '矿池地址')"
class="copy-btn"
>
复制
</el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="endTime" label="结束时间" min-width="160">
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<span>{{ formatDateTime(scope.row.endTime) }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="140">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
<el-button
type="text"
size="mini"
@click="handleViewDetail(scope.row)"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
@@ -182,11 +147,7 @@ export default {
}
} catch (e) {
console.error('获取已购矿机配置失败', e)
this.$message({
message: '获取已购矿机配置失败,请重试',
type: 'error',
showClose: true
})
this.tableData = []
this.total = 0
this.totalPage = 0
@@ -283,6 +244,22 @@ export default {
})
}
},
/**
* 查看详情
* @param {Object} row - 行数据
*/
handleViewDetail(row) {
// 跳转到详情页面传递行数据的ID
const id = row.id || row.productMachineId || row.machineId
if (id) {
this.$router.push({
name: 'purchasedMachineDetail',
params: { id: id }
})
} else {
this.$message.warning('无法获取详情缺少ID信息')
}
},
/**
* 获取状态文本
* @param {number|boolean} status - 状态值
@@ -345,49 +322,21 @@ export default {
margin-top: 12px;
}
/* 展开内容样式 */
.expand-content {
padding: 16px;
background-color: #fafafa;
}
.expand-row {
/* 地址单元格样式 */
.address-cell {
display: flex;
align-items: flex-start;
margin-bottom: 16px;
padding: 12px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #e4e7ed;
}
.expand-row:last-child {
margin-bottom: 0;
}
.expand-label {
min-width: 100px;
font-weight: 600;
color: #606266;
margin-right: 12px;
flex-shrink: 0;
}
.expand-value {
flex: 1;
display: flex;
align-items: flex-start;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.expand-value .mono-ellipsis {
.address-cell .mono-ellipsis {
font-family: monospace;
color: #303133;
line-height: 1.5;
word-break: break-all;
flex: 0 1 auto;
margin-right: 8px;
flex: 1;
min-width: 0;
}
.copy-btn {
@@ -400,13 +349,6 @@ export default {
color: #66b1ff;
}
.expand-empty {
padding: 20px;
text-align: center;
color: #909399;
font-size: 14px;
}
/* 钱包地址和URL格式化显示样式 */
.mono-ellipsis {
font-family: monospace;

View File

@@ -0,0 +1,410 @@
<template>
<div class="purchased-machine-detail">
<div class="toolbar">
<div class="left-area">
<el-button
type="text"
icon="el-icon-arrow-left"
@click="handleBack"
class="back-btn"
>
返回
</el-button>
<h2 class="page-title">已购商品详情</h2>
</div>
</div>
<div class="detail-content" v-loading="loading">
<!-- 基本信息卡片 -->
<el-card v-if="detailData" class="detail-card">
<div slot="header" class="card-header">
<span>基本信息</span>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="挖矿账户">
{{ detailData.poolUser || '—' }}
</el-descriptions-item>
<el-descriptions-item label="矿池名称">
{{ detailData.pool || '—' }}
</el-descriptions-item>
<el-descriptions-item label="矿池挖矿地址">
<div class="address-item" v-if="detailData.poolUrl">
<span class="mono-ellipsis">{{ detailData.poolUrl }}</span>
<el-button
type="text"
size="mini"
icon="el-icon-document-copy"
@click="handleCopy(detailData.poolUrl, '矿池挖矿地址')"
class="copy-btn"
>
复制
</el-button>
</div>
<span v-else></span>
</el-descriptions-item>
<el-descriptions-item label="矿池所挖币种">
{{ detailData.coin || '—' }}
</el-descriptions-item>
<el-descriptions-item label="币种对应算法">
{{ detailData.algorithm || '—' }}
</el-descriptions-item>
<el-descriptions-item label="收款钱包">
<div class="address-item" v-if="detailData.walletAddress">
<span class="mono-ellipsis" style="font-family: monospace;">{{ detailData.walletAddress }}</span>
<el-button
type="text"
size="mini"
icon="el-icon-document-copy"
@click="handleCopy(detailData.walletAddress, '收款钱包')"
class="copy-btn"
>
复制
</el-button>
</div>
<span v-else></span>
</el-descriptions-item>
<el-descriptions-item label="挖矿信息页面地址" v-if="detailData.watchUrl">
<div class="address-item">
<span class="mono-ellipsis">{{ detailData.watchUrl }}</span>
<el-button
type="text"
size="mini"
icon="el-icon-document-copy"
@click="handleCopy(detailData.watchUrl, '挖矿信息页面地址')"
class="copy-btn"
>
复制
</el-button>
</div>
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 已购矿机信息列表 -->
<el-card v-if="detailData" class="detail-card">
<div slot="header" class="card-header">
<span>已购矿机信息</span>
</div>
<el-table
:data="purchasedMachinesList"
border
stripe
style="width: 100%"
:header-cell-style="{ textAlign: 'left' }"
:cell-style="{ textAlign: 'left' }"
v-if="purchasedMachinesList && purchasedMachinesList.length > 0"
>
<el-table-column prop="workerId" label="矿工号" min-width="120">
<template #default="scope">
<span>{{ scope.row.workerId || '—' }}</span>
</template>
</el-table-column>
<el-table-column prop="power" label="实时算力" min-width="140">
<template #default="scope">
<span>{{ scope.row.power || '—' }}</span>
</template>
</el-table-column>
<el-table-column prop="recordTime" label="最近实时算力记录时间" min-width="180">
<template #default="scope">
<span>{{ formatDateTime(scope.row.recordTime) }}</span>
</template>
</el-table-column>
<el-table-column prop="startTime" label="挖矿开始时间" min-width="160">
<template #default="scope">
<span>{{ formatDateTime(scope.row.startTime) }}</span>
</template>
</el-table-column>
<el-table-column prop="endTime" label="挖矿结束时间" min-width="160">
<template #default="scope">
<span>{{ formatDateTime(scope.row.endTime) }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="140">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
<div v-else class="empty-table">
<p>暂无矿机信息</p>
</div>
</el-card>
<div v-if="!loading && !detailData" class="empty-state">
<p>未找到详情信息</p>
</div>
</div>
</div>
</template>
<script>
import { getPurchasedInfoV2 } from '../../api/order'
export default {
name: 'PurchasedMachineDetail',
data() {
return {
loading: false,
detailData: null
}
},
created() {
this.fetchDetail()
},
computed: {
/**
* 已购矿机列表(从 detailData.purchasedMachines 中提取)
*/
purchasedMachinesList() {
if (!this.detailData) return []
return Array.isArray(this.detailData.purchasedMachines) ? this.detailData.purchasedMachines : []
}
},
methods: {
/**
* 获取详情数据
*/
async fetchDetail() {
const id = this.$route.params.id
if (!id) {
this.$message.error('缺少ID参数')
this.handleBack()
return
}
this.loading = true
try {
const params = {
id: id
}
const res = await getPurchasedInfoV2(params)
if (res && (res.code === 0 || res.code === 200)) {
// 接口返回的数据可能在 res.data 中
this.detailData = res.data || res
if (!this.detailData) {
this.$message.warning('未找到对应的详情信息')
}
} else {
const errorMsg = (res && res.msg) || '获取详情失败'
this.$message.error(errorMsg)
}
} catch (e) {
console.error('获取详情失败', e)
this.$message.error('获取详情失败,请稍后重试')
} finally {
this.loading = false
}
},
/**
* 返回上一页
*/
handleBack() {
this.$router.go(-1)
},
/**
* 格式化日期时间
* @param {string|number} value - 日期时间值
* @returns {string} 格式化后的日期时间字符串
*/
formatDateTime(value) {
if (!value) return '—'
try {
const str = String(value)
return str.includes('T') ? str.replace('T', ' ') : str
} catch (e) {
return String(value)
}
},
/**
* 复制文本到剪贴板
* @param {string} text - 需要复制的内容
* @param {string} label - 语义标签,用于提示文案
*/
async handleCopy(text, label = '内容') {
if (!text) {
this.$message({
message: `${label}为空,无法复制`,
type: 'warning',
showClose: true
})
return
}
try {
const value = String(text).trim()
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(value)
this.$message({
message: `${label}已复制到剪贴板`,
type: 'success',
showClose: true
})
} else {
// 备用复制方法
const textArea = document.createElement('textarea')
textArea.value = value
textArea.style.position = 'fixed'
textArea.style.left = '-9999px'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
try {
document.execCommand('copy')
this.$message({
message: `${label}已复制到剪贴板`,
type: 'success',
showClose: true
})
} catch (err) {
this.$message({
message: '复制失败,请手动复制',
type: 'error',
showClose: true
})
}
document.body.removeChild(textArea)
}
} catch (e) {
console.error('复制失败', e)
this.$message({
message: '复制失败,请手动复制',
type: 'error',
showClose: true
})
}
},
/**
* 获取状态文本
* @param {number|boolean} status - 状态值
* @returns {string} 状态文本
*/
getStatusText(status) {
const statusNum = Number(status)
if (statusNum === 0) return '租约已到期'
if (statusNum === 1) return '挖矿中'
if (statusNum === 2) return '卖家矿机启动中'
// 兼容旧数据boolean类型
if (status === true) return '挖矿中'
return '未知状态'
},
/**
* 获取状态标签类型
* @param {number|boolean} status - 状态值
* @returns {string} el-tag 的类型
*/
getStatusType(status) {
const statusNum = Number(status)
if (statusNum === 0) return 'info' // 租约已到期 - 灰色
if (statusNum === 1) return 'success' // 挖矿中 - 绿色
if (statusNum === 2) return 'warning' // 卖家矿机启动中 - 黄色
// 兼容旧数据boolean类型
if (status === true) return 'success'
return 'info'
}
}
}
</script>
<style scoped>
.purchased-machine-detail {
padding: 4px;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.left-area {
display: flex;
align-items: center;
gap: 12px;
}
.back-btn {
padding: 0;
font-size: 14px;
color: #409eff;
}
.back-btn:hover {
color: #66b1ff;
}
.page-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #2c3e50;
}
.detail-content {
margin-top: 16px;
}
.detail-card {
margin-bottom: 16px;
}
.card-header {
font-weight: 600;
font-size: 16px;
color: #303133;
}
.address-item {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.address-item .mono-ellipsis {
font-family: monospace;
color: #303133;
line-height: 1.5;
word-break: break-all;
flex: 1;
min-width: 0;
}
.copy-btn {
flex-shrink: 0;
padding: 0 8px;
color: #409eff;
}
.copy-btn:hover {
color: #66b1ff;
}
.mono-ellipsis {
font-family: monospace;
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.empty-state {
padding: 40px;
text-align: center;
color: #909399;
font-size: 14px;
}
.empty-table {
padding: 40px;
text-align: center;
color: #909399;
font-size: 14px;
}
</style>

View File

@@ -38,7 +38,7 @@
border
stripe
size="small"
style="width: 100%"
style="width: 100%; table-layout: auto;"
:row-key="getRowKey"
:expand-row-keys="expandedRowKeys"
:row-class-name="getRowClassName"
@@ -47,7 +47,7 @@
:header-cell-style="{ textAlign: 'left' }"
:cell-style="{ textAlign: 'left' }"
>
<el-table-column type="expand" width="46">
<el-table-column v-if="rows.length > 0" type="expand" width="46">
<template #default="scope">
<div class="detail-panel">
<div class="detail-grid">
@@ -74,10 +74,10 @@
</div>
</template>
</el-table-column>
<el-table-column label="支付时间" min-width="160">
<el-table-column label="支付时间" width="160">
<template #default="scope">{{ formatFullTime(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="收款金额(USDT)" min-width="160" align="right">
<el-table-column label="收款金额(USDT)" width="140" align="right">
<template #default="scope">
<span class="amount-green">
<el-tooltip
@@ -96,30 +96,30 @@
</span>
</template>
</el-table-column>
<el-table-column label="收款链" min-width="120">
<el-table-column label="收款链" width="140">
<template #default="scope">{{ formatChain(scope.row.toChain) }}</template>
</el-table-column>
<el-table-column label="收款币种" min-width="100">
<el-table-column label="收款币种" width="100">
<template #default="scope">{{ String(scope.row.coin || '').toUpperCase() }}</template>
</el-table-column>
<el-table-column label="收款地址" min-width="260">
<el-table-column label="收款地址" min-width="200">
<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">
<el-table-column label="交易HASH" min-width="200">
<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">
<el-table-column label="支付状态" 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">
<el-table-column label="状态更新时间" width="160">
<template #default="scope">{{ formatFullTime(scope.row.updateTime) }}</template>
</el-table-column>
</el-table>
@@ -306,8 +306,8 @@ export default {
<style scoped>
.receipt-page { margin: 0; box-sizing: border-box; overflow-x: hidden; }
.card { background: #fff; border: 1px solid #eee; border-radius: 10px; padding: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.04); overflow-x: auto; }
.receipt-page { margin: 0; box-sizing: border-box; overflow-x: hidden; width: 100%; }
.card { background: #fff; border: 1px solid #eee; border-radius: 10px; padding: 12px; box-shadow: 0 4px 18px rgba(0,0,0,0.04); width: 100%; box-sizing: border-box; }
.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; }
@@ -322,6 +322,15 @@ export default {
.type-red { color: #ef4444; }
.pagination { display: flex; justify-content: flex-end; margin-top: 8px; }
/* 表格容器自适应 */
.receipt-page :deep(.el-table) {
width: 100% !important;
}
.receipt-page :deep(.el-table__body-wrapper) {
overflow-x: hidden;
}
/* 展开详情样式 */
.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; }
@@ -331,7 +340,7 @@ export default {
.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; }
.mono-ellipsis { font-family: "Monaco", "Menlo", monospace; display: inline-block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; max-width: 100%; }
/* 可点击行的轻交互提示 */
.clickable-row:hover > td { background: #f8fafc !important; cursor: pointer; }

View File

@@ -0,0 +1,118 @@
<template>
<div class="funds-page">
<!-- 页面标题卖家资金流水与买家 fundsFlow.vue 视觉对齐 -->
<h3 class="title">资金流水</h3>
<!-- Tab 容器仅卖家维度收款记录/提现记录 -->
<div class="tabs-card" aria-label="资金流水tab" tabindex="0">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="收款记录" name="receipt" />
<el-tab-pane label="提现记录" name="withdraw" />
</el-tabs>
<!--
使用 keep-alive + 动态组件
- 避免一次性挂载两个页面导致双请求
- 切换 Tab 时缓存组件状态如分页页码/展开行
-->
<keep-alive>
<component :is="activeComponentName" />
</keep-alive>
</div>
</div>
</template>
<script>
import AccountReceiptRecord from './receiptRecord.vue'
import AccountWithdrawRecord from './withdrawRecord.vue'
/**
* 卖家资金流水聚合页
* - 仅整合“收款记录/提现记录”两个页面
* - 复用原有页面组件,保证请求接口与内容展示完全不变
*/
export default {
name: 'AccountSellerFundsFlow',
components: {
AccountReceiptRecord,
AccountWithdrawRecord
},
data() {
return {
/** @type {'receipt'|'withdraw'} */
activeTab: 'receipt'
}
},
computed: {
/**
* 根据当前 tab 返回动态组件名
* @returns {'AccountReceiptRecord'|'AccountWithdrawRecord'}
*/
activeComponentName() {
return this.activeTab === 'withdraw' ? 'AccountWithdrawRecord' : 'AccountReceiptRecord'
}
},
created() {
this.syncTabFromRoute()
},
watch: {
/**
* 监听 query.tab 变化(兼容外部链接/旧路由重定向)
*/
'$route.query.tab': {
immediate: false,
handler() {
this.syncTabFromRoute()
}
}
},
methods: {
/**
* 从路由 query.tab 同步当前 tab
* 允许值receipt / withdraw其他值回退为 receipt
*/
syncTabFromRoute() {
const tab = String((this.$route && this.$route.query && this.$route.query.tab) || '')
this.activeTab = tab === 'withdraw' ? 'withdraw' : 'receipt'
},
/**
* Tab 点击:同步写入 URLreplace避免历史栈过长
*/
handleTabClick() {
const next = this.activeTab === 'withdraw' ? 'withdraw' : 'receipt'
const current = String((this.$route && this.$route.query && this.$route.query.tab) || '')
if (current === next) return
this.$router.replace({ query: { ...(this.$route.query || {}), tab: next } })
}
}
}
</script>
<style scoped>
/* 外层容器:与买家 fundsFlow.vue 的布局对齐 */
.funds-page {
margin: 0;
box-sizing: border-box;
width: 100%;
}
.title {
margin: 0 0 18px 0;
text-align: left;
font-size: 18px;
font-weight: 700;
color: #2c3e50;
}
.tabs-card {
background: #fff;
border: 1px solid #eee;
border-radius: 10px;
padding: 12px;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.04);
width: 100%;
box-sizing: border-box;
}
</style>

View File

@@ -113,6 +113,7 @@
<script>
import { getMyShop } from "@/api/shops";
import { getChainAndList, addWalletShopConfig,getProductListForShopWalletConfig,updateProductListForShopWalletConfig } from "../../api/wallet";
import { rsaEncrypt, rsaEncryptSync } from "../../utils/rsaEncrypt";
export default {
name: "AccountShopConfig",
@@ -280,7 +281,33 @@ export default {
},
async FetchAddWalletShopConfig(params) {
this.loading = true;
const res = await addWalletShopConfig(params);
// 使用 RSA 加密钱包地址
let encryptedPayAddress = params.payAddress
if (params.payAddress) {
// 优先使用同步版本(如果 JSEncrypt 已加载)
const syncEncrypted = rsaEncryptSync(params.payAddress)
if (syncEncrypted) {
encryptedPayAddress = syncEncrypted
} else {
// 如果同步版本失败,使用异步版本
const asyncEncrypted = await rsaEncrypt(params.payAddress)
if (asyncEncrypted) {
encryptedPayAddress = asyncEncrypted
} else {
this.$message.error('钱包地址加密失败,请重试')
this.loading = false
return
}
}
}
const encryptedParams = {
...params,
payAddress: encryptedPayAddress
}
const res = await addWalletShopConfig(encryptedParams);
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success("绑定成功");
this.$router.push("/account/shops");
@@ -341,10 +368,25 @@ export default {
async preCheckBeforeBind() {
try {
this.loading = true
// 先关闭弹窗,确保不会显示旧数据
this.preCheck.visible = false
this.preCheck.rows = []
const params = { chain: this.form.chain, payCoin: this.form.payCoin }
const res = await getProductListForShopWalletConfig(params)
// 检查接口是否成功code === 0 或 code === 200
const isSuccess = res && (res.code === 0 || res.code === 200)
if (!isSuccess) {
// 接口失败,直接走绑定流程,不显示弹窗
await this.submitBindWithPrice([])
return
}
// 接口成功,提取数据
const rows = Array.isArray(res && res.data) ? res.data : (Array.isArray(res && res.rows) ? res.rows : [])
if (rows && rows.length) {
// 确保 rows 是有效数组且有数据
if (Array.isArray(rows) && rows.length > 0) {
this.preCheck.rows = rows
// 初始化各币种价格输入
const coins = (this.form.payCoin || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean)
@@ -360,11 +402,15 @@ export default {
})
this.preCheck.visible = true
} else {
// 无关联商品,直接绑定并设置(机器列表为空)
// 无关联商品或数据为空,直接绑定并设置(机器列表为空),不显示弹窗
this.preCheck.visible = false
this.preCheck.rows = []
await this.submitBindWithPrice([])
}
} catch (e) {
// 接口异常不阻塞绑定流程
// 接口异常不阻塞绑定流程,直接走绑定,不显示弹窗
this.preCheck.visible = false
this.preCheck.rows = []
await this.submitBindWithPrice([])
} finally {
this.loading = false
@@ -427,21 +473,45 @@ export default {
(g.machineIds || []).forEach(id => { list.push({ productMachineId: id, price: priceStr }) })
})
}
// 使用 RSA 加密钱包地址
let encryptedPayAddress = this.form.payAddress
if (this.form.payAddress) {
// 优先使用同步版本(如果 JSEncrypt 已加载)
const syncEncrypted = rsaEncryptSync(this.form.payAddress)
if (syncEncrypted) {
encryptedPayAddress = syncEncrypted
} else {
// 如果同步版本失败,使用异步版本
const asyncEncrypted = await rsaEncrypt(this.form.payAddress)
if (asyncEncrypted) {
encryptedPayAddress = asyncEncrypted
} else {
this.$message.error('钱包地址加密失败,请重试')
this.loading = false
return
}
}
}
const payload = {
chain: this.form.chain,
symbol: this.form.payCoin,
payAddress: this.form.payAddress,
// payAddress: this.form.payAddress,//使用未加密地址
payAddress: encryptedPayAddress, // 使用加密后的地址
productMachineForWalletConfigVoList: list
}
const res = await updateProductListForShopWalletConfig(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.preCheck.visible = false
this.resetPreCheckPrices()
this.$message.success('绑定成功')
this.$router.push('/account/shops')
}else{
this.preCheck.visible = true
} else {
// 绑定失败,关闭弹窗并显示错误消息
this.preCheck.visible = false
this.resetPreCheckPrices()
}
} catch (e) {
// 错误交由全局拦截或简单提示

View File

@@ -306,6 +306,7 @@
import { getWalletInfo, withdrawBalance ,balanceRechargeList,balanceWithdrawList,getRecentlyTransaction} from '@/api/wallet'
import { getChainAndList,bindWallet } from "../../api/wallet";
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
export default {
name: 'WalletPage',
data() {
@@ -893,13 +894,55 @@ export default {
this.withdrawLoading = true
try {
/**
* 提现地址 RSA 加密(与“钱包绑定”页面保持一致:同步优先,异步兜底)
* - toAddress: 用户输入的收款地址
* - fromAddress: 当前钱包来源地址(后端可能用于校验来源)
*/
const toAddressPlain = String(this.withdrawForm.toAddress || '').trim()
const fromAddressPlain = String((this.WalletData && this.WalletData.fromAddress) || '').trim()
/** @type {string} */
let encryptedToAddress = toAddressPlain
if (encryptedToAddress) {
const syncEncrypted = rsaEncryptSync(encryptedToAddress)
if (syncEncrypted) {
encryptedToAddress = syncEncrypted
} else {
const asyncEncrypted = await rsaEncrypt(encryptedToAddress)
if (asyncEncrypted) {
encryptedToAddress = asyncEncrypted
} else {
this.$message.error('钱包地址加密失败,请重试')
return
}
}
}
/** @type {string} */
let encryptedFromAddress = fromAddressPlain
if (encryptedFromAddress) {
const syncEncrypted = rsaEncryptSync(encryptedFromAddress)
if (syncEncrypted) {
encryptedFromAddress = syncEncrypted
} else {
const asyncEncrypted = await rsaEncrypt(encryptedFromAddress)
if (asyncEncrypted) {
encryptedFromAddress = asyncEncrypted
} else {
this.$message.error('钱包地址加密失败,请重试')
return
}
}
}
// 调用后台提现API添加谷歌验证码参数
const res = await withdrawBalance({
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,
fromAddress: (this.WalletData && this.WalletData.fromAddress) || '',
toAddress: encryptedToAddress,
fromAddress: encryptedFromAddress,
code: this.withdrawForm.googleCode // 添加谷歌验证码
})

View File

@@ -37,6 +37,7 @@
:row-key="'id'" reserve-selection
:ref="'innerTable-' + shopScope.row.id"
@selection-change="sels => handleShopInnerSelectionChange(shopScope.row, sels)"
:row-class-name="getMachineRowClassName"
:header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table-column type="selection" width="46" :selectable="row => isRowSelectableByShop(shopScope.row, row)" />
<el-table-column prop="name" label="商品名称" />
@@ -711,6 +712,7 @@
import { getGoodsList, deleteBatchGoods as apiDeleteBatchGoods ,deleteBatchGoodsForIsDeleteV2,deleteBatchGoodsV2,deleteBatchGoodsForIsDelete,getGoodsListV2} from '../../api/shoppingCart'
import { addOrders,cancelOrder,getOrdersByIds,getOrdersByStatus , getMachineSupportPool,getChainAndListForSeller,getCoinPrice,getMachineSupportCoinAndAlgorithm, addOrdersV2} from '../../api/order'
import { truncateAmountByCoin, truncateTo6 } from '../../utils/amount'
import { rsaEncrypt, rsaEncryptSync } from '../../utils/rsaEncrypt'
export default {
name: 'Cart',
@@ -1081,10 +1083,12 @@ export default {
: (Array.isArray(sp && sp.productMachineDtoList) ? sp.productMachineDtoList : [])
// 记录每台机器原始租期,便于后续判断是否修改
try { machines.forEach(m => { if (m && m._origLeaseTime == null) m._origLeaseTime = Number(m.leaseTime || 1) }) } catch (e) { /* noop */ }
// 对机器列表进行排序:下架的排到最下面
const sortedMachines = this.sortMachinesByShelfStatus(machines)
return {
...sp,
id: sp && sp.id != null ? String(sp.id) : `shop-${idx}`,
productMachineDtoList: machines
productMachineDtoList: sortedMachines
}
})
this.shops = normalized
@@ -1153,10 +1157,39 @@ export default {
isRowSelectable(row, index) {
return !(Number(row && row.del) === 1 || Number(row && row.state) === 1)
},
// 判断机器是否“上架”(用于未展开时的自动筛选)
// 判断机器是否"上架"(用于未展开时的自动筛选)
isOnShelf(row) {
return !(Number(row && row.del) === 1 || Number(row && row.state) === 1)
},
/**
* 对机器列表进行排序:下架的排到最下面
* @param {Array} machines - 机器列表
* @returns {Array} 排序后的机器列表
*/
sortMachinesByShelfStatus(machines) {
if (!Array.isArray(machines)) return []
// 上架的机器排前面,下架的排后面
return [...machines].sort((a, b) => {
const aOnShelf = this.isOnShelf(a)
const bOnShelf = this.isOnShelf(b)
// 如果都是上架或都是下架,保持原有顺序
if (aOnShelf === bOnShelf) return 0
// 上架的排前面(返回-1下架的排后面返回1
return aOnShelf ? -1 : 1
})
},
/**
* 获取机器表格行的类名(用于样式)
* @param {Object} param - 包含 row, rowIndex 的对象
* @returns {string} 行类名
*/
getMachineRowClassName({ row }) {
// 如果机器已下架,返回下架样式类
if (!this.isOnShelf(row)) {
return 'off-shelf-row'
}
return ''
},
// 获取本地最大可租赁天数,默认 365
getRowMaxLeaseDaysLocal(row) {
const raw = row && row.maxLeaseDays
@@ -1510,11 +1543,13 @@ export default {
try {
withShopKeys.forEach(sp => this.ensureDefaultPaySelection(sp))
} catch (e) { /* noop */ }
// 记录每台机器的原始租期,便于判断是否被修改
// 记录每台机器的原始租期,便于判断是否被修改,并对机器列表进行排序
try {
withShopKeys.forEach(sp => {
const list = Array.isArray(sp.productMachineDtoList) ? sp.productMachineDtoList : []
list.forEach(m => { if (m && m._origLeaseTime == null) m._origLeaseTime = Number(m.leaseTime || 1) })
// 对机器列表进行排序:下架的排到最下面
sp.productMachineDtoList = this.sortMachinesByShelfStatus(list)
})
} catch (e) { /* noop */ }
this.shops = withShopKeys
@@ -1765,6 +1800,7 @@ export default {
// 为每台机器创建独立的配置项
orderMiningInfoDtoList.push({
coinConfigId: coinConfigId,
coin: config.coin || '', // 用户选择的币种/算法框里的币种
poolName: config.poolName || '',
poolUser: poolUser,
walletAddress: walletAddress,
@@ -1773,7 +1809,30 @@ export default {
machineId: String(machine.id) // 使用 machineIdstring而不是 machineIdsarray
})
})
/**
* 结算提交前:对配置中的钱包地址进行 RSA 加密(字段名不变,仍为 walletAddress
* 规则同步优先rsaEncryptSync失败再异步兜底rsaEncrypt仍失败则提示并中断结算
*/
for (const item of (orderMiningInfoDtoList || [])) {
const plainWalletAddress = String(item && item.walletAddress ? item.walletAddress : '').trim()
// 空地址无需加密(例如 walletMining=false 时可能仅提交 poolUser
if (!plainWalletAddress) continue
/** @type {string|null} */
let encryptedWalletAddress = rsaEncryptSync(plainWalletAddress)
if (!encryptedWalletAddress) {
encryptedWalletAddress = await rsaEncrypt(plainWalletAddress)
}
if (!encryptedWalletAddress) {
this.$message.error('钱包地址加密失败,请重试')
// 中断前恢复勾选 UI与其它失败分支保持一致
this.reapplySelectionsForPendingShop()
return false
}
item.walletAddress = encryptedWalletAddress
}
this.creatingOrder = true
try {
const res = await this.fetchAddOrdersV2(orderInfoVoList, orderMiningInfoDtoList, googleCode)
@@ -3317,4 +3376,13 @@ export default {
white-space: nowrap;
vertical-align: middle;
}
/* 下架商品行样式 */
:deep(.off-shelf-row) {
background-color: #f5f5f5 !important;
}
:deep(.off-shelf-row:hover > td) {
background-color: #e8e8e8 !important;
}
</style>

View File

@@ -215,9 +215,9 @@ export default {
return { text: t.text, full: t.full, truncated: t.truncated }
}
if (col.type === 'hashrate') {
const unit = col.unit ? ` ${col.unit}` : ''
const t = truncateTo6(val)
return { text: `${t.text}${unit}`, full: `${t.full}${unit}`, truncated: t.truncated }
// 接口已返回带单位的内容,直接返回字符串值,不拼接单位
const s = String(val || '')
return { text: s, full: s, truncated: false }
}
if (col.type === 'days') {
const n = Number(val)