周五固定更新

This commit is contained in:
2025-11-07 16:30:03 +08:00
parent ccf22ff707
commit bea1aa8e4c
12 changed files with 1122 additions and 221 deletions

View File

@@ -87,3 +87,13 @@ export function getMachineInfoById(data) {
}
// 查获取商城商品支持的支付方式
export function getPayTypes(data) {
return request({
url: `/lease/product/getPayTypes`,
method: 'post',
data
})
}

View File

@@ -56,43 +56,43 @@ export function closeShop(id) {
// 根据 店铺id 查询店铺商品配置信息列表
export function getShopConfig(id) {
return request({
url: `/lease/shop/getShopConfig`,
method: 'post',
data: { id }
})
}
return request({
url: `/lease/shop/getShopConfig`,
method: 'post',
data: { id }
})
}
// 新增商铺配置
// 新增商铺配置
export function addShopConfig(data) {
return request({
url: `/lease/shop/addShopConfig`,
method: 'post',
data
})
}
return request({
url: `/lease/shop/addShopConfig`,
method: 'post',
data
})
}
// 根据配置id 修改配置
// 根据配置id 修改配置
export function updateShopConfig(data) {
return request({
url: `/lease/shop/updateShopConfig`,
method: 'post',
data
})
}
return request({
url: `/lease/shop/updateShopConfig`,
method: 'post',
data
})
}
// 根据配置id 删除配置
// 根据配置id 删除配置
export function deleteShopConfig(data) {
return request({
url: `/lease/shop/deleteShopConfig`,
method: 'post',
data
})
}
return request({
url: `/lease/shop/deleteShopConfig`,
method: 'post',
data
})
}
// 钱包配置(用于修改卖家钱包地址)----获取链(一级)和币(二级) 下拉列表(获取本系统支持的链和币种)
// 钱包配置(用于修改卖家钱包地址)----获取链(一级)和币(二级) 下拉列表(获取本系统支持的链和币种)
export function getChainAndCoin(data) {
return request({
url: `/lease/shop/getChainAndCoin`,

View File

@@ -106,6 +106,28 @@ export function getRecentlyTransaction(data) {
})
}
//绑定钱包前查询商品列表
export function getProductListForShopWalletConfig(data) {
return request({
url: `/lease/product/getProductListForShopWalletConfig`,
method: 'post',
data
})
}
//设置之前商品列表的新链的机器价格
export function updateProductListForShopWalletConfig(data) {
return request({
url: `/lease/product/updateProductListForShopWalletConfig`,
method: 'post',
data
})
}

View File

@@ -204,16 +204,7 @@ export default {
});
return
}
try {
const curPath = (this.$route && this.$route.path) || ''
const from = curPath.indexOf('/account/orders') === 0 ? 'buyer' : (curPath.indexOf('/account/seller-orders') === 0 ? 'seller' : '')
try { if (from) sessionStorage.setItem('orderDetailFrom', from) } catch (e) {}
if (from) {
this.$router.push({ path: `/account/order-detail/${id}`, query: { from } })
} else {
this.$router.push(`/account/order-detail/${id}`)
}
} catch (e) {
try { this.$router.push(`/account/order-detail/${id}`) } catch (e) {
this.$message({
message: '无法跳转到详情页',
type: 'error',

View File

@@ -37,21 +37,22 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="价格范围">
<!-- <el-form-item label="价格范围">
<el-input :value="product && product.priceRange" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
</el-form-item> -->
<el-form-item label="类型">
<el-input :value="product && (product.type === 1 ? '算力套餐' : '挖矿机器')" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-input :value="product && (product.state === 1 ? '下架' : '上架')" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
</el-col>
<!-- <el-col :span="24">
<el-form-item label="图片">
@@ -64,7 +65,7 @@
<el-col :span="24">
<el-form-item label="描述">
<el-input type="textarea" :rows="3" :value="product && product.description" disabled />
<el-input type="textarea" :rows="3" :value="product && product.description" disabled />
</el-form-item>
</el-col>
</el-row>
@@ -76,9 +77,9 @@
<div slot="header" class="section-title">机器组合</div>
<div v-if="machineList && machineList.length">
<el-table :data="machineList" border stripe style="width: 100%">
<el-table-column prop="user" label="挖矿账户" min-width="80" />
<el-table-column prop="id" label="矿机ID" min-width="60" />
<el-table-column prop="miner" label="机器编号" min-width="100" />
<el-table-column prop="user" label="挖矿账户" />
<el-table-column prop="id" label="矿机ID" />
<el-table-column prop="miner" label="机器编号" />
<el-table-column label="实际算力" width="100">
<template slot="header">
<el-tooltip content="实际算力为该机器在本矿池过去24H的平均算力" effect="dark" placement="top">
@@ -100,11 +101,15 @@
:class="{ 'changed-input': isCellChanged(scope.row, 'theoryPower') }"
style="max-width: 260px;"
>
<template slot="append">{{ scope.row.unit || '' }}</template>
<template slot="append">
<el-select v-model="scope.row.unit" size="mini" :disabled="isRowDisabled(scope.row)" class="append-select append-select--unit" style="width: 90px;">
<el-option v-for="u in unitOptions" :key="u" :label="u" :value="u" />
</el-select>
</template>
</el-input>
</template>
</el-table-column>
<el-table-column label="功耗(kw/h)" min-width="140">
<el-table-column label="功耗(kw/h)" >
<template #default="scope">
<el-input
v-model="scope.row.powerDissipation"
@@ -116,16 +121,16 @@
:class="{ 'changed-input': isCellChanged(scope.row, 'powerDissipation') }"
style="max-width: 260px;"
>
<template slot="append">kw/h</template>
<!-- <template slot="append">kw/h</template> -->
</el-input>
</template>
</el-table-column>
<el-table-column label="型号" min-width="140">
<el-table-column label="型号" >
<template #default="scope">
<el-input
v-model="scope.row.type"
size="small"
placeholder="矿机型号"
:maxlength="20"
:disabled="isRowDisabled(scope.row)"
@input="handleTypeCell(scope.$index)"
@@ -134,7 +139,7 @@
/>
</template>
</el-table-column>
<el-table-column label="售价(USDT)" min-width="140">
<el-table-column label="售价" width="188">
<template slot="header">
<el-tooltip effect="dark" placement="top">
<div slot="content">
@@ -145,11 +150,11 @@
</div>
<i class="el-icon-question label-help" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
<span>售价(USDT)</span>
<span>售价按结算币种</span>
</template>
<template slot-scope="scope">
<el-input
v-model="scope.row.price"
v-model="scope.row._priceEditing"
size="small"
inputmode="decimal"
:disabled="isRowDisabled(scope.row)"
@@ -158,11 +163,20 @@
:class="{ 'changed-input': isCellChanged(scope.row, 'price') }"
style="max-width: 260px;"
>
<template slot="append">USDT</template>
<template slot="append">
<el-select v-model="scope.row._selectedPayIndex" size="mini" @change="handlePayTypeChange(scope.$index)" class="append-select append-select--coin" style="width:120px;">
<el-option
v-for="(pt, i) in (scope.row.priceList || [])"
:key="pt.payTypeId || i"
:label="[String(pt.chain||'').toUpperCase(), String(pt.coin||'').toUpperCase()].filter(Boolean).join('-')"
:value="i"
/>
</el-select>
</template>
</el-input>
</template>
</el-table-column>
<el-table-column label="最大租赁天数(天)" min-width="140">
<el-table-column label="最大租赁天数(天)" width="100">
<template #default="scope">
<el-input
v-model="scope.row.maxLeaseDays"
@@ -248,6 +262,8 @@ export default {
// 可编辑字段快照(用于变更高亮)
fieldSnapshot: {},
updateLoading:false,
// 算力单位选项(与新增出售机器页面保持一致)
unitOptions: ['KH/S','MH/S','GH/S','TH/S','PH/S'],
}
},
@@ -269,6 +285,15 @@ export default {
},
methods: {
/** 结算币种切换时,更新当前编辑价格 */
handlePayTypeChange(index) {
const row = this.machineList && this.machineList[index]
if (!row) return
const sel = Number(row._selectedPayIndex || 0)
const list = Array.isArray(row.priceList) ? row.priceList : []
const target = list[sel] || {}
this.$set(this.machineList, index, { ...row, _priceEditing: String(target.price ?? '') })
},
/**
* 判断行是否不可编辑(已售出则禁用)
* @param {Object} row - 当前行数据
@@ -304,7 +329,13 @@ export default {
if (res && res.code === 200) {
this.machineList =res.rows
const rows = Array.isArray(res.rows) ? res.rows : []
this.machineList = rows.map(r => {
const list = Array.isArray(r.priceList) ? r.priceList : []
const sel = 0
const first = list[sel] || {}
return { ...r, _selectedPayIndex: sel, _priceEditing: String(first.price ?? '') }
})
this.refreshStateSnapshot()
this.refreshFieldSnapshot()
}
@@ -338,11 +369,15 @@ export default {
for (let i = 0; i < list.length; i += 1) {
const row = list[i]
if (!row || typeof row.id === 'undefined') continue
const priceMap = {}
if (Array.isArray(row.priceList)) {
row.priceList.forEach(p => { if (p) priceMap[String(p.payTypeId ?? '')] = String(p.price ?? '') })
}
snapshot[row.id] = {
theoryPower: String(row.theoryPower ?? ''),
powerDissipation: String(row.powerDissipation ?? ''),
type: String(row.type ?? ''),
price: String(row.price ?? ''),
priceMap,
maxLeaseDays: String(row.maxLeaseDays ?? ''),
}
}
@@ -358,6 +393,14 @@ export default {
isCellChanged(row, key) {
if (!row || typeof row.id === 'undefined') return false
const snap = this.fieldSnapshot[row.id] || {}
if (key === 'price') {
const sel = Number(row._selectedPayIndex || 0)
const pt = Array.isArray(row.priceList) && row.priceList[sel] ? row.priceList[sel] : null
const pid = String(pt && pt.payTypeId ? pt.payTypeId : sel)
const cur = String(pt && pt.price != null ? pt.price : '')
const ori = String((snap.priceMap && snap.priceMap[pid]) || '')
return cur !== ori
}
const current = String(row[key] ?? '')
const original = String(snap[key] ?? '')
return current !== original
@@ -438,7 +481,7 @@ export default {
// - 功耗6 位整数 + 4 位小数
// - 价格12 位整数 + 2 位小数
// - 其他保持原逻辑6 位小数)
let v = String(this.machineList[index][key] ?? '')
let v = String(key === 'price' ? (this.machineList[index]._priceEditing ?? '') : (this.machineList[index][key] ?? ''))
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
if (firstDot !== -1) {
@@ -460,22 +503,35 @@ export default {
if (intPart.length > 12) { intPart = intPart.slice(0, 12) }
if (decPart) { decPart = decPart.slice(0, 2) }
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
// 同步到当前选中结算币种的价格
this.$set(this.machineList[index], '_priceEditing', v)
const row = this.machineList[index]
const sel = Number(row._selectedPayIndex || 0)
if (Array.isArray(row.priceList) && row.priceList[sel]) {
this.$set(row.priceList[sel], 'price', v)
}
} else {
if (firstDot !== -1) {
const [i, d] = v.split('.')
v = i + '.' + (d ? d.slice(0, 6) : '')
}
}
const row = { ...this.machineList[index], [key]: v }
this.$set(this.machineList, index, row)
if (key !== 'price') {
const row = { ...this.machineList[index], [key]: v }
this.$set(this.machineList, index, row)
}
},
handlePriceBlur(index) {
const raw = String(this.machineList[index].price ?? '')
const raw = String(this.machineList[index]._priceEditing ?? '')
const pattern = /^\d{1,12}(\.\d{1,2})?$/
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
this.$message.warning('单价必须大于0整数最多12位小数最多2位')
const row = { ...this.machineList[index], price: '' }
this.$set(this.machineList, index, row)
this.$set(this.machineList[index], '_priceEditing', '')
const row = this.machineList[index]
const sel = Number(row._selectedPayIndex || 0)
if (Array.isArray(row.priceList) && row.priceList[sel]) {
this.$set(row.priceList[sel], 'price', '')
}
}
},
handleMaxLeaseDaysInput(index) {
@@ -567,7 +623,7 @@ export default {
const row = this.machineList[i]
const rowLabel = row && (row.miner || row.id || i + 1)
const theoryRaw = String(row.theoryPower ?? '')
const priceRaw = String(row.price ?? '')
const priceRaw = String(row._priceEditing ?? '')
const typeRaw = String(row.type ?? '')
const dissRaw = String(row.powerDissipation ?? '')
const daysRaw = String(row.maxLeaseDays ?? '')
@@ -598,7 +654,7 @@ export default {
const payload = this.machineList.map(m => ({
id: m.id,
powerDissipation: Number(m.powerDissipation ?? 0),
price: Number(m.price ?? 0),
priceList: Array.isArray(m.priceList) ? m.priceList.map(p => ({ ...p, price: Number(p && p.price != null && p.price !== '' ? p.price : 0) })) : [],
state: Number(m.state ?? 0),
theoryPower: Number(m.theoryPower ?? 0),
type: m.type || '',
@@ -641,17 +697,51 @@ export default {
.empty-text { color: #909399; text-align: center; padding: 12px 0; }
.label-help { margin-left: 4px; color: #909399; cursor: help; }
/* ::v-deep .el-form-item__content{
margin-left: 52px !important;
} */
</style>
<style>
.el-input-group__append, .el-input-group__prepend{
padding: 0 5px !important;
}
.account-product-detail .el-table .el-input,
.account-product-detail .el-table .el-textarea{
width: 94% !important; /* 仅限制表格内输入宽度,避免影响上面的基础信息区域 */
}
/* 基础信息表单保持满宽,确保“描述”与上方输入左侧对齐 */
.account-product-detail .detail-form .el-input,
.account-product-detail .detail-form .el-textarea{
width: 100% !important;
}
/* 让追加区裁剪内部元素,避免 el-select 下拉箭头溢出到单元格外 */
.el-input-group__append,
.el-input-group__prepend{
overflow: hidden;
}
/* 追加在输入框右侧的下拉(单位/结算币种)细节优化 */
.append-select .el-input__inner{
/* 预留更多箭头空间,避免被右侧裁剪 */
padding-right: 28px;
height: 30px;
line-height: 30px;
}
.append-select .el-select__caret{
right: 10px; /* 箭头往内侧移动,防止被裁切 */
transform: scale(.85); /* 缩小箭头,保证完全显示 */
}
.append-select .el-input__icon{
line-height: 30px; /* 垂直居中,避免上下被裁切 */
}
/* 变化高亮:为输入框外层添加红色边框,视觉醒目但不改变布局 */
.changed-input .el-input__inner,
.changed-input input.el-input__inner {
.changed-input input.el-input__inner,
/* 带有 append 时,同步高亮右侧追加区的边框,保证整体连贯 */
.changed-input .el-input-group__append {
border-color: #f56c6c !important;
}

View File

@@ -56,8 +56,7 @@
</el-form-item>
<el-form-item label="功耗" prop="powerDissipation">
<el-input
v-model="form.powerDissipation"
placeholder="示例0.01"
v-model="form.powerDissipation"
inputmode="decimal"
@input="handleNumeric('powerDissipation')"
style="width: 50%;"
@@ -79,7 +78,22 @@
<i class="el-icon-question label-help" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
</span>
<!-- 若商品定义了多个结算币种则按链-币种动态生成多个售价输入否则回退为旧的 USDT 单价 -->
<div v-if="payTypeDefs && payTypeDefs.length" class="cost-multi">
<div v-for="pt in payTypeDefs" :key="pt.key" class="cost-item">
<el-input
v-model="form.costMap[pt.key]"
placeholder="请输入价格"
inputmode="decimal"
@input="val => handleCostMapInput(pt.key, val)"
style="width: 50%;"
>
<template slot="append">{{ pt.label }}</template>
</el-input>
</div>
</div>
<el-input
v-else
v-model="form.cost"
placeholder="请输入成本USDT"
inputmode="decimal"
@@ -158,7 +172,7 @@
</div>
</template>
</el-table-column>
<el-table-column label="售价(USDT)" min-width="160">
<el-table-column label="售价(按结算币种)" min-width="220">
<template slot="header">
<el-tooltip effect="dark" placement="top">
<div slot="content">
@@ -169,20 +183,35 @@
</div>
<i class="el-icon-question label-help" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
<span>售价(USDT)</span>
<span>售价按结算币种</span>
</template>
<template slot-scope="scope">
<el-input
v-model="scope.row.price"
placeholder="价格"
inputmode="decimal"
@input="handleRowPriceInput(scope.$index)"
@blur="handleRowPriceBlur(scope.$index)"
style="width: 100%;"
>
<template slot="append">USDT</template>
</el-input>
<div class="price-multi">
<div v-if="payTypeDefs && payTypeDefs.length" class="price-items">
<div v-for="pt in payTypeDefs" :key="pt.key" class="price-item">
<el-input
v-model="scope.row.priceMap[pt.key]"
placeholder="价格"
inputmode="decimal"
@input="() => handleRowPriceMapInput(scope.$index, pt.key)"
@blur="() => handleRowPriceMapBlur(scope.$index, pt.key)"
>
<template slot="append">{{ pt.label }}</template>
</el-input>
</div>
</div>
<el-input
v-else
v-model="scope.row.price"
placeholder="价格"
inputmode="decimal"
@input="handleRowPriceInput(scope.$index)"
@blur="handleRowPriceBlur(scope.$index)"
style="width: 100%;"
>
<template slot="append">USDT</template>
</el-input>
</div>
</template>
</el-table-column>
@@ -265,6 +294,7 @@ export default {
type: '',
unit: 'TH/S',
cost: '',
costMap: {}, // { 'CHAIN-COIN': '123.45' }
maxLeaseDays: ''
},
confirmVisible: false,
@@ -301,15 +331,18 @@ export default {
],
unit: [ { required: true, message: '请选择算力单位', trigger: 'change' } ],
cost: [
{ required: true, message: '请填写机器成本USDT', trigger: 'blur' },
{
validator: (rule, value, callback) => {
validator(rule, value, callback) {
// 若为多结算币种模式,跳过此校验(统一售价由每种币种的输入框承担)
if (Array.isArray(this.payTypeDefs) && this.payTypeDefs.length > 0) {
callback()
return
}
const str = String(value || '')
if (!str) {
callback(new Error('请填写机器成本USDT'))
return
}
// 整数最多12位小数最多2位
const pattern = /^\d{1,12}(\.\d{1,2})?$/
if (!pattern.test(str)) {
callback(new Error('成本整数最多12位小数最多2位'))
@@ -421,6 +454,7 @@ export default {
selectedMachineRows: [],
saving: false,
lastCostBaseline: 0,
lastCostMapBaseline: {}, // { key: number }
lastTypeBaseline: '',
lastMaxLeaseDaysBaseline: 0,
lastPowerDissipationBaseline: 0,
@@ -455,10 +489,58 @@ export default {
}
},
created() {
this.initPayTypesFromRoute()
this.fetchMiners()
this.lastTypeBaseline = this.form.type
// 绑定基于组件实例的校验器,避免 this 丢失
if (this.rules && this.rules.cost) {
this.$set(this.rules, 'cost', [{ validator: this.validateCost, trigger: 'blur' }])
}
},
methods: {
/** 统一售价校验:多结算币种时跳过,单价时按 USDT 校验 */
validateCost(rule, value, callback) {
if (Array.isArray(this.payTypeDefs) && this.payTypeDefs.length > 0) {
callback()
return
}
const str = String(value || '')
if (!str) { callback(new Error('请填写机器成本USDT')); return }
const pattern = /^\d{1,12}(\.\d{1,2})?$/
if (!pattern.test(str)) { callback(new Error('成本整数最多12位小数最多2位')); return }
if (Number(str) <= 0) { callback(new Error('成本必须大于 0')); return }
callback()
},
/** 解析路由参数中的支付方式,生成标准定义 */
initPayTypesFromRoute() {
this.payTypeDefs = []
try {
const raw = this.$route.query.payTypes
if (!raw) return
const arr = JSON.parse(decodeURIComponent(raw))
if (!Array.isArray(arr)) return
const defs = []
arr.forEach(it => {
const chain = String(it && it.chain ? it.chain : '').toUpperCase()
const coin = String(it && it.coin ? it.coin : '').toUpperCase()
if (!chain && !coin) return
const key = [chain, coin].filter(Boolean).join('-')
const label = key
defs.push({ chain, coin, key, label })
})
// 去重
const map = new Map()
defs.forEach(d => { if (!map.has(d.key)) map.set(d.key, d) })
this.payTypeDefs = Array.from(map.values())
// 初始化统一售价映射
const initCostMap = {}
this.payTypeDefs.forEach(d => { initCostMap[d.key] = '' })
this.form.costMap = initCostMap
this.lastCostMapBaseline = { ...initCostMap }
} catch (e) {
this.payTypeDefs = []
}
},
handleBack() {
this.$router.back()
},
@@ -516,6 +598,36 @@ export default {
this.syncCostToRows()
}
},
/** 顶部多结算币种统一售价输入 */
handleCostMapInput(key, val) {
// 价格输入整数最多12位小数最多2位允许尾随小数点
let v = String(val ?? this.form.costMap[key] ?? '')
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
}
const endsWithDot = v.endsWith('.')
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (intPart.length > 12) intPart = intPart.slice(0, 12)
if (decPart) decPart = decPart.slice(0, 2)
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
this.$set(this.form.costMap, key, v)
// 同步到行:仅当行对应价格未设置或等于旧基线
const oldBaseline = Number(this.lastCostMapBaseline[key] ?? NaN)
this.selectedMachineRows = this.selectedMachineRows.map(row => {
const cur = Number((row.priceMap && row.priceMap[key]) ?? NaN)
const shouldFollow = !Number.isFinite(cur) || cur === oldBaseline
const nextPriceMap = { ...(row.priceMap || {}) }
if (shouldFollow) nextPriceMap[key] = v
return { ...row, priceMap: nextPriceMap }
})
const num = Number(v)
if (Number.isFinite(num)) this.$set(this.lastCostMapBaseline, key, num)
},
/**
* 顶部矿机型号输入限制20字符
*/
@@ -563,18 +675,24 @@ export default {
if (m) {
// 若已存在,沿用已编辑的价格、型号和状态
const existed = this.selectedMachineRows.find(r => r.miner === minerId)
const existedPriceMap = existed && existed.priceMap ? existed.priceMap : null
const defaultPriceMap = {}
if (this.payTypeDefs && this.payTypeDefs.length) {
this.payTypeDefs.forEach(d => { defaultPriceMap[d.key] = this.form.costMap[d.key] })
}
nextRows.push({
user: m.user,
coin: m.coin,
miner: m.miner,
realPower: m.realPower,
price: existed ? existed.price : this.form.cost,
price: existed ? existed.price : this.form.cost, // 兼容单价模式
powerDissipation: existed && existed.powerDissipation !== undefined ? existed.powerDissipation : this.form.powerDissipation,
theoryPower: existed && existed.theoryPower !== undefined ? existed.theoryPower : this.form.theoryPower,
unit: existed && existed.unit ? existed.unit : this.form.unit,
type: existed ? existed.type : this.form.type,
state: existed ? existed.state : 0, // 默认上架
maxLeaseDays: existed && existed.maxLeaseDays !== undefined ? existed.maxLeaseDays : this.form.maxLeaseDays
maxLeaseDays: existed && existed.maxLeaseDays !== undefined ? existed.maxLeaseDays : this.form.maxLeaseDays,
priceMap: existedPriceMap || defaultPriceMap
})
}
})
@@ -742,6 +860,38 @@ export default {
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
this.$set(this.selectedMachineRows[index], 'price', v)
},
/** 行内多结算币种价格输入 */
handleRowPriceMapInput(index, key) {
// 价格输入整数最多12位小数最多2位允许尾随小数点
const row = this.selectedMachineRows[index]
const map = { ...(row.priceMap || {}) }
let v = String(map[key] ?? '')
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
}
const endsWithDot = v.endsWith('.')
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (intPart.length > 12) intPart = intPart.slice(0, 12)
if (decPart) decPart = decPart.slice(0, 2)
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
map[key] = v
this.$set(this.selectedMachineRows[index], 'priceMap', map)
},
handleRowPriceMapBlur(index, key) {
const row = this.selectedMachineRows[index]
const raw = String((row.priceMap && row.priceMap[key]) ?? '')
const pattern = /^\d{1,12}(\.\d{1,2})?$/
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
this.$message.warning('价格必须大于0整数最多12位小数最多2位')
const map = { ...(row.priceMap || {}) }
map[key] = ''
this.$set(this.selectedMachineRows[index], 'priceMap', map)
}
},
handleRowPriceBlur(index) {
const raw = String(this.selectedMachineRows[index].price ?? '')
const pattern = /^\d{1,12}(\.\d{1,2})?$/
@@ -865,14 +1015,27 @@ export default {
this.$message.warning('存在行的矿机型号全是空格,请修正后再试')
return
}
// 校验:已选择机器的价格必须大于0
// 校验:价格与最大租赁天数
for (let i = 0; i < this.selectedMachineRows.length; i += 1) {
const row = this.selectedMachineRows[i]
const priceNum = Number(row && row.price)
if (!Number.isFinite(priceNum) || priceNum <= 0) {
const label = (row && (row.miner || row.user)) || i + 1
this.$message.warning(`${i + 1}行(机器:${label}) 价格必须大于0`)
return
if (this.payTypeDefs && this.payTypeDefs.length) {
for (let j = 0; j < this.payTypeDefs.length; j += 1) {
const def = this.payTypeDefs[j]
const raw = String(row && row.priceMap ? row.priceMap[def.key] : '')
const num = Number(raw)
if (!/^\d{1,12}(\.\d{1,2})?$/.test(raw) || !Number.isFinite(num) || num <= 0) {
const label = (row && (row.miner || row.user)) || i + 1
this.$message.warning(`${i + 1}行(机器:${label}) 价格(${def.label})必须大于0整数最多12位小数最多2位`)
return
}
}
} else {
const priceNum = Number(row && row.price)
if (!Number.isFinite(priceNum) || priceNum <= 0) {
const label = (row && (row.miner || row.user)) || i + 1
this.$message.warning(`${i + 1}行(机器:${label}) 价格必须大于0`)
return
}
}
// 校验:逐行最大租赁天数 1-365
const rawDays = String((row && row.maxLeaseDays) ?? '')
@@ -891,17 +1054,23 @@ export default {
const [user, coin] = this.selectedMiner.split('|')
this.saving = true
try {
// 若是多结算币种,组装 priceList否则沿用单价字段
const payload = {
productId: this.form.productId,
powerDissipation: this.form.powerDissipation,
theoryPower: this.form.theoryPower,
type: this.form.type,
unit: this.form.unit,
cost: this.form.cost,
cost: this.payTypeDefs && this.payTypeDefs.length ? Number(this.form.costMap && this.form.costMap[this.payTypeDefs[0].key]) || 0 : this.form.cost,
maxLeaseDays: this.form.maxLeaseDays,
productMachineURDVos: this.selectedMachineRows.map(r => ({
miner: r.miner,
price: Number(r.price) || 0,
price: this.payTypeDefs && this.payTypeDefs.length ? undefined : (Number(r.price) || 0),
priceList: this.payTypeDefs && this.payTypeDefs.length ? this.payTypeDefs.map(d => ({
chain: d.chain,
coin: d.coin,
price: Number(r.priceMap && r.priceMap[d.key]) || 0
})) : undefined,
state: r.state || 0,
type: r.type || this.form.type,
user: r.user,
@@ -937,6 +1106,20 @@ export default {
,
watch: {
'form.cost': function() { this.syncCostToRows() },
// 当统一售价映射变化时手动同步(深度监听)
form: {
deep: true,
handler(val, oldVal) {
// 仅在 costMap 深度变化且有多支付类型时,做轻量同步(不覆盖用户已手改的值)
if (!this.payTypeDefs || !this.payTypeDefs.length) return
if (!val || !val.costMap) return
Object.keys(val.costMap).forEach(k => {
if (oldVal && oldVal.costMap && val.costMap[k] !== oldVal.costMap[k]) {
this.handleCostMapInput(k, val.costMap[k])
}
})
}
},
'form.type': function() { this.updateMachineType() },
'form.maxLeaseDays': function() { this.syncMaxLeaseDaysToRows() },
'form.powerDissipation': function() { this.syncPowerDissipationToRows() },
@@ -980,5 +1163,37 @@ export default {
text-align: left;
padding-left: 18px !important;
}
/* 多结算币种价格输入的布局优化 */
.cost-multi { display: grid; gap: 8px; }
.cost-item { display: flex; align-items: center; }
.price-multi { display: grid; gap: 8px; }
.price-items { display: grid; gap: 8px; }
/* 让 链-币种 附加区同宽、居中显示,整体对齐 */
.price-item :deep(.el-input-group__append),
.cost-item :deep(.el-input-group__append){
width: 110px;
min-width: 110px;
text-align: center;
padding: 0 8px;
background: #f8fafc;
color: #606266;
}
/* 缩小输入高度并保持垂直居中 */
.price-item :deep(.el-input__inner),
.cost-item :deep(.el-input__inner){
height: 30px;
line-height: 30px;
}
/* 让组内附加区高度与输入一致 */
.price-item :deep(.el-input-group__append),
.cost-item :deep(.el-input-group__append){
height: 30px;
line-height: 30px;
}
/* 略微收紧间距,让整体更紧凑 */
.price-multi { gap: 6px; }
.price-items { gap: 6px; }
.cost-multi { gap: 6px; }
</style>

View File

@@ -30,18 +30,32 @@
style="width: 100%"
>
<!-- <el-table-column prop="id" label="ID" width="80" /> -->
<el-table-column prop="name" label="名称" min-width="100" />
<el-table-column prop="coin" label="币种" width="100" />
<el-table-column prop="priceRange" label="价格范围" width="150" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="coin" label="币种" />
<el-table-column label="支持结算方式" >
<template #default="scope">
<div class="paytypes">
<el-tooltip
v-for="(pt, idx) in (scope.row.payTypes || [])"
:key="idx"
:content="formatPayType(pt)"
placement="top"
:open-delay="80"
>
<img :src="pt.image" :alt="formatPayType(pt)" class="paytype-icon" />
</el-tooltip>
</div>
</template>
</el-table-column>
<!-- <el-table-column label="算力" min-width="140">
<template #default="scope">
<span>{{ scope.row.power }} {{ scope.row.unit }}</span>
</template>
</el-table-column> -->
<el-table-column prop="algorithm" label="算法" min-width="120" />
<el-table-column prop="algorithm" label="算法" />
<!-- <el-table-column prop="electricityBill" label="电费" width="100" /> -->
<!-- <el-table-column prop="incomeRate" label="收益率" width="100" /> -->
<el-table-column prop="type" label="商品类型" width="130">
<el-table-column prop="type" label="商品类型" >
<template #default="scope">
<el-tag :type="scope.row.type === 1 ? 'success' : 'warning'">
{{ scope.row.type === 1 ? '算力套餐' : '挖矿机器' }}
@@ -49,9 +63,9 @@
</template>
</el-table-column>
<el-table-column prop="saleNumber" label="已售数量" min-width="60" />
<el-table-column prop="totalMachineNumber" label="该商品总机器数量" min-width="60" />
<el-table-column prop="state" label="状态" width="100">
<el-table-column prop="saleNumber" label="已售数量" />
<el-table-column prop="totalMachineNumber" label="该商品总机器数量" />
<el-table-column prop="state" label="状态" >
<template #default="scope">
<el-tag :type="scope.row.state === 1 ? 'info' : 'success'">
{{ scope.row.state === 1 ? '下架' : '上架' }}
@@ -149,6 +163,17 @@ export default {
this.fetchTableData()
},
methods: {
/** 格式化支持结算币种的展示,如 TRON-USDT */
formatPayType(item) {
try {
const chain = (item && item.chain ? String(item.chain) : '').toUpperCase()
const coin = (item && item.coin ? String(item.coin) : '').toUpperCase()
if (chain && coin) return `${chain}-${coin}`
return chain || coin || ''
} catch (e) {
return ''
}
},
/** 初始化筛选选项 */
initOptions() {
try {
@@ -372,7 +397,22 @@ export default {
this.$message.warning('缺少商品ID')
return
}
this.$router.push({ path: '/account/product-machine-add', query: { productId: row.id, coin: row.coin, name: row.name } })
let payTypesParam = ''
try {
const pts = Array.isArray(row.payTypes) ? row.payTypes : []
payTypesParam = encodeURIComponent(JSON.stringify(pts))
} catch (e) {
payTypesParam = ''
}
this.$router.push({
path: '/account/product-machine-add',
query: {
productId: row.id,
coin: row.coin,
name: row.name,
payTypes: payTypesParam
}
})
}
}
}
@@ -416,7 +456,12 @@ export default {
}
::v-deep .el-form-item__content{
text-align: left;
text-align: left;
}
/* 支持结算币种的小图标样式 */
.paytypes { display: inline-flex; align-items: center; gap: 8px; }
.paytype-icon { width: 22px; height: 22px; border-radius: 4px; display: inline-block; }
</style>

View File

@@ -58,13 +58,61 @@
</el-form-item>
</el-form>
<!-- 绑定前预检测弹窗若存在关联商品先提示用户再继续绑定 -->
<el-dialog :visible.sync="preCheck.visible" width="80vw" :close-on-click-modal="false" title="检测到关联商品" @close="handlePreCheckClose">
<div style="margin-bottom:10px;">
<el-alert
type="warning"
:closable="false"
show-icon
description="检测到以下商品与本次绑定的链/币相关。继续绑定后,可能需要为这些商品配置该新链下的价格。是否继续?"
/>
</div>
<p style="color: red; font-size: 12px; margin-top: 6px;text-align: right;">* 请填写每个商品对应币种的价格,商品包含机器统一设置价格如需单台修改请在商品列表-详情页操作</p>
<el-table :data="preCheck.rows" height="360" border :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }">
<el-table-column label="商品名称" min-width="160">
<template #default="scope">{{ scope.row.name || scope.row.productName || scope.row.title || scope.row.product || '-' }}</template>
</el-table-column>
<el-table-column label="链" min-width="120">
<template #default> {{ (form.chain || '').toUpperCase() }} </template>
</el-table-column>
<el-table-column label="币种" min-width="120">
<template #default> {{ form.payCoin.split(',').map(s=>s.trim().toUpperCase()).join('') }} </template>
</el-table-column>
<el-table-column label="总矿机数" min-width="100">
<template #default="scope">{{ scope.row.totalMachineNumber != null ? scope.row.totalMachineNumber : (scope.row.total || scope.row.totalMachines || '-') }}</template>
</el-table-column>
<el-table-column label="商品状态" min-width="100">
<template #default="scope">{{ Number(scope.row.state) === 1 ? '下架' : '上架' }}</template>
</el-table-column>
<el-table-column v-for="sym in coinsForBind" :key="'price-'+sym" :label="sym + ' 价格'" min-width="160">
<template #default="scope">
<el-input
v-model="preCheck.rowPrices[getRowKey(scope.row, scope.$index)][sym]"
size="mini"
class="price-input"
placeholder="请输入"
inputmode="decimal"
>
<template #append>{{ sym }}</template>
</el-input>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="preCheck.visible = false">取消</el-button>
<el-button type="primary" :disabled="!canSubmitPreCheck" @click="handleConfirmBindAfterPreview">继续绑定</el-button>
</template>
</el-dialog>
</div>
</div>
</template>
<script>
import { getMyShop } from "@/api/shops";
import { getChainAndList, addWalletShopConfig } from "../../api/wallet";
import { getChainAndList, addWalletShopConfig,getProductListForShopWalletConfig,updateProductListForShopWalletConfig } from "../../api/wallet";
export default {
name: "AccountShopConfig",
@@ -116,6 +164,8 @@ export default {
// },
],
loading: false,
// 绑定前预检测弹窗数据
preCheck: { visible: false, rows: [], prices: {}, rowPrices: {} },
};
},
mounted() {
@@ -282,14 +332,159 @@ export default {
return
}
this.FetchAddWalletShopConfig(this.form);
// 新增步骤:绑定前预检测商品列表
this.preCheckBeforeBind()
},
/**
* 绑定前预检测:若接口返回有关联商品,则弹窗展示;否则直接走绑定流程
*/
async preCheckBeforeBind() {
try {
this.loading = true
const params = { chain: this.form.chain, payCoin: this.form.payCoin }
const res = await getProductListForShopWalletConfig(params)
const rows = Array.isArray(res && res.data) ? res.data : (Array.isArray(res && res.rows) ? res.rows : [])
if (rows && rows.length) {
this.preCheck.rows = rows
// 初始化各币种价格输入
const coins = (this.form.payCoin || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean)
const map = {}
coins.forEach(c => { if (!(c in this.preCheck.prices)) map[c] = '' })
this.preCheck.prices = { ...map, ...this.preCheck.prices }
// 初始化每行的价格容器
this.preCheck.rowPrices = this.preCheck.rowPrices || {}
this.preCheck.rows.forEach((r, idx) => {
const key = this.getRowKey(r, idx)
if (!this.preCheck.rowPrices[key]) this.$set(this.preCheck.rowPrices, key, {})
coins.forEach(c => { if (!(c in this.preCheck.rowPrices[key])) this.$set(this.preCheck.rowPrices[key], c, '') })
})
this.preCheck.visible = true
} else {
// 无关联商品,直接绑定并设置(机器列表为空)
await this.submitBindWithPrice([])
}
} catch (e) {
// 接口异常不阻塞绑定流程
await this.submitBindWithPrice([])
} finally {
this.loading = false
}
},
handleConfirmBindAfterPreview() {
// 校验价格必填
const coins = (this.form.payCoin || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean)
// 逐行校验
for (let i = 0; i < this.preCheck.rows.length; i++) {
const row = this.preCheck.rows[i]
const key = this.getRowKey(row, i)
const priceMap = (this.preCheck.rowPrices && this.preCheck.rowPrices[key]) || {}
for (const c of coins) {
const v = priceMap[c]
if (!v || Number(v) <= 0) {
this.$message.warning(`请填写第 ${i + 1}${c} 的价格`)
return
}
}
}
const groups = this.collectMachineGroups(this.preCheck.rows)
this.preCheck.visible = false
this.submitBindWithPrice(groups)
},
/** 收集每一行对应的机器ID分组兼容不同返回结构 */
collectMachineGroups(rows) {
const groups = []
const pushId = (arr, id) => { if (id != null && id !== '') arr.push(id) };
(rows || []).forEach((r, idx) => {
const ids = []
// 兼容多种返回结构,优先使用接口包含的 machineList每台矿机对象含 productMachineId
if (Array.isArray(r && r.machineList)) r.machineList.forEach(m => pushId(ids, m && (m.productMachineId != null ? m.productMachineId : m.id)))
if (Array.isArray(r && r.productMachineIdList)) r.productMachineIdList.forEach(id => pushId(ids, id))
if (r && r.productMachineId != null) pushId(ids, r.productMachineId)
if (Array.isArray(r && r.productMachineDtoList)) r.productMachineDtoList.forEach(m => pushId(ids, (m && (m.productMachineId != null ? m.productMachineId : m.id))))
if (Array.isArray(r && r.machines)) r.machines.forEach(m => pushId(ids, (m && (m.productMachineId != null ? m.productMachineId : m.id))))
if (Array.isArray(r && r.items)) r.items.forEach(m => pushId(ids, (m && (m.productMachineId != null ? m.productMachineId : m.id))))
const key = this.getRowKey(r, idx)
groups.push({ key, machineIds: ids })
})
return groups
},
/** 生成某一行的 key优先 productId/id */
getRowKey(row, index) {
if (row && row.productId != null) return String(row.productId)
if (row && row.id != null) return `p-${row.id}`
return `idx-${index}`
},
/** 提交绑定:使用 updateProductListForShopWalletConfig 完成绑定与价格设置 */
async submitBindWithPrice(machineGroups) {
try {
this.loading = true
const coins = (this.form.payCoin || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean)
const list = []
if (Array.isArray(machineGroups) && machineGroups.length) {
machineGroups.forEach(g => {
const priceMap = (this.preCheck.rowPrices && this.preCheck.rowPrices[g.key]) || {}
const priceStr = coins.map(c => priceMap[c] || '').join(',');
(g.machineIds || []).forEach(id => { list.push({ productMachineId: id, price: priceStr }) })
})
}
const payload = {
chain: this.form.chain,
symbol: this.form.payCoin,
payAddress: this.form.payAddress,
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
}
} catch (e) {
// 错误交由全局拦截或简单提示
} finally {
this.loading = false
}
},
handleReset() {
this.form = { chain: "", payAddress: "", payCoin: "" };
this.value = []
},
// 清空预检测中的价格输入
resetPreCheckPrices() {
try {
this.preCheck.prices = {}
this.preCheck.rowPrices = {}
} catch (e) { /* noop */ }
},
// 弹窗关闭时清空价格输入
handlePreCheckClose() {
this.resetPreCheckPrices()
},
},
computed: {
coinsForBind() {
return (this.form.payCoin || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean)
},
canSubmitPreCheck() {
if (!this.preCheck || !this.preCheck.visible) return false
const coins = this.coinsForBind
if (!coins.length) return false
// 所有行都需要填写
for (let i = 0; i < (this.preCheck.rows || []).length; i++) {
const row = this.preCheck.rows[i]
const key = this.getRowKey(row, i)
const priceMap = (this.preCheck.rowPrices && this.preCheck.rowPrices[key]) || {}
for (const c of coins) {
const v = priceMap[c]
if (!v || Number(v) <= 0) return false
}
}
return true
},
/**
* 已选择币种的可读展示(中文顿号分隔)
*/
@@ -353,5 +548,11 @@ export default {
.selected-coins { display: flex; flex-wrap: wrap; gap: 8px; min-height: 32px; align-items: center; margin-left: 79px;}
.selected-coins .el-tag { border-radius: 4px; }
.selected-coins .placeholder { color: #c0c4cc; }
/* 价格输入框获得焦点时高亮为红色,提示用户输入 */
.price-input :deep(.el-input__inner:focus) {
border-color: #f56c6c !important;
box-shadow: 0 0 0 1px #f56c6c inset;
}
</style>

View File

@@ -1036,7 +1036,7 @@ export default {
items.push({
product: shop.name || '',
coin: this.toUpperText(m.coin),
user: m.user,
user: m.user,
miner: m.miner,
unitPrice: Number(unitPrice || 0),
leaseTime: leaseDays,

View File

@@ -1,7 +1,7 @@
import { getProductById } from '../../utils/productService'
import { addToCart } from '../../utils/cartManager'
import { getMachineInfo } from '../../api/products'
import { getMachineInfo, getPayTypes } from '../../api/products'
import { addCart, getGoodsList } from '../../api/shoppingCart'
export default {
@@ -13,6 +13,21 @@ export default {
// 默认展开的行keys
expandedRowKeys: [],
selectedMap: {},
// 新接口:单层矿机列表 & 支付方式
machineList: [],
paymentMethodList: [],
// 筛选状态
selectedPayKey: null,
filters: {
chain: '',
coin: '',
minPrice: null,
maxPrice: null,
minPower: null,
maxPower: null,
minPowerDissipation: null,
maxPowerDissipation: null
},
params: {
id: "",
@@ -120,6 +135,7 @@ export default {
this.expandedRowKeys = [this.productListData[0].id]
}
this.fetchGetMachineInfo(this.params)
this.fetchPayTypes()
} else {
this.$message.error('商品不存在')
this.product = false
@@ -127,6 +143,41 @@ export default {
this.fetchGetGoodsList()
},
methods: {
// 组合查询参数(带上商品 id 与筛选条件)
buildQueryParams() {
const q = { id: this.params.id }
// 仅当用户真实填写(>0时才传参默认/空值不传
const addNum = (obj, key, name) => {
const raw = obj[key]
if (raw === null || raw === undefined || raw === '') return
const n = Number(raw)
if (Number.isFinite(n) && n > 0) q[name] = n
}
// 支付方式条件:有值才传
if (this.filters.chain && String(this.filters.chain).trim()) q.chain = String(this.filters.chain).trim()
if (this.filters.coin && String(this.filters.coin).trim()) q.coin = String(this.filters.coin).trim()
addNum(this.filters, 'minPrice', 'minPrice')
addNum(this.filters, 'maxPrice', 'maxPrice')
addNum(this.filters, 'minPower', 'minPower')
addNum(this.filters, 'maxPower', 'maxPower')
addNum(this.filters, 'minPowerDissipation', 'minPowerDissipation')
addNum(this.filters, 'maxPowerDissipation', 'maxPowerDissipation')
return q
},
// 拉取支付方式
async fetchPayTypes() {
try {
const res = await getPayTypes({ 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 : []
this.paymentMethodList = list
}
} catch (e) {
// 忽略错误,保持页面可用
this.paymentMethodList = []
}
},
async fetchGetMachineInfo(params) {
this.productDetailLoading = true
@@ -134,31 +185,26 @@ export default {
console.log(res)
if (res && res.code === 200) {
console.log(res.data, 'res.rows');
this.paymentMethodList = res.data.payConfigList || []
const list =res.data.machineRangeInfoList || []
const withKeys = list.map((group, idx) => {
const fallbackId = `grp-${idx}`
const groupId = group.id || group.onlyKey || (group.productMachineRangeGroupDto && group.productMachineRangeGroupDto.id)
const firstMachineId = Array.isArray(group.productMachines) && group.productMachines.length > 0 ? group.productMachines[0].id : undefined
// 为机器行设置默认租赁天数为1并确保未选中状态
const normalizedMachines = Array.isArray(group.productMachines)
? group.productMachines.map(m => ({
...m,
leaseTime: (m && m.leaseTime && Number(m.leaseTime) > 0) ? Number(m.leaseTime) : 1,
_selected: false // 确保所有机器行初始状态为未选中
}))
: []
return { ...group, id: groupId || (firstMachineId ? `m-${firstMachineId}` : fallbackId), productMachines: normalizedMachines }
})
this.productListData = withKeys
if (this.productListData.length && (!this.expandedRowKeys || !this.expandedRowKeys.length)) {
this.expandedRowKeys = [this.productListData[0].id]
}
// 产品机器加载完成后,依据购物车集合执行一次本地禁用与勾选
// 新数据结构:机器为扁平 rows 列表;仅当后端返回有效支付方式时才覆盖,避免清空 getPayTypes 的结果
try {
const payList = res && res.data && res.data.payConfigList
if (Array.isArray(payList) && payList.length) {
this.paymentMethodList = payList
}
} 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.$nextTick(() => {
this.machinesLoaded = true
// 已取消与购物车对比:不再自动禁用或勾选
})
}
@@ -503,9 +549,12 @@ export default {
// 取消所有商品勾选(内层表格的自定义 checkbox
clearAllSelections() {
try {
// 清空选中映射
// 清空选中映射(遗留字段)
this.selectedMap = {}
// 遍历所有系列与机器,复位 _selected
if (Array.isArray(this.machineList) && this.machineList.length) {
this.machineList.forEach(m => { if (m) this.$set(m, '_selected', false) })
return
}
const groups = Array.isArray(this.productListData) ? this.productListData : []
groups.forEach(g => {
const list = Array.isArray(g.productMachines) ? g.productMachines : []

View File

@@ -16,12 +16,13 @@
class="pay-item"
:aria-label="`支付方式 ${item.payChain}`"
>
<el-tooltip :content="formatPayTooltip(item)" placement="top" :open-delay="80">
<img
class="pay-icon"
:src="item.payCoinImage"
:alt="`${item.payChain} 支付`"
:title="item.payChain"
:src="getPayImageUrl(item)"
:alt="`${(item.payChain || '').toUpperCase()} ${(item.payCoin || '').toUpperCase()}`.trim()"
:title="formatPayTooltip(item)"
tabindex="0"
role="img"
@keydown.enter.prevent="handlePayIconKeyDown(item)"
@@ -31,99 +32,149 @@
</li>
</ul>
</section>
<!-- 筛选栏 -->
<section class="filter-bar" aria-label="筛选条件">
<div class="filter-grid">
<!-- 支付方式筛选选择即触发查询 -->
<div class="filter-cell">
<label class="filter-title" for="payFilter">支付方式筛选</label>
<el-select
id="payFilter"
v-model="selectedPayKey"
placeholder="全部"
clearable
filterable
size="small"
class="filter-control"
@change="handlePayFilterChange"
>
<el-option
v-for="(opt, i) in paymentMethodList"
:key="i"
:label="formatPayTooltip(opt)"
:value="`${opt.payChain || ''}|${opt.payCoin || ''}`"
>
<div class="pay-opt">
<img :src="getPayImageUrl(opt)" class="pay-icon" alt="" />
<span>{{ (opt.payChain || '').toUpperCase() }} - {{ (opt.payCoin || '').toUpperCase() }}</span>
</div>
</el-option>
</el-select>
</div>
<!-- 价格区间 -->
<div class="filter-cell center-title">
<label class="filter-title">单价区间<span v-if="getPriceCoinSymbol()">{{ getPriceCoinSymbol() }}</span></label>
<div class="range-controls">
<el-input-number v-model="filters.minPrice" :min="0" :step="1" :precision="0" :controls="false" size="small" class="filter-control" />
<span class="filter-sep">-</span>
<el-input-number v-model="filters.maxPrice" :min="0" :step="1" :precision="0" :controls="false" size="small" class="filter-control" />
</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" />
</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">
<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">
<!-- 产品列表可展开 -->
<!-- 单层产品列表 -->
<el-table
ref="seriesTable"
ref="machineTable"
class="series-table"
:data="productListData"
:data="machineList"
row-key="id"
:expand-row-keys="expandedRowKeys"
@expand-change="handleExpandChange"
@row-click="handleSeriesRowClick"
:row-class-name="handleGetSeriesRowClassName"
:row-class-name="handleGetRowClass"
:header-cell-style="{ textAlign: 'left' }"
:cell-style="{ textAlign: 'left' }"
style="width: 100%"
>
<el-table-column type="expand" width="46">
<template #default="outer">
<!-- 子表格展开后显示该行的多个可选条目来自 productMachines -->
<el-table :data="outer.row.productMachines" size="small" style="width: 100%" :show-header="true" :ref="'innerTable-' + outer.row.id" :row-key="'id'" :reserve-selection="false" :header-cell-style="{ textAlign: 'left' }" :cell-style="{ textAlign: 'left' }" :row-class-name="handleGetInnerRowClass">
<el-table-column width="46">
<template #default="scope">
<el-checkbox
v-model="scope.row._selected"
:disabled="scope.row.saleState === 1 || scope.row.saleState === 2"
:title="(scope.row.saleState === 1 || scope.row.saleState === 2) ? '该机器已售出或售出中,无法选择' : ''"
@change="checked => handleManualSelect(outer.row, scope.row, checked)"
/>
</template>
</el-table-column>
<!-- 列宽精简避免横向滚动 -->
<el-table-column prop="theoryPower" label="理论算力" min-width="160" header-align="left" align="left" show-overflow-tooltip>
<template #default="scope">{{ scope.row.theoryPower }} {{ scope.row.unit }}</template>
</el-table-column>
<el-table-column label="实际算力" min-width="160" header-align="left" align="left" show-overflow-tooltip>
<template #default="scope">{{ scope.row.computingPower }} {{ scope.row.unit }}</template>
</el-table-column>
<el-table-column prop="powerDissipation" label="功耗(kw/h)" min-width="140" header-align="left" align="left" />
<el-table-column prop="algorithm" label="算法" min-width="120" header-align="left" align="left" />
<el-table-column prop="theoryIncome" min-width="160" header-align="left" align="left" show-overflow-tooltip>
<template #header>单机理论收入(每日) <span v-show="outer.row.productMachines[0].coin">{{outer.row.productMachines[0].coin.toUpperCase() }}</span></template>
</el-table-column>
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)" min-width="170" header-align="left" align="left" />
<!-- 矿机型号置于最后不影响上层对齐 -->
<el-table-column prop="type" label="矿机型号" header-align="left" align="left" min-width="120" />
<el-table-column label="最大可租赁(天)" min-width="140" header-align="left" align="left">
<template #default="scope">{{ getRowMaxLeaseDays(scope.row) }}</template>
</el-table-column>
<el-table-column label="租赁天数(天)" min-width="150" header-align="left" align="left">
<template #default="scope">
<el-input-number
v-model="scope.row.leaseTime"
:min="1"
:max="getRowMaxLeaseDays(scope.row)"
:step="1"
:precision="0"
size="mini"
:disabled="scope.row.saleState === 1 || scope.row.saleState === 2"
controls-position="right"
@change="val => handleLeaseDaysChange(scope.row, val)"
/>
</template>
</el-table-column>
<el-table-column prop="saleState" label="售出状态" header-align="left" align="left" min-width="110">
<template #default="scope">
<el-tag :type="scope.row.saleState === 0 ? 'info' : (scope.row.saleState === 1 ? 'danger' : 'warning')">
{{ scope.row.saleState === 0 ? '未售出' : (scope.row.saleState === 1 ? '已售出' : '售出中') }}
</el-tag>
</template>
</el-table-column>
</el-table>
<el-table-column width="46">
<template #default="scope">
<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)"
/>
</template>
</el-table-column>
<!-- 外层列宽同样收紧避免横向滚动 -->
<el-table-column label="价格 (USDT)" header-align="left" align="left" min-width="120">
<template slot-scope="scope"><span class="price-strong">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.price }}</span></template>
<el-table-column prop="theoryPower" label="理论算力" min-width="160" header-align="left" align="left" show-overflow-tooltip>
<template #default="scope">{{ scope.row.theoryPower }} {{ scope.row.unit }}</template>
</el-table-column>
<el-table-column label="理论算力范围" min-width="220" header-align="left" align="left" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.theoryPowerRange }}</template>
<el-table-column label="实际算力" min-width="160" header-align="left" align="left" show-overflow-tooltip>
<template #default="scope">{{ scope.row.computingPower }} {{ scope.row.unit }}</template>
</el-table-column>
<el-table-column label="实际算力范围" min-width="200" header-align="left" align="left" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.computingPowerRange }}</template>
<el-table-column prop="powerDissipation" label="功耗(kw/h)" min-width="140" header-align="left" align="left" />
<el-table-column prop="algorithm" label="算法" min-width="120" header-align="left" align="left" />
<el-table-column prop="theoryIncome" width="120" header-align="left" align="left" show-overflow-tooltip>
<template #header>
单机理论收入(每日)
<span v-if="getFirstCoinSymbol()">{{ getFirstCoinSymbol() }}</span>
</template>
</el-table-column>
<el-table-column label="功耗范围" min-width="160" header-align="left" align="left">
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.powerRange }}</template>
<el-table-column prop="theoryUsdtIncome" label="单机理论收入(每日/USDT)" width="120" header-align="left" align="left" />
<el-table-column prop="type" label="矿机型号" header-align="left" align="left" min-width="120" />
<el-table-column label="最大可租赁(天)" min-width="140" header-align="left" align="left">
<template #default="scope">{{ getRowMaxLeaseDays(scope.row) }}</template>
</el-table-column>
<el-table-column label="数量" min-width="100" header-align="left" align="left">
<template slot-scope="scope">{{ scope.row.productMachineRangeGroupDto && scope.row.productMachineRangeGroupDto.number }}</template>
<el-table-column label="租赁天数(天)" min-width="150" header-align="left" align="left">
<template #default="scope">
<el-input-number
v-model="scope.row.leaseTime"
:min="1"
:max="getRowMaxLeaseDays(scope.row)"
:step="1"
:precision="0"
size="mini"
:disabled="scope.row.saleState === 1 || scope.row.saleState === 2"
controls-position="right"
@change="val => handleLeaseDaysChange(scope.row, val)"
/>
</template>
</el-table-column>
<el-table-column prop="price" header-align="left" align="left" min-width="120">
<template #header>
单价 <span v-if="getPriceCoinSymbol()">{{ getPriceCoinSymbol() }}</span>
</template>
<template #default="scope"><span class="price-strong">{{ scope.row.price }}</span></template>
</el-table-column>
<el-table-column prop="saleState" label="售出状态" header-align="left" align="left" min-width="110">
<template #default="scope">
<el-tag :type="scope.row.saleState === 0 ? 'info' : (scope.row.saleState === 1 ? 'danger' : 'warning')">
{{ scope.row.saleState === 0 ? '未售出' : (scope.row.saleState === 1 ? '已售出' : '售出中') }}
</el-tag>
</template>
</el-table-column>
</el-table>
</section>
@@ -146,7 +197,10 @@
<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" label="单价(USDT)" header-align="left" align="left">
<el-table-column prop="price" header-align="left" align="left">
<template #header>
单价 <span v-if="getPriceCoinSymbol()">{{ getPriceCoinSymbol() }}</span>
</template>
<template #default="scope"><span class="price-strong">{{ scope.row.price }}</span></template>
</el-table-column>
@@ -187,6 +241,87 @@ export default {
if (n > 365) return 365
return Math.floor(n)
},
/**
* 处理支付方式图片 URL去除服务端可能带入的换行/空白)
* @param {Object} item
*/
getPayImageUrl(item) {
try {
const src = (item && item.payCoinImage) ? String(item.payCoinImage) : ''
return src.trim()
} catch (e) {
return ''
}
},
/**
* 支付方式下拉变更:选择/清空即触发请求
* @param {{payChain?: string, payCoin?: string}|null} val
*/
handlePayFilterChange(val) {
try {
const s = typeof val === 'string' ? val : ''
if (!s) {
this.filters.chain = ''
this.filters.coin = ''
} else {
const [chain, coin] = s.split('|')
this.filters.chain = (chain || '').trim()
this.filters.coin = (coin || '').trim()
}
this.handleSearchFilters()
} catch (e) { /* noop */ }
},
/**
* 组合筛选参数并请求数据
*/
handleSearchFilters() {
const params = this.buildQueryParams()
this.fetchGetMachineInfo(params)
},
/**
* 重置筛选
*/
handleResetFilters() {
this.selectedPayKey = null
this.filters = {
chain: '',
coin: '',
minPrice: null,
maxPrice: null,
minPower: null,
maxPower: null,
minPowerDissipation: null,
maxPowerDissipation: null
}
this.handleSearchFilters()
},
/**
* 获取列表第一个条目的币种,安全返回大写字符串
* 用于表头显示币种,避免空数组时报错
*/
getFirstCoinSymbol() {
try {
const list = Array.isArray(this.machineList) ? this.machineList : []
const coin = list.length && list[0] && list[0].coin ? String(list[0].coin) : ''
return coin ? coin.toUpperCase() : ''
} catch (e) {
return ''
}
},
/**
* 获取价格单位(优先读取每行的 payCoin 字段)
*/
getPriceCoinSymbol() {
try {
const list = Array.isArray(this.machineList) ? this.machineList : []
// 寻找第一个存在 payCoin 的条目
const item = list.find(it => it && it.payCoin)
const unit = item && item.payCoin ? String(item.payCoin) : ''
return unit ? unit.toUpperCase() : ''
} catch (e) {
return ''
}
},
/**
* 限制并校验租赁天数:区间 [1, max],并取整
*/
@@ -232,6 +367,52 @@ export default {
// eslint-disable-next-line no-console
console.error('handlePayIconKeyDown error:', err);
}
},
/**
* 单层:切换勾选
* @param {Object} row - 当前机器行
* @param {boolean} checked - 勾选状态
*/
handleManualSelectFlat(row, checked) {
try {
if (!row) return
if (row.saleState === 1 || row.saleState === 2) {
this.$message.warning('该机器已售出或售出中,无法选择')
this.$set(row, '_selected', false)
return
}
this.$set(row, '_selected', !!checked)
} catch (e) {
// eslint-disable-next-line no-console
console.error('handleManualSelectFlat error:', e)
}
},
/**
* 单层:行样式(售出态高亮)
*/
handleGetRowClass({ row }) {
if (!row) return ''
return (row.saleState === 1 || row.saleState === 2) ? 'sold-row' : ''
},
/**
* 覆盖 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))
if (!picked.length) {
this.$message.warning('请先勾选至少一台矿机')
return
}
if (picked.length < pickedAll.length) {
this.$message.warning('部分机器已售出或售出中,已自动为您排除')
}
this.confirmAddDialog.items = picked.slice()
this.confirmAddDialog.visible = true
this.$nextTick(() => {
try { (this.machineList || []).forEach(m => this.$set(m, '_selected', false)) } catch (e) { /* noop */ }
})
}
}
}
@@ -400,6 +581,9 @@ export default {
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.pay-item-inner { display: inline-flex; align-items: center; gap: 8px; }
.pay-text { font-size: 12px; color: #2c3e50; }
.pay-icon:hover {
transform: translateY(-1px);
}
@@ -409,6 +593,76 @@ export default {
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.2);
}
/* 筛选栏样式 */
.filter-bar {
background: #ffffff;
border: 1px solid #eef2f7;
border-radius: 8px;
padding: 12px 16px;
margin: 0 10px 16px 10px;
}
.filter-grid {
display: grid;
grid-template-columns: repeat(3, minmax(260px, 1fr));
gap: 14px 18px;
align-items: end;
}
.filter-cell {
display: flex;
flex-direction: column;
align-items: start;
gap: 6px;
}
.filter-cell.center-title .filter-title { text-align: center; }
.filter-title {
font-size: 14px;
color: #34495E;
font-weight: 600;
margin-bottom: 8px;
}
.filter-control {
width: 100%;
max-width: 320px;
}
.range-controls {
display: flex;
align-items: center;
gap: 8px;
}
.range-controls :deep(.el-input-number) { width: 150px; }
.pay-opt { display: inline-flex; align-items: center; gap: 8px; }
.filter-sep {
color: #9aa4b2;
}
.filter-actions {
display: flex;
align-items: center;
gap: 10px;
grid-column: 2 / 3; /* 放到中间这一格 */
}
.filter-actions-inline {
display: inline-flex;
align-items: center;
gap: 10px;
margin-left: 12px;
}
@media (max-width: 1200px) {
.filter-grid { grid-template-columns: repeat(2, minmax(220px, 1fr)); }
.filter-cell--span-2 { grid-column: 1 / span 1; }
.filter-actions { grid-column: 1 / -1; justify-content: flex-end; }
}
@media (max-width: 768px) {
.filter-grid {
grid-template-columns: 1fr;
}
.filter-actions { grid-column: 1 / 2; justify-content: flex-end; }
}
/* 外层系列行:整行可点击 + 视觉增强 */
:deep(.series-clickable-row) {
cursor: pointer;

View File

@@ -60,9 +60,17 @@
<h4>商品: {{ product.name }}</h4>
<p style="font-size: 16px;margin-top: 10px;font-weight: bold;">算法: {{ product.algorithm }}</p>
<div class="product-footer">
<div class="price-wrap">
<span class="product-price">价格: {{ formatPriceRange(product.priceRange) }}</span>
<span class="unit">USDT</span>
<div class="paytypes">
<span class="paytypes-label">支付方式</span>
<el-tooltip
v-for="(pt, idx) in (product.payTypes || [])"
:key="idx"
:content="formatPayType(pt)"
placement="top"
:open-delay="80"
>
<img :src="pt.image" :alt="formatPayType(pt)" class="paytype-icon" />
</el-tooltip>
</div>
<span class="product-sold" aria-label="已售数量">已售{{ product && product.saleNumber != null ? product.saleNumber : 0 }}</span>
</div>
@@ -89,6 +97,19 @@ export default {
mounted() {},
methods: {
/**
* 将 payType 显示为 CHAIN-COIN 文本
*/
formatPayType(item) {
try {
const chain = (item && item.chain ? String(item.chain) : '').toUpperCase()
const coin = (item && item.coin ? String(item.coin) : '').toUpperCase()
if (chain && coin) return `${chain}-${coin}`
return chain || coin || ''
} catch (e) {
return ''
}
},
/**
* 处理商品点击 - 跳转到详情页
*/
@@ -195,6 +216,9 @@ export default {
.price-wrap { display: inline-flex; align-items: baseline; gap: 6px; }
.unit { color: #999; font-size: 12px; }
.product-sold { color: #64748b; font-size: 12px; }
.paytypes { display: inline-flex; align-items: center; gap: 8px; }
.paytype-icon { width: 22px; height: 22px; border-radius: 4px; display: inline-block; }
.paytypes-label { color: #64748b; font-size: 12px; }
.add-cart-btn {
background: #42b983;
color: #fff;