最新需求修改中

This commit is contained in:
2025-11-28 15:30:36 +08:00
parent 868632400a
commit 485226d9dc
8 changed files with 2070 additions and 738 deletions

View File

@@ -1,8 +1,6 @@
import { getProductById } from '../../utils/productService'
import { addToCart } from '../../utils/cartManager'
import { getMachineInfo, getPayTypes } from '../../api/products'
import { addCart, getGoodsList } from '../../api/shoppingCart'
import { getMachineInfo, getPayTypes,getShopMachineList,addGoodsV2 } from '../../api/products'
import { truncateAmountByCoin, truncateTo6 } from '../../utils/amount'
export default {
@@ -142,27 +140,445 @@ export default {
pageSizes: [10, 20, 50],
currentPage: 1,
total: 0,
// 动态列表(模拟渲染)
dynamicMeta: {},
dynamicColumns: [],
dynamicRows: [],
dynamicSearch: {
visible: false,
keyword: ''
},
// 矿机种类0-ASIC1-GPU默认GPU
machineType: 1,
}
},
mounted() {
if (this.$route.params.id) {
this.params.id = this.$route.params.id
// 读取用户上次选择的矿机种类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
this.product = true
// 默认展开第一行
if (this.productListData && this.productListData.length) {
this.expandedRowKeys = [this.productListData[0].id]
}
this.fetchGetMachineInfo(this.params)//priceSort 价格powerSort 算力功耗powerDissipationSort 布尔类型true 升序false降序
this.fetchGetMachineInfo(this.buildQueryParams())
this.fetchPayTypes()
} else {
this.$message.error('商品不存在')
this.$message.warning('缺少店铺IDshopId无法加载商品列表')
this.product = false
}
this.fetchGetGoodsList()
},
methods: {
// 动态表格单元格格式化(金额/算力/天数/文本)- 统一最多显示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 */ }
},
/**
* 获取行在当前支付方式下的展示价格
* 优先匹配 selectedPayKeychain|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-ASIC1-GPU
handleMachineTypeChange(){
// 变更类型后,重新请求数据
this.fetchGetMachineInfo(this.buildQueryParams())
this.fetchPayTypes()
// 本地记住用户选择
try{
if (window && window.localStorage) {
window.localStorage.setItem('pl_machineType', String(this.machineType))
}
}catch(e){/* noop */}
},
// 行币种:优先行内 payCoin > coin其次取全局表头币种
getRowCoin(row) {
try {
@@ -217,9 +633,9 @@ export default {
this.fetchGetMachineInfo(params)
} catch (e) { /* noop */ }
},
// 组合查询参数(带上商品 id 与筛选条件
// 组合查询参数:店铺入口,必须包含 shopId 与 type0-ASIC1-GPU
buildQueryParams() {
const q = { id: this.params.id }
const q = { shopId: this.params.id, type: this.machineType }
// 分页参数始终透传
try {
if (this.params && this.params.pageNum != null) q.pageNum = this.params.pageNum
@@ -251,10 +667,12 @@ export default {
} catch (e) { /* noop */ }
return q
},
// 拉取支付方式
// 拉取支付方式(兼容 shopId
async fetchPayTypes() {
try {
const res = await getPayTypes({ productId: this.params.id })
// 现规则:商品详情由店铺入口进入,使用 shopId 查询支付方式
// 为兼容后端两种入参,优先传 shopId后端若仍使用 productId 也能兼容处理
const res = await getPayTypes({ shopId: this.params.id, productId: this.params.id })
// 接口示例:{ code: 0, data: [ { payChain, payCoin, payCoinImage, shopId } ], msg: '' }
if (res && (res.code === 0 || res.code === 200)) {
const list = Array.isArray(res.data) ? res.data : []
@@ -270,34 +688,21 @@ export default {
async fetchGetMachineInfo(params) {
this.productDetailLoading = true
const res = await getMachineInfo(params)
// 改为使用店铺机器列表接口
const res = await getShopMachineList(params)
console.log(res)
if (res && res.code === 200) {
console.log(res.data, 'res.rows');
this.total = res.total||0;
// 新数据结构:机器为扁平 rows 列表;仅当后端返回有效支付方式时才覆盖,避免清空 getPayTypes 的结果
// 若后端同步返回支付方式,刷新本地支付方式
try {
const payList = res && res.data && res.data.payConfigList
if (Array.isArray(payList) && payList.length) {
this.paymentMethodList = payList
}
} catch (e) { /* keep existing paymentMethodList */ }
const rows = (res && res.data && (res.data.rows || res.data.list)) || (res && res.rows) || []
const normalized = (Array.isArray(rows) ? rows : []).map((m, idx) => ({
...m,
id: m && (m.id !== undefined && m.id !== null) ? m.id : `m-${idx}`,
leaseTime: (m && m.leaseTime && Number(m.leaseTime) > 0) ? Number(m.leaseTime) : 1,
_selected: false
}))
this.machineList = normalized
// 清空旧的两层结构数据,避免误用
this.productListData = []
this.expandedRowKeys = []
// 机器加载后尝试设置默认筛选
this.ensureDefaultPayFilterSelection()
this.$nextTick(() => {
this.machinesLoaded = true
})
} catch (e) { /* noop */ }
// 动态表格数据(如后端有对应 rows/columns可在此接入
// 此处保留现有动态表格的模拟/替换逻辑,不再维护旧表格数据结构
}
this.productDetailLoading = false
@@ -552,88 +957,56 @@ export default {
}
},
// 打开确认弹窗:以当前界面勾选(_selected)为准,并在打开后清空左侧勾选状态
// 打开确认弹窗(基于动态表格的勾选行)
handleOpenAddToCartDialog() {
// 扫描当前所有系列下被勾选的机器
const groups = Array.isArray(this.productListData) ? this.productListData : []
const pickedAll = groups.flatMap(g => Array.isArray(g.productMachines) ? g.productMachines.filter(m => !!m && !!m._selected) : [])
const picked = pickedAll.filter(m => m && (m.saleState === 0 || m.saleState === undefined || m.saleState === null))
const rows = Array.isArray(this.dynamicRows) ? this.dynamicRows : []
const picked = rows.filter(r => !!r && !!r._selected)
if (!picked.length) {
this.$message.warning('请先勾选至少一台矿机')
return
}
if (picked.length < pickedAll.length) {
this.$message.warning('部分机器已售出或售出中,已自动为您排除')
}
// 使用弹窗中的固定快照,避免后续清空勾选影响弹窗显示
this.confirmAddDialog.items = picked.slice()
this.confirmAddDialog.items = picked.map(r => ({
...r,
leaseTime: Number(r.leaseTime || 1),
purchaseQuantity: Number(r.purchaseQuantity || 0)
}))
this.confirmAddDialog.visible = true
// 打开后立即把左侧复选框清空,避免“勾选了两个但弹窗只有一条”的不一致问题
this.$nextTick(() => {
try { this.clearAllSelections() } catch (e) { /* noop */ }
})
},
// 确认加入:调用后端购物车接口,传入裸数组 [{ productId, productMachineId }]
// 确认加入:调用 addGoodsV2按条提交GPU 不传 numbers
async handleConfirmAddSelectedToCart() {
// 以弹窗中的列表为准,避免与左侧勾选状态不一致
const allSelected = Array.isArray(this.confirmAddDialog.items) ? this.confirmAddDialog.items.filter(Boolean) : []
if (!allSelected.length) {
const items = Array.isArray(this.confirmAddDialog.items) ? this.confirmAddDialog.items.filter(Boolean) : []
if (!items.length) {
this.$message.warning('请先勾选至少一台矿机')
return
}
const productId = this.params && this.params.id ? this.params.id : (this.$route && this.$route.params && this.$route.params.id)
if (!productId) {
this.$message.error('商品ID缺失无法加入购物车')
return
}
// 裸数组,仅包含后端要求的两个字段
const payload = allSelected.map(item => ({
productId: productId,
productMachineId: item.id,
leaseTime: Number(item.leaseTime || 1)
}))
try {
const res = await this.fetchAddCart(payload)
// 若后端返回码存在,这里做一下兜底提示
if (!res || (res.code && Number(res.code) !== 200)) {
this.$message.error(res && res.msg ? res.msg : '加入购物车失败,请稍后重试')
return
// 按接口要求:一次性传数组,每个对象代表一个勾选商品
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} 台矿机到购物车`)
}
// 立即本地更新禁用状态把刚加入的机器ID合并进本地集合
try {
allSelected.forEach(item => {
if (item && item.id) this.cartMachineIdSet.add(item.id)
this.$set(item, '_selected', false)
this.$set(item, '_inCart', true)
if (!item.leaseTime || Number(item.leaseTime) <= 0) this.$set(item, 'leaseTime', 1)
})
this.$nextTick(() => this.autoSelectAndDisable())
} catch (e) { /* noop */ }
this.$message({
message: `已加入 ${allSelected.length} 台矿机到购物车`,
type: 'success',
duration: 3000,
showClose: true,
});
this.confirmAddDialog.visible = false
// 清空选中映射,然后重新加载数据(数据加载时会自动设置 _selected: false
this.selectedMap = {}
// 重新加载机器信息和购物车数据
this.fetchGetMachineInfo(this.params)
this.fetchGetGoodsList()
// 通知头部刷新服务端购物车数量
// 清空
try {
// 如果没有传数量header 会主动拉取服务端数量
window.dispatchEvent(new CustomEvent('cart-updated'))
(this.dynamicRows || []).forEach(r => { if (r) this.$set(r, '_selected', false) })
} catch (e) { /* noop */ }
// 通知头部刷新
try { window.dispatchEvent(new CustomEvent('cart-updated')) } catch (e) { /* noop */ }
} catch (e) {
console.error('加入购物车失败: ', e)
// eslint-disable-next-line no-console
console.error('addGoodsV2 error:', e)
this.$message.error('加入购物车失败,请稍后重试')
}
},