3 Commits

Author SHA1 Message Date
cbefb964d4 每周更新 2025-12-05 16:24:20 +08:00
485226d9dc 最新需求修改中 2025-11-28 15:30:36 +08:00
868632400a 新版本更改中 2025-11-21 16:23:46 +08:00
25 changed files with 4561 additions and 1391 deletions

View File

@@ -7,8 +7,8 @@ NODE_ENV = production
ENV = 'staging'
# 测试环境
VUE_APP_BASE_API = 'http://10.168.2.220:8888'
# VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
# VUE_APP_BASE_API = 'http://10.168.2.220:8888'
VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
VUE_APP_BASE_URL = 'https://test.m2pool.com/'

View File

@@ -12,51 +12,73 @@ export function addSingleOrBatchMachine(data) {
//根据矿机id 删除商品矿机
export function deleteMachine(data) {
return request({
url: `/lease/product/machine/delete`,
method: 'post',
data
})
}
return request({
url: `/lease/product/machine/delete`,
method: 'post',
data
})
}
//根据挖矿账户获取矿机列表
//根据挖矿账户获取矿机列表
export function getUserMachineList(data) {
return request({
url: `/lease/product/machine/getUserMachineList`,
method: 'post',
data
})
}
return request({
url: `/lease/product/machine/getUserMachineList`,
method: 'post',
data
})
}
//根据 登录账户 获取挖矿账户及挖矿币种集合
//根据 登录账户 获取挖矿账户及挖矿币种集合
export function getUserMinersList(data) {
return request({
url: `/lease/product/machine/getUserMinersList`,
method: 'post',
data
})
}
return request({
url: `/lease/product/machine/getUserMinersList`,
method: 'post',
data
})
}
//编辑矿机 + 矿机上下架
//编辑矿机 + 矿机上下架
export function updateMachine(data) {
return request({
url: `/lease/product/machine/updateMachine`,
method: 'post',
data
})
}
return request({
url: `/lease/product/machine/updateMachine`,
method: 'post',
data
})
}
//获取矿机列表
//获取矿机列表
export function getMachineListForUpdate(data) {
return request({
url: `/lease/product/machine/getMachineListForUpdate`,
method: 'post',
data
})
}
return request({
url: `/lease/product/machine/getMachineListForUpdate`,
method: 'post',
data
})
}
//GPU下载客户端
export function downloadClient() {
return request({
url: `/lease/user/downloadClient`,
method: 'get',
responseType: 'blob' // 关键:必须设置为 blob 才能正确下载二进制文件
})
}
//卖家页面---新增ASIC矿机
export function addAsicMachine(data) {
return request({
url: `/lease/v2/product/machine/addAsicMachine`,
method: 'post',
data
})
}

View File

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

View File

