新版本更改中

This commit is contained in:
2025-11-21 16:23:46 +08:00
parent a02c287715
commit 868632400a
4 changed files with 366 additions and 374 deletions

View File

@@ -9,7 +9,7 @@ ENV = 'staging'
# 测试环境
VUE_APP_BASE_API = 'http://10.168.2.220:8888'
# VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
VUE_APP_BASE_URL = 'https://test.m2pool.com/'
VUE_APP_BASE_URL = 'https://test.m2pool.com/'
# 路由懒加载

View File

@@ -39,10 +39,9 @@
</el-tag>
</div>
<div class="desc">{{ shop.description || '这家店还没有描述~' }}</div>
<!-- <div class="meta">
<span>店铺ID{{ shop.id || '-' }}</span>
<span>可删除{{ shop.del ? '是' : '否' }}</span>
</div> -->
<div class="meta">
<span>手续费率{{ formatFeeRate(shop.feeRate) }}</span>
</div>
<div class="actions">
<el-button size="small" type="primary" @click="handleOpenEdit">修改店铺</el-button>
<el-button size="small" type="warning" @click="handleToggleShop">
@@ -124,6 +123,14 @@
<label class="label">店铺描述</label>
<el-input type="textarea" :rows="3" v-model="editForm.description" placeholder="请输入描述" :maxlength="300" show-word-limit />
</div>
<div class="row">
<label class="label">手续费比例</label>
<el-input
v-model="editForm.feeRate"
placeholder="比例区间 0.01 - 0.1 之间最多6位小数"
@input="handleEditFeeRateInput"
/>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="visibleEdit=false">取消</el-button>
@@ -200,11 +207,12 @@ export default {
name: '',
image: '',
description: '',
feeRate: '',
del: true,
state: 0
},
visibleEdit: false,
editForm: { id: '', name: '', image: '', description: '' },
editForm: { id: '', name: '', image: '', description: '', feeRate: '' },
// 店铺配置列表
shopConfigs: [],
visibleConfigEdit: false,
@@ -259,6 +267,38 @@ export default {
this.fetchMyShop()
},
methods: {
/**
* 手续费率显示最多6位小数去除多余的0空值显示为 '-'
*/
formatFeeRate(value) {
if (value === null || value === undefined || value === '') return '-'
const num = Number(value)
if (!Number.isFinite(num)) return '-'
const fixed = num.toFixed(6)
return fixed.replace(/\.?0+$/, '')
},
/**
* 修改弹窗 - 手续费输入允许一个小数点最多6位小数允许尾随点
*/
handleEditFeeRateInput(value) {
let v = String(value ?? this.editForm.feeRate ?? '')
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
}
const endsWithDot = v.endsWith('.')
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (decPart.length > 6) decPart = decPart.slice(0, 6)
if (intPart && intPart !== '0') intPart = String(Number(intPart))
if (endsWithDot && firstDot !== -1) {
this.editForm.feeRate = `${intPart || '0'}.`
return
}
this.editForm.feeRate = decPart ? `${intPart || '0'}.${decPart}` : (intPart || '')
},
// 简单的emoji检测覆盖常见表情平面与符号范围
hasEmoji(str) {
if (!str || typeof str !== 'string') return false
@@ -291,6 +331,7 @@ export default {
name: res.data.name,
image: res.data.image,
description: res.data.description,
feeRate: res.data.feeRate,
del: !!res.data.del,
state: Number(res.data.state || 0)
}
@@ -429,7 +470,8 @@ export default {
id: res.data.id,
name: res.data.name,
image: res.data.image,
description: res.data.description
description: res.data.description,
feeRate: res.data.feeRate
}
@@ -439,7 +481,8 @@ export default {
id: this.shop.id,
name: this.shop.name,
image: this.shop.image,
description: this.shop.description
description: this.shop.description,
feeRate: this.shop.feeRate
}
this.$message.warning(res && res.msg ? res.msg : '未获取到店铺详情')
}
@@ -449,7 +492,8 @@ export default {
id: this.shop.id,
name: this.shop.name,
image: this.shop.image,
description: this.shop.description
description: this.shop.description,
feeRate: this.shop.feeRate
}
console.error('查询店铺详情失败:', error)
@@ -492,7 +536,19 @@ export default {
this.$message.warning('店铺描述不能超过300个字符')
return
}
// 手续费比例必填、0.01-0.1、最多6位小数
const rateRaw = String(this.editForm.feeRate || '').trim()
if (!rateRaw) {
this.$message.warning('请填写店铺手续费比例0.01 - 0.1最多6位小数')
return
}
const rateNum = Number(rateRaw)
const decOk = rateRaw.includes('.') ? ((rateRaw.split('.')[1] || '').length <= 6) : true
if (!Number.isFinite(rateNum) || rateNum < 0.01 || rateNum > 0.1 || !decOk) {
this.$message.warning('手续费比例需在 0.01 - 0.1 之间且小数位不超过6位')
return
}
this.editForm.feeRate = rateNum.toString()
const payload = { ...this.editForm }
const res = await updateShop(payload)
@@ -585,9 +641,9 @@ export default {
})
return
}
// 跳转到新增商品页面并传递店铺ID
// 直接跳转到“添加出售机器”页面并传递店铺ID(供后续扩展使用)
this.$router.push({
path: '/account/product-new',
path: '/account/product-machine-add',
query: { shopId: this.shop.id }
})
},

