2025-09-26 16:40:38 +08:00
|
|
|
|
|
|
|
|
|
|
import { getProductById } from '../../utils/productService'
|
2025-11-28 15:30:36 +08:00
|
|
|
|
import { getMachineInfo, getPayTypes,getShopMachineList,addGoodsV2 } from '../../api/products'
|
2025-11-20 14:28:57 +08:00
|
|
|
|
import { truncateAmountByCoin, truncateTo6 } from '../../utils/amount'
|
2025-09-26 16:40:38 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'ProductDetail',
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
product: null,
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
// 默认展开的行keys
|
|
|
|
|
|
expandedRowKeys: [],
|
|
|
|
|
|
selectedMap: {},
|
2025-11-07 16:30:03 +08:00
|
|
|
|
// 新接口:单层矿机列表 & 支付方式
|
|
|
|
|
|
machineList: [],
|
|
|
|
|
|
paymentMethodList: [],
|
|
|
|
|
|
// 筛选状态
|
|
|
|
|
|
selectedPayKey: null,
|
|
|
|
|
|
filters: {
|
|
|
|
|
|
chain: '',
|
|
|
|
|
|
coin: '',
|
|
|
|
|
|
minPrice: null,
|
|
|
|
|
|
maxPrice: null,
|
|
|
|
|
|
minPower: null,
|
|
|
|
|
|
maxPower: null,
|
|
|
|
|
|
minPowerDissipation: null,
|
2025-11-18 14:36:43 +08:00
|
|
|
|
maxPowerDissipation: null,
|
|
|
|
|
|
unit: 'GH/S'
|
2025-11-07 16:30:03 +08:00
|
|
|
|
},
|
2025-11-18 14:36:43 +08:00
|
|
|
|
// 实际算力单位选项
|
|
|
|
|
|
powerUnitOptions: ['KH/S', 'MH/S', 'GH/S', 'TH/S', 'PH/S'],
|
2025-11-14 16:17:36 +08:00
|
|
|
|
// 排序状态:true 升序,false 降序
|
|
|
|
|
|
sortStates: {
|
|
|
|
|
|
priceSort: true,
|
|
|
|
|
|
powerSort: true,
|
|
|
|
|
|
powerDissipationSort: true
|
|
|
|
|
|
},
|
|
|
|
|
|
// 当前激活的排序字段(仅当用户点击后才会传参)
|
|
|
|
|
|
activeSortField: '',
|
|
|
|
|
|
// 首次进入时是否已按价格币种设置过支付方式筛选默认值
|
|
|
|
|
|
payFilterDefaultApplied: false,
|
2025-09-26 16:40:38 +08:00
|
|
|
|
params: {
|
|
|
|
|
|
id: "",
|
2025-11-14 16:17:36 +08:00
|
|
|
|
pageNum: 1,
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
|
2025-09-26 16:40:38 +08:00
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
confirmAddDialog: {
|
|
|
|
|
|
visible: false,
|
|
|
|
|
|
items: []
|
|
|
|
|
|
},
|
|
|
|
|
|
// 购物车中已存在的当前商品机器集合:id 与 user|miner 组合键
|
|
|
|
|
|
cartMachineIdSet: new Set(),
|
|
|
|
|
|
cartCompositeKeySet: new Set(),
|
|
|
|
|
|
cartLoaded: false,
|
|
|
|
|
|
machinesLoaded: false,
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 可展开的产品系列数据
|
|
|
|
|
|
* 每个系列(group)包含多个可选条目(variants)
|
|
|
|
|
|
*/
|
|
|
|
|
|
productListData: [
|
|
|
|
|
|
// {
|
|
|
|
|
|
// id: 'grp-1',
|
|
|
|
|
|
// group: 'A系列',
|
|
|
|
|
|
// summary: {
|
|
|
|
|
|
// theoryPower: '56T',
|
|
|
|
|
|
// computingPower: '54T',
|
|
|
|
|
|
// powerDissipation: '3200W',
|
|
|
|
|
|
// algorithm: 'power',
|
|
|
|
|
|
// type: 'A-Pro',
|
|
|
|
|
|
// count: 3,
|
|
|
|
|
|
// price: '¥1000+'
|
|
|
|
|
|
// },
|
|
|
|
|
|
// variants: [
|
|
|
|
|
|
// { id: 'A-1', model: 'A1', theoryPower: '14T', computingPower: '13.5T', powerDissipation: '780W', algorithm: 'power', stock: 50, price: 999, quantity: 1 },
|
|
|
|
|
|
// { id: 'A-2', model: 'A2', theoryPower: '18T', computingPower: '17.2T', powerDissipation: '900W', algorithm: 'power', stock: 40, price: 1299, quantity: 1 },
|
|
|
|
|
|
// { id: 'A-3', model: 'A3', theoryPower: '24T', computingPower: '23.1T', powerDissipation: '1520W', algorithm: 'power', stock: 30, price: 1699, quantity: 1 }
|
|
|
|
|
|
// ]
|
|
|
|
|
|
// },
|
|
|
|
|
|
// {
|
|
|
|
|
|
// id: 'grp-2',
|
|
|
|
|
|
// group: 'B系列',
|
|
|
|
|
|
// summary: {
|
|
|
|
|
|
// theoryPower: '72T',
|
|
|
|
|
|
// computingPower: '70T',
|
|
|
|
|
|
// powerDissipation: '4100W',
|
|
|
|
|
|
// algorithm: 'power',
|
|
|
|
|
|
// type: 'B-Max',
|
|
|
|
|
|
// count: 2,
|
|
|
|
|
|
// price: '¥2000+'
|
|
|
|
|
|
// },
|
|
|
|
|
|
// variants: [
|
|
|
|
|
|
// { id: 'B-1', model: 'B1', theoryPower: '32T', computingPower: '31.2T', powerDissipation: '1800W', algorithm: 'power', stock: 28, price: 2199, quantity: 1 },
|
|
|
|
|
|
// { id: 'B-2', model: 'B2', theoryPower: '40T', computingPower: '38.8T', powerDissipation: '2300W', algorithm: 'power', stock: 18, price: 2699, quantity: 1 }
|
|
|
|
|
|
// ]
|
|
|
|
|
|
// }
|
|
|
|
|
|
],
|
|
|
|
|
|
tableData: [
|
|
|
|
|
|
// {
|
|
|
|
|
|
// theoryPower: "55656",//理论算力
|
|
|
|
|
|
// computingPower: "44545",//实际算力
|
|
|
|
|
|
// powerDissipation: "5565",//功耗
|
|
|
|
|
|
// algorithm: "power",//算法
|
|
|
|
|
|
// type: "型号1",//矿机型号
|
|
|
|
|
|
// number:2001,
|
|
|
|
|
|
// cost:"1000",//价格
|
|
|
|
|
|
// },
|
|
|
|
|
|
// {
|
|
|
|
|
|
// theoryPower: "55656",//理论算力
|
|
|
|
|
|
// computingPower: "44545",//实际算力
|
|
|
|
|
|
// powerDissipation: "5565",//功耗
|
|
|
|
|
|
// algorithm: "power",//算法
|
|
|
|
|
|
// type: "型号1",//矿机型号
|
|
|
|
|
|
// number:2001,
|
|
|
|
|
|
// cost:"1000",//价格
|
|
|
|
|
|
// },
|
|
|
|
|
|
// {
|
|
|
|
|
|
// theoryPower: "55656",//理论算力
|
|
|
|
|
|
// computingPower: "44545",//实际算力
|
|
|
|
|
|
// powerDissipation: "5565",//功耗
|
|
|
|
|
|
// algorithm: "power",//算法
|
|
|
|
|
|
// type: "型号1",//矿机型号
|
|
|
|
|
|
// number:2001,
|
|
|
|
|
|
// cost:"1000",//价格
|
|
|
|
|
|
// },
|
|
|
|
|
|
// {
|
|
|
|
|
|
// theoryPower: "55656",//理论算力
|
|
|
|
|
|
// computingPower: "44545",//实际算力
|
|
|
|
|
|
// powerDissipation: "5565",//功耗
|
|
|
|
|
|
// algorithm: "power",//算法
|
|
|
|
|
|
// type: "型号1",//矿机型号
|
|
|
|
|
|
// number:2001,
|
|
|
|
|
|
// cost:"1000",//价格
|
|
|
|
|
|
// },
|
2025-11-14 16:17:36 +08:00
|
|
|
|
|
2025-09-26 16:40:38 +08:00
|
|
|
|
],
|
2025-11-14 16:17:36 +08:00
|
|
|
|
productDetailLoading: false,
|
|
|
|
|
|
pageSizes: [10, 20, 50],
|
|
|
|
|
|
currentPage: 1,
|
|
|
|
|
|
total: 0,
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 动态列表(模拟渲染)
|
|
|
|
|
|
dynamicMeta: {},
|
|
|
|
|
|
dynamicColumns: [],
|
|
|
|
|
|
dynamicRows: [],
|
|
|
|
|
|
dynamicSearch: {
|
|
|
|
|
|
visible: false,
|
|
|
|
|
|
keyword: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
// 矿机种类:0-ASIC,1-GPU(默认GPU)
|
|
|
|
|
|
machineType: 1,
|
2025-11-14 16:17:36 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-09-26 16:40:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
mounted() {
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 读取用户上次选择的矿机种类(0: ASIC, 1: GPU)
|
|
|
|
|
|
try{
|
|
|
|
|
|
const savedType = Number(window && window.localStorage ? window.localStorage.getItem('pl_machineType') : NaN)
|
|
|
|
|
|
if (savedType === 0 || savedType === 1) {
|
|
|
|
|
|
this.machineType = savedType
|
|
|
|
|
|
}
|
|
|
|
|
|
}catch(e){/* noop */}
|
|
|
|
|
|
let arr={
|
|
|
|
|
|
"meta": {
|
|
|
|
|
|
"pageNum": 1, // 当前页码(后端分页用,可选)
|
|
|
|
|
|
"pageSize": 10, // 每页条数(后端分页用,可选)
|
|
|
|
|
|
"total": 123 // 总条数(后端分页用,可选)
|
|
|
|
|
|
},
|
|
|
|
|
|
"columns": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "model", // 列字段名:与 rows 中同名字段映射
|
|
|
|
|
|
"label": "型号", // 表头显示文本
|
|
|
|
|
|
"type": "text", // 列类型:text/amount/hashrate/days
|
|
|
|
|
|
"fixed": "left", // 是否左固定列:left/right/不传
|
|
|
|
|
|
"width": 100 // 列宽(可选)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "price", // 价格列字段名
|
|
|
|
|
|
"label": "价格", // 表头显示文本
|
|
|
|
|
|
"type": "amount", // 金额类型:仅渲染数值;单位来自行级 priceList.coin
|
|
|
|
|
|
"width": 100 // 列宽(可选)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 动态币种/算法算力列(示例:仅第一列带注释,其余同结构)
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "XTM", // 币种/算法代码作为列 key
|
|
|
|
|
|
"label": "XTM", // 表头显示名
|
|
|
|
|
|
"type": "hashrate", // 列类型:算力
|
|
|
|
|
|
"unit": "MH/s", // 单元格单位(表头不显示单位)
|
|
|
|
|
|
"icon": "https://cdn.xxx/coin/xtm.png", // 表头图标(可选)
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "NXNA",
|
|
|
|
|
|
"label": "NXNA",
|
|
|
|
|
|
"type": "hashrate",
|
|
|
|
|
|
"unit": "MH/s",
|
|
|
|
|
|
"icon": "https://cdn.xxx/coin/nxna.png"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "CLORE",
|
|
|
|
|
|
"label": "CLORE",
|
|
|
|
|
|
"type": "hashrate",
|
|
|
|
|
|
"unit": "MH/s",
|
|
|
|
|
|
"icon": "https://cdn.xxx/coin/clore.png"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "CFX",
|
|
|
|
|
|
"label": "CFX",
|
|
|
|
|
|
"type": "hashrate",
|
|
|
|
|
|
"unit": "MH/s",
|
|
|
|
|
|
"icon": "https://cdn.xxx/coin/cfx.png"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "IRON",
|
|
|
|
|
|
"label": "IRON",
|
|
|
|
|
|
"type": "hashrate",
|
|
|
|
|
|
"unit": "MH/s",
|
|
|
|
|
|
"icon": "https://cdn.xxx/coin/iron.png"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "NEXA",
|
|
|
|
|
|
"label": "NEXA",
|
|
|
|
|
|
"type": "hashrate",
|
|
|
|
|
|
"unit": "MH/s",
|
|
|
|
|
|
"icon": "https://cdn.xxx/coin/nexa.png"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "KLS",
|
|
|
|
|
|
"label": "KLS",
|
|
|
|
|
|
"type": "hashrate",
|
|
|
|
|
|
"unit": "MH/s",
|
|
|
|
|
|
"icon": "https://cdn.xxx/coin/kls.png"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "RVN",
|
|
|
|
|
|
"label": "RVN",
|
|
|
|
|
|
"type": "hashrate",
|
|
|
|
|
|
"unit": "MH/s",
|
|
|
|
|
|
"icon": "https://cdn.xxx/coin/rvn.png"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "ERG",
|
|
|
|
|
|
"label": "ERG",
|
|
|
|
|
|
"type": "hashrate",
|
|
|
|
|
|
"unit": "MH/s",
|
|
|
|
|
|
"icon": "https://cdn.xxx/coin/erg.png"
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "XEL",
|
|
|
|
|
|
"label": "XEL",
|
|
|
|
|
|
"type": "hashrate",
|
|
|
|
|
|
"unit": "kH/s",
|
|
|
|
|
|
"icon": "https://cdn.xxx/coin/xel.png"
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 尾部汇总列
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "monthIncome",
|
|
|
|
|
|
"label": "最大月收益",
|
|
|
|
|
|
"type": "amount",
|
|
|
|
|
|
"currency": "USDT",//固定显示单位
|
|
|
|
|
|
"period": "month",
|
|
|
|
|
|
"width": 110
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "maxLeaseDays",
|
|
|
|
|
|
"label": "最大租赁天数",
|
|
|
|
|
|
"type": "days",
|
|
|
|
|
|
"width": 80
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
"rows": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "gpu_100_hbm3", // 唯一标识(加入购物车传 id)
|
|
|
|
|
|
"model": "H100 80GB HBM3", // 型号,对应 columns.key = model
|
|
|
|
|
|
"price": 142160.54, // 行价格基准值(当无 priceList 时展示)
|
|
|
|
|
|
"priceList": [ // 价格列表:随支付方式变更价格与单位
|
|
|
|
|
|
{
|
|
|
|
|
|
"chain": "TRON", // 支付链
|
|
|
|
|
|
"coin": "USDT", // 币种(价格单位来源)
|
|
|
|
|
|
"price": 142160.54 // 该支付方式下的单价
|
|
|
|
|
|
},
|
|
|
|
|
|
{ "chain": "TRON", "coin": "NEXA", "price": 142050.00 },
|
|
|
|
|
|
{ "chain": "ETH", "coin": "USDT", "price": 142100.00 },
|
|
|
|
|
|
{ "chain": "ETH", "coin": "ETH", "price": 142000.00 }
|
|
|
|
|
|
],
|
|
|
|
|
|
"saleNumbers": 120, // 总机器数(仅 ASIC 展示,后端只读返回)
|
|
|
|
|
|
"saleOutNumbers": 18, // 已售数量(仅 ASIC 展示,后端只读返回)
|
|
|
|
|
|
|
|
|
|
|
|
"XTM": 255.0, // 对应列 key 的算力数值(可为 null)
|
|
|
|
|
|
"NXNA": 255.0,
|
|
|
|
|
|
"CLORE": 343.0,
|
|
|
|
|
|
"CFX": null,
|
|
|
|
|
|
"IRON": null,
|
|
|
|
|
|
"NEXA": null,
|
|
|
|
|
|
"KLS": null,
|
|
|
|
|
|
"RVN": 255.0,
|
|
|
|
|
|
"ERG": 900.0,
|
|
|
|
|
|
"XEL": null,
|
|
|
|
|
|
|
|
|
|
|
|
"monthIncome": 425.01, // 最大月收益(列定义 currency=USDT,固定以 USDT 展示)
|
|
|
|
|
|
"maxLeaseDays": 10368 // 最大租赁天数(单位:天)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "gpu_rtx_5090",
|
|
|
|
|
|
"model": "RTX 5090",
|
|
|
|
|
|
"price": 14216.05,
|
|
|
|
|
|
"priceList": [
|
|
|
|
|
|
{ "chain": "TRON", "coin": "USDT", "price": 14216.05 },
|
|
|
|
|
|
{ "chain": "TRON", "coin": "NEXA", "price": 14199.00 },
|
|
|
|
|
|
{ "chain": "ETH", "coin": "USDT", "price": 14180.00 },
|
|
|
|
|
|
{ "chain": "ETH", "coin": "ETH", "price": 14150.00 }
|
|
|
|
|
|
],
|
|
|
|
|
|
"saleNumbers": 80,
|
|
|
|
|
|
"saleOutNumbers": 22,
|
|
|
|
|
|
|
|
|
|
|
|
"XTM": 28.7,
|
|
|
|
|
|
"NXNA": 100.5,
|
|
|
|
|
|
"CLORE": 100.5,
|
|
|
|
|
|
"CFX": 210.0,
|
|
|
|
|
|
"IRON": 133.5,
|
|
|
|
|
|
"NEXA": 450.0,
|
|
|
|
|
|
"KLS": 133.5,
|
|
|
|
|
|
"RVN": 100.5,
|
|
|
|
|
|
"ERG": 575.0,
|
|
|
|
|
|
"XEL": 120.0,
|
|
|
|
|
|
|
|
|
|
|
|
"monthIncome": 267.84,
|
|
|
|
|
|
"maxLeaseDays": 1645
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "gpu_rtx_4090",
|
|
|
|
|
|
"model": "RTX 4090",
|
|
|
|
|
|
"price": 12083.65,
|
|
|
|
|
|
"priceList": [
|
|
|
|
|
|
{ "chain": "TRON", "coin": "USDT", "price": 12083.65 },
|
|
|
|
|
|
{ "chain": "TRON", "coin": "NEXA", "price": 12050.00 },
|
|
|
|
|
|
{ "chain": "ETH", "coin": "USDT", "price": 12020.00 },
|
|
|
|
|
|
{ "chain": "ETH", "coin": "ETH", "price": 11999.00 }
|
|
|
|
|
|
],
|
|
|
|
|
|
"saleNumbers": 64,
|
|
|
|
|
|
"saleOutNumbers": 9,
|
|
|
|
|
|
|
|
|
|
|
|
"XTM": 16.6,
|
|
|
|
|
|
"NXNA": 65.0,
|
|
|
|
|
|
"CLORE": 65.0,
|
|
|
|
|
|
"CFX": 130.0,
|
|
|
|
|
|
"IRON": 82.5,
|
|
|
|
|
|
"NEXA": 320.0,
|
|
|
|
|
|
"KLS": 82.5,
|
|
|
|
|
|
"RVN": 65.0,
|
|
|
|
|
|
"ERG": 265.0,
|
|
|
|
|
|
"XEL": 40.6,
|
|
|
|
|
|
|
|
|
|
|
|
"monthIncome": 155.00,
|
|
|
|
|
|
"maxLeaseDays": 2418
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
// 模拟数据写入动态表格(仅演示)
|
|
|
|
|
|
try{
|
|
|
|
|
|
this.dynamicMeta = arr.meta || {}
|
|
|
|
|
|
this.dynamicColumns = Array.isArray(arr.columns) ? arr.columns : []
|
|
|
|
|
|
this.dynamicRows = (Array.isArray(arr.rows) ? arr.rows : []).map(r => ({
|
|
|
|
|
|
saleNumbers: 0,
|
|
|
|
|
|
saleOutNumbers: 0,
|
|
|
|
|
|
leaseTime: 1,
|
|
|
|
|
|
purchaseQuantity: 0,
|
|
|
|
|
|
...r
|
|
|
|
|
|
}))
|
|
|
|
|
|
// 根据价格列表设置默认支付方式,确保筛选框与价格展示一致
|
|
|
|
|
|
this.ensureDefaultPayFilterFromPrices()
|
|
|
|
|
|
}catch(e){/* noop */}
|
|
|
|
|
|
// 仅当路由携带 shopId 时,才发起店铺商品请求
|
|
|
|
|
|
const routeShopId =
|
|
|
|
|
|
(this.$route && this.$route.params && (this.$route.params.shopId || this.$route.params.id)) ||
|
|
|
|
|
|
(this.$route && this.$route.query && this.$route.query.shopId)
|
|
|
|
|
|
if (routeShopId) {
|
|
|
|
|
|
this.params.id = routeShopId
|
2025-09-26 16:40:38 +08:00
|
|
|
|
this.product = true
|
2025-11-28 15:30:36 +08:00
|
|
|
|
this.fetchGetMachineInfo(this.buildQueryParams())
|
2025-11-07 16:30:03 +08:00
|
|
|
|
this.fetchPayTypes()
|
2025-09-26 16:40:38 +08:00
|
|
|
|
} else {
|
2025-11-28 15:30:36 +08:00
|
|
|
|
this.$message.warning('缺少店铺ID(shopId),无法加载商品列表')
|
2025-09-26 16:40:38 +08:00
|
|
|
|
this.product = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 动态表格单元格格式化(金额/算力/天数/文本)- 统一最多显示6位小数,hover展示完整
|
|
|
|
|
|
formatDynamicCell(row, col) {
|
|
|
|
|
|
try{
|
|
|
|
|
|
let val = row[col.key]
|
|
|
|
|
|
if (val === null || val === undefined || val === '') return { text: '—', full: '—', truncated: false }
|
|
|
|
|
|
if (col.type === 'amount') {
|
|
|
|
|
|
// 价格列:单位取 priceList 里的 coin;默认取第一条;选择支付方式后按选择展示
|
|
|
|
|
|
if (col.key === 'price') {
|
|
|
|
|
|
if (Array.isArray(row.priceList) && row.priceList.length) {
|
|
|
|
|
|
const pv = this.getDisplayPrice(row)
|
|
|
|
|
|
const pc = this.getDisplayPriceCoin(row)
|
|
|
|
|
|
if (pv !== null && pv !== undefined) val = pv
|
|
|
|
|
|
const nPrice = val
|
|
|
|
|
|
const t = truncateTo6(nPrice)
|
|
|
|
|
|
const coinUnit = (pc || '').toString().toUpperCase()
|
|
|
|
|
|
return {
|
|
|
|
|
|
text: coinUnit ? `${t.text} ${coinUnit}` : t.text,
|
|
|
|
|
|
full: coinUnit ? `${t.full} ${coinUnit}` : t.full,
|
|
|
|
|
|
truncated: t.truncated
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 无 priceList:仅展示数值,不附加任何单位
|
|
|
|
|
|
const t = truncateTo6(val)
|
|
|
|
|
|
return { text: t.text, full: t.full, truncated: t.truncated }
|
|
|
|
|
|
}
|
|
|
|
|
|
// 列级优先:若列声明 currency=USDT,则固定展示为 "xx.xx USDT"
|
|
|
|
|
|
const colCurrency = (col.currency || '').toString().toUpperCase()
|
|
|
|
|
|
if (colCurrency === 'USDT') {
|
|
|
|
|
|
const t = truncateTo6(val)
|
|
|
|
|
|
return { text: `${t.text} USDT`, full: `${t.full} USDT`, truncated: t.truncated }
|
|
|
|
|
|
}
|
|
|
|
|
|
// 兜底:不再使用 meta 的货币符号,直接返回数值
|
|
|
|
|
|
const t = truncateTo6(val)
|
|
|
|
|
|
return { text: t.text, full: t.full, truncated: t.truncated }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (col.type === 'hashrate') {
|
|
|
|
|
|
const unit = col.unit ? ` ${col.unit}` : ''
|
|
|
|
|
|
const t = truncateTo6(val)
|
|
|
|
|
|
return { text: `${t.text}${unit}`, full: `${t.full}${unit}`, truncated: t.truncated }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (col.type === 'days') {
|
|
|
|
|
|
const n = Number(val)
|
|
|
|
|
|
if (!Number.isFinite(n)) return { text: String(val), full: String(val), truncated: false }
|
|
|
|
|
|
const s = `${Math.floor(n)} 天`
|
|
|
|
|
|
return { text: s, full: s, truncated: false }
|
|
|
|
|
|
}
|
|
|
|
|
|
const s = String(val)
|
|
|
|
|
|
return { text: s, full: s, truncated: false }
|
|
|
|
|
|
}catch(e){ return { text: '—', full: '—', truncated: false } }
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 如果存在 priceList,则用第一条的 chain|coin 作为默认筛选值,
|
|
|
|
|
|
* 使“支付方式筛选”与价格列默认展示一致
|
|
|
|
|
|
*/
|
|
|
|
|
|
ensureDefaultPayFilterFromPrices() {
|
|
|
|
|
|
try{
|
|
|
|
|
|
if (this.payFilterDefaultApplied) return
|
|
|
|
|
|
const rows = Array.isArray(this.dynamicRows) ? this.dynamicRows : []
|
|
|
|
|
|
const firstWithPriceList = rows.find(r => Array.isArray(r && r.priceList) && r.priceList.length)
|
|
|
|
|
|
if (!firstWithPriceList) return
|
|
|
|
|
|
const first = firstWithPriceList.priceList[0]
|
|
|
|
|
|
const chain = String(first && first.chain || '').trim()
|
|
|
|
|
|
const coin = String(first && first.coin || '').trim()
|
|
|
|
|
|
if (!chain && !coin) return
|
|
|
|
|
|
this.selectedPayKey = `${chain}|${coin}`
|
|
|
|
|
|
this.filters.chain = chain
|
|
|
|
|
|
this.filters.coin = coin
|
|
|
|
|
|
this.payFilterDefaultApplied = true
|
|
|
|
|
|
}catch(e){ /* noop */ }
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取行在当前支付方式下的展示价格
|
|
|
|
|
|
* 优先匹配 selectedPayKey(chain|coin),否则回退 priceList[0];再否则回退 row.price
|
|
|
|
|
|
*/
|
|
|
|
|
|
getDisplayPrice(row){
|
|
|
|
|
|
try{
|
|
|
|
|
|
const list = Array.isArray(row && row.priceList) ? row.priceList : []
|
|
|
|
|
|
if (!list.length) return row && row.price
|
|
|
|
|
|
const key = this.selectedPayKey
|
|
|
|
|
|
if (key) {
|
|
|
|
|
|
const [chainRaw, coinRaw] = String(key).split('|')
|
|
|
|
|
|
const chain = String(chainRaw || '').toUpperCase().trim()
|
|
|
|
|
|
const coin = String(coinRaw || '').toUpperCase().trim()
|
|
|
|
|
|
const hit = list.find(it =>
|
|
|
|
|
|
String(it && it.chain).toUpperCase().trim() === chain &&
|
|
|
|
|
|
String(it && it.coin).toUpperCase().trim() === coin
|
|
|
|
|
|
)
|
|
|
|
|
|
if (hit && hit.price !== undefined && hit.price !== null) return hit.price
|
|
|
|
|
|
}
|
|
|
|
|
|
const first = list[0]
|
|
|
|
|
|
if (first && first.price !== undefined && first.price !== null) return first.price
|
|
|
|
|
|
return row && row.price
|
|
|
|
|
|
}catch(e){ return row && row.price }
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取行在当前支付方式下价格的币种(coin)
|
|
|
|
|
|
*/
|
|
|
|
|
|
getDisplayPriceCoin(row){
|
|
|
|
|
|
try{
|
|
|
|
|
|
const list = Array.isArray(row && row.priceList) ? row.priceList : []
|
|
|
|
|
|
if (!list.length) return ''
|
|
|
|
|
|
const key = this.selectedPayKey
|
|
|
|
|
|
if (key) {
|
|
|
|
|
|
const [chainRaw, coinRaw] = String(key).split('|')
|
|
|
|
|
|
const chain = String(chainRaw || '').toUpperCase().trim()
|
|
|
|
|
|
const coin = String(coinRaw || '').toUpperCase().trim()
|
|
|
|
|
|
const hit = list.find(it =>
|
|
|
|
|
|
String(it && it.chain).toUpperCase().trim() === chain &&
|
|
|
|
|
|
String(it && it.coin).toUpperCase().trim() === coin
|
|
|
|
|
|
)
|
|
|
|
|
|
if (hit && hit.coin) return String(hit.coin)
|
|
|
|
|
|
}
|
|
|
|
|
|
const first = list[0]
|
|
|
|
|
|
return first && first.coin ? String(first.coin) : ''
|
|
|
|
|
|
}catch(e){ return '' }
|
|
|
|
|
|
},
|
|
|
|
|
|
_truncate(num, decimals=2){
|
|
|
|
|
|
try{
|
|
|
|
|
|
const f = Math.pow(10, decimals)
|
|
|
|
|
|
return (Math.floor(Number(num)*f)/f).toFixed(decimals)
|
|
|
|
|
|
}catch(e){ return String(num) }
|
|
|
|
|
|
},
|
|
|
|
|
|
// 判断是否为“框出来部分”的最后一列(最后一个 hashrate 列)
|
|
|
|
|
|
isLastHashrateColumn(colIdx){
|
|
|
|
|
|
try{
|
|
|
|
|
|
const cols = this.getRenderedColumns()
|
|
|
|
|
|
for (let i = cols.length - 1; i >= 0; i--) {
|
|
|
|
|
|
if (String(cols[i] && cols[i].type).toLowerCase() === 'hashrate') {
|
|
|
|
|
|
return i === colIdx
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}catch(e){ return false }
|
|
|
|
|
|
},
|
|
|
|
|
|
// 仅渲染前 8 个算力列,后接其它非算力列(如收益、回收期)
|
|
|
|
|
|
getRenderedColumns(){
|
|
|
|
|
|
try{
|
|
|
|
|
|
const cols = Array.isArray(this.dynamicColumns) ? this.dynamicColumns : []
|
|
|
|
|
|
const hashrate = cols.filter(c => String(c && c.type).toLowerCase() === 'hashrate').slice(0, 8)
|
|
|
|
|
|
const others = cols.filter(c => String(c && c.type).toLowerCase() !== 'hashrate')
|
|
|
|
|
|
return [...hashrate, ...others]
|
|
|
|
|
|
}catch(e){ return [] }
|
|
|
|
|
|
},
|
|
|
|
|
|
// 打开动态搜索弹窗
|
|
|
|
|
|
handleOpenDynamicSearch(){
|
|
|
|
|
|
this.dynamicSearch.visible = true
|
|
|
|
|
|
this.dynamicSearch.keyword = ''
|
|
|
|
|
|
},
|
|
|
|
|
|
// 确认搜索:向后端请求新的 columns/rows,替换动态表格
|
|
|
|
|
|
async handleConfirmDynamicSearch(){
|
|
|
|
|
|
const keyword = (this.dynamicSearch.keyword || '').trim()
|
|
|
|
|
|
this.dynamicSearch.visible = false
|
|
|
|
|
|
await this.fetchDynamicTable({ shopId: this.params.id, type: 1, keyword })
|
|
|
|
|
|
},
|
|
|
|
|
|
// 拉取动态表格数据(占位实现:如果后端已就绪,直接替换为真实接口)
|
|
|
|
|
|
async fetchDynamicTable(params){
|
|
|
|
|
|
try{
|
|
|
|
|
|
// 这里预留与后端对接:
|
|
|
|
|
|
// 期待返回格式:{ code, data: { meta, columns, rows } }
|
|
|
|
|
|
// 示例中用本地 mock 演示:根据 keyword 过滤/调整列
|
|
|
|
|
|
// 如果没有 keyword,就还原初始 mock
|
|
|
|
|
|
if (!params || !params.keyword) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 简单模拟:当 keyword 命中 'ERG',只保留 ERG + 价格/型号/回收期 等少量列
|
|
|
|
|
|
const kw = String(params.keyword).toUpperCase()
|
|
|
|
|
|
const baseCols = (this.dynamicColumns || []).filter(c => ['model','price','maxLeaseDays','monthIncome'].includes(c.key))
|
|
|
|
|
|
const hitCols = (this.dynamicColumns || []).filter(c => String(c.label || c.key).toUpperCase().includes(kw))
|
|
|
|
|
|
const nextCols = [...(baseCols.length?baseCols:[this.dynamicColumns[0]||[]]), ...hitCols]
|
|
|
|
|
|
if (nextCols.length) {
|
|
|
|
|
|
this.dynamicColumns = nextCols
|
|
|
|
|
|
// 行数据无需特别处理(真实环境后端会按列同步返回),这里保留原 rows
|
|
|
|
|
|
}
|
|
|
|
|
|
}catch(e){
|
|
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
|
|
console.warn('fetchDynamicTable mock error', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
// 切换矿机种类:0-ASIC,1-GPU
|
|
|
|
|
|
handleMachineTypeChange(){
|
|
|
|
|
|
// 变更类型后,重新请求数据
|
|
|
|
|
|
this.fetchGetMachineInfo(this.buildQueryParams())
|
|
|
|
|
|
this.fetchPayTypes()
|
|
|
|
|
|
// 本地记住用户选择
|
|
|
|
|
|
try{
|
|
|
|
|
|
if (window && window.localStorage) {
|
|
|
|
|
|
window.localStorage.setItem('pl_machineType', String(this.machineType))
|
|
|
|
|
|
}
|
|
|
|
|
|
}catch(e){/* noop */}
|
|
|
|
|
|
},
|
2025-11-14 16:17:36 +08:00
|
|
|
|
// 行币种:优先行内 payCoin > coin,其次取全局表头币种
|
|
|
|
|
|
getRowCoin(row) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const c = (row && (row.payCoin || row.coin)) || this.getPriceCoinSymbol() || ''
|
|
|
|
|
|
return String(c).toUpperCase()
|
|
|
|
|
|
} catch (e) { return '' }
|
|
|
|
|
|
},
|
|
|
|
|
|
// 金额格式化:不补0、不四舍五入;返回 {text,truncated,full}
|
|
|
|
|
|
formatAmount(value, coin) {
|
|
|
|
|
|
return truncateAmountByCoin(value, coin)
|
|
|
|
|
|
},
|
2025-11-20 14:28:57 +08:00
|
|
|
|
// 数值格式化:最多6位小数,截断不补0
|
|
|
|
|
|
formatNum6(value) {
|
|
|
|
|
|
return truncateTo6(value)
|
|
|
|
|
|
},
|
2025-11-14 16:17:36 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 首次加载时,将“支付方式筛选”的默认选中值设为与价格列币种一致,
|
|
|
|
|
|
* 并同步 filters.chain/filters.coin;仅执行一次,不触发额外查询。
|
|
|
|
|
|
*/
|
|
|
|
|
|
ensureDefaultPayFilterSelection() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (this.payFilterDefaultApplied) return
|
|
|
|
|
|
const payList = Array.isArray(this.paymentMethodList) ? this.paymentMethodList : []
|
|
|
|
|
|
if (!payList.length) return
|
|
|
|
|
|
const coinSymbol = (this.getPriceCoinSymbol && this.getPriceCoinSymbol()) || ''
|
|
|
|
|
|
if (!coinSymbol) return
|
|
|
|
|
|
const hit = payList.find(it => String(it && it.payCoin).toUpperCase() === String(coinSymbol).toUpperCase())
|
|
|
|
|
|
if (!hit) return
|
|
|
|
|
|
const key = `${hit.payChain || ''}|${hit.payCoin || ''}`
|
|
|
|
|
|
this.selectedPayKey = key
|
|
|
|
|
|
this.filters.chain = String(hit.payChain || '').trim()
|
|
|
|
|
|
this.filters.coin = String(hit.payCoin || '').trim()
|
|
|
|
|
|
this.payFilterDefaultApplied = true
|
|
|
|
|
|
} catch (e) { /* noop */ }
|
|
|
|
|
|
},
|
|
|
|
|
|
// 切换排序:field in ['priceSort','powerSort','powerDissipationSort']
|
|
|
|
|
|
handleToggleSort(field) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!this.sortStates) this.sortStates = {}
|
|
|
|
|
|
if (this.activeSortField !== field) {
|
|
|
|
|
|
// 切换到新的字段:默认从升序开始(true)
|
|
|
|
|
|
// 先将其它字段复位为升序(▲)
|
|
|
|
|
|
Object.keys(this.sortStates).forEach(k => { this.sortStates[k] = true })
|
|
|
|
|
|
this.activeSortField = field
|
|
|
|
|
|
// 后端默认升序,首次点击应为降序
|
|
|
|
|
|
this.sortStates[field] = false
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 同一字段:升降序切换
|
|
|
|
|
|
this.sortStates[field] = !this.sortStates[field]
|
|
|
|
|
|
}
|
|
|
|
|
|
const params = this.buildQueryParams()
|
|
|
|
|
|
this.fetchGetMachineInfo(params)
|
|
|
|
|
|
} catch (e) { /* noop */ }
|
|
|
|
|
|
},
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 组合查询参数:店铺入口,必须包含 shopId 与 type(0-ASIC,1-GPU)
|
2025-11-07 16:30:03 +08:00
|
|
|
|
buildQueryParams() {
|
2025-11-28 15:30:36 +08:00
|
|
|
|
const q = { shopId: this.params.id, type: this.machineType }
|
2025-11-14 16:17:36 +08:00
|
|
|
|
// 分页参数始终透传
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (this.params && this.params.pageNum != null) q.pageNum = this.params.pageNum
|
|
|
|
|
|
if (this.params && this.params.pageSize != null) q.pageSize = this.params.pageSize
|
|
|
|
|
|
} catch (e) { /* noop */ }
|
2025-11-07 16:30:03 +08:00
|
|
|
|
// 仅当用户真实填写(>0)时才传参;默认/空值不传
|
|
|
|
|
|
const addNum = (obj, key, name) => {
|
|
|
|
|
|
const raw = obj[key]
|
|
|
|
|
|
if (raw === null || raw === undefined || raw === '') return
|
|
|
|
|
|
const n = Number(raw)
|
|
|
|
|
|
if (Number.isFinite(n) && n > 0) q[name] = n
|
|
|
|
|
|
}
|
|
|
|
|
|
// 支付方式条件:有值才传
|
|
|
|
|
|
if (this.filters.chain && String(this.filters.chain).trim()) q.chain = String(this.filters.chain).trim()
|
|
|
|
|
|
if (this.filters.coin && String(this.filters.coin).trim()) q.coin = String(this.filters.coin).trim()
|
2025-11-18 14:36:43 +08:00
|
|
|
|
if (this.filters.unit && String(this.filters.unit).trim()) q.unit = String(this.filters.unit).trim()
|
2025-11-07 16:30:03 +08:00
|
|
|
|
addNum(this.filters, 'minPrice', 'minPrice')
|
|
|
|
|
|
addNum(this.filters, 'maxPrice', 'maxPrice')
|
|
|
|
|
|
addNum(this.filters, 'minPower', 'minPower')
|
|
|
|
|
|
addNum(this.filters, 'maxPower', 'maxPower')
|
|
|
|
|
|
addNum(this.filters, 'minPowerDissipation', 'minPowerDissipation')
|
|
|
|
|
|
addNum(this.filters, 'maxPowerDissipation', 'maxPowerDissipation')
|
2025-11-14 16:17:36 +08:00
|
|
|
|
// 排序参数:仅在用户点击某一列后传当前列
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (this.activeSortField) {
|
|
|
|
|
|
const s = this.sortStates || {}
|
|
|
|
|
|
q[this.activeSortField] = !!s[this.activeSortField]
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) { /* noop */ }
|
2025-11-07 16:30:03 +08:00
|
|
|
|
return q
|
|
|
|
|
|
},
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 拉取支付方式(兼容 shopId)
|
2025-11-07 16:30:03 +08:00
|
|
|
|
async fetchPayTypes() {
|
|
|
|
|
|
try {
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 现规则:商品详情由店铺入口进入,使用 shopId 查询支付方式
|
|
|
|
|
|
// 为兼容后端两种入参,优先传 shopId,后端若仍使用 productId 也能兼容处理
|
|
|
|
|
|
const res = await getPayTypes({ shopId: this.params.id, productId: this.params.id })
|
2025-11-07 16:30:03 +08:00
|
|
|
|
// 接口示例:{ code: 0, data: [ { payChain, payCoin, payCoinImage, shopId } ], msg: '' }
|
|
|
|
|
|
if (res && (res.code === 0 || res.code === 200)) {
|
|
|
|
|
|
const list = Array.isArray(res.data) ? res.data : []
|
|
|
|
|
|
this.paymentMethodList = list
|
2025-11-14 16:17:36 +08:00
|
|
|
|
// 支付方式加载后尝试设置默认筛选
|
|
|
|
|
|
this.ensureDefaultPayFilterSelection()
|
2025-11-07 16:30:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 忽略错误,保持页面可用
|
|
|
|
|
|
this.paymentMethodList = []
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-09-26 16:40:38 +08:00
|
|
|
|
|
|
|
|
|
|
async fetchGetMachineInfo(params) {
|
|
|
|
|
|
this.productDetailLoading = true
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 改为使用店铺机器列表接口
|
|
|
|
|
|
const res = await getShopMachineList(params)
|
2025-09-26 16:40:38 +08:00
|
|
|
|
console.log(res)
|
|
|
|
|
|
if (res && res.code === 200) {
|
|
|
|
|
|
console.log(res.data, 'res.rows');
|
2025-11-14 16:17:36 +08:00
|
|
|
|
this.total = res.total||0;
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 若后端同步返回支付方式,刷新本地支付方式
|
2025-11-07 16:30:03 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const payList = res && res.data && res.data.payConfigList
|
|
|
|
|
|
if (Array.isArray(payList) && payList.length) {
|
|
|
|
|
|
this.paymentMethodList = payList
|
|
|
|
|
|
}
|
2025-11-28 15:30:36 +08:00
|
|
|
|
} catch (e) { /* noop */ }
|
|
|
|
|
|
// 动态表格数据(如后端有对应 rows/columns,可在此接入)
|
|
|
|
|
|
// 此处保留现有动态表格的模拟/替换逻辑,不再维护旧表格数据结构
|
2025-09-26 16:40:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.productDetailLoading = false
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 加载商品详情
|
|
|
|
|
|
*/
|
|
|
|
|
|
async loadProduct() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.loading = true
|
|
|
|
|
|
const productId = this.$route.params.id
|
|
|
|
|
|
this.product = await getProductById(productId)
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.product) {
|
|
|
|
|
|
this.$message({
|
2025-11-14 16:17:36 +08:00
|
|
|
|
message: '商品不存在',
|
|
|
|
|
|
type: 'error',
|
|
|
|
|
|
showClose: true
|
2025-09-26 16:40:38 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载商品详情失败:', error)
|
|
|
|
|
|
this.$message({
|
2025-11-14 16:17:36 +08:00
|
|
|
|
message: '加载商品详情失败,请稍后重试',
|
|
|
|
|
|
type: 'error',
|
|
|
|
|
|
showClose: true
|
2025-09-26 16:40:38 +08:00
|
|
|
|
})
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.loading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
//加入购物车
|
|
|
|
|
|
async fetchAddCart(params) {
|
|
|
|
|
|
const res = await addCart(params)
|
2025-11-14 16:17:36 +08:00
|
|
|
|
|
2025-09-26 16:40:38 +08:00
|
|
|
|
return res
|
|
|
|
|
|
},
|
|
|
|
|
|
//查询购物车列表
|
|
|
|
|
|
async fetchGetGoodsList(params) {
|
|
|
|
|
|
const res = await getGoodsList(params)
|
|
|
|
|
|
// 统计当前商品在购物车中已有的机器ID,用于禁用和默认勾选
|
|
|
|
|
|
try {
|
|
|
|
|
|
const productId = this.params && this.params.id ? Number(this.params.id) : Number(this.$route.params.id)
|
|
|
|
|
|
// 兼容两种返回结构:1) 旧:直接是商品分组数组 2) 新:店铺数组 → shoppingCartInfoDtoList
|
|
|
|
|
|
const rawRows = Array.isArray(res && res.rows)
|
|
|
|
|
|
? res.rows
|
|
|
|
|
|
: Array.isArray(res && res.data && res.data.rows)
|
|
|
|
|
|
? res.data.rows
|
|
|
|
|
|
: Array.isArray(res && res.data)
|
|
|
|
|
|
? res.data
|
|
|
|
|
|
: []
|
|
|
|
|
|
// 扁平化为商品分组
|
|
|
|
|
|
const groups = rawRows.length && rawRows[0] && Array.isArray(rawRows[0].shoppingCartInfoDtoList)
|
|
|
|
|
|
? rawRows.flatMap(shop => Array.isArray(shop.shoppingCartInfoDtoList) ? shop.shoppingCartInfoDtoList : [])
|
|
|
|
|
|
: rawRows
|
|
|
|
|
|
const matched = groups.filter(g => Number(g.productId) === productId)
|
|
|
|
|
|
const ids = new Set()
|
|
|
|
|
|
const compositeKeys = new Set()
|
|
|
|
|
|
matched.forEach(r => {
|
|
|
|
|
|
const list = Array.isArray(r.productMachineDtoList) ? r.productMachineDtoList : []
|
|
|
|
|
|
list.forEach(m => {
|
|
|
|
|
|
if (!m) return
|
|
|
|
|
|
if (m.id !== undefined && m.id !== null) ids.add(String(m.id))
|
|
|
|
|
|
if (m.user && m.miner) compositeKeys.add(`${String(m.user)}|${String(m.miner)}`)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
this.cartMachineIdSet = ids
|
|
|
|
|
|
this.cartCompositeKeySet = compositeKeys
|
|
|
|
|
|
// 计算购物车总数量并通知头部,避免页面初次加载时徽标显示为0
|
|
|
|
|
|
try {
|
|
|
|
|
|
const totalCount = groups.reduce((sum, g) => sum + (Array.isArray(g && g.productMachineDtoList) ? g.productMachineDtoList.length : 0), 0)
|
|
|
|
|
|
if (Number.isFinite(totalCount)) {
|
|
|
|
|
|
window.dispatchEvent(new CustomEvent('cart-updated', { detail: { count: totalCount } }))
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) { /* noop */ }
|
|
|
|
|
|
// 展开表格渲染后,默认勾选并禁用这些行
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
this.cartLoaded = true
|
|
|
|
|
|
this.autoSelectAndDisable()
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('解析购物车数据失败', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理返回
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleBack() {
|
|
|
|
|
|
this.$router.push('/productList')
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 点击系列行:切换展开/收起
|
|
|
|
|
|
* @param {Object} row - 当前行
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleSeriesRowClick(row) {
|
|
|
|
|
|
const key = row.id
|
|
|
|
|
|
const lockedIds = Object.keys(this.selectedMap).filter(k => (this.selectedMap[k] || []).length > 0)
|
|
|
|
|
|
const opened = this.expandedRowKeys.includes(key)
|
|
|
|
|
|
if (opened) {
|
|
|
|
|
|
// 关闭当前行,仅保留已勾选的行展开
|
|
|
|
|
|
this.expandedRowKeys = lockedIds
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 打开当前行,同时保留已勾选的行展开
|
|
|
|
|
|
this.expandedRowKeys = Array.from(new Set([key, ...lockedIds]))
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 外层系列行样式
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleGetSeriesRowClassName() {
|
|
|
|
|
|
return 'series-clickable-row'
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 子表选择变化
|
|
|
|
|
|
handleInnerSelectionChange(parentRow, selections) {
|
|
|
|
|
|
const key = parentRow.id
|
|
|
|
|
|
this.$set(this.selectedMap, key, selections)
|
|
|
|
|
|
const lockedIds = Object.keys(this.selectedMap).filter(k => (this.selectedMap[k] || []).length > 0)
|
|
|
|
|
|
// 更新展开:锁定的行始终展开
|
|
|
|
|
|
const openedSet = new Set(this.expandedRowKeys)
|
|
|
|
|
|
lockedIds.forEach(id => openedSet.add(id))
|
|
|
|
|
|
// 清理不再勾选且不是当前展开的行
|
|
|
|
|
|
this.expandedRowKeys = Array.from(openedSet).filter(id => lockedIds.includes(id) || id === key || this.expandedRowKeys.includes(id))
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 展开行变化时:已取消自动与购物车对比,无需勾选/禁用
|
|
|
|
|
|
handleExpandChange(row, expandedRows) {
|
|
|
|
|
|
// no-op
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 已取消对比购物车的自动勾选/禁用逻辑
|
2025-11-14 16:17:36 +08:00
|
|
|
|
autoSelectAndDisable() { },
|
2025-09-26 16:40:38 +08:00
|
|
|
|
|
|
|
|
|
|
// 选择器可选控制:已在购物车中的机器不可再选
|
|
|
|
|
|
isSelectable(row, index) {
|
|
|
|
|
|
// 不再通过 selectable 禁用,以便勾选可见;通过行样式和交互阻止点击
|
|
|
|
|
|
return true
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 判断在特定父行下是否已选择(配合自定义checkbox使用)
|
|
|
|
|
|
isSelectedByParent(parentRow, row) {
|
|
|
|
|
|
const key = parentRow && parentRow.id
|
|
|
|
|
|
const list = (key && this.selectedMap[key]) || []
|
|
|
|
|
|
return !!list.find(it => it && it.id === row.id)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 手动切换选择(自定义checkbox与 selectedMap 同步),并维护每行的 _selected 状态
|
|
|
|
|
|
handleManualSelect(parentRow, row, checked) {
|
2025-10-20 10:15:13 +08:00
|
|
|
|
// 禁用:已售出或售出中的机器不可选择
|
|
|
|
|
|
if (row && (row.saleState === 1 || row.saleState === 2)) {
|
|
|
|
|
|
this.$message.warning('该机器已售出或售出中,无法选择')
|
|
|
|
|
|
this.$set(row, '_selected', false)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-09-26 16:40:38 +08:00
|
|
|
|
const key = parentRow.id
|
|
|
|
|
|
const list = (this.selectedMap[key] && [...this.selectedMap[key]]) || []
|
|
|
|
|
|
const idx = list.findIndex(it => it && it.id === row.id)
|
|
|
|
|
|
if (checked && idx === -1) list.push(row)
|
|
|
|
|
|
if (!checked && idx > -1) list.splice(idx, 1)
|
|
|
|
|
|
this.$set(this.selectedMap, key, list)
|
|
|
|
|
|
this.$set(row, '_selected', !!checked)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 为子表中已在购物车的行添加只读样式,并阻止点击取消
|
2025-10-20 10:15:13 +08:00
|
|
|
|
handleGetInnerRowClass({ row }) {
|
|
|
|
|
|
if (!row) return ''
|
|
|
|
|
|
return (row.saleState === 1 || row.saleState === 2) ? 'sold-row' : ''
|
2025-09-26 16:40:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 子行:减少数量
|
|
|
|
|
|
* @param {number} groupIndex - 系列索引
|
|
|
|
|
|
* @param {number} variantIndex - 变体索引
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleDecreaseVariantQuantity(groupIndex, variantIndex) {
|
|
|
|
|
|
const item = this.productListData[groupIndex].variants[variantIndex]
|
|
|
|
|
|
if (item.quantity > 1) {
|
|
|
|
|
|
item.quantity--
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 子行:增加数量
|
|
|
|
|
|
* @param {number} groupIndex - 系列索引
|
|
|
|
|
|
* @param {number} variantIndex - 变体索引
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleIncreaseVariantQuantity(groupIndex, variantIndex) {
|
|
|
|
|
|
const item = this.productListData[groupIndex].variants[variantIndex]
|
|
|
|
|
|
if (item.quantity < 99) {
|
|
|
|
|
|
item.quantity++
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 子行:输入数量校验
|
|
|
|
|
|
* @param {number} groupIndex - 系列索引
|
|
|
|
|
|
* @param {number} variantIndex - 变体索引
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleVariantQuantityInput(groupIndex, variantIndex) {
|
|
|
|
|
|
const item = this.productListData[groupIndex].variants[variantIndex]
|
|
|
|
|
|
const q = Number(item.quantity)
|
|
|
|
|
|
if (!q || q < 1) item.quantity = 1
|
|
|
|
|
|
if (q > 99) item.quantity = 99
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 子行:加入购物车
|
|
|
|
|
|
* @param {Object} variant - 子项行数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleAddVariantToCart(variant) {
|
|
|
|
|
|
if (!variant || !variant.onlyKey) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
addToCart({
|
|
|
|
|
|
id: variant.onlyKey,
|
|
|
|
|
|
title: variant.model,
|
|
|
|
|
|
price: variant.price,
|
|
|
|
|
|
quantity: variant.quantity
|
|
|
|
|
|
})
|
|
|
|
|
|
this.$message.success(`已添加 ${variant.quantity} 件 ${variant.model} 到购物车`)
|
|
|
|
|
|
variant.quantity = 1
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('添加到购物车失败:', error)
|
2025-11-14 16:17:36 +08:00
|
|
|
|
|
2025-09-26 16:40:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
// 统一加入购物车
|
|
|
|
|
|
handleAddSelectedToCart() {
|
|
|
|
|
|
const allSelected = Object.values(this.selectedMap).flat().filter(Boolean)
|
|
|
|
|
|
if (!allSelected.length) {
|
|
|
|
|
|
this.$message.warning('请先勾选至少一台矿机')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
allSelected.forEach(item => {
|
|
|
|
|
|
addToCart({
|
|
|
|
|
|
id: item.onlyKey || item.id,
|
|
|
|
|
|
title: item.type || item.model || '矿机',
|
|
|
|
|
|
price: item.price,
|
|
|
|
|
|
quantity: 1,
|
|
|
|
|
|
leaseTime: Number(item.leaseTime || 1)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
this.$message.success(`已加入 ${allSelected.length} 台矿机到购物车`)
|
|
|
|
|
|
this.selectedMap = {}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('统一加入购物车失败', e)
|
2025-11-14 16:17:36 +08:00
|
|
|
|
|
2025-09-26 16:40:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 打开确认弹窗(基于动态表格的勾选行)
|
2025-09-26 16:40:38 +08:00
|
|
|
|
handleOpenAddToCartDialog() {
|
2025-11-28 15:30:36 +08:00
|
|
|
|
const rows = Array.isArray(this.dynamicRows) ? this.dynamicRows : []
|
|
|
|
|
|
const picked = rows.filter(r => !!r && !!r._selected)
|
2025-09-26 16:40:38 +08:00
|
|
|
|
if (!picked.length) {
|
|
|
|
|
|
this.$message.warning('请先勾选至少一台矿机')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-11-28 15:30:36 +08:00
|
|
|
|
this.confirmAddDialog.items = picked.map(r => ({
|
|
|
|
|
|
...r,
|
|
|
|
|
|
leaseTime: Number(r.leaseTime || 1),
|
|
|
|
|
|
purchaseQuantity: Number(r.purchaseQuantity || 0)
|
|
|
|
|
|
}))
|
2025-09-26 16:40:38 +08:00
|
|
|
|
this.confirmAddDialog.visible = true
|
|
|
|
|
|
},
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 确认加入:调用 addGoodsV2(按条提交),GPU 不传 numbers
|
2025-09-26 16:40:38 +08:00
|
|
|
|
async handleConfirmAddSelectedToCart() {
|
2025-11-28 15:30:36 +08:00
|
|
|
|
const items = Array.isArray(this.confirmAddDialog.items) ? this.confirmAddDialog.items.filter(Boolean) : []
|
|
|
|
|
|
if (!items.length) {
|
2025-09-26 16:40:38 +08:00
|
|
|
|
this.$message.warning('请先勾选至少一台矿机')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 按接口要求:一次性传数组,每个对象代表一个勾选商品
|
|
|
|
|
|
const payload = items.map(it => {
|
|
|
|
|
|
const obj = {
|
|
|
|
|
|
id: it.id,
|
|
|
|
|
|
leaseTime: Number(it.leaseTime || 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.machineType === 0) {
|
|
|
|
|
|
obj.numbers = Number(it.purchaseQuantity || 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
return obj
|
|
|
|
|
|
})
|
|
|
|
|
|
const res = await addGoodsV2(payload)
|
|
|
|
|
|
if (!res || !(res.code === 0 || res.code === 200)) {
|
|
|
|
|
|
this.$message.error('部分商品加入购物车失败,请重试')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.success(`已加入 ${items.length} 台矿机到购物车`)
|
2025-09-26 16:40:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
this.confirmAddDialog.visible = false
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 清空勾选
|
2025-09-26 16:40:38 +08:00
|
|
|
|
try {
|
2025-11-28 15:30:36 +08:00
|
|
|
|
(this.dynamicRows || []).forEach(r => { if (r) this.$set(r, '_selected', false) })
|
2025-09-26 16:40:38 +08:00
|
|
|
|
} catch (e) { /* noop */ }
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// 通知头部刷新
|
|
|
|
|
|
try { window.dispatchEvent(new CustomEvent('cart-updated')) } catch (e) { /* noop */ }
|
2025-09-26 16:40:38 +08:00
|
|
|
|
} catch (e) {
|
2025-11-28 15:30:36 +08:00
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
|
|
console.error('addGoodsV2 error:', e)
|
2025-09-26 16:40:38 +08:00
|
|
|
|
this.$message.error('加入购物车失败,请稍后重试')
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 取消所有商品勾选(内层表格的自定义 checkbox)
|
|
|
|
|
|
clearAllSelections() {
|
|
|
|
|
|
try {
|
2025-11-07 16:30:03 +08:00
|
|
|
|
// 清空选中映射(遗留字段)
|
2025-09-26 16:40:38 +08:00
|
|
|
|
this.selectedMap = {}
|
2025-11-07 16:30:03 +08:00
|
|
|
|
if (Array.isArray(this.machineList) && this.machineList.length) {
|
|
|
|
|
|
this.machineList.forEach(m => { if (m) this.$set(m, '_selected', false) })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-09-26 16:40:38 +08:00
|
|
|
|
const groups = Array.isArray(this.productListData) ? this.productListData : []
|
|
|
|
|
|
groups.forEach(g => {
|
|
|
|
|
|
const list = Array.isArray(g.productMachines) ? g.productMachines : []
|
|
|
|
|
|
list.forEach(m => { if (m) this.$set(m, '_selected', false) })
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (e) { /* noop */ }
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 减少数量
|
|
|
|
|
|
* @param {number} rowIndex - 表格行索引
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleDecreaseQuantity(rowIndex) {
|
|
|
|
|
|
if (this.tableData[rowIndex].quantity > 1) {
|
|
|
|
|
|
this.tableData[rowIndex].quantity--
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 增加数量
|
|
|
|
|
|
* @param {number} rowIndex - 表格行索引
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleIncreaseQuantity(rowIndex) {
|
|
|
|
|
|
if (this.tableData[rowIndex].quantity < 99) {
|
|
|
|
|
|
this.tableData[rowIndex].quantity++
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理数量输入
|
|
|
|
|
|
* @param {number} rowIndex - 表格行索引
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleQuantityInput(rowIndex) {
|
|
|
|
|
|
const quantity = this.tableData[rowIndex].quantity
|
|
|
|
|
|
if (quantity < 1) {
|
|
|
|
|
|
this.tableData[rowIndex].quantity = 1
|
|
|
|
|
|
} else if (quantity > 99) {
|
|
|
|
|
|
this.tableData[rowIndex].quantity = 99
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理数量输入框失焦
|
|
|
|
|
|
* @param {number} rowIndex - 表格行索引
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleQuantityBlur(rowIndex) {
|
|
|
|
|
|
const quantity = this.tableData[rowIndex].quantity
|
|
|
|
|
|
if (!quantity || quantity < 1) {
|
|
|
|
|
|
this.tableData[rowIndex].quantity = 1
|
|
|
|
|
|
} else if (quantity > 99) {
|
|
|
|
|
|
this.tableData[rowIndex].quantity = 99
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 添加到购物车
|
|
|
|
|
|
* @param {Object} rowData - 表格行数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
handleAddToCart(rowData) {
|
|
|
|
|
|
if (!rowData || rowData.quantity < 1) {
|
|
|
|
|
|
this.$message.warning('请选择有效的数量')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
addToCart({
|
|
|
|
|
|
id: rowData.date, // 使用矿机名称作为ID
|
|
|
|
|
|
title: rowData.date,
|
|
|
|
|
|
price: rowData.price,
|
|
|
|
|
|
quantity: rowData.quantity,
|
|
|
|
|
|
leaseTime: Number(rowData.leaseTime || 1)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.$message.success(`已添加 ${rowData.quantity} 件 ${rowData.date} 到购物车`)
|
|
|
|
|
|
|
|
|
|
|
|
// 重置数量
|
|
|
|
|
|
rowData.quantity = 1
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('添加到购物车失败:', error)
|
|
|
|
|
|
this.$message.error('添加到购物车失败,请稍后重试')
|
|
|
|
|
|
}
|
2025-11-14 16:17:36 +08:00
|
|
|
|
},
|
|
|
|
|
|
handleSizeChange(val) {
|
|
|
|
|
|
console.log(`每页 ${val} 条`);
|
|
|
|
|
|
this.params.pageSize = val;
|
|
|
|
|
|
this.params.pageNum = 1;
|
|
|
|
|
|
this.currentPage = 1;
|
|
|
|
|
|
// 携带当前激活的排序字段
|
|
|
|
|
|
this.fetchGetMachineInfo(this.buildQueryParams());
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
handleCurrentChange(val) {
|
|
|
|
|
|
console.log(`当前页: ${val}`);
|
|
|
|
|
|
this.params.pageNum = val;
|
|
|
|
|
|
// 携带当前激活的排序字段
|
|
|
|
|
|
this.fetchGetMachineInfo(this.buildQueryParams());
|
|
|
|
|
|
},
|
2025-09-26 16:40:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|