@@ -97,3 +97,83 @@ export function getPayTypes(data) {
}
// 卖家页面---矿机列表
export function getShopMachineListForSeller(data) {
return request({
url: `/lease/v2/product/machine/getShopMachineListForSeller`,
method: 'post',
data
})
}
// 更新设置GPU商品列表的信息
export function updateGpuMachine(data) {
return request({
url: `/lease/v2/product/machine/updateGpuMachine`,
method: 'post',
data
})
}
// 修改商品列表ASIC商品信息
export function updateAsicMachine(data) {
return request({
url: `/lease/v2/product/machine/updateAsicMachine`,
method: 'post',
data
})
}
// 删除ASIC 或者GPU
export function deleteMachine(data) {
return request({
url: `/lease/v2/product/machine/deleteMachine`,
method: 'post',
data
})
}
// 获取商场页面的店铺列表
export function getShopList(data) {
return request({
url: `/lease/v2/product/machine/getShopList`,
method: 'post',
data
})
}
// 获取店铺详情
export function getShopMachineList(data) {
return request({
url: `/lease/v2/product/machine/getShopMachineList`,
method: 'post',
data
})
}
// 获取店铺详情
export function addGoodsV2(data) {
return request({
url: `/lease/v2/shopping/cart/addGoodsV2`,
method: 'post',
data
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@
必须添加出售机器否则买家无法下单买家点击某个商品后会看到该商品下的机器明细并进行选购
</li>
</ol>
<div class="guide-note">提示建议先创建店铺 完成钱包绑定 创建商品 添加出售机器的顺序避免漏配导致无法收款或无法下单</div>
<div class="guide-note">提示建议先创建店铺 完成钱包绑定 创建商品的顺序避免漏配导致无法收款或无法下单</div>
</div>
</el-card>
@@ -39,10 +39,9 @@
</el-tag>
</div>
<div class="desc">{{ shop.description || '这家店还没有描述~' }}</div>
<!-- <div class="meta">
<span>店铺ID{{ shop.id || '-' }}</span>
<span>可删除{{ shop.del ? '是' : '否' }}</span>
</div> -->
<div class="meta">
<span>手续费率{{ formatFeeRate(shop.feeRate) }}</span>
</div>
<div class="actions">
<el-button size="small" type="primary" @click="handleOpenEdit">修改店铺</el-button>
<el-button size="small" type="warning" @click="handleToggleShop">
@@ -62,8 +61,8 @@
<span>已绑定钱包</span>
</div>
<el-table :data="shopConfigs" border style="width: 100%">
<el-table-column prop="chain" label="链" width="140" />
<el-table-column label="支付币种" >
<el-table-column prop="chain" label="链" width="120" />
<el-table-column label="支付币种" width="120" >
<template slot-scope="scope">
<div class="coin-list">
<template v-if="Array.isArray(scope.row.children) && scope.row.children.length">
@@ -92,8 +91,16 @@
</el-table-column> -->
<el-table-column prop="payAddress" label="收款钱包地址" show-overflow-tooltip />
<el-table-column label="操作" width="180" fixed="right">
<el-table-column label="余额" >
<template slot-scope="scope">
<span class="balance-num">{{ formatAmount(scope.row) }}</span>
<span class="balance-unit"> {{ formatCoin(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<template slot-scope="scope">
<el-button type="text" style="color:#409EFF" @click="handleWithdraw(scope.row)">提现</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" @click="handleEditConfig(scope.row)">修改</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" style="color:#e74c3c" @click="handleDeleteConfig(scope.row)">删除</el-button>
@@ -110,6 +117,85 @@
<el-empty v-else description="正在加载店铺信息..." />
<!-- 提现对话框仅使用本页行数据 -->
<el-dialog
:title="withdrawDialogTitle"
:visible.sync="withdrawDialogVisible"
width="720px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form :model="withdrawForm" :rules="withdrawRules" ref="withdrawForm" label-width="120px">
<!-- 提现链 -->
<el-form-item label="提现链">
<el-input :value="String((currentWithdrawRow.chain || '')).toUpperCase()" :disabled="true" />
</el-form-item>
<!-- 提现币种 -->
<el-form-item label="提现币种">
<el-input :value="displayWithdrawSymbol" :disabled="true" />
</el-form-item>
<!-- 提现金额 -->
<el-form-item label="提现金额" prop="amount">
<el-input
v-model="withdrawForm.amount"
placeholder="请输入提现金额"
inputmode="decimal"
@input="handleAmountInput"
>
<template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input>
<div class="balance-info">
可用余额 {{ availableWithdrawBalance }} {{ displayWithdrawSymbol }}
</div>
</el-form-item>
<!-- 手续费 -->
<el-form-item label="手续费">
<el-input v-model="withdrawForm.fee" :disabled="true">
<template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input>
<div class="fee-info">网络手续费 {{ withdrawForm.fee || '0' }} {{ displayWithdrawSymbol }}</div>
</el-form-item>
<!-- 实际到账 -->
<el-form-item label="实际到账">
<el-input :value="actualAmount" :disabled="true">
<template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input>
<div class="actual-amount-info">实际到账 {{ actualAmount }} {{ displayWithdrawSymbol }}</div>
</el-form-item>
<!-- 收款地址 -->
<el-form-item label="收款地址" prop="toAddress">
<el-input
v-model="withdrawForm.toAddress"
placeholder="请输入收款钱包地址"
:disabled="!withdrawAddressEditable"
ref="withdrawToAddressInput"
>
<template slot="append">
<el-button type="text" @click="handleEditAddressClick">修改</el-button>
</template>
</el-input>
<div class="address-tip">请确认地址正确错误地址将导致资产丢失</div>
</el-form-item>
<!-- 谷歌验证码 -->
<el-form-item label="谷歌验证码" prop="googleCode">
<el-input
v-model="withdrawForm.googleCode"
placeholder="请输入6位谷歌验证码"
maxlength="6"
@input="handleGoogleCodeInput"
>
<template slot="prepend">
<i class="el-icon-key"></i>
</template>
</el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="withdrawDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="withdrawLoading" @click="confirmWithdraw">确认提现</el-button>
</div>
</el-dialog>
<!-- 修改店铺弹窗 -->
<el-dialog title="修改店铺" :visible.sync="visibleEdit" width="520px">
<div class="row">
@@ -124,6 +210,14 @@
<label class="label">店铺描述</label>
<el-input type="textarea" :rows="3" v-model="editForm.description" placeholder="请输入描述" :maxlength="300" show-word-limit />
</div>
<div class="row">
<label class="label">手续费比例</label>
<el-input
v-model="editForm.feeRate"
placeholder="比例区间 0.01 - 0.1 之间最多6位小数"
@input="handleEditFeeRateInput"
/>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="visibleEdit=false">取消</el-button>
@@ -132,43 +226,7 @@
</el-dialog>
<!-- 修改钱包绑定配置弹窗参数保持与列表一致 -->
<el-dialog title="修改配置" :visible.sync="visibleConfigEdit" width="560px">
<div class="row">
<label class="label">支付链</label>
<el-input v-model="configForm.chainLabel" placeholder="-" disabled />
</div>
<div class="row">
<label class="label">支付币种</label>
<el-select
class="input"
size="middle"
v-model="configForm.payCoins"
multiple
collapse-tags
filterable
placeholder="请选择币种"
>
<el-option
v-for="item in editCoinOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="row">
<label class="label">已选择币种</label>
<div class="selected-coin-list">
<el-tag
v-for="c in selectedCoinLabels"
:key="c"
type="warning"
effect="light"
closable
@close="removeSelectedCoin(c)"
>{{ c }}</el-tag>
<span v-if="!selectedCoinLabels.length" style="color:#c0c4cc">未选择</span>
</div>
</div>
<div class="row">
<label class="label">钱包地址</label>
<el-input v-model="configForm.payAddress" placeholder="请输入钱包地址" />
@@ -186,7 +244,7 @@
import { getMyShop, updateShop, deleteShop, queryShop, closeShop ,updateShopConfig,deleteShopConfig,getChainAndCoin} from '@/api/shops'
import { coinList } from '@/utils/coinList'
import { getShopConfig } from '@/api/wallet'
import { getShopConfig,getShopConfigV2 ,withdrawBalanceForSeller,updateShopConfigV2} from '@/api/wallet'
export default {
@@ -200,11 +258,12 @@ export default {
name: '',
image: '',
description: '',
feeRate: '',
del: true,
state: 0
},
visibleEdit: false,
editForm: { id: '', name: '', image: '', description: '' },
editForm: { id: '', name: '', image: '', description: '', feeRate: '' },
// 店铺配置列表
shopConfigs: [],
visibleConfigEdit: false,
@@ -219,7 +278,19 @@ export default {
{ label: 'BSC (BEP20)', value: 'bsc' },
{ label: 'Nexa', value: 'nexa' },
],
shopLoading: false
shopLoading: false,
/* 提现弹窗状态(仅本页使用) */
withdrawDialogVisible: false,
withdrawLoading: false,
currentWithdrawRow: {},
withdrawForm: {
amount: '',
toAddress: '',
fee: '0.00',
googleCode: ''
},
withdrawAddressEditable: false,
withdrawRules: {}
}
},
computed: {
@@ -253,12 +324,231 @@ export default {
selectedCoinLabels() {
const map = new Map((this.editCoinOptions || []).map(o => [String(o.value), String(o.label).toUpperCase()]))
return (this.configForm.payCoins || []).map(v => map.get(String(v)) || String(v).toUpperCase())
},
/* 提现弹窗标题:如 USDT提现 */
withdrawDialogTitle() {
const sym = String((this.currentWithdrawRow && this.currentWithdrawRow.payCoin) || '').toUpperCase() || 'USDT'
return `${sym}提现`
},
/* 提现币种(大写) */
displayWithdrawSymbol() {
return String((this.currentWithdrawRow && this.currentWithdrawRow.payCoin) || '').toUpperCase()
},
/* 可用余额最多6位小数显示 */
availableWithdrawBalance() {
const n = Number((this.currentWithdrawRow && this.currentWithdrawRow.balance) || 0)
return this.formatDec6(n)
},
/* 实际到账金额 = 可用余额 - 手续费(只显示可用余额,不展示总余额/冻结) */
actualAmount() {
const amountInt = this.toScaledInt(this.withdrawForm.amount)
const feeInt = this.toScaledInt(this.withdrawForm.fee)
if (!Number.isFinite(amountInt) || !Number.isFinite(feeInt)) return '0'
const res = amountInt - feeInt
return res > 0 ? this.formatDec6FromInt(res) : '0'
}
},
created() {
this.fetchMyShop()
},
methods: {
/** 余额展示:带币种单位 */
formatBalance(row) {
try {
const num = Number(row && row.balance)
const valid = Number.isFinite(num)
const coin = String(row && row.payCoin ? row.payCoin : '').toUpperCase()
if (!valid) return '-'
const text = String(num)
return coin ? `${text} ${coin}` : text
} catch (e) {
return '-'
}
},
/** 仅数字部分(用于红色显示) */
formatAmount(row) {
try {
const num = Number(row && row.balance)
if (!Number.isFinite(num)) return '-'
return String(num)
} catch (e) {
return '-'
}
},
/** 仅币种单位(大写) */
formatCoin(row) {
return String(row && row.payCoin ? row.payCoin : '').toUpperCase()
},
/** 打开提现对话框(行数据驱动) */
async handleWithdraw(row) {
this.currentWithdrawRow = row || {}
const fee = Number(row && (row.serviceCharge != null ? row.serviceCharge : row.charge))
this.withdrawForm.fee = Number.isFinite(fee) ? this.formatDec6(fee) : '0.00'
this.withdrawForm.amount = ''
// 收款地址默认填充为当前行地址,且禁用编辑
this.withdrawForm.toAddress = row && row.payAddress ? row.payAddress : ''
this.withdrawForm.googleCode = ''
this.withdrawAddressEditable = false
// 初始化校验规则
this.withdrawRules = {
amount: [
{ required: true, message: '请输入提现金额', trigger: 'blur' },
{ validator: this.validateWithdrawAmount, trigger: 'blur' }
],
// 地址为只读已填,不再要求用户输入
googleCode: [
{ required: true, message: '请输入谷歌验证码', trigger: 'blur' },
{ validator: this.validateGoogleCode, trigger: 'blur' }
]
}
this.withdrawDialogVisible = true
},
/* 点击“修改”启用收款地址编辑并聚焦 */
handleEditAddressClick() {
this.withdrawAddressEditable = true
this.$nextTick(() => {
const input = this.$refs.withdrawToAddressInput
if (input && input.focus) input.focus()
})
},
/* 提现金额输入(<=6位小数 */
handleAmountInput(v) {
let s = String(v || '')
s = s.replace(/[^0-9.]/g, '')
const i = s.indexOf('.')
if (i !== -1) {
s = s.slice(0, i + 1) + s.slice(i + 1).replace(/\./g, '')
const [intPart, decPart = ''] = s.split('.')
s = intPart + '.' + decPart.slice(0, 6)
}
this.withdrawForm.amount = s
},
/* 谷歌验证码仅数字 */
handleGoogleCodeInput(v) {
this.withdrawForm.googleCode = String(v || '').replace(/\D/g, '')
},
/* 确认提现 - 调用后端卖家提现接口 */
confirmWithdraw() {
this.$refs.withdrawForm.validate(async (valid) => {
if (!valid) return
this.withdrawLoading = true
try {
const row = this.currentWithdrawRow || {}
const payload = {
toChain: row.chain,
toSymbol: row.payCoin,
amount: Number(this.withdrawForm.amount),
toAddress: this.withdrawForm.toAddress,
fromAddress: row.payAddress || this.withdrawForm.toAddress || '',
code: this.withdrawForm.googleCode,
serviceCharge: Number(this.withdrawForm.fee) || 0
}
const res = await withdrawBalanceForSeller(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('提现申请已提交,请等待处理')
this.withdrawDialogVisible = false
this.fetchShopConfigs(this.shop.id)
}
} catch (e) {
console.error('卖家提现失败', e)
} finally {
this.withdrawLoading = false
}
})
},
/* 工具最多6位小数显示 */
formatDec6(value) {
if (value === null || value === undefined || value === '') return '0'
let s = String(value)
if (/e/i.test(s)) {
const n = Number(value)
if (!Number.isFinite(n)) return '0'
s = n.toFixed(20).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
}
const m = s.match(/^(-?)(\d+)(?:\.(\d+))?$/)
if (!m) return s
let intPart = m[2]
let decPart = m[3] || ''
if (decPart.length > 6) decPart = decPart.slice(0, 6)
return decPart ? `${intPart}.${decPart}` : intPart
},
/* 工具:金额字符串 -> 10^6 精度整数 */
toScaledInt(amountStr, decimals = 6) {
if (amountStr === null || amountStr === undefined) return 0
const normalized = String(amountStr).trim()
if (normalized === '') return 0
const re = new RegExp(`^\\d+(?:\\.(\\d{0,${decimals}}))?$`)
const match = normalized.match(re)
if (!match) {
const n = Number(normalized)
if (!Number.isFinite(n)) return 0
const scale = Math.pow(10, decimals)
return Math.round(n * scale)
}
const [intPart, decPartRaw] = normalized.split('.')
const decPart = (decPartRaw || '').padEnd(decimals, '0').slice(0, decimals)
const scale = Math.pow(10, decimals)
return Number(intPart) * scale + Number(decPart)
},
/* 工具10^6 精度整数 -> 最多6位小数字符串 */
formatDec6FromInt(intVal) {
const sign = intVal < 0 ? '-' : ''
const abs = Math.abs(intVal)
const scale = Math.pow(10, 6)
const intPart = Math.floor(abs / scale)
const decPart = String(abs % scale).padStart(6, '0')
const s = `${sign}${intPart}.${decPart}`
return s.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
},
/* 校验:提现金额 */
validateWithdrawAmount(rule, value, callback) {
const amtInt = this.toScaledInt(value)
if (!Number.isFinite(amtInt) || amtInt <= 0) { callback(new Error('请输入有效的金额')); return }
const feeInt = this.toScaledInt(this.withdrawForm.fee)
const balanceInt = this.toScaledInt((this.currentWithdrawRow && this.currentWithdrawRow.balance) || 0)
if (amtInt >= balanceInt) { callback(new Error('提现金额必须小于可用余额')); return }
if (amtInt <= feeInt) { callback(new Error('提现金额必须大于手续费')); return }
if (amtInt < 1000000) { callback(new Error('最小提现金额为 1')); return }
callback()
},
/* 校验:谷歌验证码 */
validateGoogleCode(rule, value, callback) {
const v = String(value || '')
if (!/^\d{6}$/.test(v)) { callback(new Error('谷歌验证码必须是6位数字')); return }
callback()
},
/**
* 手续费率显示最多6位小数去除多余的0空值显示为 '-'
*/
formatFeeRate(value) {
if (value === null || value === undefined || value === '') return '-'
const num = Number(value)
if (!Number.isFinite(num)) return '-'
const fixed = num.toFixed(6)
return fixed.replace(/\.?0+$/, '')
},
/**
* 修改弹窗 - 手续费输入允许一个小数点最多6位小数允许尾随点
*/
handleEditFeeRateInput(value) {
let v = String(value ?? this.editForm.feeRate ?? '')
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
}
const endsWithDot = v.endsWith('.')
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (decPart.length > 6) decPart = decPart.slice(0, 6)
if (intPart && intPart !== '0') intPart = String(Number(intPart))
if (endsWithDot && firstDot !== -1) {
this.editForm.feeRate = `${intPart || '0'}.`
return
}
this.editForm.feeRate = decPart ? `${intPart || '0'}.${decPart}` : (intPart || '')
},
// 简单的emoji检测覆盖常见表情平面与符号范围
hasEmoji(str) {
if (!str || typeof str !== 'string') return false
@@ -291,6 +581,7 @@ export default {
name: res.data.name,
image: res.data.image,
description: res.data.description,
feeRate: res.data.feeRate,
del: !!res.data.del,
state: Number(res.data.state || 0)
}
@@ -321,7 +612,7 @@ export default {
}
try {
const res = await getShopConfig({id:shopId})
const res = await getShopConfigV2({id:shopId})
if (res && (res.code === 0 || res.code === 200) && Array.isArray(res.data)) {
// 直接使用后端返回的数据children: [{payCoin,image}]
this.shopConfigs = res.data
@@ -389,15 +680,7 @@ export default {
},
submitConfigEdit() {
// 基础校验
if (!this.configForm.chainLabel && !this.configForm.chainValue) {
this.$message.warning('请选择支付链')
return
}
if (!this.configForm.payCoins || this.configForm.payCoins.length === 0) {
this.$message.warning('请选择支付币种')
return
}
// 仅校验钱包地址
const addr = (this.configForm.payAddress || '').trim()
if (!addr) {
this.$message.warning('请输入钱包地址')
@@ -405,11 +688,17 @@ export default {
}
const payload = {
id: this.configForm.id,
chain: this.configForm.chainValue || this.configForm.chainLabel,
payCoin: (this.configForm.payCoins || []).join(','),
chain: this.configForm.chainValue || this.configForm.chainLabel || '',
payAddress: this.configForm.payAddress
}
this.updateShopConfig(payload)
;(async () => {
const res = await updateShopConfigV2(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('保存成功')
this.visibleConfigEdit = false
this.fetchShopConfigs(this.shop.id)
}
})()
},
removeSelectedCoin(labelUpper) {
const label = String(labelUpper || '').toLowerCase()
@@ -429,7 +718,8 @@ export default {
id: res.data.id,
name: res.data.name,
image: res.data.image,
description: res.data.description
description: res.data.description,
feeRate: res.data.feeRate
}
@@ -439,7 +729,8 @@ export default {
id: this.shop.id,
name: this.shop.name,
image: this.shop.image,
description: this.shop.description
description: this.shop.description,
feeRate: this.shop.feeRate
}
this.$message.warning(res && res.msg ? res.msg : '未获取到店铺详情')
}
@@ -449,7 +740,8 @@ export default {
id: this.shop.id,
name: this.shop.name,
image: this.shop.image,
description: this.shop.description
description: this.shop.description,
feeRate: this.shop.feeRate
}
console.error('查询店铺详情失败:', error)
@@ -492,7 +784,19 @@ export default {
this.$message.warning('店铺描述不能超过300个字符')
return
}
// 手续费比例必填、0.01-0.1、最多6位小数
const rateRaw = String(this.editForm.feeRate || '').trim()
if (!rateRaw) {
this.$message.warning('请填写店铺手续费比例0.01 - 0.1最多6位小数')
return
}
const rateNum = Number(rateRaw)
const decOk = rateRaw.includes('.') ? ((rateRaw.split('.')[1] || '').length <= 6) : true
if (!Number.isFinite(rateNum) || rateNum < 0.01 || rateNum > 0.1 || !decOk) {
this.$message.warning('手续费比例需在 0.01 - 0.1 之间且小数位不超过6位')
return
}
this.editForm.feeRate = rateNum.toString()
const payload = { ...this.editForm }
const res = await updateShop(payload)
@@ -585,9 +889,9 @@ export default {
})
return
}
// 跳转到新增商品页面并传递店铺ID
// 直接跳转到“添加出售机器”页面并传递店铺ID(供后续扩展使用)
this.$router.push({
path: '/account/product-new',
path: '/account/product-machine-add',
query: { shopId: this.shop.id }
})
},
@@ -634,6 +938,14 @@ export default {
.guide-note { margin-top: 10px; color: #6b7280; font-size: 13px; background: #f9fafb; border: 1px dashed #e5e7eb; padding: 8px 10px; border-radius: 8px; }
.coin-list { display: flex; align-items: center; gap: 8px; }
.coin-img { width: 20px; height: 20px; border-radius: 4px; display: inline-block; }
/* 提现弹窗样式(与钱包页统一) */
.balance-info { font-size: 12px; color: #666; margin-top: 4px; text-align: left; }
.fee-info { font-size: 12px; color: #e6a23c; margin-top: 4px; text-align: left; }
.actual-amount-info { font-size: 12px; color: #67c23a; margin-top: 4px; text-align: left; font-weight: 500; }
.address-tip { font-size: 12px; color: #f56c6c; margin-top: 4px; line-height: 1.4; text-align: left; }
/* 余额数字红色显示 */
.balance-num { color: #ff4d4f; font-weight: 600; }
.balance-unit { color: #606266; }
</style>
<style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
<h2 class="panel-title">新增店铺</h2>
<div class="panel-body">
<div class="row">
<label class="label">店铺名称</label>
<label class="label required">店铺名称</label>
<el-input v-model="form.name" placeholder="请输入店铺名称" :maxlength="30" show-word-limit />
</div>
<!-- <div class="row">
@@ -28,7 +28,23 @@
</div>
</div>
<div class="row">
<el-button type="primary" @click="handleCreate">创建店铺</el-button>
<label class="label required">手续费比例</label>
<el-input
v-model="form.feeRate"
placeholder="比例区间 0.01 - 0.1 之间最多6位小数"
@input="handleFeeRateInput"
/>
</div>
<div class="row" style="margin-top:-6px;">
<div></div>
<div style="color:#909399; font-size:12px; text-align:left;">
为提升您的店铺曝光您可为平台交易设置手续费比例该手续费为商家向平台支付的交易佣金,手续费比例将作为影响店铺排名的关键因素,该比例越高您的店铺排名就越靠前
</div>
</div>
<div class="row" style="margin-top:50px;">
<div class="actions-center">
<el-button class="btn-wide" type="primary" @click="handleCreate">创建店铺</el-button>
</div>
</div>
</div>
</div>
@@ -39,7 +55,7 @@ import { getAddShop } from "@/api/shops";
export default {
data() {
return {
form: { name: "", description: "", image: "" },
form: { name: "", description: "", image: "", feeRate: "" },
};
},
mounted() {},
@@ -50,6 +66,32 @@ export default {
const emojiRegex = /[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{1FA70}-\u{1FAFF}\u2600-\u27BF]/u
return emojiRegex.test(str)
},
handleFeeRateInput(value) {
// 仅允许数字与一个小数点限制小数位最多6位保留尾随小数点便于继续输入
let v = String(value ?? this.form.feeRate ?? '')
// 过滤非法字符
v = v.replace(/[^0-9.]/g, '')
// 保留第一个小数点,其余移除
const firstDot = v.indexOf('.')
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
}
const endsWithDot = v.endsWith('.')
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
// 小数位最多6位
if (decPart.length > 6) decPart = decPart.slice(0, 6)
// 规整前导0范围本就小于1
if (intPart && intPart !== '0') intPart = String(Number(intPart))
// 允许输入以小数点结尾的临时态(例如 "0."
if (endsWithDot && firstDot !== -1) {
this.form.feeRate = `${intPart || '0'}.`
return
}
// 常规态:有小数部分或仅整数部分
this.form.feeRate = decPart ? `${intPart || '0'}.${decPart}` : (intPart || '')
},
async fetchAddShop() {
const res = await getAddShop(this.form);
if (res && res.code==200) {
@@ -133,7 +175,27 @@ export default {
return
}
// 手续费比例校验必填、0.01-0.1 且最多6位小数
const rateRaw = String(this.form.feeRate || '').trim()
if (!rateRaw) {
this.$message({
message: '请填写店铺手续费比例0.01 - 0.1最多6位小数',
type: 'warning',
showClose: true
})
return
}
const rateNum = Number(rateRaw)
const decOk = rateRaw.includes('.') ? ((rateRaw.split('.')[1] || '').length <= 6) : true
if (!Number.isFinite(rateNum) || rateNum < 0.01 || rateNum > 0.1 || !decOk) {
this.$message({
message: '手续费比例需在 0.01 - 0.1 之间且小数位不超过6位',
type: 'warning',
showClose: true
})
return
}
this.form.feeRate = rateNum.toString()
this.fetchAddShop(this.form)
},
@@ -149,7 +211,7 @@ export default {
}
.row {
display: grid;
grid-template-columns: 100px 1fr;
grid-template-columns: 140px 1fr;
gap: 12px;
align-items: center;
margin-bottom: 12px;
@@ -157,6 +219,21 @@ export default {
.label {
color: #666;
text-align: right;
white-space: nowrap; /* 左侧文字不换行 */
word-break: keep-all;
}
.actions-center {
grid-column: 1 / -1; /* 跨两列,居中显示 */
text-align: center;
}
.btn-wide {
min-width: 200px;
padding: 10px 28px;
}
.label.required::before {
content: '*';
color: #f56c6c;
margin-right: 4px;
}
.textarea-wrapper {

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,8 @@
import { getProductById } from '../../utils/productService'
import { addToCart } from '../../utils/cartManager'
import { getMachineInfo, getPayTypes } from '../../api/products'
import { addCart, getGoodsList } from '../../api/shoppingCart'
import { getMachineInfo, getPayTypes,getShopMachineList,addGoodsV2 } from '../../api/products'
import { truncateAmountByCoin, truncateTo6 } from '../../utils/amount'
import { getGoodsListV2 } from '../../api/shoppingCart'
export default {
name: 'ProductDetail',
@@ -142,27 +141,244 @@ export default {
pageSizes: [10, 20, 50],
currentPage: 1,
total: 0,
// 动态列表(模拟渲染)
dynamicMeta: {},
dynamicColumns: [],
dynamicRows: [],
dynamicSearch: {
visible: false,
keyword: ''
},
// 矿机种类0-ASIC1-GPU默认GPU
machineType: 1,
}
},
mounted() {
if (this.$route.params.id) {
this.params.id = this.$route.params.id
// 读取用户上次选择的矿机种类0: ASIC, 1: GPU
try{
const savedType = Number(window && window.localStorage ? window.localStorage.getItem('pl_machineType') : NaN)
if (savedType === 0 || savedType === 1) {
this.machineType = savedType
}
}catch(e){/* noop */}
// 不再使用本地模拟数据,动态表格完全依赖后端返回的 columns/rows
// 仅当路由携带 shopId 时,才发起店铺商品请求
const routeShopId =
(this.$route && this.$route.params && (this.$route.params.shopId || this.$route.params.id)) ||
(this.$route && this.$route.query && this.$route.query.shopId)
if (routeShopId) {
this.params.id = routeShopId
this.product = true
// 默认展开第一行
if (this.productListData && this.productListData.length) {
this.expandedRowKeys = [this.productListData[0].id]
}
this.fetchGetMachineInfo(this.params)//priceSort 价格powerSort 算力功耗powerDissipationSort 布尔类型true 升序false降序
this.fetchGetMachineInfo(this.buildQueryParams())
this.fetchPayTypes()
} else {
this.$message.error('商品不存在')
this.$message.warning('缺少店铺IDshopId无法加载商品列表')
this.product = false
}
this.fetchGetGoodsList()
},
methods: {
// 动态表格单元格格式化(金额/算力/天数/文本)- 统一最多显示6位小数hover展示完整
formatDynamicCell(row, col) {
try{
let val = row[col.key]
if (val === null || val === undefined || val === '') return { text: '—', full: '—', truncated: false }
if (col.type === 'amount') {
// 价格列:单位取 priceList 里的 coin默认取第一条选择支付方式后按选择展示
if (col.key === 'price') {
if (Array.isArray(row.priceList) && row.priceList.length) {
const pv = this.getDisplayPrice(row)
const pc = this.getDisplayPriceCoin(row)
if (pv !== null && pv !== undefined) val = pv
const nPrice = val
const t = truncateTo6(nPrice)
const coinUnit = (pc || '').toString().toUpperCase()
return {
text: coinUnit ? `${t.text} ${coinUnit}` : t.text,
full: coinUnit ? `${t.full} ${coinUnit}` : t.full,
truncated: t.truncated
}
}
// 无 priceList仅展示数值不附加任何单位
const t = truncateTo6(val)
return { text: t.text, full: t.full, truncated: t.truncated }
}
// 列级优先:若列声明 currency=USDT则固定展示为 "xx.xx USDT"
const colCurrency = (col.currency || '').toString().toUpperCase()
if (colCurrency === 'USDT') {
const t = truncateTo6(val)
return { text: `${t.text} USDT`, full: `${t.full} USDT`, truncated: t.truncated }
}
// 兜底:不再使用 meta 的货币符号,直接返回数值
const t = truncateTo6(val)
return { text: t.text, full: t.full, truncated: t.truncated }
}
if (col.type === 'hashrate') {
const unit = col.unit ? ` ${col.unit}` : ''
const t = truncateTo6(val)
return { text: `${t.text}${unit}`, full: `${t.full}${unit}`, truncated: t.truncated }
}
if (col.type === 'days') {
const n = Number(val)
if (!Number.isFinite(n)) return { text: String(val), full: String(val), truncated: false }
const s = `${Math.floor(n)}`
return { text: s, full: s, truncated: false }
}
const s = String(val)
return { text: s, full: s, truncated: false }
}catch(e){ return { text: '—', full: '—', truncated: false } }
},
/**
* 如果存在 priceList则用第一条的 chain|coin 作为默认筛选值,
* 使“支付方式筛选”与价格列默认展示一致
*/
ensureDefaultPayFilterFromPrices() {
try{
if (this.payFilterDefaultApplied) return
const rows = Array.isArray(this.dynamicRows) ? this.dynamicRows : []
const firstWithPriceList = rows.find(r => Array.isArray(r && r.priceList) && r.priceList.length)
if (!firstWithPriceList) return
const first = firstWithPriceList.priceList[0]
const chain = String(first && first.chain || '').trim()
const coin = String(first && first.coin || '').trim()
if (!chain && !coin) return
this.selectedPayKey = `${chain}|${coin}`
this.filters.chain = chain
this.filters.coin = coin
this.payFilterDefaultApplied = true
}catch(e){ /* noop */ }
},
/**
* 获取行在当前支付方式下的展示价格
* 优先匹配 selectedPayKeychain|coin否则回退 priceList[0];再否则回退 row.price
*/
getDisplayPrice(row){
try{
const list = Array.isArray(row && row.priceList) ? row.priceList : []
if (!list.length) return row && row.price
const key = this.selectedPayKey
if (key) {
const [chainRaw, coinRaw] = String(key).split('|')
const chain = String(chainRaw || '').toUpperCase().trim()
const coin = String(coinRaw || '').toUpperCase().trim()
const hit = list.find(it =>
String(it && it.chain).toUpperCase().trim() === chain &&
String(it && it.coin).toUpperCase().trim() === coin
)
if (hit && hit.price !== undefined && hit.price !== null) return hit.price
}
const first = list[0]
if (first && first.price !== undefined && first.price !== null) return first.price
return row && row.price
}catch(e){ return row && row.price }
},
/**
* 获取行在当前支付方式下价格的币种coin
*/
getDisplayPriceCoin(row){
try{
const list = Array.isArray(row && row.priceList) ? row.priceList : []
if (!list.length) return ''
const key = this.selectedPayKey
if (key) {
const [chainRaw, coinRaw] = String(key).split('|')
const chain = String(chainRaw || '').toUpperCase().trim()
const coin = String(coinRaw || '').toUpperCase().trim()
const hit = list.find(it =>
String(it && it.chain).toUpperCase().trim() === chain &&
String(it && it.coin).toUpperCase().trim() === coin
)
if (hit && hit.coin) return String(hit.coin)
}
const first = list[0]
return first && first.coin ? String(first.coin) : ''
}catch(e){ return '' }
},
_truncate(num, decimals=2){
try{
const f = Math.pow(10, decimals)
return (Math.floor(Number(num)*f)/f).toFixed(decimals)
}catch(e){ return String(num) }
},
// 判断是否为“框出来部分”的最后一列(最后一个 hashrate 列)
isLastHashrateColumn(colIdx){
try{
const cols = this.getRenderedColumns()
for (let i = cols.length - 1; i >= 0; i--) {
if (String(cols[i] && cols[i].type).toLowerCase() === 'hashrate') {
return i === colIdx
}
}
return false
}catch(e){ return false }
},
// 仅渲染前 8 个算力列,后接其它非算力列(如收益、回收期)
getRenderedColumns(){
try{
const cols = Array.isArray(this.dynamicColumns) ? this.dynamicColumns : []
const hashrate = cols.filter(c => String(c && c.type).toLowerCase() === 'hashrate').slice(0, 8)
const others = cols.filter(c => String(c && c.type).toLowerCase() !== 'hashrate')
return [...hashrate, ...others]
}catch(e){ return [] }
},
// 打开动态搜索弹窗
handleOpenDynamicSearch(){
this.dynamicSearch.visible = true
this.dynamicSearch.keyword = ''
},
// 确认搜索:向后端请求新的 columns/rows替换动态表格
async handleConfirmDynamicSearch(){
const keyword = (this.dynamicSearch.keyword || '').trim()
this.dynamicSearch.visible = false
await this.fetchDynamicTable({ shopId: this.params.id, type: 1, keyword })
},
// 拉取动态表格数据(占位实现:如果后端已就绪,直接替换为真实接口)
async fetchDynamicTable(params){
try{
// 这里预留与后端对接:
// 期待返回格式:{ code, data: { meta, columns, rows } }
// 示例中用本地 mock 演示:根据 keyword 过滤/调整列
// 如果没有 keyword就还原初始 mock
if (!params || !params.keyword) {
return
}
// 简单模拟:当 keyword 命中 'ERG',只保留 ERG + 价格/型号/回收期 等少量列
const kw = String(params.keyword).toUpperCase()
const baseCols = (this.dynamicColumns || []).filter(c => ['model','price','maxLeaseDays','monthIncome'].includes(c.key))
const hitCols = (this.dynamicColumns || []).filter(c => String(c.label || c.key).toUpperCase().includes(kw))
const nextCols = [...(baseCols.length?baseCols:[this.dynamicColumns[0]||[]]), ...hitCols]
if (nextCols.length) {
this.dynamicColumns = nextCols
// 行数据无需特别处理(真实环境后端会按列同步返回),这里保留原 rows
}
}catch(e){
// eslint-disable-next-line no-console
console.warn('fetchDynamicTable mock error', e)
}
},
// 切换矿机种类0-ASIC1-GPU
handleMachineTypeChange(){
// 切换前清空所有已勾选状态与确认弹窗
try {
if (Array.isArray(this.dynamicRows)) {
this.dynamicRows.forEach(r => { if (r) this.$set(r, '_selected', false) })
}
if (this.confirmAddDialog) {
this.confirmAddDialog.items = []
this.confirmAddDialog.visible = false
}
} catch (e) { /* noop */ }
// 变更类型后,重新请求数据与支付方式
this.fetchGetMachineInfo(this.buildQueryParams())
this.fetchPayTypes()
// 本地记住用户选择
try{
if (window && window.localStorage) {
window.localStorage.setItem('pl_machineType', String(this.machineType))
}
}catch(e){/* noop */}
},
// 行币种:优先行内 payCoin > coin其次取全局表头币种
getRowCoin(row) {
try {
@@ -217,9 +433,9 @@ export default {
this.fetchGetMachineInfo(params)
} catch (e) { /* noop */ }
},
// 组合查询参数(带上商品 id 与筛选条件
// 组合查询参数:店铺入口,必须包含 shopId 与 type0-ASIC1-GPU
buildQueryParams() {
const q = { id: this.params.id }
const q = { shopId: this.params.id, type: this.machineType }
// 分页参数始终透传
try {
if (this.params && this.params.pageNum != null) q.pageNum = this.params.pageNum
@@ -251,10 +467,12 @@ export default {
} catch (e) { /* noop */ }
return q
},
// 拉取支付方式
// 拉取支付方式(兼容 shopId
async fetchPayTypes() {
try {
const res = await getPayTypes({ productId: this.params.id })
// 现规则:商品详情由店铺入口进入,使用 shopId 查询支付方式
// 为兼容后端两种入参,优先传 shopId后端若仍使用 productId 也能兼容处理
const res = await getPayTypes({ shopId: this.params.id, productId: this.params.id })
// 接口示例:{ code: 0, data: [ { payChain, payCoin, payCoinImage, shopId } ], msg: '' }
if (res && (res.code === 0 || res.code === 200)) {
const list = Array.isArray(res.data) ? res.data : []
@@ -270,34 +488,34 @@ export default {
async fetchGetMachineInfo(params) {
this.productDetailLoading = true
const res = await getMachineInfo(params)
console.log(res)
if (res && res.code === 200) {
console.log(res.data, 'res.rows');
this.total = res.total||0;
// 新数据结构:机器为扁平 rows 列表;仅当后端返回有效支付方式时才覆盖,避免清空 getPayTypes 的结果
// 改为使用店铺机器列表接口
const res = await getShopMachineList(params)
if (res && (res.code === 200 || res.code === 0)) {
const root = (res && res.data) ? res.data : res
const columns = Array.isArray(root.columns) ? root.columns : (Array.isArray(res.columns) ? res.columns : [])
const rows = Array.isArray(root.rows) ? root.rows : (Array.isArray(res.rows) ? res.rows : [])
const total = Number(root.total != null ? root.total : (res.total != null ? res.total : 0))
this.total = Number.isFinite(total) ? total : 0
// 动态表格:列与行
this.dynamicColumns = columns
this.dynamicRows = rows.map(r => ({
saleNumbers: 0,
saleOutNumbers: 0,
leaseTime: 1,
purchaseQuantity: 1,
_selected: false,
...r
}))
// 根据 rows 的 priceList 设置默认支付方式
this.ensureDefaultPayFilterFromPrices()
// 若后端同步返回支付方式,刷新本地支付方式
try {
const payList = res && res.data && res.data.payConfigList
const payList = root && root.payConfigList
if (Array.isArray(payList) && payList.length) {
this.paymentMethodList = payList
this.ensureDefaultPayFilterSelection()
}
} catch (e) { /* keep existing paymentMethodList */ }
const rows = (res && res.data && (res.data.rows || res.data.list)) || (res && res.rows) || []
const normalized = (Array.isArray(rows) ? rows : []).map((m, idx) => ({
...m,
id: m && (m.id !== undefined && m.id !== null) ? m.id : `m-${idx}`,
leaseTime: (m && m.leaseTime && Number(m.leaseTime) > 0) ? Number(m.leaseTime) : 1,
_selected: false
}))
this.machineList = normalized
// 清空旧的两层结构数据,避免误用
this.productListData = []
this.expandedRowKeys = []
// 机器加载后尝试设置默认筛选
this.ensureDefaultPayFilterSelection()
this.$nextTick(() => {
this.machinesLoaded = true
})
} catch (e) { /* noop */ }
}
this.productDetailLoading = false
@@ -337,7 +555,7 @@ export default {
},
//查询购物车列表
async fetchGetGoodsList(params) {
const res = await getGoodsList(params)
const res = await getGoodsListV2(params)
// 统计当前商品在购物车中已有的机器ID用于禁用和默认勾选
try {
const productId = this.params && this.params.id ? Number(this.params.id) : Number(this.$route.params.id)
@@ -457,6 +675,16 @@ export default {
this.$set(row, '_selected', false)
return
}
// 无价格:不可选择
try {
const hasPrice = (Array.isArray(row && row.priceList) && row.priceList.some(it => it && it.price !== null && it.price !== undefined))
|| (row && row.price !== null && row.price !== undefined && row.price !== '')
if (!hasPrice) {
this.$message.warning('该机器暂无价格,无法选择')
this.$set(row, '_selected', false)
return
}
} catch (e) { /* noop */ }
const key = parentRow.id
const list = (this.selectedMap[key] && [...this.selectedMap[key]]) || []
const idx = list.findIndex(it => it && it.id === row.id)
@@ -552,88 +780,60 @@ export default {
}
},
// 打开确认弹窗:以当前界面勾选(_selected)为准,并在打开后清空左侧勾选状态
// 打开确认弹窗(基于动态表格的勾选行)
handleOpenAddToCartDialog() {
// 扫描当前所有系列下被勾选的机器
const groups = Array.isArray(this.productListData) ? this.productListData : []
const pickedAll = groups.flatMap(g => Array.isArray(g.productMachines) ? g.productMachines.filter(m => !!m && !!m._selected) : [])
const picked = pickedAll.filter(m => m && (m.saleState === 0 || m.saleState === undefined || m.saleState === null))
const rows = Array.isArray(this.dynamicRows) ? this.dynamicRows : []
const picked = rows.filter(r => !!r && !!r._selected)
if (!picked.length) {
this.$message.warning('请先勾选至少一台矿机')
return
}
if (picked.length < pickedAll.length) {
this.$message.warning('部分机器已售出或售出中,已自动为您排除')
}
// 使用弹窗中的固定快照,避免后续清空勾选影响弹窗显示
this.confirmAddDialog.items = picked.slice()
this.confirmAddDialog.items = picked.map(r => ({
...r,
leaseTime: Number(r.leaseTime || 1),
purchaseQuantity: Number(r.purchaseQuantity || 1)
}))
this.confirmAddDialog.visible = true
// 打开后立即把左侧复选框清空,避免“勾选了两个但弹窗只有一条”的不一致问题
this.$nextTick(() => {
try { this.clearAllSelections() } catch (e) { /* noop */ }
})
},
// 确认加入:调用后端购物车接口,传入裸数组 [{ productId, productMachineId }]
// 确认加入:调用 addGoodsV2按条提交GPU 不传 numbers
async handleConfirmAddSelectedToCart() {
// 以弹窗中的列表为准,避免与左侧勾选状态不一致
const allSelected = Array.isArray(this.confirmAddDialog.items) ? this.confirmAddDialog.items.filter(Boolean) : []
if (!allSelected.length) {
const items = Array.isArray(this.confirmAddDialog.items) ? this.confirmAddDialog.items.filter(Boolean) : []
if (!items.length) {
this.$message.warning('请先勾选至少一台矿机')
return
}
const productId = this.params && this.params.id ? this.params.id : (this.$route && this.$route.params && this.$route.params.id)
if (!productId) {
this.$message.error('商品ID缺失无法加入购物车')
return
}
// 裸数组,仅包含后端要求的两个字段
const payload = allSelected.map(item => ({
productId: productId,
productMachineId: item.id,
leaseTime: Number(item.leaseTime || 1)
}))
try {
const res = await this.fetchAddCart(payload)
// 若后端返回码存在,这里做一下兜底提示
if (!res || (res.code && Number(res.code) !== 200)) {
this.$message.error(res && res.msg ? res.msg : '加入购物车失败,请稍后重试')
return
}
// 立即本地更新禁用状态把刚加入的机器ID合并进本地集合
try {
allSelected.forEach(item => {
if (item && item.id) this.cartMachineIdSet.add(item.id)
this.$set(item, '_selected', false)
this.$set(item, '_inCart', true)
if (!item.leaseTime || Number(item.leaseTime) <= 0) this.$set(item, 'leaseTime', 1)
// 按接口要求:一次性传数组,每个对象代表一个勾选商品
const payload = items.map(it => {
const obj = {
id: it.id,
leaseTime: Number(it.leaseTime || 1)
}
if (this.machineType === 0) {
obj.numbers = Number(it.purchaseQuantity || 1)
}
return obj
})
const res = await addGoodsV2(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message({
message: `已加入 ${items.length} 台矿机到购物车`,
type: 'success',
duration: 3000,
showClose: true
})
this.$nextTick(() => this.autoSelectAndDisable())
} catch (e) { /* noop */ }
this.$message({
message: `已加入 ${allSelected.length} 台矿机到购物车`,
type: 'success',
duration: 3000,
showClose: true,
});
}
this.confirmAddDialog.visible = false
// 清空选中映射,然后重新加载数据(数据加载时会自动设置 _selected: false
this.selectedMap = {}
// 重新加载机器信息和购物车数据
this.fetchGetMachineInfo(this.params)
this.fetchGetGoodsList()
// 通知头部刷新服务端购物车数量
// 清空
try {
// 如果没有传数量header 会主动拉取服务端数量
window.dispatchEvent(new CustomEvent('cart-updated'))
(this.dynamicRows || []).forEach(r => { if (r) this.$set(r, '_selected', false) })
} catch (e) { /* noop */ }
// 通知头部刷新
try { window.dispatchEvent(new CustomEvent('cart-updated')) } catch (e) { /* noop */ }
} catch (e) {
console.error('加入购物车失败: ', e)
// eslint-disable-next-line no-console
console.error('addGoodsV2 error:', e)
this.$message.error('加入购物车失败,请稍后重试')
}
},

View File

@@ -35,6 +35,16 @@
<!-- 筛选栏 -->
<section class="filter-bar" aria-label="筛选条件">
<div class="filter-grid">
<!-- 矿机种类放在支付方式筛选前面 -->
<div class="filter-cell">
<label class="filter-title">矿机种类</label>
<div style="display:inline-flex;align-items:center;">
<el-radio-group v-model="machineType" size="small" @change="handleMachineTypeChange">
<el-radio-button :label="1">GPU</el-radio-button>
<el-radio-button :label="0">ASIC</el-radio-button>
</el-radio-group>
</div>
</div>
<!-- 支付方式筛选选择即触发查询 -->
<div class="filter-cell">
<label class="filter-title" for="payFilter">支付方式筛选</label>
@@ -47,6 +57,7 @@
size="small"
class="filter-control"
@change="handlePayFilterChange"
style="max-width: 260px;"
>
<template #prefix>
<img
@@ -80,292 +91,177 @@
</div>
</div>
<!-- 实际算力区间 -->
<div class="filter-cell center-title">
<label class="filter-title">实际算力</label>
<div class="range-controls">
<el-input-number v-model="filters.minPower" :min="0" :step="0.1" :precision="2" :controls="false" size="small" class="filter-control" />
<span class="filter-sep">-</span>
<el-input-number v-model="filters.maxPower" :min="0" :step="0.1" :precision="2" :controls="false" size="small" class="filter-control" />
<el-select v-model="filters.unit" placeholder="单位" size="small" class="filter-control" style="max-width: 140px;">
<el-option v-for="u in powerUnitOptions" :key="u" :label="u" :value="u" />
</el-select>
</div>
</div>
<!-- 功耗区间第二行左侧占两列 -->
<div class="filter-cell filter-cell--span-2 center-title">
<label class="filter-title">功耗(kw/h)</label>
<div class="range-controls">
<el-input-number v-model="filters.minPowerDissipation" :min="0" :step="0.1" :precision="2" :controls="false" size="small" class="filter-control" />
<span class="filter-sep">-</span>
<el-input-number v-model="filters.maxPowerDissipation" :min="0" :step="0.1" :precision="2" :controls="false" size="small" class="filter-control" />
<div class="filter-actions-inline">
<!-- 操作按钮 -->
<div class="filter-cell filter-actions">
<div class="action-row">
<el-button type="primary" size="small" @click="handleSearchFilters" aria-label="执行筛选">筛选查询</el-button>
<el-button size="small" @click="handleResetFilters" aria-label="重置筛选">重置</el-button>
</div>
</div>
</div>
<!-- 操作区已合并到功耗区间后面 -->
</div>
</section>
<section class="productList">
<!-- 单层产品列表 -->
<!-- 动态表格基于后端返回的 columns/rows 渲染 -->
<section v-if="dynamicColumns && dynamicColumns.length" class="dynamic-hashrate" aria-label="动态收益表">
<el-table
ref="machineTable"
class="series-table"
:data="machineList"
row-key="id"
:row-class-name="handleGetRowClass"
:data="dynamicRows"
border
stripe
size="small"
class="dynamic-table"
:header-cell-style="{ textAlign: 'left' }"
:cell-style="{ textAlign: 'left' }"
style="width: 100%"
>
<el-table-column width="46">
<template #default="scope">
<!-- 勾选框列首列 -->
<el-table-column width="46" fixed="left">
<template #default="{ row }">
<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 => handleManualSelectFlat(scope.row, checked)"
v-model="row._selected"
:title="isRowDisabled(row) ? (row && (row.saleState === 1 || row.saleState === 2) ? '该机器已售出或售出中,无法选择' : '该机器暂无价格,无法选择') : '选择该矿机'"
:disabled="isRowDisabled(row)"
@change="checked => handleManualSelectFlat(row, checked)"
/>
</template>
</el-table-column>
<el-table-column prop="theoryPower" label="理论算力" header-align="left" align="left" show-overflow-tooltip>
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.theoryPower).truncated"
:content="formatNum6(scope.row.theoryPower).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.theoryPower).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.theoryPower).text }}</span>
</span>
{{ scope.row.unit }}
</template>
</el-table-column>
<el-table-column header-align="left" align="left" show-overflow-tooltip>
<el-table-column
v-for="(col, colIdx) in getRenderedColumns()"
:key="col.key || colIdx"
:prop="col.key"
:label="col.label"
:fixed="col.fixed || false"
show-overflow-tooltip
>
<template #header>
<span class="sortable" :class="{ active: activeSortField==='powerSort' }" @click="handleToggleSort('powerSort')">
实际算力
<i class="sort-arrow" :class="[(sortStates && sortStates.powerSort) ? 'asc' : 'desc', activeSortField==='powerSort' ? 'active' : '']"></i>
</span>
<div class="col-header" :title="col.label">
<img v-if="col.icon" :src="col.icon" class="col-icon" alt="" />
<span>{{ col.label }}</span>
<!-- 算力列标题不再展示单位仅展示图标和名称 -->
<el-button
v-if="isLastHashrateColumn(colIdx)"
type="text"
class="more-action"
@click.stop="handleOpenDynamicSearch"
>更多</el-button>
</div>
</template>
<template #default="scope">
<span class="num-strong">
<template #default="{ row }">
<span :class="getCellClass(col)">
<el-tooltip
v-if="formatNum6(scope.row.computingPower).truncated"
:content="formatNum6(scope.row.computingPower).full"
v-if="formatDynamicCell(row, col).truncated"
:content="formatDynamicCell(row, col).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.computingPower).text }}
<i class="el-icon-more amount-more"></i>
</span>
<span>{{ formatDynamicCell(row, col).text }}</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.computingPower).text }}</span>
<span v-else>{{ formatDynamicCell(row, col).text }}</span>
</span>
{{ scope.row.unit }}
</template>
</el-table-column>
<!-- 仅在 ASIC 时显示总机器数量已售数量 -->
<el-table-column v-if="machineType === 0" prop="saleNumbers" label="总机器数">
<template #default="scope">
<span>{{ scope.row.saleNumbers != null ? scope.row.saleNumbers : '—' }}</span>
</template>
</el-table-column>
<el-table-column v-if="machineType === 0" prop="saleOutNumbers" label="已售数量">
<template #default="scope">
<span>{{ scope.row.saleOutNumbers != null ? scope.row.saleOutNumbers : '—' }}</span>
</template>
</el-table-column>
<el-table-column prop="powerDissipation" header-align="left" align="left">
<template #header>
<span class="sortable" :class="{ active: activeSortField==='powerDissipationSort' }" @click="handleToggleSort('powerDissipationSort')">
功耗(kw/h)
<i class="sort-arrow" :class="[(sortStates && sortStates.powerDissipationSort) ? 'asc' : 'desc', activeSortField==='powerDissipationSort' ? 'active' : '']"></i>
</span>
</template>
</el-table-column>
<el-table-column prop="algorithm" label="算法" header-align="left" align="left" />
<el-table-column prop="theoryIncome" header-align="left" align="left" show-overflow-tooltip>
<template #header>
单机理论收入(每日)
<span v-if="getFirstCoinSymbol()">{{ getFirstCoinSymbol() }}</span>
</template>
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.theoryIncome).truncated"
:content="formatNum6(scope.row.theoryIncome).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.theoryIncome).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.theoryIncome).text }}</span>
</span>
</template>
</el-table-column>
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)" header-align="left" align="left">
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.theoryUsdtIncome).truncated"
:content="formatNum6(scope.row.theoryUsdtIncome).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.theoryUsdtIncome).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.theoryUsdtIncome).text }}</span>
</span>
</template>
</el-table-column>
<el-table-column prop="type" label="矿机型号" header-align="left" align="left" />
<el-table-column label="最大可租赁(天)" header-align="left" align="left">
<template #default="scope">{{ getRowMaxLeaseDays(scope.row) }}</template>
</el-table-column>
<el-table-column label="租赁天数(天)" header-align="left" align="left">
<!-- 租赁天数始终显示用户手动填写 -->
<el-table-column prop="leaseTime" label="租赁天数(天)">
<template #default="scope">
<el-input-number
class="input-full"
v-model="scope.row.leaseTime"
:min="1"
:max="getRowMaxLeaseDays(scope.row)"
:step="1"
:precision="0"
:step="1"
:controls="false"
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="price" header-align="left" align="center" >
<template #header>
<span class="sortable" :class="{ active: activeSortField==='priceSort' }" @click="handleToggleSort('priceSort')">
单价 <span v-if="getPriceCoinSymbol()">{{ getPriceCoinSymbol() }}</span>
<i class="sort-arrow" :class="[(sortStates && sortStates.priceSort) ? 'asc' : 'desc', activeSortField==='priceSort' ? 'active' : '']"></i>
</span>
</template>
</el-table-column>
<!-- ASIC 专用购买数量用户输入 -->
<el-table-column v-if="machineType === 0" prop="purchaseQuantity" label="购买数量">
<template #default="scope">
<span class="price-strong">
<el-tooltip
v-if="formatAmount(scope.row.price, getRowCoin(scope.row)).truncated"
:content="formatAmount(scope.row.price, getRowCoin(scope.row)).full"
placement="top"
>
<span>
{{ formatAmount(scope.row.price, getRowCoin(scope.row)).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatAmount(scope.row.price, getRowCoin(scope.row)).text }}</span>
</span>
<el-input-number
class="input-full"
v-model="scope.row.purchaseQuantity"
:min="1"
:max="getRowMaxPurchase(scope.row)"
:precision="0"
:step="1"
:controls="false"
size="mini"
:disabled="getRowMaxPurchase(scope.row) <= 0"
@change="val => handlePurchaseQuantityChange(scope.row, val)"
/>
</template>
</el-table-column>
<el-table-column prop="saleState" label="售出状态" width="110" header-align="left" align="left" >
<!-- 总价ASIC=价格*天数*购买数量GPU=价格*天数 -->
<el-table-column prop="totalAmount" label="总价" header-align="left" align="left">
<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>
<span class="price-strong">{{ formatConfirmTotalText(scope.row) }}</span>
</template>
</el-table-column>
</el-table>
<!-- 动态表格 - 搜索弹窗 -->
<el-dialog
title="搜索币种/算法"
:visible.sync="dynamicSearch.visible"
width="420px"
>
<div class="dynamic-search-bar" style="display:flex;gap:10px;align-items:center;">
<el-input
v-model="dynamicSearch.keyword"
placeholder="输入币种代码或算法关键词"
clearable
@keyup.enter.native="handleConfirmDynamicSearch"
/>
<el-button type="primary" @click="handleConfirmDynamicSearch">搜索</el-button>
</div>
</el-dialog>
</section>
<div style="margin: 18px; text-align: right;">
<el-button type="primary" size="small" @click="handleOpenAddToCartDialog">加入购物车</el-button>
</div>
<!-- 确认加入购物车弹窗 -->
<el-dialog :visible.sync="confirmAddDialog.visible" width="80vw" :title="`确认加入购物车(共 ${confirmAddDialog.items.length} 台)`">
<el-dialog :visible.sync="confirmAddDialog.visible" width="70vw" :title="`确认加入购物车(共 ${confirmAddDialog.items.length} 台)`">
<div>
<el-table :data="confirmAddDialog.items" height="360" border stripe :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table-column prop="theoryPower" label="理论算力" header-align="left" align="left">
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.theoryPower).truncated"
:content="formatNum6(scope.row.theoryPower).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.theoryPower).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.theoryPower).text }}</span>
</span>
{{ scope.row.unit }}
</template>
</el-table-column>
<el-table-column label="实际算力" header-align="left" align="left">
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.computingPower).truncated"
:content="formatNum6(scope.row.computingPower).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.computingPower).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.computingPower).text }}</span>
</span>
{{ scope.row.unit }}
</template>
</el-table-column>
<el-table-column prop="algorithm" label="算法" width="120" header-align="left" align="left" />
<el-table-column prop="powerDissipation" label="功耗(kw/h)" header-align="left" align="left">
<template #default="scope">
<span class="num-strong">
<el-tooltip
v-if="formatNum6(scope.row.powerDissipation).truncated"
:content="formatNum6(scope.row.powerDissipation).full"
placement="top"
>
<span>
{{ formatNum6(scope.row.powerDissipation).text }}
<i class="el-icon-more amount-more"></i>
</span>
</el-tooltip>
<span v-else>{{ formatNum6(scope.row.powerDissipation).text }}</span>
</span>
</template>
</el-table-column>
<el-table-column label="租赁天数(天)" header-align="left" align="left">
<template #default="scope">{{ Number(scope.row.leaseTime || 1) }}</template>
</el-table-column>
<el-table-column prop="price" header-align="left" align="left">
<template #header>
单价 <span v-if="getPriceCoinSymbol()">{{ getPriceCoinSymbol() }}</span>
</template>
<el-table-column prop="model" label="型号" header-align="left" align="left" />
<el-table-column prop="price" label="价格" header-align="left" align="left">
<template #default="scope">
<span class="price-strong">
<el-tooltip
v-if="formatAmount(scope.row.price, getRowCoin(scope.row)).truncated"
:content="formatAmount(scope.row.price, getRowCoin(scope.row)).full"
v-if="formatDynamicCell(scope.row, { key: 'price', type: 'amount' }).truncated"
:content="formatDynamicCell(scope.row, { key: 'price', type: 'amount' }).full"
placement="top"
>
<span>
{{ formatAmount(scope.row.price, getRowCoin(scope.row)).text }}
<i class="el-icon-more amount-more"></i>
</span>
<span>{{ formatDynamicCell(scope.row, { key: 'price', type: 'amount' }).text }}</span>
</el-tooltip>
<span v-else>{{ formatAmount(scope.row.price, getRowCoin(scope.row)).text }}</span>
<span v-else>{{ formatDynamicCell(scope.row, { key: 'price', type: 'amount' }).text }}</span>
</span>
</template>
</el-table-column>
<el-table-column prop="leaseTime" label="租赁天数(天)" header-align="left" align="left">
<template #default="scope">{{ Number(scope.row.leaseTime || 1) }}</template>
</el-table-column>
<el-table-column v-if="machineType === 0" prop="purchaseQuantity" label="购买数量" header-align="left" align="left">
<template #default="scope">{{ Number(scope.row.purchaseQuantity || 0) }}</template>
</el-table-column>
<el-table-column prop="totalAmount" label="总价" header-align="left" align="left">
<template #default="scope">
<span class="price-strong">{{ formatConfirmTotalText(scope.row) }}</span>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
@@ -406,6 +302,157 @@ export default {
name: 'ProductDetail',
mixins: [Index],
methods: {
/**
* 判断列是否为价格/金额相关列,以控制样式为红色
* - 优先依据 col.type === 'amount'
* - 其次依据 key 命名包含 'price' / 'amount'
* - 再次依据 label 文案包含 '价' / '金额'
* @param {Object} col
* @returns {string} CSS 类名
*/
getCellClass(col) {
try {
if (!col) return 'num-strong'
if (String(col.type).toLowerCase() === 'amount') return 'price-strong'
const key = String(col.key || '').toLowerCase()
if (key.includes('price') || key.includes('amount')) return 'price-strong'
const label = String(col.label || '')
if (label.includes('价') || label.includes('金额')) return 'price-strong'
return 'num-strong'
} catch (e) {
return 'num-strong'
}
},
/**
* 纯字符串方式相乘,避免浮点误差
* @param {Array<string|number>} values
* @returns {string} 不带单位的结果字符串,精确还原小数位
*/
multiplyAsDecimal(values) {
const toIntScale = (n) => {
const s = String(n || 0).trim()
if (!s.includes('.')) return { int: BigInt(s || '0'), scale: 0 }
const [w, f] = s.split('.')
const frac = (f || '').replace(/[^0-9]/g, '')
const whole = (w || '0').replace(/[^0-9-]/g, '')
const sign = whole.startsWith('-') ? '-' : ''
const intStr = (sign ? whole.slice(1) : whole) + frac
const safe = intStr.replace(/^0+(?=\d)/, '') || '0'
return { int: BigInt(sign + safe), scale: frac.length }
}
let acc = 1n
let scale = 0
for (const v of values) {
const { int, scale: sc } = toIntScale(v)
acc = acc * int
scale += sc
}
const neg = acc < 0n
const absStr = (neg ? (-acc) : acc).toString()
if (scale === 0) return (neg ? '-' : '') + absStr
const pad = scale - absStr.length
const s = pad > 0 ? ('0'.repeat(pad) + absStr) : absStr
const i = s.length - scale
let out = s.slice(0, i) + '.' + s.slice(i)
// 去除多余的前导/尾随零
out = out.replace(/^(-?)0+(?=\d)/, '$1')
out = out.replace(/\.?0+$/, '')
if (out.startsWith('.')) out = '0' + out
if (out === '' || out === '-') out = '0'
return (neg ? '-' : '') + out
},
/**
* 截断到最多6位小数不四舍五入
* @param {string} s
* @param {number} maxDecimals
* @returns {string}
*/
truncateDecimalString(s, maxDecimals = 6) {
const str = String(s || '0')
if (!str.includes('.')) return str
const [w, f] = str.split('.')
if (f.length <= maxDecimals) return str
return `${w}.${f.slice(0, maxDecimals)}`
},
/**
* 确认弹窗“总价”展示(附币种)
* ASIC: price*leaseTime*purchaseQuantityGPU: price*leaseTime
* 采用字符串整数相乘避免精度问题展示最多6位小数截断不四舍五入
*/
formatConfirmTotalText(row) {
try {
const price = this.getDisplayPrice ? this.getDisplayPrice(row) : (row && row.price)
const coin = (this.getDisplayPriceCoin && this.getDisplayPriceCoin(row)) || ''
const lease = Number(row && row.leaseTime) || 1
const nums = [price, lease]
if (this.machineType === 0) {
const qty = Number(row && row.purchaseQuantity) || 1
nums.push(qty)
}
const mul = this.multiplyAsDecimal(nums)
const text = this.truncateDecimalString(mul, 6)
const unit = (coin || '').toString().toUpperCase()
return unit ? `${text} ${unit}` : text
} catch (e) {
return '—'
}
},
/**
* 获取该行可购买的最大数量(<= 总机器数)
* @param {Object} row
* @returns {number}
*/
getRowMaxPurchase(row) {
try {
const n = Number(row && row.saleNumbers)
if (!Number.isFinite(n) || n < 0) return 0
return Math.floor(n)
} catch (e) { return 0 }
},
/**
* 购买数量变更时,强制校验区间 [1, max] 且取整
* @param {Object} row
* @param {number} value
*/
handlePurchaseQuantityChange(row, value) {
try {
const max = this.getRowMaxPurchase(row)
let v = Number(value)
if (!Number.isFinite(v)) v = 1
if (v < 1) v = 1
if (max > 0 && v > max) v = max
v = Math.floor(v)
this.$set(row, 'purchaseQuantity', v)
} catch (e) {
this.$set(row, 'purchaseQuantity', 1)
}
},
/**
* 行是否存在任意价格(用于禁用勾选)
* 规则:
* - 若存在 priceList任一项的 price 非 null/undefined 即视为有价格
* - 否则回退 row.price 是否有效
*/
hasAnyPrice(row) {
try {
if (!row) return false
if (Array.isArray(row.priceList) && row.priceList.length) {
return row.priceList.some(it => it && it.price !== null && it.price !== undefined)
}
const v = row.price
return v !== null && v !== undefined && v !== ''
} catch (e) { return false }
},
/**
* 该行是否应禁用选择:已售出/售出中 或 无价格
*/
isRowDisabled(row) {
try {
if (!row) return true
if (row.saleState === 1 || row.saleState === 2) return true
return !this.hasAnyPrice(row)
} catch (e) { return true }
},
/**
* 获取行的最大可租赁天数
* 规则:优先 row.maxLeaseDays否则回退 365保证区间 [1, 365]
@@ -476,18 +523,9 @@ export default {
* 重置筛选
*/
handleResetFilters() {
this.selectedPayKey = null
this.filters = {
chain: '',
coin: '',
minPrice: null,
maxPrice: null,
minPower: null,
maxPower: null,
minPowerDissipation: null,
maxPowerDissipation: null,
unit: 'GH/S'
}
// 仅重置“单价区间”的值,不影响支付方式筛选及其它条件
this.filters.minPrice = null
this.filters.maxPrice = null
this.handleSearchFilters()
},
/**
@@ -593,21 +631,18 @@ export default {
* 覆盖 mixin 的多层版本:基于单层勾选打开确认弹窗
*/
handleOpenAddToCartDialog() {
const list = Array.isArray(this.machineList) ? this.machineList : []
const pickedAll = list.filter(it => !!it && !!it._selected)
const picked = pickedAll.filter(m => m && (m.saleState === 0 || m.saleState === undefined || m.saleState === null))
const list = Array.isArray(this.dynamicRows) ? this.dynamicRows : []
const picked = list.filter(it => !!it && !!it._selected)
if (!picked.length) {
this.$message.warning('请先勾选至少一台矿机')
return
}
if (picked.length < pickedAll.length) {
this.$message.warning('部分机器已售出或售出中,已自动为您排除')
}
this.confirmAddDialog.items = picked.slice()
this.confirmAddDialog.items = picked.map(r => ({
...r,
leaseTime: Number(r.leaseTime || 1),
purchaseQuantity: Number(r.purchaseQuantity || 0)
}))
this.confirmAddDialog.visible = true
this.$nextTick(() => {
try { (this.machineList || []).forEach(m => this.$set(m, '_selected', false)) } catch (e) { /* noop */ }
})
}
}
}
@@ -802,10 +837,11 @@ export default {
margin: 0 10px 16px 10px;
}
.filter-grid {
display: grid;
grid-template-columns: repeat(3, minmax(260px, 1fr));
gap: 14px 18px;
align-items: end;
display: flex;
flex-wrap: wrap;
gap: 10px 16px;
align-items: flex-end;
justify-content: flex-start;
}
.filter-cell {
display: flex;
@@ -837,11 +873,11 @@ export default {
color: #9aa4b2;
}
.filter-actions {
display: flex;
display: inline-flex;
align-items: center;
gap: 10px;
grid-column: 2 / 3; /* 放到中间这一格 */
}
.action-row { display: inline-flex; align-items: center; gap: 10px; }
.filter-actions-inline {
display: inline-flex;
@@ -850,6 +886,42 @@ export default {
margin-left: 12px;
}
/* 动态表格样式 */
.dynamic-hashrate {
margin: 10px;
background: #fff;
border: 1px solid #eef2f7;
border-radius: 8px;
padding: 10px;
}
.dynamic-table :deep(.el-table__header th) {
background: #fafcff;
}
.col-header { display: inline-flex; align-items: center; gap: 6px; }
.col-icon { width: 16px; height: 16px; border-radius: 3px; }
.col-unit { color: #94a3b8; font-size: 12px; }
.more-action { margin-left: 8px; color: #2563eb; font-weight: 600; }
.more-action {
font-size: 12px;
padding: 0 4px;
height: auto;
line-height: 1;
border: none;
}
.more-action:hover {
color: #1d4ed8;
text-decoration: underline;
}
.dynamic-search-bar :deep(.el-input__inner) {
font-size: 12px;
}
.el-dialog__title {
font-size: 16px !important;
font-weight: 600;
}
.input-full { width: 100%; }
:deep(.el-input-number.input-full) { width: 100%; }
@media (max-width: 1200px) {
.filter-grid { grid-template-columns: repeat(2, minmax(220px, 1fr)); }
.filter-cell--span-2 { grid-column: 1 / span 1; }

View File

@@ -1,4 +1,4 @@
import { getProductList } from '../../api/products'
import { getShopList } from '../../api/products'
export default {
name: 'ProductList',
data() {
@@ -172,134 +172,60 @@ export default {
screenCurrency: "",
searchAlgorithm: "",
params:{
coin: "",
algorithm: ""
pageNum: "1",
pageSize: "10",
keyword:""
},
productListLoading:false,
}
},
mounted() {
this.fetchGetList()
this.fetchShopList()
},
methods: {
/**
* 价格裁剪为两位小数(不四舍五入)
* 兼容区间字符串:"min-max" 或 单值
*/
formatPriceRange(input) {
try {
if (input === null || input === undefined) return '0.00'
const raw = String(input)
if (raw.includes('-')) {
const [lo, hi] = raw.split('-')
return `${this._truncate2(lo)}-${this._truncate2(hi)}`
}
return this._truncate2(raw)
} catch (e) {
return '0.00'
}
},
/**
* 将任意数字字符串截断为 2 位小数(不四舍五入)。
*/
_truncate2(val) {
if (val === null || val === undefined) return '0.00'
const str = String(val).trim()
if (!str) return '0.00'
const [intPart, decPart = ''] = str.split('.')
const two = decPart.slice(0, 2)
return `${intPart}.${two.padEnd(2, '0')}`
},
handleCurrencyChange(val){
try{
// 清空时el-select 的 clear 同时触发 change避免重复请求交由 handleCurrencyClear 处理
if (val === undefined || val === null || val === '') return
// 选择具体币种时,合并算法关键词一起查询
this.params.coin = val
const keyword = (this.searchAlgorithm || '').trim()
const req = keyword ? { coin: val, algorithm: keyword } : { coin: val }
this.fetchGetList(req)
// 可在此发起接口getProductList({ coin: val })
// this.fetchGetList({ coin: val })
}catch(e){
console.error('处理币种变更失败', e)
}
},
async fetchGetList(params) {
this.productListLoading = true
try {
const res = await getProductList(params)
console.log('API响应:', res)
if (res && res.code === 200) {
this.products = res.rows || []
console.log('商品数据:', this.products)
} else {
console.error('API返回错误:', res)
// 获取商场页面的店铺列表
async fetchShopList(params) {
this.productListLoading = true
try{
// 仅允许 pageNum、pageSize、keyword 三个参数
const payload = {
pageNum: this.params.pageNum,
pageSize: this.params.pageSize
}
const kw = params && typeof params === 'object' ? params.keyword : this.params.keyword
if (kw) payload.keyword = kw
const res = await getShopList(payload)
if (res && (res.code === 0 || res.code === 200)) {
// 这里直接将店铺列表赋值到 products用于页面渲染
this.products = Array.isArray(res.rows) ? res.rows : []
} else {
this.products = []
}
}catch(e){
console.error('获取店铺列表失败:', e)
this.products = []
}
} catch (error) {
console.error('获取商品列表失败:', error)
this.products = []
// 添加一些测试数据,避免页面空白
this.products = [
// {
// id: 1,
// name: "测试商品1",
// algorithm: "测试算法1",
// priceRange: "100-200",
// image: "https://img.yzcdn.cn/vant/apple-1.jpg"
// },
// {
// id: 2,
// name: "测试商品2",
// algorithm: "测试算法2",
// priceRange: "200-300",
// image: "https://img.yzcdn.cn/vant/apple-1.jpg"
// }
]
}
this.productListLoading = false
},
this.productListLoading = false
},
// 算法搜索(使用同一接口,传入 algorithm 参数)
handleAlgorithmSearch() {
const keyword = (this.searchAlgorithm || '').trim()
const next = { ...this.params }
if (keyword) {
next.algorithm = keyword
this.params.algorithm = keyword
} else {
delete next.algorithm
this.params.algorithm = ""
}
// 不重置下拉,只根据算法关键词查询
if (next.algorithm) this.fetchGetList({ ...next, coin: this.screenCurrency || undefined })
else this.fetchGetList(this.screenCurrency ? { coin: this.screenCurrency } : undefined)
this.params.keyword = keyword
this.fetchShopList(keyword ? { keyword } : undefined)
},
// 清空下拉时:只清 coin保留算法条件
handleCurrencyClear() {
this.screenCurrency = ""
this.params.coin = ""
const keyword = (this.searchAlgorithm || '').trim()
if (keyword) this.fetchGetList({ algorithm: keyword })
else this.fetchGetList()
},
// 清空算法时:只清 algorithm保留下拉 coin
// 清空搜索关键字
handleAlgorithmClear() {
this.searchAlgorithm = ""
this.params.algorithm = ""
const coin = this.screenCurrency
if (coin) this.fetchGetList({ coin })
else this.fetchGetList()
this.params.keyword = ""
this.fetchShopList()
},
handleProductClick(product) {
if (product.id || product.id == 0) {
this.$router.push(`/product/${product.id}`);
const id = (product && (product.shopId != null ? product.shopId : product.id))
if (id !== undefined && id !== null) {
this.$router.push(`/product/${id}`);
}

View File

@@ -3,41 +3,15 @@
<section class="container">
<h1 class="page-title">商品列表</h1>
<section class="filter-section">
<label class="required" style="margin-bottom: 10px">币种选择</label>
<div class="filter-row">
<!-- 币种下拉 -->
<el-select
class="input"
size="middle"
ref="screen"
v-model="screenCurrency"
placeholder="请选择"
@change="handleCurrencyChange"
@clear="handleCurrencyClear"
clearable
>
<el-option
v-for="item in currencyList"
:key="item.value"
:label="item.label"
:value="item.value"
>
<div style="display: flex; align-items: center">
<img :src="item.imgUrl" style="float: left; width: 20px" />
<span style="float: left; margin-left: 5px">{{ item.label }}</span>
</div>
</el-option>
</el-select>
<!-- 算法搜索框 -->
<el-input
v-model="searchAlgorithm"
size="middle"
placeholder="输入算法关键词"
placeholder="输入币种或算法搜索"
clearable
@clear="handleAlgorithmClear"
@keyup.enter.native="handleAlgorithmSearch"
style="width: 240px;"
class="search-input"
>
<template #append>
<el-button type="primary" @click="handleAlgorithmSearch">搜索</el-button>
@@ -47,8 +21,8 @@
</section>
<div class="product-list-grid">
<div
v-for="product in products"
:key="product.id"
v-for="(product, idx) in products"
:key="product.shopId || product.id || idx"
class="product-item"
@click="handleProductClick(product)"
tabindex="0"
@@ -57,8 +31,22 @@
<!-- <img :src="product.image || 'https://img.yzcdn.cn/vant/apple-1.jpg'" :alt="product.name" class="product-image" /> -->
<img src="../../assets/imgs/commodity.png" :alt="product.name" class="product-image" />
<div class="product-info">
<h4>商品: {{ product.name }}</h4>
<p style="font-size: 16px;margin-top: 10px;font-weight: bold;">算法: {{ product.algorithm }}</p>
<h4 class="title-line">
<span class="label">店铺</span>
<span class="value ellipsis" :title="product.shopName || product.name">{{ product.shopName || product.name }}</span>
</h4>
<p class="info-line coin-line">
<span class="label">币种</span>
<el-tooltip :content="product.coin" placement="top" :open-delay="80">
<span class="value ellipsis" tabindex="0" :aria-label="`币种 ${product.coin}`">{{ product.coin }}</span>
</el-tooltip>
</p>
<p class="info-line algorithm-line">
<span class="label">算法</span>
<el-tooltip :content="product.algorithm" placement="top" :open-delay="80">
<span class="value ellipsis" tabindex="0" :aria-label="`算法 ${product.algorithm}`">{{ product.algorithm }}</span>
</el-tooltip>
</p>
<div class="product-footer">
<div class="paytypes">
<span class="paytypes-label">支付方式</span>
@@ -74,7 +62,6 @@
</div>
<div class="right-meta">
<span class="product-sold" aria-label="已售数量">已售{{ product && product.saleNumber != null ? product.saleNumber : 0 }}</span>
<span class="shop-name">店铺{{ product && (product.shopName || product.name) }}</span>
</div>
</div>
</div>
@@ -90,7 +77,6 @@
</template>
<script>
import { listProducts } from "../../utils/productService";
import { addToCart } from "../../utils/cartManager";
import Index from "./index";
@@ -194,20 +180,70 @@ export default {
height: 40vh;
}
.product-image {
width: 68%;
height:65%;
width: 57%;
height:55%;
object-fit: cover;
margin-bottom: 12px;
}
.product-info {
width: 100%;
}
.title-line{
display: flex;
align-items: baseline;
gap: 6px;
font-size: 14px;
margin: 0 0 4px 0;
font-weight: normal;
}
.title-line .label{
font-weight: 700;
}
.info-line {
display: flex;
align-items: center;
gap: 6px;
}
.info-line .label { color: #334155; font-weight: 700; }
.info-line .value {
flex: 1;
min-width: 0;
}
.ellipsis {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.coin-line { font-size: 14px; margin-top: 8px; }
.algorithm-line { font-size: 14px; margin-top: 6px; }
.algorithm-line .value.bold { font-weight: bold; }
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.search-input{
width: 420px;
}
/* 轻度统一高度与圆角,保留 Element 默认视觉 */
::v-deep .search-input .el-input__inner{
height: 40px;
line-height: 40px;
border-radius: 6px 0 0 6px;
padding: 0 14px;
}
::v-deep .search-input .el-input__inner::placeholder{
color: #9aa4b2; /* 略浅的占位符 */
}
::v-deep .search-input .el-input__inner:focus{
box-shadow: 0 0 0 2px rgba(64,158,255,.12);
}
::v-deep .search-input .el-input-group__append .el-button{
height: 40px;
border-radius: 0 6px 6px 0;
padding: 0 16px;
}
.right-meta{
display: flex;
flex-direction: column;

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long