View File

@@ -5,22 +5,41 @@
<h2 class="title">添加出售机器</h2>
</div>
<el-alert
<!-- <el-alert
class="notice-alert"
type="warning"
show-icon
:closable="false"
title="新增出售机器必须在 M2pool 有挖矿算力记录才能添加出租"
description="建议稳定在 M2pool 矿池挖矿 24 小时之后,再添加出售该机器"
/>
/> -->
<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 style="width: 50%;" />
<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>
</el-form-item>
<!-- 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>
<el-form-item label="矿机型号">
<el-input style="width: 50%;" v-model="form.type" placeholder="示例:龍珠" :maxlength="20" @input="handleTypeInput" />
@@ -65,19 +84,8 @@
</el-input>
</el-form-item>
<el-form-item label="统一售价" prop="cost">
<span slot="label">
统一售价
<el-tooltip effect="dark" placement="top">
<div slot="content">
卖家最终收款金额 = 机器售价 × 波动率<br/>
波动率规则<br/>
10% - 5%包含5%波动率 = 1按售价结算<br/>
25%以上波动率 = 实际算力 / 理论算力且不会超过 1,即最终结算时不会超过机器售价
</div>
<i class="el-icon-question label-help" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
</span>
<el-form-item label="统一售价" prop="cost" :required="true">
<span slot="label">统一售价</span>
<!-- 若商品定义了多个结算币种则按链-币种动态生成多个售价输入否则回退为旧的 USDT 单价 -->
<div v-if="payTypeDefs && payTypeDefs.length" class="cost-multi">
<div v-for="pt in payTypeDefs" :key="pt.key" class="cost-item">
@@ -105,156 +113,20 @@
</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>
<!-- 出售机器数量统一使用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"
/>
</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="挖矿账户" />
<el-table-column prop="miner" label="机器编号" />
<el-table-column prop="realPower" label="实际算力(MH/S)">
<template slot="header">
<el-tooltip content="实际算力为该机器在本矿池过去24H的平均算力" effect="dark" placement="top">
<i class="el-icon-question" style="margin-right: 4px; color: #909399;" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
<span>实际算力(MH/S)</span>
</template>
</el-table-column>
<el-table-column label="功耗(kw/h)" min-width="120">
<template #default="scope">
<el-input
v-model="scope.row.powerDissipation"
placeholder="示例0.01"
inputmode="decimal"
@input="handleRowPowerDissipationInput(scope.$index)"
@blur="handleRowPowerDissipationBlur(scope.$index)"
style="width: 100%;"
>
<template slot="append">kw/h</template>
</el-input>
</template>
</el-table-column>
<el-table-column label="理论算力" min-width="160">
<template #default="scope">
<div style="display: flex; align-items: center; gap: 8px;">
<el-input
v-model="scope.row.theoryPower"
placeholder="理论算力"
inputmode="decimal"
@input="handleRowTheoryPowerInput(scope.$index)"
@blur="handleRowTheoryPowerBlur(scope.$index)"
style="width: 100%"
/>
<el-select
v-model="scope.row.unit"
placeholder="单位"
style="width:150px;"
@change="val => handleRowUnitChange(scope.$index, val)"
>
<el-option label="KH/S" value="KH/S" />
<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>
</div>
</template>
</el-table-column>
<el-table-column label="售价(按结算币种)" min-width="220">
<template slot="header">
<el-tooltip effect="dark" placement="top">
<div slot="content">
卖家最终收款金额 = 机器售价 × 波动率<br/>
波动率规则<br/>
10% - 5%包含5%波动率 = 1按售价结算<br/>
25%以上波动率 = 实际算力 / 理论算力且不会超过 1,即最终结算时不会超过机器售价
</div>
<i class="el-icon-question label-help" aria-label="帮助" tabindex="0"></i>
</el-tooltip>
<span>售价按结算币种</span>
</template>
<template slot-scope="scope">
<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>
<el-table-column label="最大租赁天数(天)" min-width="120">
<template #default="scope">
<el-input
v-model="scope.row.maxLeaseDays"
placeholder="1-365"
inputmode="numeric"
@input="handleRowMaxLeaseDaysInput(scope.$index)"
@blur="handleRowMaxLeaseDaysBlur(scope.$index)"
style="width: 100%;"
>
<template slot="append"></template>
</el-input>
</template>
</el-table-column>
<el-table-column label="矿机型号">
<template #default="scope">
<el-input
v-model="scope.row.type"
placeholder="矿机型号"
@input="handleRowTypeInput(scope.$index)"
@blur="handleRowTypeBlur(scope.$index)"
:maxlength="20"
style="width: 100%;"
/>
</template>
</el-table-column>
<el-table-column label="上下架状态" width="100">
<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>
@@ -275,11 +147,31 @@
<el-button type="primary" :loading="saving" @click="doSubmit">确认上架已选择机器</el-button>
</span>
</el-dialog>
<!-- 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>
</div>
</template>
<script>
import { getUserMinersList, getUserMachineList, addSingleOrBatchMachine } from '../../api/machine'
import { addSingleOrBatchMachine } from '../../api/machine'
export default {
name: 'AccountProductMachineAdd',
@@ -289,6 +181,13 @@ export default {
productId: Number(this.$route.query.productId) || null,
coin: this.$route.query.coin || '',
productName: this.$route.query.name || '',
/** 矿机种类ASIC 或 GPU默认 ASIC */
machineCategory: 'ASIC',
/** 出售机器数量(仅 ASIC 模式使用) */
sellCount: '',
/** ASIC 模式下币种/算法输入(逗号分隔的原始文本) */
coinsInput: '',
algorithmsInput: '',
powerDissipation: null,
theoryPower: null,
type: '',
@@ -300,7 +199,42 @@ export default {
confirmVisible: false,
rules: {
productName: [ { required: true, message: '商品名称不能为空', trigger: 'change' } ],
coin: [ { required: true, message: '币种不能为空', trigger: 'change' } ],
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'
}
],
powerDissipation: [
{ required: true, message: '功耗不能为空', trigger: 'blur' },
{
@@ -460,6 +394,12 @@ export default {
lastPowerDissipationBaseline: 0,
lastTheoryPowerBaseline: 0,
lastUnitBaseline: 'TH/S',
/** GPU 引导弹窗可见性 */
gpuDialogVisible: false,
/** GPU 客户端下载地址可通过环境变量配置VUE_APP_GPU_CLIENT_URL */
clientDownloadUrl: process.env.VUE_APP_GPU_CLIENT_URL || '',
/** 是否点击过下载客户端(用于控制“已启动客户端”按钮禁用态) */
hasDownloadedClient: false,
params:{
cost:353400,
powerDissipation:0.01,
@@ -490,7 +430,6 @@ export default {
},
created() {
this.initPayTypesFromRoute()
this.fetchMiners()
this.lastTypeBaseline = this.form.type
// 绑定基于组件实例的校验器,避免 this 丢失
if (this.rules && this.rules.cost) {
@@ -498,6 +437,74 @@ export default {
}
},
methods: {
/**
* 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
},
/** 统一售价校验:多结算币种时跳过,单价时按 USDT 校验 */
validateCost(rule, value, callback) {
if (Array.isArray(this.payTypeDefs) && this.payTypeDefs.length > 0) {
@@ -594,9 +601,6 @@ export default {
}
}
this.form[key] = v
if (key === 'cost') {
this.syncCostToRows()
}
},
/** 顶部多结算币种统一售价输入 */
handleCostMapInput(key, val) {
@@ -615,18 +619,6 @@ export default {
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字符
@@ -652,100 +644,9 @@ export default {
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)
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, // 兼容单价模式
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,
priceMap: existedPriceMap || defaultPriceMap
})
}
})
this.selectedMachineRows = nextRows
},
/**
* 同步顶部功耗到行(行未自定义或无效则跟随)
*/
syncPowerDissipationToRows() {
const newVal = Number(this.form.powerDissipation)
if (!Number.isFinite(newVal)) return
const oldBaseline = this.lastPowerDissipationBaseline
this.selectedMachineRows = this.selectedMachineRows.map(row => {
const rowNum = Number(row.powerDissipation)
if (!Number.isFinite(rowNum) || rowNum === oldBaseline) {
return { ...row, powerDissipation: newVal }
}
return row
})
this.lastPowerDissipationBaseline = newVal
},
/**
* 同步顶部理论算力到行(行未自定义或无效则跟随)
*/
syncTheoryPowerToRows() {
const newVal = Number(this.form.theoryPower)
if (!Number.isFinite(newVal)) return
const oldBaseline = this.lastTheoryPowerBaseline
this.selectedMachineRows = this.selectedMachineRows.map(row => {
const rowNum = Number(row.theoryPower)
if (!Number.isFinite(rowNum) || rowNum === oldBaseline) {
return { ...row, theoryPower: newVal }
}
return row
})
this.lastTheoryPowerBaseline = newVal
},
/**
* 同步顶部单位到行(行未自定义或等于旧基线时跟随)
*/
syncUnitToRows() {
const newUnit = this.form.unit
if (!newUnit) return
const oldBaseline = this.lastUnitBaseline
this.selectedMachineRows = this.selectedMachineRows.map(row => {
const rowUnit = row.unit
if (!rowUnit || rowUnit === oldBaseline) {
return { ...row, unit: newUnit }
}
return row
})
this.lastUnitBaseline = newUnit
},
/**
* 行内功耗输入限制整数最多6位小数最多4位
*/
@@ -998,13 +899,23 @@ export default {
this.$message.warning('缺少商品ID')
return
}
if (!this.selectedMiner) {
this.$message.warning('请先选择挖矿账户')
return
}
if (!this.selectedMachines.length) {
this.$message.warning('请至少选择一台机器')
return
// 现在统一按出售数量提交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
}
}
// 校验:矿机型号不可全空格(允许为空或包含空格的正常文本)
const isOnlySpaces = (v) => typeof v === 'string' && v.length > 0 && v.trim().length === 0
@@ -1017,70 +928,43 @@ export default {
this.$message.warning('存在行的矿机型号全是空格,请修正后再试')
return
}
// 校验:价格与最大租赁天数
for (let i = 0; i < this.selectedMachineRows.length; i += 1) {
const row = this.selectedMachineRows[i]
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) ?? '')
const n = Number(rawDays)
if (!/^\d{1,3}$/.test(rawDays) || !Number.isInteger(n) || n < 1 || n > 365) {
const label = (row && (row.miner || row.user)) || i + 1
this.$message.warning(`${i + 1}行(机器:${label}) 最大租赁天数需为 1-365 的整数`)
return
}
}
// 统一售价与最大租赁天数已在表单级校验中处理,无需逐机校验
// 通过所有预校验后,弹出确认框
this.confirmVisible = true
}
,
async doSubmit() {
const [user, coin] = this.selectedMiner.split('|')
const [user, coin] = ['','']
this.saving = true
try {
// 若是多结算币种,组装 priceList否则沿用单价字段
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
}))
const payload = {
productId: this.form.productId,
// 逗号分隔:后台若需要数组可在服务端拆分
coin: (this.form.coinsInput || this.form.coin || '').toString(),
algorithm: (this.form.algorithmsInput || '').toString(),
powerDissipation: this.form.powerDissipation,
theoryPower: this.form.theoryPower,
type: this.form.type,
unit: this.form.unit,
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: 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,
maxLeaseDays: Number(r.maxLeaseDays) || Number(this.form.maxLeaseDays) || 0,
powerDissipation: Number(r.powerDissipation) || Number(this.form.powerDissipation) || 0,
theoryPower: Number(r.theoryPower) || Number(this.form.theoryPower) || 0,
unit: r.unit || this.form.unit
}))
productMachineURDVos
}
console.log(payload,"请求参数")
@@ -1105,32 +989,7 @@ 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() },
'form.theoryPower': function() { this.syncTheoryPowerToRows() },
'form.unit': function() { this.syncUnitToRows() },
selectedMachines() {
this.updateSelectedMachineRows()
}
}
}
</script>
@@ -1144,7 +1003,7 @@ export default {
.notice-alert :deep(.el-alert__description) { text-align: left; }
.label-help { margin-left: 4px; color: #909399; cursor: help; }
.form-card { margin-bottom: 12px; }
.actions { text-align: right; }
.actions { text-align: left; }
/* 统一左对齐,控件宽度 50% */
.product-machine-add :deep(.el-form-item__content) {

View File

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