Files
webs/power_leasing/src/views/account/myShops.vue
2026-01-16 15:03:50 +08:00

1285 lines
48 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="panel" >
<h2 class="panel-title">我的店铺</h2>
<div class="panel-body">
<!-- 店铺层级说明 -->
<el-card class="guide-card" shadow="never" style="margin-bottom: 16px;">
<div slot="header" class="guide-header">店铺层级说明</div>
<div class="guide-content">
<p class="hierarchy">层级结构店铺 商品 出售机器</p>
<ol class="guide-steps">
<li>
<b>店铺唯一</b>每个用户在平台<strong>仅能创建一个店铺</strong>创建成功后
请在本页点击 <b>钱包绑定</b>配置自己的收款地址支持不同链与币种
</li>
<li>
<b>创建商品</b>完成钱包绑定后即可在我的店铺页面 点击<b>新增商品</b>按钮
<ul class="guide-substeps">
<li>
<b>ASIC 商品创建</b>选择矿机种类为 ASIC填写页面商品信息后创建商品可按 <b>币种</b> 进行分类管理创建的商品会在商城对买家展示
商品可理解为不同算法币种的机器集合分类
</li>
<li>
<b>GPU 商品创建</b>选择矿机种类为 GPU查看页面注意事项并下载对应客户端启动后读取自动创建创建完成请进入 <b>商品列表</b> 为该商品手动配置售价等相关信息并上架
</li>
</ul>
</li>
</ol>
<div class="guide-note">提示建议先创建店铺 完成钱包绑定 创建商品的顺序避免漏配导致无法收款或无法下单</div>
</div>
</el-card>
<el-card v-if="loaded && hasShop" class="shop-card" shadow="hover">
<div class="shop-row">
<div class="shop-cover">
<img :src="shop.image || defaultCover" alt="店铺封面" />
</div>
<div class="shop-info">
<div class="shop-title">
<span class="name">{{ shop.name || '未命名店铺' }}</span>
<el-tag size="small" :type="shopStateTagType">
{{ shopStateText }}
</el-tag>
</div>
<div class="desc">{{ shop.description || '这家店还没有描述~' }}</div>
<div class="meta">
<span>手续费率{{ formatFeeRate(shop.feeRate) }}</span>
</div>
<div class="actions">
<el-button size="small" type="primary" @click="handleOpenEdit">修改店铺</el-button>
<el-button size="small" type="warning" @click="handleToggleShop">
{{ shop.state === 2 ? '开启店铺' : '关闭店铺' }}
</el-button>
<el-button size="small" type="danger" @click="handleDelete">删除店铺</el-button>
<el-button size="small" type="success" @click="handleAddProduct">新增商品</el-button>
<el-button size="small" type="success" @click="handleWalletBind">钱包绑定</el-button>
</div>
</div>
</div>
</el-card>
<!-- 店铺配置表格 -->
<el-card v-if="loaded && hasShop" class="shop-config-card" shadow="never" style="margin-top: 16px;">
<div slot="header" class="clearfix">
<span>已绑定钱包</span>
</div>
<el-table :data="shopConfigs" border style="width: 100%">
<el-table-column prop="chain" label="链" width="120" />
<el-table-column label="支付币种" width="120" >
<template slot-scope="scope">
<div class="coin-list">
<template v-if="Array.isArray(scope.row.children) && scope.row.children.length">
<el-tooltip
v-for="(c, idx) in scope.row.children"
:key="idx"
:content="String(c && c.payCoin ? c.payCoin : '').toUpperCase()"
placement="top"
>
<img
v-if="c && c.image"
class="coin-img"
:src="c.image"
:alt="(c.payCoin || '').toUpperCase()"
/>
</el-tooltip>
</template>
<template v-else>
{{ String(scope.row.payCoin || '').toUpperCase() }}
</template>
</div>
</template>
</el-table-column>
<!-- <el-table-column prop="payType" label="币种类型" width="120">
<template slot-scope="scope">{{ scope.row.payType === 1 ? '稳定币' : '虚拟币' }}</template>
</el-table-column> -->
<el-table-column prop="payAddress" label="收款钱包地址" show-overflow-tooltip />
<el-table-column label="余额" >
<template slot-scope="scope">
<span class="balance-num">{{ formatAmount(scope.row) }}</span>
<span class="balance-unit"> {{ formatCoin(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<template slot-scope="scope">
<el-button type="text" style="color:#409EFF" @click="handleWithdraw(scope.row)">提现</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" @click="handleEditConfig(scope.row)">修改</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" style="color:#e74c3c" @click="handleDeleteConfig(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<div v-else-if="loaded && !hasShop" class="no-shop">
<el-empty description="暂无店铺">
<el-button type="primary" @click="handleGoNew">新建店铺</el-button>
</el-empty>
</div>
<el-empty v-else description="正在加载店铺信息..." />
<!-- 提现对话框仅使用本页行数据 -->
<el-dialog
:title="withdrawDialogTitle"
:visible.sync="withdrawDialogVisible"
width="720px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form :model="withdrawForm" :rules="withdrawRules" ref="withdrawForm" label-width="120px">
<!-- 提现链 -->
<el-form-item label="提现链">
<el-input :value="String((currentWithdrawRow.chain || '')).toUpperCase()" :disabled="true" />
</el-form-item>
<!-- 提现币种 -->
<el-form-item label="提现币种">
<el-input :value="displayWithdrawSymbol" :disabled="true" />
</el-form-item>
<!-- 提现金额 -->
<el-form-item label="提现金额" prop="amount">
<el-input
v-model="withdrawForm.amount"
placeholder="请输入提现金额"
inputmode="decimal"
@input="handleAmountInput"
>
<template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input>
<div class="balance-info">
可用余额 {{ availableWithdrawBalance }} {{ displayWithdrawSymbol }}
</div>
</el-form-item>
<!-- 手续费 -->
<el-form-item label="手续费">
<el-input v-model="withdrawForm.fee" :disabled="true">
<template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input>
<div class="fee-info">网络手续费 {{ withdrawForm.fee || '0' }} {{ displayWithdrawSymbol }}</div>
</el-form-item>
<!-- 实际到账 -->
<el-form-item label="实际到账">
<el-input :value="actualAmount" :disabled="true">
<template slot="append">{{ displayWithdrawSymbol }}</template>
</el-input>
<div class="actual-amount-info">实际到账 {{ actualAmount }} {{ displayWithdrawSymbol }}</div>
</el-form-item>
<!-- 收款地址 -->
<el-form-item label="收款地址" prop="toAddress">
<el-input
v-model="withdrawForm.toAddress"
placeholder="请输入收款钱包地址"
:disabled="!withdrawAddressEditable"
ref="withdrawToAddressInput"
>
<template slot="append">
<el-button type="text" @click="handleEditAddressClick">修改</el-button>
</template>
</el-input>
<div class="address-tip">请确认地址正确错误地址将导致资产丢失</div>
</el-form-item>
<!-- 谷歌验证码 -->
<el-form-item label="谷歌验证码" prop="googleCode">
<el-input
v-model="withdrawForm.googleCode"
placeholder="请输入6位谷歌验证码"
maxlength="6"
@input="handleGoogleCodeInput"
>
<template slot="prepend">
<i class="el-icon-key"></i>
</template>
</el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="withdrawDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="withdrawLoading" @click="confirmWithdraw">确认提现</el-button>
</div>
</el-dialog>
<!-- 修改店铺弹窗 -->
<el-dialog title="修改店铺" :visible.sync="visibleEdit" width="520px">
<div class="row">
<label class="label">店铺名称</label>
<el-input v-model="editForm.name" placeholder="请输入店铺名称" :maxlength="30" show-word-limit />
</div>
<!-- <div class="row">
<label class="label">店铺封面</label>
<el-input v-model="editForm.image" placeholder="请输入图片地址" />
</div> -->
<div class="row">
<label class="label">店铺描述</label>
<el-input type="textarea" :rows="3" v-model="editForm.description" placeholder="请输入描述" :maxlength="300" show-word-limit />
</div>
<div class="row">
<label class="label">手续费比例</label>
<el-input
v-model="editForm.feeRate"
placeholder="比例区间 0.01 - 0.1 之间最多6位小数"
@input="handleEditFeeRateInput"
/>
</div>
<div class="row">
<label class="label">谷歌验证码</label>
<el-input
v-model="editForm.gCode"
placeholder="请输入6位谷歌验证码"
maxlength="6"
@input="handleEditShopGoogleCodeInput"
/>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="visibleEdit=false">取消</el-button>
<el-button type="primary" @click="submitEdit">保存</el-button>
</span>
</el-dialog>
<!-- 修改钱包绑定配置弹窗参数保持与列表一致 -->
<el-dialog title="修改配置" :visible.sync="visibleConfigEdit" width="560px" @close="handleConfigEditClose">
<div class="row">
<label class="label">钱包地址</label>
<el-input v-model="configForm.payAddress" placeholder="请输入钱包地址" />
</div>
<div class="row">
<label class="label">谷歌验证码</label>
<el-input
v-model="configForm.googleCode"
placeholder="请输入6位谷歌验证码"
maxlength="6"
@input="handleConfigGoogleCodeInput"
>
<template slot="prepend">
<i class="el-icon-key"></i>
</template>
</el-input>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="visibleConfigEdit=false">取消</el-button>
<el-button type="primary" @click="submitConfigEdit">确认修改</el-button>
</span>
</el-dialog>
</div>
</div>
</template>
<script>
import { getMyShop, updateShop, deleteShop, queryShop, closeShop ,updateShopConfig,deleteShopConfig,getChainAndCoin} from '@/api/shops'
import { coinList } from '@/utils/coinList'
import { getShopConfig,getShopConfigV2 ,withdrawBalanceForSeller,updateShopConfigV2} from '@/api/wallet'
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
import { getGoogleStatus } from '@/api/verification'
export default {
name: 'AccountMyShops',
data() {
return {
loaded: false,
defaultCover: 'https://dummyimage.com/120x120/eee/999.png&text=Shop',
shop: {
id: 0,
name: '',
image: '',
description: '',
feeRate: '',
del: true,
state: 0
},
visibleEdit: false,
editForm: { id: '', name: '', image: '', description: '', feeRate: '', gCode: '' },
// 店铺配置列表
shopConfigs: [],
visibleConfigEdit: false,
configForm: { id: '', chainLabel: '', chainValue: '', payAddress: '', payCoins: [], payCoin: '', googleCode: '' },
productOptions: [],
coinOptions: coinList || [],
editCoinOptionsApi: [],
// 支付链选项(可与后端接口对齐后替换为动态)
chainOptions: [
{ label: 'Tron (TRC20)', value: 'tron' },
{ label: 'Ethereum (ERC20)', value: 'ethereum' },
{ label: 'BSC (BEP20)', value: 'bsc' },
{ label: 'Nexa', value: 'nexa' },
],
shopLoading: false,
/* 提现弹窗状态(仅本页使用) */
withdrawDialogVisible: false,
withdrawLoading: false,
currentWithdrawRow: {},
withdrawForm: {
amount: '',
toAddress: '',
fee: '0.00',
googleCode: ''
},
withdrawAddressEditable: false,
withdrawRules: {}
}
},
computed: {
shopStateText() {
// 0 待审核 1 审核通过(店铺开启) 2 店铺关闭
if (this.shop.state === 0) return '待审核'
if (this.shop.state === 1) return '店铺开启'
if (this.shop.state === 2) return '店铺关闭'
return '未知状态'
},
shopStateTagType() {
// 标签配色:待审核=warning开启=success关闭=info
if (this.shop.state === 0) return 'warning'
if (this.shop.state === 1) return 'success'
if (this.shop.state === 2) return 'info'
return 'info'
},
hasShop() {
return !!(this.shop && Number(this.shop.id) > 0)
},
canCreateShop() {
return !this.hasShop
},
/**
* 弹窗可选币种:稳定币/虚拟币分流
*/
editCoinOptions() {
if (Array.isArray(this.editCoinOptionsApi) && this.editCoinOptionsApi.length) return this.editCoinOptionsApi
return this.coinOptions
},
selectedCoinLabels() {
const map = new Map((this.editCoinOptions || []).map(o => [String(o.value), String(o.label).toUpperCase()]))
return (this.configForm.payCoins || []).map(v => map.get(String(v)) || String(v).toUpperCase())
},
/* 提现弹窗标题:如 USDT提现 */
withdrawDialogTitle() {
const sym = String((this.currentWithdrawRow && this.currentWithdrawRow.payCoin) || '').toUpperCase() || 'USDT'
return `${sym}提现`
},
/* 提现币种(大写) */
displayWithdrawSymbol() {
return String((this.currentWithdrawRow && this.currentWithdrawRow.payCoin) || '').toUpperCase()
},
/* 可用余额最多6位小数显示 */
availableWithdrawBalance() {
const n = Number((this.currentWithdrawRow && this.currentWithdrawRow.balance) || 0)
return this.formatDec6(n)
},
/* 实际到账金额 = 可用余额 - 手续费(只显示可用余额,不展示总余额/冻结) */
actualAmount() {
const amountInt = this.toScaledInt(this.withdrawForm.amount)
const feeInt = this.toScaledInt(this.withdrawForm.fee)
if (!Number.isFinite(amountInt) || !Number.isFinite(feeInt)) return '0'
const res = amountInt - feeInt
return res > 0 ? this.formatDec6FromInt(res) : '0'
}
},
created() {
this.fetchMyShop()
},
methods: {
/**
* 修改店铺谷歌验证码输入仅数字最多6位
*/
handleEditShopGoogleCodeInput(v) {
this.editForm.gCode = String(v || '').replace(/\D/g, '').slice(0, 6)
},
/**
* 钱包相关敏感操作前置校验必须已开启双重验证Google Authenticator
* getGoogleStatus 返回值0 开启1 未绑定2 关闭
*
* - status=0允许继续操作
* - status=1/2弹窗提示并阻止操作可跳转到“安全设置”页面开启/绑定
*
* @param {string} actionLabel - 操作名称(用于文案:如“提现/修改/删除”)
* @returns {Promise<boolean>} 是否允许继续
*/
async ensureGoogleStatusEnabledForWalletOp(actionLabel) {
try {
const res = await getGoogleStatus()
if (!res || !(res.code === 0 || res.code === 200)) {
this.$message.error('获取双重验证状态失败,请稍后重试')
return false
}
const status = (res && res.data && res.data.status != null) ? res.data.status : (res.data ?? 1)
if (Number(status) === 0) return true
const title = '安全提示'
const reason = Number(status) === 1 ? '您尚未绑定双重验证' : '您已关闭双重验证'
const message = `
<div class="google-2fa-guard__content">
<div class="google-2fa-guard__title">${reason}</div>
<div class="google-2fa-guard__desc">
请先在<strong>安全设置</strong>中绑定并开启双重验证后,才可以进行<strong>${actionLabel || '该'}操作</strong>。
</div>
</div>
`
await this.$confirm(message, title, {
confirmButtonText: '去安全设置',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true,
customClass: 'google-2fa-guard-dialog'
})
this.$router.push('/account/security-settings')
return false
} catch (e) {
// 用户取消弹窗或接口异常都视为不允许继续
return false
}
},
/** 余额展示:带币种单位 */
formatBalance(row) {
try {
const num = Number(row && row.balance)
const valid = Number.isFinite(num)
const coin = String(row && row.payCoin ? row.payCoin : '').toUpperCase()
if (!valid) return '-'
const text = String(num)
return coin ? `${text} ${coin}` : text
} catch (e) {
return '-'
}
},
/** 仅数字部分(用于红色显示) */
formatAmount(row) {
try {
const num = Number(row && row.balance)
if (!Number.isFinite(num)) return '-'
return String(num)
} catch (e) {
return '-'
}
},
/** 仅币种单位(大写) */
formatCoin(row) {
return String(row && row.payCoin ? row.payCoin : '').toUpperCase()
},
/** 打开提现对话框(行数据驱动) */
async handleWithdraw(row) {
const ok = await this.ensureGoogleStatusEnabledForWalletOp('提现')
if (!ok) return
this.currentWithdrawRow = row || {}
const fee = Number(row && (row.serviceCharge != null ? row.serviceCharge : row.charge))
this.withdrawForm.fee = Number.isFinite(fee) ? this.formatDec6(fee) : '0.00'
this.withdrawForm.amount = ''
// 收款地址默认填充为当前行地址,且禁用编辑
this.withdrawForm.toAddress = row && row.payAddress ? row.payAddress : ''
this.withdrawForm.googleCode = ''
this.withdrawAddressEditable = false
// 初始化校验规则
this.withdrawRules = {
amount: [
{ required: true, message: '请输入提现金额', trigger: 'blur' },
{ validator: this.validateWithdrawAmount, trigger: 'blur' }
],
toAddress: [
{ required: true, message: '请输入收款钱包地址', trigger: 'blur' },
{ validator: this.validateWithdrawToAddress, trigger: 'blur' }
],
googleCode: [
{ required: true, message: '请输入谷歌验证码', trigger: 'blur' },
{ validator: this.validateGoogleCode, trigger: 'blur' }
]
}
this.withdrawDialogVisible = true
},
/* 点击“修改”启用收款地址编辑并聚焦 */
handleEditAddressClick() {
this.withdrawAddressEditable = true
this.$nextTick(() => {
const input = this.$refs.withdrawToAddressInput
if (input && input.focus) input.focus()
})
},
/* 提现金额输入(<=6位小数 */
handleAmountInput(v) {
let s = String(v || '')
s = s.replace(/[^0-9.]/g, '')
const i = s.indexOf('.')
if (i !== -1) {
s = s.slice(0, i + 1) + s.slice(i + 1).replace(/\./g, '')
const [intPart, decPart = ''] = s.split('.')
s = intPart + '.' + decPart.slice(0, 6)
}
this.withdrawForm.amount = s
},
/* 谷歌验证码仅数字 */
handleGoogleCodeInput(v) {
this.withdrawForm.googleCode = String(v || '').replace(/\D/g, '')
},
/* 确认提现 - 调用后端卖家提现接口 */
confirmWithdraw() {
this.$refs.withdrawForm.validate(async (valid) => {
if (!valid) return
this.withdrawLoading = true
try {
const row = this.currentWithdrawRow || {}
/**
* 提现地址 RSA 加密(与“钱包绑定”页面保持一致:同步优先,异步兜底)
* - toAddress: 用户输入/默认地址
* - fromAddress: 当前绑定的钱包地址(后端可能用于校验来源)
*/
const toAddressPlain = String(this.withdrawForm.toAddress || '').trim()
const fromAddressPlain = String(row.payAddress || this.withdrawForm.toAddress || '').trim()
/** @type {string} */
let encryptedToAddress = toAddressPlain
if (encryptedToAddress) {
const syncEncrypted = rsaEncryptSync(encryptedToAddress)
if (syncEncrypted) {
encryptedToAddress = syncEncrypted
} else {
const asyncEncrypted = await rsaEncrypt(encryptedToAddress)
if (asyncEncrypted) {
encryptedToAddress = asyncEncrypted
} else {
this.$message.error('钱包地址加密失败,请重试')
return
}
}
}
/** @type {string} */
let encryptedFromAddress = fromAddressPlain
if (encryptedFromAddress) {
const syncEncrypted = rsaEncryptSync(encryptedFromAddress)
if (syncEncrypted) {
encryptedFromAddress = syncEncrypted
} else {
const asyncEncrypted = await rsaEncrypt(encryptedFromAddress)
if (asyncEncrypted) {
encryptedFromAddress = asyncEncrypted
} else {
this.$message.error('钱包地址加密失败,请重试')
return
}
}
}
const payload = {
toChain: row.chain,
toSymbol: row.payCoin,
amount: Number(this.withdrawForm.amount),
toAddress: encryptedToAddress,
fromAddress: encryptedFromAddress,
code: this.withdrawForm.googleCode,
serviceCharge: Number(this.withdrawForm.fee) || 0
}
const res = await withdrawBalanceForSeller(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('提现申请已提交,请等待处理')
this.withdrawDialogVisible = false
this.fetchShopConfigs(this.shop.id)
}
} catch (e) {
console.error('卖家提现失败', e)
} finally {
this.withdrawLoading = false
}
})
},
/* 工具最多6位小数显示 */
formatDec6(value) {
if (value === null || value === undefined || value === '') return '0'
let s = String(value)
if (/e/i.test(s)) {
const n = Number(value)
if (!Number.isFinite(n)) return '0'
s = n.toFixed(20).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
}
const m = s.match(/^(-?)(\d+)(?:\.(\d+))?$/)
if (!m) return s
let intPart = m[2]
let decPart = m[3] || ''
if (decPart.length > 6) decPart = decPart.slice(0, 6)
return decPart ? `${intPart}.${decPart}` : intPart
},
/* 工具:金额字符串 -> 10^6 精度整数 */
toScaledInt(amountStr, decimals = 6) {
if (amountStr === null || amountStr === undefined) return 0
const normalized = String(amountStr).trim()
if (normalized === '') return 0
const re = new RegExp(`^\\d+(?:\\.(\\d{0,${decimals}}))?$`)
const match = normalized.match(re)
if (!match) {
const n = Number(normalized)
if (!Number.isFinite(n)) return 0
const scale = Math.pow(10, decimals)
return Math.round(n * scale)
}
const [intPart, decPartRaw] = normalized.split('.')
const decPart = (decPartRaw || '').padEnd(decimals, '0').slice(0, decimals)
const scale = Math.pow(10, decimals)
return Number(intPart) * scale + Number(decPart)
},
/* 工具10^6 精度整数 -> 最多6位小数字符串 */
formatDec6FromInt(intVal) {
const sign = intVal < 0 ? '-' : ''
const abs = Math.abs(intVal)
const scale = Math.pow(10, 6)
const intPart = Math.floor(abs / scale)
const decPart = String(abs % scale).padStart(6, '0')
const s = `${sign}${intPart}.${decPart}`
return s.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
},
/* 校验:提现金额 */
validateWithdrawAmount(rule, value, callback) {
const amtInt = this.toScaledInt(value)
if (!Number.isFinite(amtInt) || amtInt <= 0) { callback(new Error('请输入有效的金额')); return }
const feeInt = this.toScaledInt(this.withdrawForm.fee)
const balanceInt = this.toScaledInt((this.currentWithdrawRow && this.currentWithdrawRow.balance) || 0)
// 允许提现金额等于可用余额,但不能大于
if (amtInt > balanceInt) { callback(new Error('提现金额不能大于可用余额')); return }
// 提现金额必须大于手续费
if (amtInt <= feeInt) { callback(new Error('提现金额必须大于手续费')); return }
// 实际到账金额(提现金额 - 手续费必须大于0
const actualInt = amtInt - feeInt
if (actualInt <= 0) { callback(new Error('提现金额扣除手续费后必须大于0')); return }
// 最小提现金额为 1
if (amtInt < 1000000) { callback(new Error('最小提现金额为 1')); return }
callback()
},
/* 校验:谷歌验证码 */
validateGoogleCode(rule, value, callback) {
const v = String(value || '')
if (!/^\d{6}$/.test(v)) { callback(new Error('谷歌验证码必须是6位数字')); return }
callback()
},
/**
* 校验:收款地址不能为空
* @param {any} rule
* @param {string} value
* @param {(err?: Error) => void} callback
*/
validateWithdrawToAddress(rule, value, callback) {
const v = String(value || '').trim()
if (!v) { callback(new Error('请输入收款钱包地址')); return }
callback()
},
/**
* 手续费率显示最多6位小数去除多余的0空值显示为 '-'
*/
formatFeeRate(value) {
if (value === null || value === undefined || value === '') return '-'
const num = Number(value)
if (!Number.isFinite(num)) return '-'
const fixed = num.toFixed(6)
return fixed.replace(/\.?0+$/, '')
},
/**
* 修改弹窗 - 手续费输入允许一个小数点最多6位小数允许尾随点
*/
handleEditFeeRateInput(value) {
let v = String(value ?? this.editForm.feeRate ?? '')
v = v.replace(/[^0-9.]/g, '')
const firstDot = v.indexOf('.')
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
}
const endsWithDot = v.endsWith('.')
const parts = v.split('.')
let intPart = parts[0] || ''
let decPart = parts[1] || ''
if (decPart.length > 6) decPart = decPart.slice(0, 6)
if (intPart && intPart !== '0') intPart = String(Number(intPart))
if (endsWithDot && firstDot !== -1) {
this.editForm.feeRate = `${intPart || '0'}.`
return
}
this.editForm.feeRate = decPart ? `${intPart || '0'}.${decPart}` : (intPart || '')
},
// 简单的emoji检测覆盖常见表情平面与符号范围
hasEmoji(str) {
if (!str || typeof str !== 'string') return false
const emojiRegex = /[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{1FA70}-\u{1FAFF}\u2600-\u27BF]/u
return emojiRegex.test(str)
},
/**
* 重置店铺状态为无店铺状态
*/
resetShopState() {
this.shop = {
id: 0,
name: '',
image: '',
description: '',
del: true,
state: 0
}
this.shopConfigs = []
},
async fetchMyShop() {
try {
const res = await getMyShop()
// 预期格式:{"code":0,"data":{"del":true,"description":"","id":0,"image":"","name":"","state":0},"msg":""}
if (res && (res.code === 0 || res.code === 200) && res.data) {
this.shop = {
id: res.data.id,
name: res.data.name,
image: res.data.image,
description: res.data.description,
feeRate: res.data.feeRate,
del: !!res.data.del,
state: Number(res.data.state || 0)
}
// 同步加载钱包绑定
this.fetchShopConfigs(res.data.id)
} else {
// 当接口返回错误或没有数据时,重置店铺状态
this.resetShopState()
if (res && res.msg) {
console.warn('获取店铺数据失败:', res.msg)
}
}
} catch (error) {
console.error('获取店铺信息失败:', error)
// 当接口报错如500错误重置店铺状态
this.resetShopState()
} finally {
this.loaded = true
}
},
async fetchShopConfigs(shopId) {
// 如果店铺ID无效直接清空配置
if (!shopId || shopId <= 0) {
this.shopConfigs = []
return
}
try {
const res = await getShopConfigV2({id:shopId})
if (res && (res.code === 0 || res.code === 200) && Array.isArray(res.data)) {
// 直接使用后端返回的数据children: [{payCoin,image}]
this.shopConfigs = res.data
} else {
this.shopConfigs = []
}
} catch (e) {
console.warn('获取店铺配置失败:', e)
this.shopConfigs = []
}
},
async updateShopConfig(params) {
const res = await updateShopConfig(params)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('保存成功')
this.visibleConfigEdit = false
this.fetchShopConfigs(this.shop.id)
}
},
async deleteShopConfig(params) {
const res = await deleteShopConfig(params)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('删除成功')
this.fetchShopConfigs(this.shop.id)
}
},
async handleEditConfig(row) {
const ok = await this.ensureGoogleStatusEnabledForWalletOp('修改')
if (!ok) return
try {
const res = await getChainAndCoin({ id: row.id })
if (res && (res.code === 0 || res.code === 200) && res.data) {
const d = res.data || {}
const children = Array.isArray(d.children) ? d.children : []
this.editCoinOptionsApi = children.map(c => ({ label: c.label, value: c.value }))
const preSelected = children.filter(c => Number(c.hasBind) === 1).map(c => c.value)
this.configForm = {
id: row.id,
chainLabel: d.label || '',
chainValue: d.value || '',
payAddress: d.address || '',
payCoins: preSelected,
payCoin: preSelected.join(','),
googleCode: ''
}
} else {
// 回退:使用行内已有数据
this.editCoinOptionsApi = []
const chainLabel = row.chain || ''
const payCoinStr = String(row.payCoin || '')
const payCoins = payCoinStr ? payCoinStr.split(',') : []
this.configForm = {
id: row.id,
chainLabel,
chainValue: row.chain || '',
payAddress: row.payAddress || '',
payCoins,
payCoin: payCoins.join(','),
googleCode: ''
}
}
this.visibleConfigEdit = true
} catch (e) {
this.visibleConfigEdit = true
}
},
async handleDeleteConfig(row) {
const ok = await this.ensureGoogleStatusEnabledForWalletOp('删除')
if (!ok) return
try {
const { value } = await this.$prompt(
'请输入 6 位谷歌验证码以删除该钱包绑定配置',
'安全验证',
{
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'warning',
inputPlaceholder: '6位数字验证码',
inputPattern: /^\d{6}$/,
inputErrorMessage: '谷歌验证码必须是6位数字'
}
)
const gCode = String(value || '').trim()
if (!/^\d{6}$/.test(gCode)) {
this.$message.warning('谷歌验证码必须是6位数字')
return
}
await this.deleteShopConfig({ id: row.id, gCode })
} catch (e) {
// 用户取消或弹窗关闭
}
},
/**
* 处理谷歌验证码输入(仅允许数字)
*/
handleConfigGoogleCodeInput(v) {
this.configForm.googleCode = String(v || '').replace(/\D/g, '')
},
/**
* 修改配置弹窗关闭时清空验证码
*/
handleConfigEditClose() {
this.configForm.googleCode = ''
},
/**
* 提交配置修改
*/
async submitConfigEdit() {
// 校验钱包地址
const addr = (this.configForm.payAddress || '').trim()
if (!addr) {
this.$message.warning('请输入钱包地址')
return
}
// 校验谷歌验证码
const googleCode = String(this.configForm.googleCode || '').trim()
if (!googleCode) {
this.$message.warning('请输入谷歌验证码')
return
}
if (!/^\d{6}$/.test(googleCode)) {
this.$message.warning('谷歌验证码必须是6位数字')
return
}
/**
* 使用 RSA 加密钱包地址(与"钱包绑定"页面保持一致:同步优先,异步兜底)
* @type {string}
*/
let encryptedPayAddress = addr
if (encryptedPayAddress) {
const syncEncrypted = rsaEncryptSync(encryptedPayAddress)
if (syncEncrypted) {
encryptedPayAddress = syncEncrypted
} else {
const asyncEncrypted = await rsaEncrypt(encryptedPayAddress)
if (asyncEncrypted) {
encryptedPayAddress = asyncEncrypted
} else {
this.$message.error('钱包地址加密失败,请重试')
return
}
}
}
const payload = {
id: this.configForm.id,
chain: this.configForm.chainValue || this.configForm.chainLabel || '',
payAddress: encryptedPayAddress,
gcode: googleCode
}
try {
const res = await updateShopConfigV2(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('保存成功')
this.visibleConfigEdit = false
this.fetchShopConfigs(this.shop.id)
}
} catch (e) {
console.error('修改配置失败', e)
}
},
removeSelectedCoin(labelUpper) {
const label = String(labelUpper || '').toLowerCase()
const map = new Map((this.editCoinOptions || []).map(o => [String(o.label).toLowerCase(), String(o.value)]))
const value = map.get(label)
if (!value) return
this.configForm.payCoins = (this.configForm.payCoins || []).filter(v => String(v) !== String(value))
},
async handleOpenEdit() {
const ok = await this.ensureGoogleStatusEnabledForWalletOp('修改店铺')
if (!ok) return
try {
// 先打开弹窗,提供更快的视觉反馈
this.visibleEdit = true
// 查询最新店铺详情
const res = await queryShop({ id: this.shop.id })
if (res && (res.code === 0 || res.code === 200) && res.data) {
this.editForm = {
id: res.data.id,
name: res.data.name,
image: res.data.image,
description: res.data.description,
feeRate: res.data.feeRate,
gCode: ''
}
} else {
// 回退到当前展示的数据
this.editForm = {
id: this.shop.id,
name: this.shop.name,
image: this.shop.image,
description: this.shop.description,
feeRate: this.shop.feeRate,
gCode: ''
}
this.$message.warning(res && res.msg ? res.msg : '未获取到店铺详情')
}
} catch (error) {
// 出错时回退到当前展示的数据
this.editForm = {
id: this.shop.id,
name: this.shop.name,
image: this.shop.image,
description: this.shop.description,
feeRate: this.shop.feeRate,
gCode: ''
}
console.error('查询店铺详情失败:', error)
}
},
/**
* 提交店铺修改
* 规则:允许输入空格,但不允许内容为“全是空格”
*/
async submitEdit() {
try {
const { name, image, description } = this.editForm
const isOnlySpaces = (v) => typeof v === 'string' && v.length > 0 && v.trim().length === 0
if (isOnlySpaces(name)) {
this.$message.error('店铺名称不能全是空格')
return
}
if (!name) {
this.$message.error('店铺名称不能为空')
return
}
if (this.hasEmoji(name)) {
this.$message.warning('店铺名称不能包含表情符号')
return
}
if (isOnlySpaces(image)) {
this.$message.error('店铺封面不能全是空格')
return
}
if (isOnlySpaces(description)) {
this.$message.error('店铺描述不能全是空格')
return
}
// 长度限制名称≤30描述≤300
if (name && name.length > 30) {
this.$message.warning('店铺名称不能超过30个字符')
return
}
if (description && description.length > 300) {
this.$message.warning('店铺描述不能超过300个字符')
return
}
// 手续费比例必填、0.01-0.1、最多6位小数
const rateRaw = String(this.editForm.feeRate || '').trim()
if (!rateRaw) {
this.$message.warning('请填写店铺手续费比例0.01 - 0.1最多6位小数')
return
}
const rateNum = Number(rateRaw)
const decOk = rateRaw.includes('.') ? ((rateRaw.split('.')[1] || '').length <= 6) : true
if (!Number.isFinite(rateNum) || rateNum < 0.01 || rateNum > 0.1 || !decOk) {
this.$message.warning('手续费比例需在 0.01 - 0.1 之间且小数位不超过6位')
return
}
this.editForm.feeRate = rateNum.toString()
// 谷歌验证码:必填 6 位数字
const gCode = String(this.editForm.gCode || '').trim()
if (!/^\d{6}$/.test(gCode)) {
this.$message.warning('请输入6位谷歌验证码')
return
}
const payload = { ...this.editForm, gCode }
const res = await updateShop(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message({
message: '已保存',
type: 'success',
showClose: true
})
this.visibleEdit = false
this.fetchMyShop()
} else {
this.$message({
message: res.msg || '保存失败',
type: 'error',
showClose: true
})
}
} catch (error) {
console.error('更新店铺失败:', error)
console.log('更新店铺失败,请稍后重试')
}
},
async handleDelete() {
const ok = await this.ensureGoogleStatusEnabledForWalletOp('删除店铺')
if (!ok) return
try {
const { value } = await this.$prompt(
'删除店铺将不可恢复,请输入 6 位谷歌验证码确认删除',
'安全验证',
{
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'warning',
inputPlaceholder: '6位数字验证码',
inputPattern: /^\d{6}$/,
inputErrorMessage: '谷歌验证码必须是6位数字'
}
)
const gCode = String(value || '').trim()
if (!/^\d{6}$/.test(gCode)) {
this.$message.warning('谷歌验证码必须是6位数字')
return
}
const res = await deleteShop(this.shop.id, gCode)
if (res && (res.code === 0 || res.code === 200)) {
this.$message({
message: '删除成功',
type: 'success',
showClose: true
})
// 删除成功后,先重置店铺状态,然后尝试重新获取
this.resetShopState()
this.loaded = false
// 延迟一下再尝试获取,给服务器时间处理删除操作
setTimeout(() => {
this.fetchMyShop()
}, 500)
}
} catch (e) {
// 用户取消
}
},
async handleToggleShop() {
try {
const isClosed = this.shop.state === 2
const confirmMsg = isClosed ? '确定开启店铺吗?' : '确定关闭该店铺吗?关闭后用户将无法访问'
await this.$confirm(confirmMsg, '提示', { type: 'warning' })
const res = await closeShop(this.shop.id)
if (res && (res.code === 0 || res.code === 200)) {
this.$message({
message: isClosed ? '店铺已开启' : '店铺已关闭',
type: 'success',
showClose: true
})
this.fetchMyShop()
} else {
// this.$message.error(res && res.msg ? res.msg : '操作失败')
console.log(`操作失败`);
}
} catch (e) {
// 用户取消
}
},
handleGoNew() {
if (!this.canCreateShop) {
this.$message({
message: '每个用户仅允许一个店铺,无法新建',
type: 'warning',
showClose: true
})
return
}
this.$router.push('/account/shop-new')
},
/**
* 跳转到新增商品页面
*/
handleAddProduct() {
if (!this.hasShop) {
this.$message({
message: '请先创建店铺',
type: 'warning',
showClose: true
})
return
}
// 直接跳转到“添加出售机器”页面并传递店铺ID供后续扩展使用
this.$router.push({
path: '/account/product-machine-add',
query: { shopId: this.shop.id }
})
},
/**
* 跳转到钱包绑定页面
*/
handleWalletBind() {
if (!this.hasShop) {
this.$message({
message: '请先创建店铺',
type: 'warning',
showClose: true
})
return
}
this.$router.push('/account/shop-config')
}
}
}
</script>
<style scoped>
.panel-title { margin: 0 0 12px 0; font-size: 18px; font-weight: 700; }
.shop-card { border-radius: 8px; }
.shop-row { display: grid; grid-template-columns: 120px 1fr; gap: 16px; align-items: center; }
.shop-cover img { width: 120px; height: 120px; object-fit: cover; border-radius: 8px; border: 1px solid #eee; }
.shop-info { display: flex; flex-direction: column; gap: 8px; }
.shop-title { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 16px; }
.desc { color: #666; }
.meta { color: #999; display: flex; gap: 16px; font-size: 12px; }
.actions { margin-top: 8px; display: flex; gap: 8px; }
.guide-card { border: 1px solid #eef2f7; border-radius: 10px; }
.guide-header { text-align: center; font-weight: 700; color: #2c3e50; background: #f9fafb; border-bottom: 1px solid #eef2f7; padding: 10px 12px; border-radius: 10px 10px 0 0; }
.guide-content { padding: 4px 6px; text-align: left; }
.guide-card .hierarchy { margin: 0 0 8px 0; color: #111827; font-weight: 700; font-size: 14px; }
.guide-steps { margin: 0; padding-left: 18px; color: #374151; }
.guide-steps li { line-height: 1.9; margin: 6px 0; }
.guide-steps b { color: #111827; }
.guide-substeps { margin: 6px 0 0 0; padding-left: 18px; list-style: disc; }
.guide-substeps li { line-height: 1.8; margin: 4px 0; }
.guide-note { margin-top: 10px; color: #6b7280; font-size: 13px; background: #f9fafb; border: 1px dashed #e5e7eb; padding: 8px 10px; border-radius: 8px; }
.coin-list { display: flex; align-items: center; gap: 8px; }
.coin-img { width: 20px; height: 20px; border-radius: 4px; display: inline-block; }
/* 提现弹窗样式(与钱包页统一) */
.balance-info { font-size: 12px; color: #666; margin-top: 4px; text-align: left; }
.fee-info { font-size: 12px; color: #e6a23c; margin-top: 4px; text-align: left; }
.actual-amount-info { font-size: 12px; color: #67c23a; margin-top: 4px; text-align: left; font-weight: 500; }
.address-tip { font-size: 12px; color: #f56c6c; margin-top: 4px; line-height: 1.4; text-align: left; }
/* 余额数字红色显示 */
.balance-num { color: #ff4d4f; font-weight: 600; }
.balance-unit { color: #606266; }
</style>
<style>
/* 全局弹窗宽度微调(仅当前页面生效)*/
.el-dialog__body .row { margin-bottom: 12px; }
/* 双重验证拦截弹窗 - 轻渐变美化(仅当前页面生效) */
.google-2fa-guard-dialog {
border-radius: 12px;
overflow: hidden;
}
.google-2fa-guard-dialog .el-message-box__header {
/* 与导航按钮同色系:#667eea -> #764ba2降低透明度让其更淡 */
background: linear-gradient(135deg, rgba(102, 126, 234, 0.16), rgba(118, 75, 162, 0.10));
border-bottom: 1px solid rgba(102, 126, 234, 0.10);
padding: 14px 16px 10px;
}
.google-2fa-guard-dialog .el-message-box__title {
font-weight: 700;
color: #1f2d3d;
}
.google-2fa-guard-dialog .el-message-box__content {
padding: 14px 18px 6px;
}
.google-2fa-guard-dialog .el-message-box__message {
color: #374151;
line-height: 1.7;
}
.google-2fa-guard-dialog .google-2fa-guard__title {
font-size: 15px;
font-weight: 700;
color: #111827;
margin-bottom: 6px;
}
.google-2fa-guard-dialog .google-2fa-guard__desc {
font-size: 13px;
color: #4b5563;
}
.google-2fa-guard-dialog .el-message-box__btns {
padding: 10px 16px 14px;
}
.google-2fa-guard-dialog .el-button--primary {
border: none;
/* 与导航按钮一致的紫色渐变 */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.google-2fa-guard-dialog .el-button--primary:hover,
.google-2fa-guard-dialog .el-button--primary:focus {
filter: brightness(1.02);
}
/* 弹窗表单统一对齐与留白优化 */
.el-dialog__body .row {
display: grid;
grid-template-columns: 96px 1fr;
column-gap: 12px;
align-items: center;
}
.el-dialog__body .row .el-radio-group {
display: inline-flex;
align-items: center;
gap: 24px;
padding-left: 0; /* 与输入框左边缘对齐 */
margin-left: 0; /* 去除可能的默认缩进 */
}
.el-dialog__body .label {
text-align: right;
color: #666;
font-weight: 500;
}
.el-dialog__footer {
padding-top: 4px;
}
/* 已选择币种 - 靠左对齐且换行友好 */
.selected-coin-list { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-start; }
.selected-coin-list .el-tag { margin-right: 0; }
</style>