Files
webs/power_leasing/src/views/account/productMachineAdd.vue

1061 lines
39 KiB
Vue
Raw Normal View History

2025-09-26 16:40:38 +08:00
<template>
<div class="product-machine-add">
<div class="header">
<el-button type="text" @click="handleBack">返回</el-button>
<h2 class="title">添加出售机器</h2>
</div>
2025-11-21 16:23:46 +08:00
<!-- <el-alert
2025-10-20 10:15:13 +08:00
class="notice-alert"
type="warning"
show-icon
:closable="false"
title="新增出售机器必须在 M2pool 有挖矿算力记录才能添加出租"
description="建议稳定在 M2pool 矿池挖矿 24 小时之后,再添加出售该机器"
2025-11-21 16:23:46 +08:00
/> -->
2025-10-20 10:15:13 +08:00
2025-09-26 16:40:38 +08:00
<el-card shadow="never" class="form-card">
<el-form ref="machineForm" :model="form" :rules="rules" label-width="160px" size="small">
2025-11-21 16:23:46 +08:00
<el-form-item label="矿机种类">
<el-radio-group v-model="form.machineCategory" @change="handleMachineCategoryChange">
<el-radio label="ASIC">ASIC</el-radio>
<el-radio label="GPU">GPU</el-radio>
</el-radio-group>
2025-09-26 16:40:38 +08:00
</el-form-item>
2025-11-21 16:23:46 +08:00
<!-- ASIC币种与算法支持多个逗号分隔 -->
<el-form-item label="币种(多个用逗号隔开)" prop="coinsInput" :required="form.machineCategory === 'ASIC'">
<el-input
v-model="form.coinsInput"
placeholder="例如USDT, BTC, ETH"
style="width: 50%;"
/>
</el-form-item>
<el-form-item label="算法(多个用逗号隔开)" prop="algorithmsInput" :required="form.machineCategory === 'ASIC'">
<el-input
v-model="form.algorithmsInput"
placeholder="例如SHA-256, ETHASH"
style="width: 50%;"
/>
</el-form-item>
<div style="text-align:left; color:#909399; font-size:12px; margin:-6px 0 10px 160px;">
输入多个用逗号隔开
</div>
2025-09-26 16:40:38 +08:00
<el-form-item label="矿机型号">
2025-10-20 10:15:13 +08:00
<el-input style="width: 50%;" v-model="form.type" placeholder="示例:龍珠" :maxlength="20" @input="handleTypeInput" />
2025-09-26 16:40:38 +08:00
</el-form-item>
<el-form-item label="理论算力" prop="theoryPower">
<el-input
v-model="form.theoryPower"
placeholder="请输入单机理论算力"
inputmode="decimal"
@input="handleNumeric('theoryPower')"
2025-10-20 10:15:13 +08:00
style="width: 50%;"
2025-09-26 16:40:38 +08:00
/>
</el-form-item>
<el-form-item label="算力单位" prop="unit">
<el-select v-model="form.unit" placeholder="请选择算力单位">
2025-10-20 10:15:13 +08:00
<el-option label="KH/S" value="KH/S" />
2025-09-26 16:40:38 +08:00
<el-option label="MH/S" value="MH/S" />
<el-option label="GH/S" value="GH/S" />
<el-option label="TH/S" value="TH/S" />
<el-option label="PH/S" value="PH/S" />
</el-select>
</el-form-item>
2025-10-20 10:15:13 +08:00
<el-form-item label="最大租赁天数" prop="maxLeaseDays">
<el-input
v-model="form.maxLeaseDays"
placeholder="1-365"
inputmode="numeric"
@input="handleNumeric('maxLeaseDays')"
style="width: 50%;"
>
<template slot="append"></template>
</el-input>
</el-form-item>
<el-form-item label="功耗" prop="powerDissipation">
<el-input
2025-11-07 16:30:03 +08:00
v-model="form.powerDissipation"
2025-10-20 10:15:13 +08:00
inputmode="decimal"
@input="handleNumeric('powerDissipation')"
style="width: 50%;"
>
<template slot="append">kw/h</template>
</el-input>
</el-form-item>
2025-11-21 16:23:46 +08:00
<el-form-item label="统一售价" prop="cost" :required="true">
<span slot="label">统一售价</span>
2025-11-07 16:30:03 +08:00
<!-- 若商品定义了多个结算币种则按链-币种动态生成多个售价输入否则回退为旧的 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>
2025-10-20 10:15:13 +08:00
<el-input
2025-11-07 16:30:03 +08:00
v-else
2025-10-20 10:15:13 +08:00
v-model="form.cost"
placeholder="请输入成本USDT"
inputmode="decimal"
@input="handleNumeric('cost')"
style="width: 50%;"
>
<template slot="append">USDT</template>
</el-input>
</el-form-item>
2025-09-26 16:40:38 +08:00
2025-11-21 16:23:46 +08:00
<!-- 出售机器数量统一使用GPU 仅作引导不在本页提交 -->
<el-form-item label="出售机器数量(台)" prop="sellCount" :required="true">
<el-input
v-model="form.sellCount"
placeholder="0 - 9999"
inputmode="numeric"
style="width: 50%;"
@input="handleSellCountInput"
@blur="handleSellCountBlur"
/>
2025-09-26 16:40:38 +08:00
</el-form-item>
</el-form>
</el-card>
<div class="actions">
<el-button @click="handleBack">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSave">确认添加</el-button>
</div>
<!-- 上架确认弹窗 -->
<el-dialog
title="请确认上架信息"
:visible.sync="confirmVisible"
width="400px"
>
<div>
<p>请仔细确认已选择机器列表价格及相关参数定义</p>
<p style="text-align: left;">机器上架后一经售出在机器出售期间不能修改价格及机器参数</p>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="confirmVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="doSubmit">确认上架已选择机器</el-button>
</span>
</el-dialog>
2025-11-21 16:23:46 +08:00
<!-- GPU 引导弹窗 -->
<el-dialog title="GPU 客户端指引" :visible.sync="gpuDialogVisible" width="520px" @close="handleGpuDialogClosed">
<div style="text-align:left; line-height:1.8;">
<div style="display:flex; align-items:center; gap:12px; margin-bottom:12px;">
<el-button type="primary" @click="handleDownloadClient">下载客户端</el-button>
<el-button type="success" :disabled="!hasDownloadedClient" @click="handleGpuClientStarted">已启动客户端</el-button>
</div>
<div style="color:#555;">
<div style="margin-bottom:6px; font-weight:600;">注意事项</div>
<ol style="padding-left:18px;">
<li>请直接下载客户端后并启动客户端显示 GPU 信息后启动完成点击已启动客户端按钮若未完全启动点击按钮会创建失败</li>
<li>涉及多台主机每个主机都需要配置相同客户端</li>
<li>客户端身份信息和本网站身份信息一致</li>
</ol>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="gpuDialogVisible=false">关闭</el-button>
</span>
</el-dialog>
2025-09-26 16:40:38 +08:00
</div>
</template>
<script>
2025-11-21 16:23:46 +08:00
import { addSingleOrBatchMachine } from '../../api/machine'
2025-09-26 16:40:38 +08:00
export default {
name: 'AccountProductMachineAdd',
data() {
return {
form: {
productId: Number(this.$route.query.productId) || null,
coin: this.$route.query.coin || '',
productName: this.$route.query.name || '',
2025-11-21 16:23:46 +08:00
/** 矿机种类ASIC 或 GPU默认 ASIC */
machineCategory: 'ASIC',
/** 出售机器数量(仅 ASIC 模式使用) */
sellCount: '',
/** ASIC 模式下币种/算法输入(逗号分隔的原始文本) */
coinsInput: '',
algorithmsInput: '',
2025-09-26 16:40:38 +08:00
powerDissipation: null,
theoryPower: null,
type: '',
unit: 'TH/S',
2025-10-20 10:15:13 +08:00
cost: '',
2025-11-07 16:30:03 +08:00
costMap: {}, // { 'CHAIN-COIN': '123.45' }
2025-10-20 10:15:13 +08:00
maxLeaseDays: ''
2025-09-26 16:40:38 +08:00
},
confirmVisible: false,
rules: {
productName: [ { required: true, message: '商品名称不能为空', trigger: 'change' } ],
2025-11-21 16:23:46 +08:00
coinsInput: [
{
validator: (rule, value, callback) => {
if (String(this.form.machineCategory).toUpperCase() !== 'ASIC') { callback(); return }
const s = String(value || '').trim()
if (!s) { callback(new Error('请输入币种,多个用逗号隔开')); return }
callback()
},
trigger: 'blur'
}
],
algorithmsInput: [
{
validator: (rule, value, callback) => {
if (String(this.form.machineCategory).toUpperCase() !== 'ASIC') { callback(); return }
const s = String(value || '').trim()
if (!s) { callback(new Error('请输入算法,多个用逗号隔开')); return }
callback()
},
trigger: 'blur'
}
],
sellCount: [
{
validator: (rule, value, callback) => {
if (this.form.machineCategory !== 'ASIC') { callback(); return }
const raw = String(value ?? '')
if (raw === '') { callback(new Error('请输入出售机器数量')); return }
if (!/^\d{1,4}$/.test(raw)) { callback(new Error('请输入 0-9999 的整数')); return }
const n = Number(raw)
if (!Number.isInteger(n) || n < 0 || n > 9999) { callback(new Error('范围需在 0-9999')); return }
callback()
},
trigger: 'blur'
}
],
2025-09-26 16:40:38 +08:00
powerDissipation: [
{ required: true, message: '功耗不能为空', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const str = String(value || '')
if (!str) { callback(new Error('功耗不能为空')); return }
const pattern = /^\d{1,6}(\.\d{1,4})?$/
if (!pattern.test(str)) { callback(new Error('功耗整数最多6位小数最多4位')); return }
if (Number(str) <= 0) { callback(new Error('功耗必须大于0')); return }
callback()
},
trigger: 'blur'
}
],
theoryPower: [
{ required: true, message: '理论算力不能为空', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const str = String(value || '')
if (!str) { callback(new Error('理论算力不能为空')); return }
const pattern = /^\d{1,6}(\.\d{1,4})?$/
if (!pattern.test(str)) { callback(new Error('理论算力整数最多6位小数最多4位')); return }
if (Number(str) <= 0) { callback(new Error('理论算力必须大于0')); return }
callback()
},
trigger: 'blur'
}
],
unit: [ { required: true, message: '请选择算力单位', trigger: 'change' } ],
cost: [
{
2025-11-07 16:30:03 +08:00
validator(rule, value, callback) {
// 若为多结算币种模式,跳过此校验(统一售价由每种币种的输入框承担)
if (Array.isArray(this.payTypeDefs) && this.payTypeDefs.length > 0) {
callback()
return
}
2025-09-26 16:40:38 +08:00
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()
},
trigger: 'blur'
}
]
2025-10-20 10:15:13 +08:00
,
maxLeaseDays: [
{ required: true, message: '请填写最大租赁天数', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const raw = String(value ?? '')
if (!raw) { callback(new Error('请填写最大租赁天数')); return }
if (!/^\d{1,3}$/.test(raw)) { callback(new Error('仅允许整数,范围 1-365')); return }
const n = Number(raw)
if (!Number.isInteger(n) || n < 1 || n > 365) { callback(new Error('范围需在 1-365 天')); return }
callback()
},
trigger: 'blur'
}
]
2025-09-26 16:40:38 +08:00
},
miners: [
// {
// "user": "lx_888",
// "miner": null,
// "coin": "nexa"
// },
// {
// "user": "lx999",
// "miner": null,
// "coin": "nexa"
// },
// {
// "user": "lx88",
// "miner": null,
// "coin": "nexa"
// },
// {
// "user": "lx6666",
// "miner": null,
// "coin": "nexa"
// },
// {
// "user": "lx_999",
// "miner": null,
// "coin": "nexa"
// },
// {
// "user": "Lx_6966",
// "miner": null,
// "coin": "nexa"
// },
// {
// "user": "LX_666",
// "miner": null,
// "coin": "nexa"
// },
],
minersLoading: false,
selectedMiner: '', // 格式 user|coin
machineOptions: [
// {
// "user": "lx_888",
// "miner": `iusfhufhu`,
// "coin": "nexa"
// },
// {
// "user": "lx999",
// "miner": `iusfhufhu2`,
// "coin": "nexa"
// },
// {
// "user": "lx88",
// "miner": `iusfhufhu3`,
// "coin": "nexa"
// },
// {
// "user": "lx6666",
// "miner": `iusfhufhu4`,
// "coin": "nexa"
// },
// {
// "user": "lx_999",
// "miner": `iusfhufhu5`,
// "coin": "nexa"
// },
// {
// "user": "Lx_6966",
// "miner": `iusfhufhu6`,
// "coin": "nexa"
// },
// {
// "user": "LX_666",
// "miner": `iusfhufhu7`,
// "coin": "nexa"
// },
],
machinesLoading: false,
selectedMachines: [],
selectedMachineRows: [],
saving: false,
lastCostBaseline: 0,
2025-11-07 16:30:03 +08:00
lastCostMapBaseline: {}, // { key: number }
2025-09-26 16:40:38 +08:00
lastTypeBaseline: '',
2025-10-20 10:15:13 +08:00
lastMaxLeaseDaysBaseline: 0,
lastPowerDissipationBaseline: 0,
lastTheoryPowerBaseline: 0,
lastUnitBaseline: 'TH/S',
2025-11-21 16:23:46 +08:00
/** GPU 引导弹窗可见性 */
gpuDialogVisible: false,
/** GPU 客户端下载地址可通过环境变量配置VUE_APP_GPU_CLIENT_URL */
clientDownloadUrl: process.env.VUE_APP_GPU_CLIENT_URL || '',
/** 是否点击过下载客户端(用于控制“已启动客户端”按钮禁用态) */
hasDownloadedClient: false,
2025-09-26 16:40:38 +08:00
params:{
cost:353400,
powerDissipation:0.01,
theoryPower:1000,
type:"",
unit:"TH/S",
productId:1,
productMachineURDVos:[
{
"user":"lx_888",
"miner":"iusfhufhu",
"price":353400,
"type":"",
"state":0
},
{
"user":"lx_888",
"miner":"iusfhufhu2",
"price":353400,
"type":"",
"state":0
},
]
}
}
},
created() {
2025-11-07 16:30:03 +08:00
this.initPayTypesFromRoute()
2025-09-26 16:40:38 +08:00
this.lastTypeBaseline = this.form.type
2025-11-07 16:30:03 +08:00
// 绑定基于组件实例的校验器,避免 this 丢失
if (this.rules && this.rules.cost) {
this.$set(this.rules, 'cost', [{ validator: this.validateCost, trigger: 'blur' }])
}
2025-09-26 16:40:38 +08:00
},
methods: {
2025-11-21 16:23:46 +08:00
/**
* ASIC 模式出售机器数量输入仅允许 0-9999 的整数
*/
handleSellCountInput() {
let v = String(this.form.sellCount ?? '')
// 仅数字
v = v.replace(/\D/g, '')
// 限制最多4位
if (v.length > 4) v = v.slice(0, 4)
// 限制最大 9999
if (v) {
const n = Number(v)
if (n > 9999) v = '9999'
}
this.form.sellCount = v
},
handleSellCountBlur() {
const raw = String(this.form.sellCount ?? '')
if (raw === '') return
const n = Number(raw)
if (!Number.isInteger(n) || n < 0 || n > 9999) {
this.$message.warning('出售机器数量需为 0-9999 的整数')
this.form.sellCount = ''
}
},
/**
* 矿机种类变更若选择 GPU弹出引导弹窗ASIC 则保持当前页面逻辑
* @param {string} val
*/
handleMachineCategoryChange(val) {
if (String(val).toUpperCase() === 'GPU') {
this.gpuDialogVisible = true
// 首次打开时禁用“已启动客户端”按钮
this.hasDownloadedClient = false
} else {
this.gpuDialogVisible = false
}
},
/**
* 下载 GPU 客户端
*/
handleDownloadClient() {
const url = (this.clientDownloadUrl || '').trim()
if (!url) {
this.$message.warning('暂无客户端下载地址,请联系管理员')
return
}
try {
window.open(url, '_blank')
} catch (e) {
this.$message.error('打开下载链接失败,请稍后重试')
}
// 点击下载后即可启用“已启动客户端”按钮
this.hasDownloadedClient = true
},
/**
* GPU 客户端已启动跳转至商品列表
*/
handleGpuClientStarted() {
this.$router.push('/productList')
},
/**
* GPU 弹窗关闭自动恢复为 ASIC
*/
handleGpuDialogClosed() {
this.form.machineCategory = 'ASIC'
this.gpuDialogVisible = false
},
2025-11-07 16:30:03 +08:00
/** 统一售价校验:多结算币种时跳过,单价时按 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 = []
}
},
2025-09-26 16:40:38 +08:00
handleBack() {
this.$router.back()
},
handleNumeric(key) {
// 仅允许数字和一个小数点
let v = String(this.form[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('.')
if (key === 'cost') {
// 成本整数最多12位小数最多2位
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)
} else if (key === 'powerDissipation' || key === 'theoryPower') {
// 功耗/理论算力整数最多6位小数最多4位
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (intPart.length > 6) {
intPart = intPart.slice(0, 6)
}
if (decPart) {
decPart = decPart.slice(0, 4)
}
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
2025-10-20 10:15:13 +08:00
} else if (key === 'maxLeaseDays') {
// 最大租赁天数:仅整数,范围 1-365输入阶段限制为最多3位数字
v = v.replace(/\D/g, '')
if (v.length > 3) v = v.slice(0, 3)
this.form[key] = v
this.syncMaxLeaseDaysToRows()
return
2025-09-26 16:40:38 +08:00
} else {
// 其他最多6位小数保持原有逻辑
if (firstDot !== -1) {
const [intPart, decPart] = v.split('.')
v = intPart + '.' + (decPart ? decPart.slice(0, 6) : '')
}
}
this.form[key] = v
},
2025-11-07 16:30:03 +08:00
/** 顶部多结算币种统一售价输入 */
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)
},
2025-09-26 16:40:38 +08:00
/**
* 顶部矿机型号输入限制20字符
*/
handleTypeInput() {
if (typeof this.form.type === 'string' && this.form.type.length > 20) {
this.form.type = this.form.type.slice(0, 20)
}
},
syncCostToRows() {
const newCost = Number(this.form.cost)
if (!Number.isFinite(newCost)) {
return
}
const oldBaseline = this.lastCostBaseline
this.selectedMachineRows = this.selectedMachineRows.map(row => {
const priceNum = Number(row.price)
if (!Number.isFinite(priceNum) || priceNum === oldBaseline) {
return { ...row, price: newCost }
}
return row
})
this.lastCostBaseline = newCost
},
updateMachineType() {
2025-11-21 16:23:46 +08:00
// 仅记录最近一次外层输入,避免无用同步逻辑
2025-09-26 16:40:38 +08:00
this.lastTypeBaseline = this.form.type
},
2025-10-20 10:15:13 +08:00
/**
* 行内功耗输入限制整数最多6位小数最多4位
*/
handleRowPowerDissipationInput(index) {
let v = String(this.selectedMachineRows[index].powerDissipation ?? '')
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
}
const endsWithDot = v.endsWith('.')
2025-10-20 10:15:13 +08:00
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (intPart.length > 6) intPart = intPart.slice(0, 6)
if (decPart) decPart = decPart.slice(0, 4)
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
2025-10-20 10:15:13 +08:00
this.$set(this.selectedMachineRows[index], 'powerDissipation', v)
},
/**
* 行内功耗校验
*/
handleRowPowerDissipationBlur(index) {
const raw = String(this.selectedMachineRows[index].powerDissipation ?? '')
const pattern = /^\d{1,6}(\.\d{1,4})?$/
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
this.$message.warning('功耗需大于0整数最多6位小数最多4位')
this.$set(this.selectedMachineRows[index], 'powerDissipation', '')
}
},
/**
* 行内理论算力输入限制整数最多6位小数最多4位
*/
handleRowTheoryPowerInput(index) {
let v = String(this.selectedMachineRows[index].theoryPower ?? '')
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
}
const endsWithDot = v.endsWith('.')
2025-10-20 10:15:13 +08:00
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (intPart.length > 6) intPart = intPart.slice(0, 6)
if (decPart) decPart = decPart.slice(0, 4)
v = decPart.length ? `${intPart}.${decPart}` : (endsWithDot ? `${intPart}.` : intPart)
2025-10-20 10:15:13 +08:00
this.$set(this.selectedMachineRows[index], 'theoryPower', v)
},
/**
* 行内理论算力校验
*/
handleRowTheoryPowerBlur(index) {
const raw = String(this.selectedMachineRows[index].theoryPower ?? '')
const pattern = /^\d{1,6}(\.\d{1,4})?$/
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
this.$message.warning('理论算力需大于0整数最多6位小数最多4位')
this.$set(this.selectedMachineRows[index], 'theoryPower', '')
}
},
/**
* 行内单位变更
*/
handleRowUnitChange(index, value) {
this.$set(this.selectedMachineRows[index], 'unit', value)
},
syncMaxLeaseDaysToRows() {
const raw = this.form.maxLeaseDays
const n = Number(raw)
if (!Number.isInteger(n)) return
const oldBaseline = this.lastMaxLeaseDaysBaseline
this.selectedMachineRows = this.selectedMachineRows.map(row => {
const rowNum = Number(row.maxLeaseDays)
if (!Number.isInteger(rowNum) || rowNum === oldBaseline) {
return { ...row, maxLeaseDays: n }
}
return row
})
this.lastMaxLeaseDaysBaseline = n
},
handleRowMaxLeaseDaysInput(index) {
let v = String(this.selectedMachineRows[index].maxLeaseDays ?? '')
v = v.replace(/\D/g, '')
if (v.length > 3) v = v.slice(0, 3)
this.$set(this.selectedMachineRows[index], 'maxLeaseDays', v)
},
handleRowMaxLeaseDaysBlur(index) {
const raw = String(this.selectedMachineRows[index].maxLeaseDays ?? '')
if (!/^\d{1,3}$/.test(raw)) {
this.$message.warning('最大租赁天数需为 1-365 的整数')
this.$set(this.selectedMachineRows[index], 'maxLeaseDays', '')
return
}
const n = Number(raw)
if (!Number.isInteger(n) || n < 1 || n > 365) {
this.$message.warning('最大租赁天数需为 1-365 的整数')
this.$set(this.selectedMachineRows[index], 'maxLeaseDays', '')
}
},
2025-09-26 16:40:38 +08:00
handleRowPriceInput(index) {
// 价格输入整数最多12位小数最多2位允许尾随小数点
let v = String(this.selectedMachineRows[index].price ?? '')
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.selectedMachineRows[index], 'price', v)
},
2025-11-07 16:30:03 +08:00
/** 行内多结算币种价格输入 */
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)
}
},
2025-09-26 16:40:38 +08:00
handleRowPriceBlur(index) {
const raw = String(this.selectedMachineRows[index].price ?? '')
const pattern = /^\d{1,12}(\.\d{1,2})?$/
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
this.$message.warning('价格必须大于0整数最多12位小数最多2位')
this.$set(this.selectedMachineRows[index], 'price', '')
}
},
handleRowTypeInput(index) {
// 处理矿机型号输入
const raw = String(this.selectedMachineRows[index].type || '')
const v = raw.length > 20 ? raw.slice(0, 20) : raw
this.$set(this.selectedMachineRows[index], 'type', v)
},
handleRowTypeBlur(index) {
const raw = this.selectedMachineRows[index].type
const isOnlySpaces = (v) => typeof v === 'string' && v.length > 0 && v.trim().length === 0
if (isOnlySpaces(raw)) {
this.$message.warning('矿机型号不能全是空格')
this.$set(this.selectedMachineRows[index], 'type', '')
}
},
handleToggleState(index) {
// 切换上下架状态0上架1下架
const currentState = this.selectedMachineRows[index].state
this.$set(this.selectedMachineRows[index], 'state', currentState === 0 ? 1 : 0)
},
async fetchMiners() {
this.minersLoading = true
try {
// 按商品币种筛选挖矿账户
const res = await getUserMinersList({ coin: this.form.coin || "" })
const data = res?.data
let list = []
if (Array.isArray(data)) {
list = data
} else if (data && typeof data === 'object') {
// 现在的结构是 { coin: [ { user, coin }, ... ], coin2: [...] }
Object.keys(data).forEach(coinKey => {
const arr = Array.isArray(data[coinKey]) ? data[coinKey] : []
arr.forEach(item => {
if (item && item.user && item.coin) {
list.push({ user: item.user, coin: item.coin, miner: item.miner || null })
}
})
})
} else if (data && data.additionalProperties1) {
list = [data.additionalProperties1]
}
// 如页面带了 product coin则仅展示该币种的账户
if (this.form.coin) {
list = list.filter(i => i.coin === this.form.coin)
}
this.miners = list
} catch (e) {
console.error('获取挖矿账户失败', e)
} finally {
this.minersLoading = false
}
},
async handleMinerChange(val) {
this.selectedMachines = []
if (!val) {
this.machineOptions = []
return
}
const [user, coin] = val.split('|')
this.machinesLoading = true
try {
// 按照API文档要求传递 userMinerVo 对象
const userMinerVo = {
coin: coin,
user: user
}
const res = await getUserMachineList(userMinerVo)
const data = res?.data || []
this.machineOptions = Array.isArray(data) ? data : []
// 调试信息
console.log('选择挖矿账户:', { user, coin })
console.log('获取机器列表响应:', res)
console.log('机器列表数据:', this.machineOptions)
} catch (e) {
console.error('获取机器列表失败', e)
2025-10-20 10:15:13 +08:00
2025-09-26 16:40:38 +08:00
} finally {
this.machinesLoading = false
}
},
async handleSave() {
// 表单校验(除矿机型号外其他必填)
try {
const ok = await this.$refs.machineForm.validate()
if (!ok) {
return
}
} catch (e) {
return
}
if (!this.form.productId) {
this.$message.warning('缺少商品ID')
return
}
2025-11-21 16:23:46 +08:00
// 现在统一按出售数量提交GPU 模式不在本页提交)
{
// ASIC校验出售机器数量允许 0-9999为 0 则提示)
const raw = String(this.form.sellCount ?? '')
if (raw === '') {
this.$message.warning('请输入出售机器数量')
return
}
const n = Number(raw)
if (!Number.isInteger(n) || n < 0 || n > 9999) {
this.$message.warning('出售机器数量需为 0-9999 的整数')
return
}
if (n === 0) {
this.$message.warning('出售机器数量为 0无需提交')
return
}
2025-09-26 16:40:38 +08:00
}
// 校验:矿机型号不可全空格(允许为空或包含空格的正常文本)
const isOnlySpaces = (v) => typeof v === 'string' && v.length > 0 && v.trim().length === 0
if (isOnlySpaces(this.form.type)) {
this.$message.warning('矿机型号不能全是空格')
return
}
const invalidTypeRowIndex = this.selectedMachineRows.findIndex(r => isOnlySpaces(r.type))
if (invalidTypeRowIndex !== -1) {
this.$message.warning('存在行的矿机型号全是空格,请修正后再试')
return
}
2025-11-21 16:23:46 +08:00
// 统一售价与最大租赁天数已在表单级校验中处理,无需逐机校验
2025-09-26 16:40:38 +08:00
// 通过所有预校验后,弹出确认框
this.confirmVisible = true
}
,
async doSubmit() {
2025-11-21 16:23:46 +08:00
const [user, coin] = ['','']
2025-09-26 16:40:38 +08:00
this.saving = true
try {
2025-11-07 16:30:03 +08:00
// 若是多结算币种,组装 priceList否则沿用单价字段
2025-11-21 16:23:46 +08:00
const count = Number(this.form.sellCount)
const productMachineURDVos = Array.from({ length: count }).map(() => ({
price: this.payTypeDefs && this.payTypeDefs.length ? undefined : (Number(this.form.cost) || 0),
priceList: this.payTypeDefs && this.payTypeDefs.length ? this.payTypeDefs.map(d => ({
chain: d.chain,
coin: d.coin,
price: Number(this.form.costMap && this.form.costMap[d.key]) || 0
})) : undefined,
state: 0,
type: this.form.type,
maxLeaseDays: Number(this.form.maxLeaseDays) || 0,
powerDissipation: Number(this.form.powerDissipation) || 0,
theoryPower: Number(this.form.theoryPower) || 0,
unit: this.form.unit
}))
2025-09-26 16:40:38 +08:00
const payload = {
productId: this.form.productId,
2025-11-21 16:23:46 +08:00
// 逗号分隔:后台若需要数组可在服务端拆分
coin: (this.form.coinsInput || this.form.coin || '').toString(),
algorithm: (this.form.algorithmsInput || '').toString(),
2025-09-26 16:40:38 +08:00
powerDissipation: this.form.powerDissipation,
theoryPower: this.form.theoryPower,
type: this.form.type,
unit: this.form.unit,
2025-11-07 16:30:03 +08:00
cost: this.payTypeDefs && this.payTypeDefs.length ? Number(this.form.costMap && this.form.costMap[this.payTypeDefs[0].key]) || 0 : this.form.cost,
2025-10-20 10:15:13 +08:00
maxLeaseDays: this.form.maxLeaseDays,
2025-11-21 16:23:46 +08:00
productMachineURDVos
2025-09-26 16:40:38 +08:00
}
console.log(payload,"请求参数")
const res = await addSingleOrBatchMachine(payload)
if (res && (res.code === 0 || res.code === 200)) {
2025-10-31 14:09:58 +08:00
this.$message({
message: '添加成功',
duration: 3000,
showClose: true,
type: 'success'
})
2025-09-26 16:40:38 +08:00
this.confirmVisible = false
this.$router.back()
2025-10-20 10:15:13 +08:00
}
2025-09-26 16:40:38 +08:00
} catch (e) {
console.error('添加出售机器失败', e)
console.log('添加失败')
} finally {
this.saving = false
}
}
}
2025-11-21 16:23:46 +08:00
2025-09-26 16:40:38 +08:00
}
</script>
<style scoped>
.product-machine-add { padding: 8px; }
.header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.title { margin: 0; font-size: 18px; font-weight: 600; }
2025-10-20 10:15:13 +08:00
.notice-alert { margin-bottom: 12px; }
.notice-alert :deep(.el-alert__content) { text-align: left; }
.notice-alert :deep(.el-alert__title),
.notice-alert :deep(.el-alert__description) { text-align: left; }
.label-help { margin-left: 4px; color: #909399; cursor: help; }
2025-09-26 16:40:38 +08:00
.form-card { margin-bottom: 12px; }
2025-11-21 16:23:46 +08:00
.actions { text-align: left; }
2025-09-26 16:40:38 +08:00
/* 统一左对齐,控件宽度 50% */
.product-machine-add :deep(.el-form-item__content) {
justify-content: flex-start;
}
2025-10-20 10:15:13 +08:00
/* .product-machine-add :deep(.el-input),
2025-09-26 16:40:38 +08:00
.product-machine-add :deep(.el-select),
.product-machine-add :deep(.el-textarea) {
width: 50%;
2025-10-20 10:15:13 +08:00
} */
2025-09-26 16:40:38 +08:00
.product-machine-add :deep(.el-input-group__append) {
background: #f5f7fa;
color: #606266;
border-left: 1px solid #dcdfe6;
}
::v-deep .el-form-item__content{
text-align: left;
padding-left: 18px !important;
}
2025-11-07 16:30:03 +08:00
/* 多结算币种价格输入的布局优化 */
.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; }
2025-09-26 16:40:38 +08:00
</style>