周五固定更新

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

@@ -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>