Files
webs/power_leasing/src/views/account/productMachineAdd.vue
2026-01-23 17:00:06 +08:00

1848 lines
60 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div class="product-machine-add">
<div class="header">
<el-button type="text" @click="handleBack">返回</el-button>
<h2 class="title">创建商品</h2>
</div>
<!-- <el-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>