每周更新

This commit is contained in:
2025-12-05 16:24:20 +08:00
parent 485226d9dc
commit cbefb964d4
21 changed files with 2546 additions and 700 deletions

View File

@@ -22,7 +22,7 @@
必须添加出售机器否则买家无法下单买家点击某个商品后会看到该商品下的机器明细并进行选购
</li>
</ol>
<div class="guide-note">提示建议先创建店铺 完成钱包绑定 创建商品 添加出售机器的顺序避免漏配导致无法收款或无法下单</div>
<div class="guide-note">提示建议先创建店铺 完成钱包绑定 创建商品的顺序避免漏配导致无法收款或无法下单</div>
</div>
</el-card>
@@ -61,8 +61,8 @@
<span>已绑定钱包</span>
</div>
<el-table :data="shopConfigs" border style="width: 100%">
<el-table-column prop="chain" label="链" width="140" />
<el-table-column label="支付币种" >
<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">
@@ -89,10 +89,18 @@
<!-- <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="操作" width="180" fixed="right">
<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>
@@ -108,6 +116,85 @@
</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">
@@ -139,43 +226,7 @@
</el-dialog>
<!-- 修改钱包绑定配置弹窗参数保持与列表一致 -->
<el-dialog title="修改配置" :visible.sync="visibleConfigEdit" width="560px">
<div class="row">
<label class="label">支付链</label>
<el-input v-model="configForm.chainLabel" placeholder="-" disabled />
</div>
<div class="row">
<label class="label">支付币种</label>
<el-select
class="input"
size="middle"
v-model="configForm.payCoins"
multiple
collapse-tags
filterable
placeholder="请选择币种"
>
<el-option
v-for="item in editCoinOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="row">
<label class="label">已选择币种</label>
<div class="selected-coin-list">
<el-tag
v-for="c in selectedCoinLabels"
:key="c"
type="warning"
effect="light"
closable
@close="removeSelectedCoin(c)"
>{{ c }}</el-tag>
<span v-if="!selectedCoinLabels.length" style="color:#c0c4cc">未选择</span>
</div>
</div>
<div class="row">
<label class="label">钱包地址</label>
<el-input v-model="configForm.payAddress" placeholder="请输入钱包地址" />
@@ -193,7 +244,7 @@
import { getMyShop, updateShop, deleteShop, queryShop, closeShop ,updateShopConfig,deleteShopConfig,getChainAndCoin} from '@/api/shops'
import { coinList } from '@/utils/coinList'
import { getShopConfig } from '@/api/wallet'
import { getShopConfig,getShopConfigV2 ,withdrawBalanceForSeller,updateShopConfigV2} from '@/api/wallet'
export default {
@@ -227,7 +278,19 @@ export default {
{ label: 'BSC (BEP20)', value: 'bsc' },
{ label: 'Nexa', value: 'nexa' },
],
shopLoading: false
shopLoading: false,
/* 提现弹窗状态(仅本页使用) */
withdrawDialogVisible: false,
withdrawLoading: false,
currentWithdrawRow: {},
withdrawForm: {
amount: '',
toAddress: '',
fee: '0.00',
googleCode: ''
},
withdrawAddressEditable: false,
withdrawRules: {}
}
},
computed: {
@@ -261,12 +324,199 @@ export default {
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: {
/** 余额展示:带币种单位 */
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) {
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' }
],
// 地址为只读已填,不再要求用户输入
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 || {}
const payload = {
toChain: row.chain,
toSymbol: row.payCoin,
amount: Number(this.withdrawForm.amount),
toAddress: this.withdrawForm.toAddress,
fromAddress: row.payAddress || this.withdrawForm.toAddress || '',
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 }
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()
},
/**
* 手续费率显示最多6位小数去除多余的0空值显示为 '-'
*/
@@ -362,7 +612,7 @@ export default {
}
try {
const res = await getShopConfig({id:shopId})
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
@@ -430,15 +680,7 @@ export default {
},
submitConfigEdit() {
// 基础校验
if (!this.configForm.chainLabel && !this.configForm.chainValue) {
this.$message.warning('请选择支付链')
return
}
if (!this.configForm.payCoins || this.configForm.payCoins.length === 0) {
this.$message.warning('请选择支付币种')
return
}
// 仅校验钱包地址
const addr = (this.configForm.payAddress || '').trim()
if (!addr) {
this.$message.warning('请输入钱包地址')
@@ -446,11 +688,17 @@ export default {
}
const payload = {
id: this.configForm.id,
chain: this.configForm.chainValue || this.configForm.chainLabel,
payCoin: (this.configForm.payCoins || []).join(','),
chain: this.configForm.chainValue || this.configForm.chainLabel || '',
payAddress: this.configForm.payAddress
}
this.updateShopConfig(payload)
;(async () => {
const res = await updateShopConfigV2(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('保存成功')
this.visibleConfigEdit = false
this.fetchShopConfigs(this.shop.id)
}
})()
},
removeSelectedCoin(labelUpper) {
const label = String(labelUpper || '').toLowerCase()
@@ -690,6 +938,14 @@ export default {
.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>