1848 lines
60 KiB
Vue
1848 lines
60 KiB
Vue
<template>
|
||
<div class="product-machine-add">
|
||
<div class="header">
|
||
<el-button type="text" @click="handleBack">返回</el-button>
|
||
<h2 class="title">创建商品</h2>
|
||
</div>
|
||
|
||
<!-- <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-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>
|
||
<!-- GPU 引导内容 -->
|
||
<div v-if="form.machineCategory === 'GPU'" class="gpu-guide-section">
|
||
<el-card shadow="never" class="gpu-guide-card">
|
||
<div class="gpu-guide-content">
|
||
<div class="gpu-guide-title">注意事项:</div>
|
||
<ol class="gpu-guide-list">
|
||
<li>
|
||
GPU商品需先点击下方按钮,下载并在GPU所在主机启动客户端,在下载包中会有启动客户端的操作指引文档
|
||
</li>
|
||
<li>
|
||
成功在GPU主机启动客户端后,GPU信息会自行添加至商品列表中,点击下方前往商品列表按钮可以前往该页面
|
||
</li>
|
||
<li>
|
||
客户端和您的卖家账号绑定,如果您需要在本卖家账号<span v-if="userEmail"> ({{ userEmail }})</span>添加多个GPU商品,可通过下列两种方法实现:<br />
|
||
方法一:点击下方下载客户端按钮,将下载好的完整客户端包复制到不同的GPU主机并且启动,启动后客户端所在主机的GPU数据会自动添加到商品列表中
|
||
(推荐) <br />
|
||
方法二:在不同的客户端主机登陆您的卖家账号,进入本页面,点击下载对应操作系统客户端,成功启动客户端后也可自动添加GPU数据到商品列表中
|
||
</li>
|
||
<li>目前只支持NVIDIA系列GPU</li>
|
||
</ol>
|
||
<div class="gpu-guide-buttons">
|
||
<el-button type="primary" @click="handleDownloadClient('windows')"
|
||
>Windows 客户端下载</el-button
|
||
>
|
||
<el-button type="primary" @click="handleDownloadClient('linux')"
|
||
>Linux 客户端下载</el-button
|
||
>
|
||
<el-button type="success" @click="handleGpuClientStarted"
|
||
>前往商品列表</el-button
|
||
>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
<!-- ASIC:币种/算法/理论算力/单位 同行,支持多行动态增删 -->
|
||
<el-form-item
|
||
v-if="form.machineCategory === 'ASIC'"
|
||
label="币种/算法/算力/单位"
|
||
prop="coinAndAlgoList"
|
||
:required="true"
|
||
>
|
||
<div class="coin-algo-rows">
|
||
<div
|
||
class="coin-algo-line"
|
||
v-for="(row, idx) in form.coinAndAlgoList"
|
||
:key="idx"
|
||
>
|
||
<el-select
|
||
v-model="row.coin"
|
||
placeholder="请选择币种"
|
||
class="coin-input"
|
||
@change="handleCoinChange(idx, $event)"
|
||
:loading="loadingCoins"
|
||
filterable
|
||
clearable
|
||
>
|
||
<el-option
|
||
v-for="coin in coinOptions"
|
||
:key="coin"
|
||
:label="coin"
|
||
:value="coin"
|
||
/>
|
||
</el-select>
|
||
<el-select
|
||
v-model="row.algorithm"
|
||
placeholder="请选择算法"
|
||
class="algo-input"
|
||
:loading="loadingAlgos[idx]"
|
||
:disabled="!row.coin"
|
||
filterable
|
||
clearable
|
||
>
|
||
<el-option
|
||
v-for="algo in (algoOptionsMap[row.coin] || [])"
|
||
:key="algo"
|
||
:label="algo"
|
||
:value="algo"
|
||
/>
|
||
</el-select>
|
||
<el-input
|
||
v-model="row.theoryPower"
|
||
placeholder="理论算力"
|
||
inputmode="decimal"
|
||
class="power-input"
|
||
@input="handleCoinRowTheoryInput(idx)"
|
||
/>
|
||
<el-select
|
||
v-model="row.unit"
|
||
placeholder="单位"
|
||
class="unit-select"
|
||
@change="handleCoinRowUnitChange(idx, $event)"
|
||
>
|
||
<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>
|
||
<el-button
|
||
class="op-btn"
|
||
type="primary"
|
||
icon="el-icon-plus"
|
||
circle
|
||
@click="handleAddCoinAlgoRow"
|
||
:aria-label="'新增一行'"
|
||
/>
|
||
<el-button
|
||
v-if="form.coinAndAlgoList.length > 1"
|
||
class="op-btn"
|
||
icon="el-icon-minus"
|
||
circle
|
||
@click="handleRemoveCoinAlgoRow(idx)"
|
||
:aria-label="'删除该行'"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="form.machineCategory === 'ASIC'" label="矿机型号" prop="type" :required="true">
|
||
<el-input
|
||
style="width: 50%"
|
||
v-model="form.type"
|
||
placeholder="示例:龍珠"
|
||
:maxlength="20"
|
||
@input="handleTypeInput"
|
||
/>
|
||
</el-form-item>
|
||
<!-- 理论算力与单位已合并到上面的同行多行输入 -->
|
||
<el-form-item v-if="form.machineCategory === 'ASIC'" label="最大租赁天数" prop="maxLeaseDays">
|
||
<el-input
|
||
v-model="form.maxLeaseDays"
|
||
placeholder="1-365"
|
||
inputmode="numeric"
|
||
@input="handleNumeric('maxLeaseDays')"
|
||
style="width: 50%"
|
||
>
|
||
<template slot="append">天</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item v-if="form.machineCategory === 'ASIC'" label="功耗" prop="powerDissipation">
|
||
<el-input
|
||
v-model="form.powerDissipation"
|
||
inputmode="decimal"
|
||
@input="handleNumeric('powerDissipation')"
|
||
style="width: 50%"
|
||
>
|
||
<template slot="append">kw/h</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
|
||
<el-form-item
|
||
v-if="form.machineCategory === 'ASIC'"
|
||
label="统一售价"
|
||
:prop="payTypeDefs && payTypeDefs.length ? 'costMap' : '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">
|
||
<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="请输入成本"
|
||
inputmode="decimal"
|
||
@input="handleNumeric('cost')"
|
||
style="width: 50%"
|
||
>
|
||
<template slot="append">USDT</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
|
||
<!-- 出售机器数量(统一使用,GPU 仅作引导不在本页提交) -->
|
||
<el-form-item
|
||
v-if="form.machineCategory === 'ASIC'"
|
||
label="出售机器数量(台)"
|
||
prop="sellCount"
|
||
:required="true"
|
||
>
|
||
<el-input
|
||
v-model="form.sellCount"
|
||
placeholder="1 - 9999"
|
||
inputmode="numeric"
|
||
style="width: 50%"
|
||
@input="handleSellCountInput"
|
||
@blur="handleSellCountBlur"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<div v-if="form.machineCategory === 'ASIC'" class="actions">
|
||
<el-button @click="handleBack">取消</el-button>
|
||
<el-button type="primary" :loading="saving" @click="handleSave"
|
||
>确认创建</el-button
|
||
>
|
||
</div>
|
||
|
||
<!-- 钱包未绑定提示弹窗 -->
|
||
<el-dialog
|
||
title="提示"
|
||
:visible.sync="walletBindDialogVisible"
|
||
width="400px"
|
||
:close-on-click-modal="false"
|
||
:show-close="false"
|
||
:close-on-press-escape="false"
|
||
class="wallet-bind-dialog"
|
||
>
|
||
<div class="wallet-bind-content">
|
||
<i class="el-icon-warning wallet-warning-icon"></i>
|
||
<p class="wallet-bind-message">
|
||
请先在我的店铺绑定钱包地址后才能创建商品
|
||
</p>
|
||
<el-button type="primary" @click="handleGoToWalletBind" class="wallet-bind-btn">
|
||
去绑定钱包
|
||
</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 上架确认弹窗 -->
|
||
<el-dialog
|
||
title="请确认上架信息"
|
||
:visible.sync="confirmVisible"
|
||
width="560px"
|
||
>
|
||
<div style="text-align: left; line-height: 1.9">
|
||
<div>
|
||
币种:<b>{{ confirmData.coin }}</b>
|
||
</div>
|
||
<div>
|
||
算法:<b>{{ confirmData.algorithm }}</b>
|
||
</div>
|
||
<div>
|
||
最大租赁天数:<b>{{ confirmData.maxLeaseDays || "-" }}</b>
|
||
</div>
|
||
<div>
|
||
出售机器数量:<b>{{ confirmData.saleNumbers || "-" }}</b>
|
||
</div>
|
||
<div style="margin-top: 8px">售价:</div>
|
||
<el-table
|
||
:data="confirmData.priceList"
|
||
border
|
||
size="mini"
|
||
style="width: 100%"
|
||
>
|
||
<el-table-column prop="chain" label="链" width="120" />
|
||
<el-table-column prop="coin" label="币种" width="120" />
|
||
<el-table-column label="价格">
|
||
<template slot-scope="scope">
|
||
{{ scope.row.price }}
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<p style="color: #666; margin-top: 12px">
|
||
请仔细确认以上参数无误后提交。
|
||
</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 {
|
||
addSingleOrBatchMachine,
|
||
downloadClient,
|
||
addAsicMachine,getSupportCoin,getSupportAlgo
|
||
} from "../../api/machine";
|
||
import { getPayTypes } from "../../api/products";
|
||
import request from "../../utils/request";
|
||
export default {
|
||
name: "AccountProductMachineAdd",
|
||
data() {
|
||
return {
|
||
form: {
|
||
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:币种/算法/理论算力/单位 行编辑(最多10行) */
|
||
coinAndAlgoList: [
|
||
{ coin: "", algorithm: "", theoryPower: "", unit: "TH/S" },
|
||
],
|
||
powerDissipation: null,
|
||
type: "",
|
||
cost: "",
|
||
costMap: {}, // { 'CHAIN-COIN': '123.45' }
|
||
maxLeaseDays: "",
|
||
},
|
||
confirmVisible: false,
|
||
confirmData: {
|
||
coin: "",
|
||
algorithm: "",
|
||
maxLeaseDays: "",
|
||
saleNumbers: "",
|
||
priceList: [],
|
||
},
|
||
rules: {
|
||
productName: [
|
||
{ required: true, message: "商品名称不能为空", trigger: "change" },
|
||
],
|
||
type: [
|
||
{ required: true, message: "矿机型号不能为空", trigger: "blur" },
|
||
{
|
||
validator: (rule, value, callback) => {
|
||
const s = String(value || "");
|
||
// 不允许全是空格
|
||
if (s && s.trim().length === 0) {
|
||
callback(new Error("矿机型号不能全是空格"));
|
||
return;
|
||
}
|
||
callback();
|
||
},
|
||
trigger: "blur",
|
||
},
|
||
],
|
||
coinAndAlgoList: [
|
||
{
|
||
validator: (rule, value, callback) =>
|
||
this.validateCoinAlgoRows(rule, value, 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" },
|
||
{
|
||
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",
|
||
},
|
||
],
|
||
cost: [
|
||
{
|
||
validator(rule, value, callback) {
|
||
// 若为多结算币种模式,跳过此校验(统一售价由每种币种的输入框承担)
|
||
if (
|
||
Array.isArray(this.payTypeDefs) &&
|
||
this.payTypeDefs.length > 0
|
||
) {
|
||
callback();
|
||
return;
|
||
}
|
||
const str = String(value || "");
|
||
if (!str) {
|
||
callback(new Error("请填写机器成本"));
|
||
return;
|
||
}
|
||
const pattern = /^\d{1,12}(\.\d{1,2})?$/;
|
||
if (!pattern.test(str)) {
|
||
callback(new Error("成本整数最多12位,小数最多2位"));
|
||
return;
|
||
}
|
||
if (Number(str) <= 0) {
|
||
callback(new Error("成本必须大于 0"));
|
||
return;
|
||
}
|
||
callback();
|
||
},
|
||
trigger: "blur",
|
||
},
|
||
],
|
||
maxLeaseDays: [
|
||
{ required: true, message: "请填写最大租赁天数", trigger: "blur" },
|
||
{
|
||
validator: (rule, value, callback) => {
|
||
const raw = String(value ?? "");
|
||
if (!raw) {
|
||
callback(new Error("请填写最大租赁天数"));
|
||
return;
|
||
}
|
||
if (!/^\d{1,3}$/.test(raw)) {
|
||
callback(new Error("仅允许整数,范围 1-365"));
|
||
return;
|
||
}
|
||
const n = Number(raw);
|
||
if (!Number.isInteger(n) || n < 1 || n > 365) {
|
||
callback(new Error("范围需在 1-365 天"));
|
||
return;
|
||
}
|
||
callback();
|
||
},
|
||
trigger: "blur",
|
||
},
|
||
],
|
||
},
|
||
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,
|
||
lastCostMapBaseline: {}, // { key: number }
|
||
lastTypeBaseline: "",
|
||
lastMaxLeaseDaysBaseline: 0,
|
||
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,
|
||
/** 支持的支付方式定义(用于动态渲染统一售价输入组) */
|
||
payTypeDefs: [],
|
||
/** 币种选项列表 */
|
||
coinOptions: [],
|
||
/** 算法选项映射 { coin: [algo1, algo2, ...] } */
|
||
algoOptionsMap: {},
|
||
/** 加载币种状态 */
|
||
loadingCoins: false,
|
||
/** 加载算法状态映射 { index: boolean } */
|
||
loadingAlgos: {},
|
||
/** 钱包未绑定提示弹窗 */
|
||
walletBindDialogVisible: false,
|
||
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,
|
||
},
|
||
],
|
||
},
|
||
userEmail:"",
|
||
};
|
||
},
|
||
created() {
|
||
this.initPayTypesFromRoute();
|
||
this.lastTypeBaseline = this.form.type;
|
||
// 绑定基于组件实例的校验器,避免 this 丢失
|
||
if (this.rules && this.rules.cost) {
|
||
this.$set(this.rules, "cost", [
|
||
{ validator: this.validateCost, trigger: "blur" },
|
||
]);
|
||
}
|
||
// 多币种价格专用校验
|
||
this.$set(this.rules, "costMap", [
|
||
{ validator: this.validateCostMap, trigger: "blur" },
|
||
]);
|
||
|
||
this.getPayTypes();
|
||
this.loadSupportCoins();
|
||
|
||
this.userEmail = this.getLeasEmailFromStorage();
|
||
},
|
||
methods: {
|
||
/**
|
||
* 安全获取本地缓存的邮箱(兼容:纯字符串 / JSON 字符串)
|
||
*
|
||
* 背景:项目里 `leasEmail` 存储方式不一致,有的地方直接 `setItem('leasEmail', email)` 存纯字符串,
|
||
* 如果这里强行 JSON.parse 会导致 created hook 直接报错并中断页面逻辑。
|
||
*
|
||
* @returns {string} 邮箱;获取失败返回空字符串
|
||
*/
|
||
getLeasEmailFromStorage() {
|
||
const raw = localStorage.getItem("leasEmail");
|
||
if (!raw) return "";
|
||
|
||
const trimmed = String(raw).trim();
|
||
if (!trimmed) return "";
|
||
|
||
// 常见情况:直接存的纯字符串邮箱
|
||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[") && !trimmed.startsWith('"')) {
|
||
return trimmed;
|
||
}
|
||
|
||
// 兼容情况:存了 JSON(例如 '"a@b.com"' 或 { email: '...' })
|
||
try {
|
||
const parsed = JSON.parse(trimmed);
|
||
if (typeof parsed === "string") return parsed.trim();
|
||
if (parsed && typeof parsed === "object") {
|
||
const v = parsed.email || parsed.leasEmail || parsed.userEmail;
|
||
return v ? String(v).trim() : "";
|
||
}
|
||
return "";
|
||
} catch (e) {
|
||
// JSON 解析失败兜底:按纯字符串处理,避免抛错
|
||
return trimmed;
|
||
}
|
||
},
|
||
/** ASIC 行校验:币种/算法/理论算力/单位 */
|
||
validateCoinAlgoRows(rule, value, callback) {
|
||
try {
|
||
const rows = Array.isArray(this.form.coinAndAlgoList)
|
||
? this.form.coinAndAlgoList
|
||
: [];
|
||
if (!rows.length) {
|
||
callback(new Error("请至少添加一行币种/算法/算力/单位"));
|
||
return;
|
||
}
|
||
const powerPattern = /^\d{1,6}(\.\d{1,4})?$/;
|
||
for (let i = 0; i < rows.length; i += 1) {
|
||
const r = rows[i] || {};
|
||
const coin = String(r.coin || "").trim();
|
||
const algo = String(r.algorithm || "").trim();
|
||
const power = String(r.theoryPower || "").trim();
|
||
const unit = String(r.unit || "").trim();
|
||
if (!coin) {
|
||
callback(new Error(`第 ${i + 1} 行:请选择币种`));
|
||
return;
|
||
}
|
||
if (!algo) {
|
||
callback(new Error(`第 ${i + 1} 行:请选择算法`));
|
||
return;
|
||
}
|
||
if (!power || !powerPattern.test(power) || Number(power) <= 0) {
|
||
callback(
|
||
new Error(
|
||
`第 ${i + 1} 行:理论算力需大于0,整数最多6位,小数最多4位`
|
||
)
|
||
);
|
||
return;
|
||
}
|
||
if (!unit) {
|
||
callback(new Error(`第 ${i + 1} 行:请选择算力单位`));
|
||
return;
|
||
}
|
||
}
|
||
callback();
|
||
} catch (e) {
|
||
callback(new Error("请检查币种/算法/算力/单位填写"));
|
||
}
|
||
},
|
||
/**
|
||
* 加载支持的币种列表
|
||
*/
|
||
async loadSupportCoins() {
|
||
this.loadingCoins = true;
|
||
try {
|
||
const res = await getSupportCoin();
|
||
if (res && (res.code === 0 || res.code === 200)) {
|
||
const data = res.data || [];
|
||
// 处理返回的数据,可能是数组或对象
|
||
if (Array.isArray(data)) {
|
||
this.coinOptions = data.map(item => {
|
||
// 如果是对象,取 coin 字段;如果是字符串,直接使用
|
||
return typeof item === 'string' ? item : (item.coin || item.name || item);
|
||
}).filter(Boolean);
|
||
} else if (data && typeof data === 'object') {
|
||
// 如果是对象,尝试提取币种列表
|
||
this.coinOptions = Object.keys(data).map(key => {
|
||
const item = data[key];
|
||
return typeof item === 'string' ? item : (item.coin || item.name || key);
|
||
}).filter(Boolean);
|
||
}
|
||
// 去重并排序
|
||
this.coinOptions = [...new Set(this.coinOptions)].sort();
|
||
}
|
||
} catch (e) {
|
||
console.error("加载币种列表失败", e);
|
||
|
||
} finally {
|
||
this.loadingCoins = false;
|
||
}
|
||
},
|
||
/**
|
||
* 币种选择变化处理
|
||
* @param {number} index - 行索引
|
||
* @param {string} coin - 选择的币种
|
||
*/
|
||
async handleCoinChange(index, coin) {
|
||
// 清空当前行的算法选择
|
||
this.$set(this.form.coinAndAlgoList[index], "algorithm", "");
|
||
// 如果选择了币种,加载对应的算法列表
|
||
if (coin) {
|
||
await this.loadAlgorithmsForCoin(coin, index);
|
||
}
|
||
},
|
||
/**
|
||
* 加载指定币种支持的算法列表
|
||
* @param {string} coin - 币种名称
|
||
* @param {number} index - 行索引(用于显示加载状态)
|
||
*/
|
||
async loadAlgorithmsForCoin(coin, index) {
|
||
if (!coin) return;
|
||
|
||
// 如果已经加载过该币种的算法,直接返回
|
||
if (this.algoOptionsMap[coin] && this.algoOptionsMap[coin].length > 0) {
|
||
return;
|
||
}
|
||
|
||
// 设置加载状态
|
||
this.$set(this.loadingAlgos, index, true);
|
||
|
||
try {
|
||
const res = await getSupportAlgo(coin);
|
||
if (res && (res.code === 0 || res.code === 200)) {
|
||
const data = res.data || [];
|
||
let algorithms = [];
|
||
|
||
// 处理返回的数据,可能是数组或对象
|
||
if (Array.isArray(data)) {
|
||
algorithms = data.map(item => {
|
||
// 如果是对象,取 algorithm 或 algo 字段;如果是字符串,直接使用
|
||
return typeof item === 'string' ? item : (item.algorithm || item.algo || item.name || item);
|
||
}).filter(Boolean);
|
||
} else if (data && typeof data === 'object') {
|
||
// 如果是对象,尝试提取算法列表
|
||
algorithms = Object.keys(data).map(key => {
|
||
const item = data[key];
|
||
return typeof item === 'string' ? item : (item.algorithm || item.algo || item.name || key);
|
||
}).filter(Boolean);
|
||
}
|
||
|
||
// 去重并排序,保存到映射中
|
||
this.$set(this.algoOptionsMap, coin, [...new Set(algorithms)].sort());
|
||
}
|
||
} catch (e) {
|
||
console.error(`加载币种 ${coin} 的算法列表失败`, e);
|
||
|
||
// 设置空数组,避免重复请求
|
||
this.$set(this.algoOptionsMap, coin, []);
|
||
} finally {
|
||
this.$set(this.loadingAlgos, index, false);
|
||
}
|
||
},
|
||
/** 行:理论算力输入限制(6整数4小数) */
|
||
handleCoinRowTheoryInput(index) {
|
||
let v = String(this.form.coinAndAlgoList[index].theoryPower ?? "");
|
||
v = v.replace(/[^0-9.]/g, "");
|
||
const firstDot = v.indexOf(".");
|
||
if (firstDot !== -1) {
|
||
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, "");
|
||
}
|
||
const endsWithDot = v.endsWith(".");
|
||
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;
|
||
this.$set(this.form.coinAndAlgoList[index], "theoryPower", v);
|
||
},
|
||
/** 行:单位变更 */
|
||
handleCoinRowUnitChange(index, value) {
|
||
this.$set(this.form.coinAndAlgoList[index], "unit", value);
|
||
},
|
||
/** 新增一行 */
|
||
handleAddCoinAlgoRow() {
|
||
if (this.form.coinAndAlgoList.length >= 10) {
|
||
this.$message.warning("最多添加 10 行");
|
||
return;
|
||
}
|
||
const last = this.form.coinAndAlgoList[
|
||
this.form.coinAndAlgoList.length - 1
|
||
] || { unit: "TH/S" };
|
||
const newIndex = this.form.coinAndAlgoList.length;
|
||
this.form.coinAndAlgoList.push({
|
||
coin: "",
|
||
algorithm: "",
|
||
theoryPower: "",
|
||
unit: last.unit || "TH/S",
|
||
});
|
||
// 初始化新行的加载状态
|
||
this.$set(this.loadingAlgos, newIndex, false);
|
||
},
|
||
/** 删除一行 */
|
||
handleRemoveCoinAlgoRow(index) {
|
||
if (this.form.coinAndAlgoList.length <= 1) return;
|
||
this.form.coinAndAlgoList.splice(index, 1);
|
||
},
|
||
/** 从多行聚合 coin CSV */
|
||
buildCoinCsvFromRows() {
|
||
const set = new Set();
|
||
const rows = Array.isArray(this.form.coinAndAlgoList)
|
||
? this.form.coinAndAlgoList
|
||
: [];
|
||
rows.forEach((r) => {
|
||
const token = String(r.coin || "")
|
||
.split(/[,\s,、]+/)
|
||
.map((s) => s.trim().toUpperCase())
|
||
.filter(Boolean);
|
||
token.forEach((t) => set.add(t));
|
||
});
|
||
return Array.from(set).join(",");
|
||
},
|
||
/** 从多行聚合 algorithm CSV */
|
||
buildAlgoCsvFromRows() {
|
||
const set = new Set();
|
||
const rows = Array.isArray(this.form.coinAndAlgoList)
|
||
? this.form.coinAndAlgoList
|
||
: [];
|
||
rows.forEach((r) => {
|
||
const token = String(r.algorithm || "")
|
||
.split(/[,\s,、]+/)
|
||
.map((s) => s.trim().toUpperCase())
|
||
.filter(Boolean);
|
||
token.forEach((t) => set.add(t));
|
||
});
|
||
return Array.from(set).join(",");
|
||
},
|
||
/** 实时过滤币种输入中的中文字符(仅保留英文/数字/分隔符) */
|
||
handleCoinsInput() {
|
||
let v = String(this.form.coinsInput || "");
|
||
// 去除中文
|
||
v = v.replace(/[\u4e00-\u9fa5]/g, "");
|
||
this.form.coinsInput = v;
|
||
},
|
||
/** 实时过滤算法输入中的中文字符(仅保留英文/数字/连字符/分隔符) */
|
||
handleAlgorithmsInput() {
|
||
let v = String(this.form.algorithmsInput || "");
|
||
v = v.replace(/[\u4e00-\u9fa5]/g, "");
|
||
this.form.algorithmsInput = v;
|
||
},
|
||
/** 将输入按中英文逗号、空格分割,去空,统一英文逗号连接;可选:统一大写 */
|
||
normalizeCsv(input, upper = true) {
|
||
const arr = String(input || "")
|
||
.split(/[,\s,、]+/)
|
||
.map((s) => s.trim())
|
||
.filter(Boolean);
|
||
const list = upper ? arr.map((s) => s.toUpperCase()) : arr;
|
||
return list.join(",");
|
||
},
|
||
/** 从 payTypeDefs 与 costMap 生成价格列表 */
|
||
buildPriceList() {
|
||
const list = [];
|
||
const defs = Array.isArray(this.payTypeDefs) ? this.payTypeDefs : [];
|
||
defs.forEach((d) => {
|
||
const key = d.key;
|
||
const priceRaw = this.form.costMap ? this.form.costMap[key] : "";
|
||
const priceNum = Number(priceRaw);
|
||
// 允许为空/非数字则不加入
|
||
if (!Number.isFinite(priceNum) || priceNum <= 0) return;
|
||
list.push({
|
||
chain: d.chain,
|
||
coin: d.coin,
|
||
price: priceNum,
|
||
});
|
||
});
|
||
return list;
|
||
},
|
||
async getPayTypes() {
|
||
try {
|
||
const res = await getPayTypes();
|
||
// 期望结构:{ code:200, data:[ { payChain, payCoin, payCoinImage } ] }
|
||
if (res && (res.code === 0 || res.code === 200)) {
|
||
const list = Array.isArray(res.data) ? res.data : [];
|
||
const defs = [];
|
||
const seen = new Set();
|
||
list.forEach((it) => {
|
||
const chain = String(
|
||
it && it.payChain ? it.payChain : ""
|
||
).toUpperCase();
|
||
const coin = String(
|
||
it && it.payCoin ? it.payCoin : ""
|
||
).toUpperCase();
|
||
if (!chain && !coin) return;
|
||
const key = [chain, coin].filter(Boolean).join("-");
|
||
if (seen.has(key)) return;
|
||
seen.add(key);
|
||
defs.push({
|
||
chain,
|
||
coin,
|
||
key,
|
||
label: key,
|
||
image: it && it.payCoinImage ? String(it.payCoinImage) : "",
|
||
});
|
||
});
|
||
// 根据接口结果渲染"统一售价"输入组
|
||
this.payTypeDefs = defs;
|
||
const nextCostMap = {};
|
||
this.payTypeDefs.forEach((d) => {
|
||
// 保留已输入的数值;否则置空
|
||
nextCostMap[d.key] =
|
||
(this.form.costMap && this.form.costMap[d.key]) || "";
|
||
});
|
||
this.form.costMap = nextCostMap;
|
||
|
||
// 如果返回的支付方式列表为空,显示提示弹窗
|
||
if (defs.length === 0) {
|
||
this.walletBindDialogVisible = true;
|
||
}
|
||
} else {
|
||
// 如果接口返回失败或数据格式不正确,也显示提示弹窗
|
||
this.walletBindDialogVisible = true;
|
||
}
|
||
} catch (e) {
|
||
// 接口调用失败,显示提示弹窗
|
||
this.walletBindDialogVisible = true;
|
||
}
|
||
},
|
||
/**
|
||
* 跳转到钱包绑定页面
|
||
*/
|
||
handleGoToWalletBind() {
|
||
this.$router.push('/account/shop-config');
|
||
},
|
||
/**
|
||
* 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;
|
||
// 当输入框为空时,清除验证错误
|
||
if (!v || v === "") {
|
||
this.$nextTick(() => {
|
||
this.$refs.machineForm && this.$refs.machineForm.clearValidate('sellCount');
|
||
});
|
||
}
|
||
},
|
||
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 = "";
|
||
}
|
||
},
|
||
/**
|
||
* 矿机种类变更
|
||
* @param {string} val
|
||
*/
|
||
handleMachineCategoryChange(val) {
|
||
// GPU 引导内容已直接显示在页面上,无需弹窗
|
||
},
|
||
/**
|
||
* 下载 GPU 客户端
|
||
*/
|
||
handleDownloadClient(types) {
|
||
// 走后端接口下载客户端程序
|
||
const userEmail = this.getLeasEmailFromStorage();
|
||
console.log(userEmail, "userEmail");
|
||
|
||
if (!userEmail) {
|
||
this.$message.warning("未获取到登录邮箱,无法下载客户端,请重新登录后再试");
|
||
return;
|
||
}
|
||
|
||
this.downloadUrl = `${request.defaults.baseURL}/lease/user/downloadClient?userEmail=${encodeURIComponent(
|
||
userEmail
|
||
)}&type=${encodeURIComponent(types || "")}`;
|
||
let a = document.createElement(`a`);
|
||
a.href = this.downloadUrl;
|
||
a.click();
|
||
},
|
||
/**
|
||
* GPU 客户端已启动:跳转至商品列表
|
||
*/
|
||
handleGpuClientStarted() {
|
||
// 跳转到“个人中心-商品列表”页面
|
||
this.$router.push("/account/products");
|
||
},
|
||
/** 统一售价校验:多结算币种时跳过,单价时按 USDT 校验 */
|
||
validateCost(rule, value, callback) {
|
||
// 多支付方式:逐个校验 costMap
|
||
if (Array.isArray(this.payTypeDefs) && this.payTypeDefs.length > 0) {
|
||
return this.validateCostMap(rule, value, callback);
|
||
}
|
||
const str = String(value || "");
|
||
if (!str) {
|
||
callback(new Error("请填写机器成本"));
|
||
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();
|
||
},
|
||
// 多支付方式下的价格校验:要求每个支付方式都需填写有效价格
|
||
validateCostMap(rule, value, callback) {
|
||
try {
|
||
const defs = Array.isArray(this.payTypeDefs) ? this.payTypeDefs : [];
|
||
if (!defs.length) {
|
||
callback();
|
||
return;
|
||
}
|
||
const pattern = /^\d{1,12}(\.\d{1,2})?$/;
|
||
for (let i = 0; i < defs.length; i += 1) {
|
||
const d = defs[i];
|
||
const key = d.key;
|
||
const v =
|
||
this.form && this.form.costMap
|
||
? String(this.form.costMap[key] || "")
|
||
: "";
|
||
if (!v) {
|
||
callback(new Error(`请填写 ${d.label} 的价格`));
|
||
return;
|
||
}
|
||
if (!pattern.test(v)) {
|
||
callback(new Error(`${d.label} 价格整数最多12位,小数最多2位`));
|
||
return;
|
||
}
|
||
if (Number(v) <= 0) {
|
||
callback(new Error(`${d.label} 价格必须大于0`));
|
||
return;
|
||
}
|
||
}
|
||
callback();
|
||
} catch (e) {
|
||
callback(new Error("价格填写有误,请检查"));
|
||
}
|
||
},
|
||
/** 解析路由参数中的支付方式,生成标准定义 */
|
||
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();
|
||
},
|
||
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 if (key === "maxLeaseDays") {
|
||
// 最大租赁天数:仅整数,范围 1-365,输入阶段限制为最多3位数字
|
||
v = v.replace(/\D/g, "");
|
||
if (v.length > 3) v = v.slice(0, 3);
|
||
this.form[key] = v;
|
||
this.syncMaxLeaseDaysToRows();
|
||
return;
|
||
} else {
|
||
// 其他:最多6位小数(保持原有逻辑)
|
||
if (firstDot !== -1) {
|
||
const [intPart, decPart] = v.split(".");
|
||
v = intPart + "." + (decPart ? decPart.slice(0, 6) : "");
|
||
}
|
||
}
|
||
this.form[key] = v;
|
||
},
|
||
/** 顶部多结算币种统一售价输入 */
|
||
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);
|
||
},
|
||
/**
|
||
* 顶部矿机型号输入:限制20字符
|
||
*/
|
||
handleTypeInput() {
|
||
if (typeof this.form.type === "string" && this.form.type.length > 20) {
|
||
this.form.type = this.form.type.slice(0, 20);
|
||
}
|
||
// 当输入框为空时,清除验证错误
|
||
if (!this.form.type || this.form.type.trim() === "") {
|
||
this.$nextTick(() => {
|
||
this.$refs.machineForm && this.$refs.machineForm.clearValidate('type');
|
||
});
|
||
}
|
||
},
|
||
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.lastTypeBaseline = this.form.type;
|
||
},
|
||
/**
|
||
* 行内功耗输入(限制:整数最多6位,小数最多4位)
|
||
*/
|
||
handleRowPowerDissipationInput(index) {
|
||
let v = String(this.selectedMachineRows[index].powerDissipation ?? "");
|
||
v = v.replace(/[^0-9.]/g, "");
|
||
const firstDot = v.indexOf(".");
|
||
if (firstDot !== -1) {
|
||
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, "");
|
||
}
|
||
const endsWithDot = v.endsWith(".");
|
||
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;
|
||
this.$set(this.selectedMachineRows[index], "powerDissipation", v);
|
||
},
|
||
/**
|
||
* 行内功耗校验
|
||
*/
|
||
handleRowPowerDissipationBlur(index) {
|
||
const raw = String(
|
||
this.selectedMachineRows[index].powerDissipation ?? ""
|
||
);
|
||
const pattern = /^\d{1,6}(\.\d{1,4})?$/;
|
||
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
|
||
this.$message.warning("功耗需大于0,整数最多6位,小数最多4位");
|
||
this.$set(this.selectedMachineRows[index], "powerDissipation", "");
|
||
}
|
||
},
|
||
/**
|
||
* 行内理论算力输入(限制:整数最多6位,小数最多4位)
|
||
*/
|
||
handleRowTheoryPowerInput(index) {
|
||
let v = String(this.selectedMachineRows[index].theoryPower ?? "");
|
||
v = v.replace(/[^0-9.]/g, "");
|
||
const firstDot = v.indexOf(".");
|
||
if (firstDot !== -1) {
|
||
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, "");
|
||
}
|
||
const endsWithDot = v.endsWith(".");
|
||
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;
|
||
this.$set(this.selectedMachineRows[index], "theoryPower", v);
|
||
},
|
||
/**
|
||
* 行内理论算力校验
|
||
*/
|
||
handleRowTheoryPowerBlur(index) {
|
||
const raw = String(this.selectedMachineRows[index].theoryPower ?? "");
|
||
const pattern = /^\d{1,6}(\.\d{1,4})?$/;
|
||
if (!raw || Number(raw) <= 0 || !pattern.test(raw)) {
|
||
this.$message.warning("理论算力需大于0,整数最多6位,小数最多4位");
|
||
this.$set(this.selectedMachineRows[index], "theoryPower", "");
|
||
}
|
||
},
|
||
/**
|
||
* 行内单位变更
|
||
*/
|
||
handleRowUnitChange(index, value) {
|
||
this.$set(this.selectedMachineRows[index], "unit", value);
|
||
},
|
||
syncMaxLeaseDaysToRows() {
|
||
const raw = this.form.maxLeaseDays;
|
||
const n = Number(raw);
|
||
if (!Number.isInteger(n)) return;
|
||
const oldBaseline = this.lastMaxLeaseDaysBaseline;
|
||
this.selectedMachineRows = this.selectedMachineRows.map((row) => {
|
||
const rowNum = Number(row.maxLeaseDays);
|
||
if (!Number.isInteger(rowNum) || rowNum === oldBaseline) {
|
||
return { ...row, maxLeaseDays: n };
|
||
}
|
||
return row;
|
||
});
|
||
this.lastMaxLeaseDaysBaseline = n;
|
||
},
|
||
handleRowMaxLeaseDaysInput(index) {
|
||
let v = String(this.selectedMachineRows[index].maxLeaseDays ?? "");
|
||
v = v.replace(/\D/g, "");
|
||
if (v.length > 3) v = v.slice(0, 3);
|
||
this.$set(this.selectedMachineRows[index], "maxLeaseDays", v);
|
||
},
|
||
handleRowMaxLeaseDaysBlur(index) {
|
||
const raw = String(this.selectedMachineRows[index].maxLeaseDays ?? "");
|
||
if (!/^\d{1,3}$/.test(raw)) {
|
||
this.$message.warning("最大租赁天数需为 1-365 的整数");
|
||
this.$set(this.selectedMachineRows[index], "maxLeaseDays", "");
|
||
return;
|
||
}
|
||
const n = Number(raw);
|
||
if (!Number.isInteger(n) || n < 1 || n > 365) {
|
||
this.$message.warning("最大租赁天数需为 1-365 的整数");
|
||
this.$set(this.selectedMachineRows[index], "maxLeaseDays", "");
|
||
}
|
||
},
|
||
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);
|
||
},
|
||
/** 行内多结算币种价格输入 */
|
||
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})?$/;
|
||
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);
|
||
} 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
|
||
// }
|
||
// 现在统一按出售数量提交(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;
|
||
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;
|
||
}
|
||
// 统一售价与最大租赁天数已在表单级校验中处理,无需逐机校验
|
||
// 组装确认数据并弹框
|
||
const coinStr = this.buildCoinCsvFromRows();
|
||
const algoStr = this.buildAlgoCsvFromRows();
|
||
this.confirmData = {
|
||
coin: coinStr || "-",
|
||
algorithm: algoStr || "-",
|
||
maxLeaseDays: this.form.maxLeaseDays,
|
||
saleNumbers: this.form.sellCount,
|
||
priceList: this.buildPriceList(),
|
||
};
|
||
this.confirmVisible = true;
|
||
},
|
||
async doSubmit() {
|
||
this.saving = true;
|
||
try {
|
||
// 统一售卖新增接口参数
|
||
const list = (this.form.coinAndAlgoList || []).map((r) => ({
|
||
coin: String(r.coin || "")
|
||
.toUpperCase()
|
||
.trim(),
|
||
algorithm: String(r.algorithm || "")
|
||
.toUpperCase()
|
||
.trim(),
|
||
theoryPower: Number(r.theoryPower) || 0,
|
||
unit: r.unit,
|
||
}));
|
||
const payload = {
|
||
// 逗号分隔(中英文逗号都兼容),统一为英文逗号并大写
|
||
coinAndAlgoList: list,
|
||
maxLeaseDays: Number(this.form.maxLeaseDays) || 0,
|
||
name: this.form.type,
|
||
powerDissipation: Number(this.form.powerDissipation) || 0,
|
||
saleNumbers: Number(this.form.sellCount) || 0,
|
||
priceList: this.buildPriceList(),
|
||
};
|
||
// 过滤空价目
|
||
payload.priceList = (payload.priceList || []).filter(
|
||
(p) => Number(p.price) > 0
|
||
);
|
||
|
||
console.log(payload, "请求参数");
|
||
|
||
const res = await addAsicMachine(payload);
|
||
if (res && (res.code === 0 || res.code === 200)) {
|
||
this.$message({
|
||
message: "创建成功",
|
||
duration: 3000,
|
||
showClose: true,
|
||
type: "success",
|
||
});
|
||
this.confirmVisible = false;
|
||
this.$router.push("/account/products");
|
||
}
|
||
} catch (e) {
|
||
console.error("创建商品失败", e);
|
||
console.log("创建失败");
|
||
} finally {
|
||
this.saving = false;
|
||
}
|
||
},
|
||
},
|
||
};
|
||
</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;
|
||
}
|
||
.notice-alert {
|
||
margin-bottom: 12px;
|
||
}
|
||
.notice-alert :deep(.el-alert__content) {
|
||
text-align: left;
|
||
}
|
||
.notice-alert :deep(.el-alert__title),
|
||
.notice-alert :deep(.el-alert__description) {
|
||
text-align: left;
|
||
}
|
||
.label-help {
|
||
margin-left: 4px;
|
||
color: #909399;
|
||
cursor: help;
|
||
}
|
||
.form-card {
|
||
margin-bottom: 12px;
|
||
}
|
||
.actions {
|
||
text-align: left;
|
||
}
|
||
|
||
/* 统一左对齐,控件宽度 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;
|
||
}
|
||
|
||
/* 多结算币种价格输入的布局优化 */
|
||
.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;
|
||
}
|
||
/* ASIC 币种/算法/算力/单位 多行 */
|
||
.coin-algo-rows {
|
||
display: grid;
|
||
gap: 8px;
|
||
width: 100%;
|
||
}
|
||
.coin-algo-line {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.coin-algo-line .coin-input {
|
||
width: 18%;
|
||
min-width: 140px;
|
||
}
|
||
.coin-algo-line .algo-input {
|
||
width: 24%;
|
||
min-width: 160px;
|
||
}
|
||
.coin-algo-line .power-input {
|
||
width: 20%;
|
||
min-width: 140px;
|
||
}
|
||
.coin-algo-line .unit-select {
|
||
width: 16%;
|
||
min-width: 120px;
|
||
}
|
||
.coin-algo-line .op-btn {
|
||
flex: 0 0 auto;
|
||
}
|
||
/* GPU 引导区域样式 */
|
||
.gpu-guide-section {
|
||
margin-bottom: 12px;
|
||
margin-left: 86px;
|
||
}
|
||
.gpu-guide-card {
|
||
padding: 20px;
|
||
background: #f9fafb;
|
||
width: 85%;
|
||
}
|
||
.gpu-guide-content {
|
||
text-align: left;
|
||
line-height: 1.7;
|
||
font-size: 15px;
|
||
color: #555;
|
||
}
|
||
.gpu-guide-title {
|
||
margin-bottom: 8px;
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
color: #333;
|
||
}
|
||
.gpu-guide-list {
|
||
padding-left: 18px;
|
||
margin: 0;
|
||
font-size: 15px;
|
||
line-height: 1.8;
|
||
}
|
||
.gpu-guide-list li {
|
||
margin-bottom: 8px;
|
||
}
|
||
.gpu-guide-list li:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
.gpu-guide-buttons {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
/* 钱包绑定提示弹窗样式 */
|
||
.wallet-bind-dialog :deep(.el-dialog__header) {
|
||
padding: 16px 20px 12px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.wallet-bind-dialog :deep(.el-dialog__title) {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.wallet-bind-dialog :deep(.el-dialog__body) {
|
||
padding: 24px 20px;
|
||
}
|
||
|
||
.wallet-bind-content {
|
||
text-align: center;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.wallet-warning-icon {
|
||
font-size: 40px;
|
||
color: #E6A23C;
|
||
margin-bottom: 16px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.wallet-bind-message {
|
||
font-size: 14px;
|
||
color: #666;
|
||
margin: 0 0 24px 0;
|
||
line-height: 1.6;
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.wallet-bind-btn {
|
||
width: 160px;
|
||
height: 36px;
|
||
font-size: 14px;
|
||
border-radius: 4px;
|
||
}
|
||
</style>
|
||
|