Files
webs/power_leasing/src/views/account/productMachineAdd.vue
2025-09-26 16:40:38 +08:00

669 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="product-machine-add">
<div class="header">
<el-button type="text" @click="handleBack">返回</el-button>
<h2 class="title">添加出售机器</h2>
</div>
<el-card shadow="never" class="form-card">
<el-form ref="machineForm" :model="form" :rules="rules" label-width="160px" size="small">
<el-form-item label="商品名称">
<el-input v-model="form.productName" disabled />
</el-form-item>
<el-form-item label="功耗" prop="powerDissipation">
<el-input
v-model="form.powerDissipation"
placeholder="示例0.01"
inputmode="decimal"
@input="handleNumeric('powerDissipation')"
>
<template slot="append">kw/h</template>
</el-input>
</el-form-item>
<el-form-item label="机器成本价格" prop="cost">
<el-input
v-model="form.cost"
placeholder="请输入成本USDT"
inputmode="decimal"
@input="handleNumeric('cost')"
>
<template slot="append">USDT</template>
</el-input>
</el-form-item>
<el-form-item label="矿机型号">
<el-input v-model="form.type" placeholder="示例:龍珠" :maxlength="20" @input="handleTypeInput" />
</el-form-item>
<el-form-item label="理论算力" prop="theoryPower">
<el-input
v-model="form.theoryPower"
placeholder="请输入单机理论算力"
inputmode="decimal"
@input="handleNumeric('theoryPower')"
/>
</el-form-item>
<el-form-item label="算力单位" prop="unit">
<el-select v-model="form.unit" placeholder="请选择算力单位">
<el-option label="MH/S" value="MH/S" />
<el-option label="GH/S" value="GH/S" />
<el-option label="TH/S" value="TH/S" />
<el-option label="PH/S" value="PH/S" />
</el-select>
</el-form-item>
<el-form-item label="选择挖矿账户">
<el-select v-model="selectedMiner" filterable clearable placeholder="请选择挖矿账户" @change="handleMinerChange" :loading="minersLoading">
<el-option v-for="m in miners" :key="m.user + '_' + m.coin" :label="m.user + '' + m.coin + ''" :value="m.user + '|' + m.coin" />
</el-select>
</el-form-item>
<el-form-item label="选择机器(可多选)">
<el-select v-model="selectedMachines" multiple filterable collapse-tags placeholder="请选择机器" :loading="machinesLoading" :disabled="!selectedMiner">
<el-option v-for="m in machineOptions" :key="m.user + '_' + m.miner" :label="m.miner + '' + m.user + ''" :value="m.miner" />
</el-select>
</el-form-item>
</el-form>
</el-card>
<!-- 已选择机器列表 -->
<el-card shadow="never" class="form-card" v-if="selectedMachineRows.length">
<div slot="header" class="section-title">已选择机器</div>
<el-table :data="selectedMachineRows" border stripe style="width: 100%">
<el-table-column prop="user" label="挖矿账户" min-width="160" />
<el-table-column prop="miner" label="机器编号" min-width="160" />
<el-table-column label="价格(USDT)" min-width="220">
<template #default="scope">
<el-input
v-model="scope.row.price"
placeholder="价格"
inputmode="decimal"
@input="handleRowPriceInput(scope.$index)"
@blur="handleRowPriceBlur(scope.$index)"
style="width: 70%;"
>
<template slot="append">USDT</template>
</el-input>
</template>
</el-table-column>
<el-table-column label="矿机型号" min-width="200">
<template #default="scope">
<el-input
v-model="scope.row.type"
placeholder="矿机型号"
@input="handleRowTypeInput(scope.$index)"
@blur="handleRowTypeBlur(scope.$index)"
:maxlength="20"
style="width: 70%;"
/>
</template>
</el-table-column>
<el-table-column label="上下架状态" min-width="120">
<template #default="scope">
<el-button
:type="scope.row.state === 0 ? 'success' : 'info'"
size="mini"
@click="handleToggleState(scope.$index)"
>
{{ scope.row.state === 0 ? '上架' : '下架' }}
</el-button>
</template>
</el-table-column>
</el-table>
</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>
</div>
</template>
<script>
import { getUserMinersList, getUserMachineList, addSingleOrBatchMachine } from '../../api/machine'
export default {
name: 'AccountProductMachineAdd',
data() {
return {
form: {
productId: Number(this.$route.query.productId) || null,
coin: this.$route.query.coin || '',
productName: this.$route.query.name || '',
powerDissipation: null,
theoryPower: null,
type: '',
unit: 'TH/S',
cost: ''
},
confirmVisible: false,
rules: {
productName: [ { required: true, message: '商品名称不能为空', trigger: 'change' } ],
coin: [ { required: true, message: '币种不能为空', trigger: 'change' } ],
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: [
{ required: true, message: '请填写机器成本USDT', trigger: 'blur' },
{
validator: (rule, value, callback) => {
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位'))
return
}
if (Number(str) <= 0) {
callback(new Error('成本必须大于 0'))
return
}
callback()
},
trigger: 'blur'
}
]
},
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,
lastTypeBaseline: '',
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() {
this.fetchMiners()
this.lastTypeBaseline = this.form.type
},
methods: {
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)
} else {
// 其他最多6位小数保持原有逻辑
if (firstDot !== -1) {
const [intPart, decPart] = v.split('.')
v = intPart + '.' + (decPart ? decPart.slice(0, 6) : '')
}
}
this.form[key] = v
if (key === 'cost') {
this.syncCostToRows()
}
},
/**
* 顶部矿机型号输入限制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() {
// 当外层矿机型号变动时,同步更新机器列表中的型号
// 但如果用户手动改过某行型号,则不覆盖
this.selectedMachineRows = this.selectedMachineRows.map(row => {
// 如果该行型号为空或等于旧型号,则更新为新型号
if (!row.type || row.type === this.lastTypeBaseline) {
return { ...row, type: this.form.type }
}
return row
})
this.lastTypeBaseline = this.form.type
},
updateSelectedMachineRows() {
// 依据 selectedMachines 与 machineOptions 同步生成行数据
const map = new Map()
this.machineOptions.forEach(m => {
map.set(m.miner, m)
})
const nextRows = []
this.selectedMachines.forEach(minerId => {
const m = map.get(minerId)
if (m) {
// 若已存在,沿用已编辑的价格、型号和状态
const existed = this.selectedMachineRows.find(r => r.miner === minerId)
nextRows.push({
user: m.user,
coin: m.coin,
miner: m.miner,
price: existed ? existed.price : this.form.cost,
type: existed ? existed.type : this.form.type,
state: existed ? existed.state : 0 // 默认上架
})
}
})
this.selectedMachineRows = nextRows
},
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)
},
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)
this.$message.error('获取机器列表失败,请重试')
} 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
}
if (!this.selectedMiner) {
this.$message.warning('请先选择挖矿账户')
return
}
if (!this.selectedMachines.length) {
this.$message.warning('请至少选择一台机器')
return
}
// 校验:矿机型号不可全空格(允许为空或包含空格的正常文本)
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
}
// 校验已选择机器的价格必须大于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
}
}
// 通过所有预校验后,弹出确认框
this.confirmVisible = true
}
,
async doSubmit() {
const [user, coin] = this.selectedMiner.split('|')
this.saving = true
try {
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,
productMachineURDVos: this.selectedMachineRows.map(r => ({
miner: r.miner,
price: Number(r.price) || 0,
state: r.state || 0,
type: r.type || this.form.type,
user: r.user
}))
}
console.log(payload,"请求参数")
const res = await addSingleOrBatchMachine(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('添加成功')
this.confirmVisible = false
this.$router.back()
} else {
this.$message.error(res?.msg || '添加失败')
}
} catch (e) {
console.error('添加出售机器失败', e)
console.log('添加失败')
} finally {
this.saving = false
}
}
}
,
watch: {
'form.cost': function() { this.syncCostToRows() },
'form.type': function() { this.updateMachineType() },
selectedMachines() {
this.updateSelectedMachineRows()
}
}
}
</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; }
.form-card { margin-bottom: 12px; }
.actions { text-align: right; }
/* 统一左对齐,控件宽度 50% */
.product-machine-add :deep(.el-form-item__content) {
justify-content: flex-start;
}
.product-machine-add :deep(.el-input),
.product-machine-add :deep(.el-select),
.product-machine-add :deep(.el-textarea) {
width: 50%;
}
.product-machine-add :deep(.el-input-group__append) {
background: #f5f7fa;
color: #606266;
border-left: 1px solid #dcdfe6;
}
::v-deep .el-form-item__content{
text-align: left;
padding-left: 18px !important;
}
</style>