新版本更改中
This commit is contained in:
@@ -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/'
|
||||
|
||||
|
||||
# 路由懒加载
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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/>
|
||||
1)0% - 5%(包含5%):波动率 = 1(按售价结算)<br/>
|
||||
2)5%以上:波动率 = 实际算力 / 理论算力,且不会超过 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/>
|
||||
1)0% - 5%(包含5%):波动率 = 1(按售价结算)<br/>
|
||||
2)5%以上:波动率 = 实际算力 / 理论算力,且不会超过 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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user