每周更新

This commit is contained in:
2025-12-31 13:59:05 +08:00
parent a325efb57f
commit a83485b4bc
14 changed files with 897 additions and 50 deletions

View File

@@ -71,6 +71,39 @@ export function updatePassword(data) {
}
//注销账户
export function closeAccount(data) {
return request({
url: `/lease/auth/closeAccount`,
method: 'post',
data
})
}
//注销邮箱验证码
export function sendCloseAccount(data) {
return request({
url: `/lease/auth/sendCloseAccount`,
method: 'post',
data
})
}
//个人中心修改密码
export function updatePasswordInCenter(data) {
return request({
url: `/lease/auth/updatePasswordInCenter`,
method: 'post',
data
})
}

View File

@@ -9,7 +9,7 @@
:key="nav.path"
:to="nav.path"
class="nav-btn"
active-class="active"
:class="{ active: isNavActive(nav.path) }"
:title="nav.description"
>
<span class="nav-icon">{{ nav.icon }}</span>
@@ -30,9 +30,16 @@
</button>
</div>
<!-- 已登录显示用户邮箱和退出按钮 -->
<!-- 已登录显示用户邮箱安全设置和退出按钮 -->
<div v-else class="user-info">
<span class="user-email">{{ userEmail }}</span>
<router-link
to="/account/security-settings"
class="security-link"
active-class="active"
>
安全设置
</router-link>
<el-button
type="text"
size="small"
@@ -113,6 +120,19 @@ export default {
window.removeEventListener('login-status-changed', this.handleLoginStatusChanged)
},
methods: {
/**
* 判断导航按钮是否应该高亮
* 在安全设置页面时,个人中心不应该高亮
*/
isNavActive(path) {
const currentPath = (this.$route && this.$route.path) || ''
// 如果是安全设置页面,个人中心不高亮
if (currentPath === '/account/security-settings' && path === '/account') {
return false
}
// 其他情况使用 Vue Router 的默认匹配逻辑(包含匹配)
return currentPath === path || (path !== '/' && currentPath.startsWith(path + '/'))
},
loadCart() {
this.cart = readCart()
},
@@ -438,6 +458,36 @@ export default {
color: #2c3e50;
font-size: 14px;
font-weight: 600;
margin-right: 12px;
}
/* 安全设置链接样式 - 与导航按钮一致 */
.security-link {
display: inline-flex;
align-items: center;
color: #2c3e50;
font-size: 16px;
font-weight: 600;
text-decoration: none;
padding: 12px 20px;
margin-right: 8px;
border-radius: 8px;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
}
.security-link:hover {
background: #f5f7ff;
color: #667eea;
transform: translateY(-2px);
}
.security-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
transform: translateY(-2px);
}
/* 退出按钮样式 */

View File

@@ -147,10 +147,34 @@
</div>
<div v-show="isExpanded('consume', row, idx)" class="expand-panel">
<div class="expand-grid">
<div class="expand-item"><span class="label">订单号</span><span class="value mono">{{ row.orderId || '' }}</span></div>
<div class="expand-item"><span class="label">支付地址</span><span class="value mono-ellipsis" :title="row.fromAddress">{{ row.fromAddress || '' }}</span></div>
<div class="expand-item"><span class="label">收款地址</span><span class="value mono-ellipsis" :title="row.toAddress">{{ row.toAddress || '' }}</span></div>
<div class="expand-item" v-if="row.txHash"><span class="label">交易哈希</span><span class="value mono-ellipsis" :title="row.txHash">{{ row.txHash }}</span></div>
<div class="expand-item">
<span class="label">订单号</span>
<div class="value value-row">
<span class="mono-ellipsis" :title="row.orderId">{{ row.orderId || '' }}</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click.stop="handleCopy(row.orderId, '订单号')">复制</el-button>
</div>
</div>
<div class="expand-item">
<span class="label">支付地址</span>
<div class="value value-row">
<span class="mono-ellipsis" :title="row.fromAddress">{{ row.fromAddress || '' }}</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click.stop="handleCopy(row.fromAddress, '支付地址')">复制</el-button>
</div>
</div>
<div class="expand-item">
<span class="label">收款地址</span>
<div class="value value-row">
<span class="mono-ellipsis" :title="row.toAddress">{{ row.toAddress || '' }}</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click.stop="handleCopy(row.toAddress, '收款地址')">复制</el-button>
</div>
</div>
<div class="expand-item" v-if="row.txHash">
<span class="label">交易哈希</span>
<div class="value value-row">
<span class="mono-ellipsis" :title="row.txHash">{{ row.txHash }}</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click.stop="handleCopy(row.txHash, '交易哈希')">复制</el-button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -16,20 +16,20 @@
<div class="user-role" role="group" aria-label="导航分组切换">
<button
class="role-button"
:class="{ active: activeRole === 'buyer' }"
:class="{ active: activeRole === 'buyer' && !isSecuritySettingsPage }"
@click="handleClickRole('buyer')"
@keydown.enter.prevent="handleClickRole('buyer')"
@keydown.space.prevent="handleClickRole('buyer')"
:aria-pressed="activeRole === 'buyer'"
:aria-pressed="activeRole === 'buyer' && !isSecuritySettingsPage"
tabindex="0"
>买家相关</button>
<button
class="role-button"
:class="{ active: activeRole === 'seller' }"
:class="{ active: activeRole === 'seller' && !isSecuritySettingsPage }"
@click="handleClickRole('seller')"
@keydown.enter.prevent="handleClickRole('seller')"
@keydown.space.prevent="handleClickRole('seller')"
:aria-pressed="activeRole === 'seller'"
:aria-pressed="activeRole === 'seller' && !isSecuritySettingsPage"
tabindex="0"
>卖家相关</button>
</div>
@@ -68,7 +68,6 @@ export default {
// { label: '充值记录', to: '/account/rechargeRecord' },
// { label: '提现记录', to: '/account/withdrawalHistory' },
{ label: '资金流水', to: '/account/funds-flow' },
{ label: '安全设置', to: '/account/security-settings' },
],
// 卖家侧导航
sellerLinks: [
@@ -77,7 +76,6 @@ export default {
{ label: '商品列表', to: '/account/products' },
{ label: '已售出订单', to: '/account/seller-orders' },
{ label: '资金流水', to: '/account/seller-funds-flow' },
{ label: '安全设置', to: '/account/security-settings' },
],
}
},
@@ -97,6 +95,14 @@ export default {
displayedLinks() {
return this.activeRole === 'buyer' ? this.buyerLinks : this.sellerLinks
},
/**
* 判断当前是否在安全设置页面
* @returns {boolean}
*/
isSecuritySettingsPage() {
const path = (this.$route && this.$route.path) || ''
return path === '/account/security-settings'
},
},
mounted() {
const getVal = (key) => {
@@ -179,7 +185,10 @@ export default {
'/account/shop-config'
]
// 安全设置页面买家和卖家都可见,不参与分组判断
// 在安全设置页面时,清除分组高亮(不设置 activeRole
if (path === '/account/security-settings') {
// 清除分组高亮,让所有分组按钮都不高亮
this.activeRole = null
return
}
const shouldBuyer = buyerPrefixes.some(p => path.indexOf(p) === 0)

View File

@@ -229,12 +229,25 @@
</span>
</el-dialog>
<!-- 修改钱包绑定配置弹窗参数保持与列表一致 -->
<el-dialog title="修改配置" :visible.sync="visibleConfigEdit" width="560px">
<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>
@@ -272,7 +285,7 @@ export default {
// 店铺配置列表
shopConfigs: [],
visibleConfigEdit: false,
configForm: { id: '', chainLabel: '', chainValue: '', payAddress: '', payCoins: [], payCoin: '' },
configForm: { id: '', chainLabel: '', chainValue: '', payAddress: '', payCoins: [], payCoin: '', googleCode: '' },
productOptions: [],
coinOptions: coinList || [],
editCoinOptionsApi: [],
@@ -554,8 +567,14 @@ export default {
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 > 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()
},
@@ -701,7 +720,8 @@ export default {
chainValue: d.value || '',
payAddress: d.address || '',
payCoins: preSelected,
payCoin: preSelected.join(',')
payCoin: preSelected.join(','),
googleCode: ''
}
} else {
// 回退:使用行内已有数据
@@ -715,7 +735,8 @@ export default {
chainValue: row.chain || '',
payAddress: row.payAddress || '',
payCoins,
payCoin: payCoins.join(',')
payCoin: payCoins.join(','),
googleCode: ''
}
}
this.visibleConfigEdit = true
@@ -727,19 +748,42 @@ export default {
this.deleteShopConfig({id:row.id})
},
/**
* 处理谷歌验证码输入(仅允许数字)
*/
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 加密钱包地址(与钱包绑定页面保持一致:同步优先,异步兜底)
* 使用 RSA 加密钱包地址(与"钱包绑定"页面保持一致:同步优先,异步兜底)
* @type {string}
*/
let encryptedPayAddress = addr
@@ -761,7 +805,8 @@ export default {
const payload = {
id: this.configForm.id,
chain: this.configForm.chainValue || this.configForm.chainLabel || '',
payAddress: encryptedPayAddress
payAddress: encryptedPayAddress,
gcode: googleCode
}
try {
const res = await updateShopConfigV2(payload)

View File

@@ -301,18 +301,38 @@
v-for="(row, idx) in editDialog.form.coinAndAlgoList"
:key="'edit-ca-' + idx"
>
<el-input
<el-select
v-model="row.coin"
placeholder="币种"
placeholder="请选择币种"
class="coin-input"
@input="editHandleCoinInput(idx)"
/>
<el-input
@change="editHandleCoinChange(idx, $event)"
:loading="loadingCoins"
filterable
clearable
>
<el-option
v-for="coin in coinOptions"
:key="coin"
:label="coin"
:value="coin"
/>
</el-select>
<el-select
v-model="row.algorithm"
placeholder="算法"
placeholder="请选择算法"
class="algo-input"
@input="editHandleAlgorithmInput(idx)"
/>
:loading="loadingAlgos[idx]"
:disabled="!row.coin"
filterable
clearable
>
<el-option
v-for="algo in (algoOptionsMap[row.coin] || [])"
:key="algo"
:label="algo"
:value="algo"
/>
</el-select>
<el-input
v-model="row.theoryPower"
placeholder="理论算力"
@@ -417,7 +437,7 @@
* - 支持查看详情、修改、删除
*/
import { updateProduct,deleteMachine, deleteProduct, getMachineInfo,getShopMachineListForSeller,getPayTypes, updateGpuMachine,updateAsicMachine} from '../../api/products'
import { getSupportCoin,getSupportAlgo } from '../../api/machine'
// 本页用户偏好存储键记住矿机种类ASIC/GPU
const MACHINE_TYPE_KEY = 'account_products_machine_type'
@@ -438,6 +458,12 @@ export default {
total: 0,
},
coinOptions: [],
/** 算法选项映射 { coin: [algo1, algo2, ...] } */
algoOptionsMap: {},
/** 加载币种状态 */
loadingCoins: false,
/** 加载算法状态映射 { index: boolean } */
loadingAlgos: {},
editDialog: {
visible: false,
saving: false,
@@ -537,19 +563,96 @@ export default {
}
},
methods: {
/** 编辑弹窗:币种输入过滤(仅字母数字,转大写) */
editHandleCoinInput(index) {
const r = this.editDialog.form.coinAndAlgoList[index]
let v = String(r.coin || '')
v = v.replace(/[\u4e00-\u9fa5]/g, '').replace(/[^A-Za-z0-9]/g, '')
this.$set(this.editDialog.form.coinAndAlgoList[index], 'coin', v.toUpperCase())
/**
* 加载支持的币种列表
*/
async loadSupportCoins() {
this.loadingCoins = true
try {
const res = await getSupportCoin()
if (res && (res.code === 0 || res.code === 200)) {
const data = res.data || []
// 处理返回的数据,可能是数组或对象
if (Array.isArray(data)) {
this.coinOptions = data.map(item => {
// 如果是对象,取 coin 字段;如果是字符串,直接使用
return typeof item === 'string' ? item : (item.coin || item.name || item)
}).filter(Boolean)
} else if (data && typeof data === 'object') {
// 如果是对象,尝试提取币种列表
this.coinOptions = Object.keys(data).map(key => {
const item = data[key]
return typeof item === 'string' ? item : (item.coin || item.name || key)
}).filter(Boolean)
}
// 去重并排序
this.coinOptions = [...new Set(this.coinOptions)].sort()
}
} catch (e) {
console.error('加载币种列表失败', e)
} finally {
this.loadingCoins = false
}
},
/** 编辑弹窗:算法输入过滤(仅字母数字和-,转大写) */
editHandleAlgorithmInput(index) {
const r = this.editDialog.form.coinAndAlgoList[index]
let v = String(r.algorithm || '')
v = v.replace(/[\u4e00-\u9fa5]/g, '').replace(/[^A-Za-z0-9-]/g, '')
this.$set(this.editDialog.form.coinAndAlgoList[index], 'algorithm', v.toUpperCase())
/**
* 币种选择变化处理(编辑弹窗)
* @param {number} index - 行索引
* @param {string} coin - 选择的币种
*/
async editHandleCoinChange(index, coin) {
// 清空当前行的算法选择
this.$set(this.editDialog.form.coinAndAlgoList[index], 'algorithm', '')
// 如果选择了币种,加载对应的算法列表
if (coin) {
await this.editLoadAlgorithmsForCoin(coin, index)
}
},
/**
* 加载指定币种支持的算法列表(编辑弹窗)
* @param {string} coin - 币种名称
* @param {number} index - 行索引(用于显示加载状态)
*/
async editLoadAlgorithmsForCoin(coin, index) {
if (!coin) return
// 如果已经加载过该币种的算法,直接返回
if (this.algoOptionsMap[coin] && this.algoOptionsMap[coin].length > 0) {
return
}
// 设置加载状态
this.$set(this.loadingAlgos, index, true)
try {
const res = await getSupportAlgo(coin)
if (res && (res.code === 0 || res.code === 200)) {
const data = res.data || []
let algorithms = []
// 处理返回的数据,可能是数组或对象
if (Array.isArray(data)) {
algorithms = data.map(item => {
// 如果是对象,取 algorithm 或 algo 字段;如果是字符串,直接使用
return typeof item === 'string' ? item : (item.algorithm || item.algo || item.name || item)
}).filter(Boolean)
} else if (data && typeof data === 'object') {
// 如果是对象,尝试提取算法列表
algorithms = Object.keys(data).map(key => {
const item = data[key]
return typeof item === 'string' ? item : (item.algorithm || item.algo || item.name || key)
}).filter(Boolean)
}
// 去重并排序,保存到映射中
this.$set(this.algoOptionsMap, coin, [...new Set(algorithms)].sort())
}
} catch (e) {
console.error(`加载币种 ${coin} 的算法列表失败`, e)
// 设置空数组,避免重复请求
this.$set(this.algoOptionsMap, coin, [])
} finally {
this.$set(this.loadingAlgos, index, false)
}
},
/** 编辑弹窗理论算力限制6整数+4小数 */
editHandleRowTheoryInput(index) {
@@ -580,8 +683,11 @@ export default {
return
}
const last = list[list.length - 1] || { unit: 'TH/S' }
const newIndex = list.length
list.push({ coin: '', algorithm: '', theoryPower: '', unit: last.unit || 'TH/S', coinAndPowerId: null })
this.$set(this.editDialog.form, 'coinAndAlgoList', list)
// 初始化新行的加载状态
this.$set(this.loadingAlgos, newIndex, false)
},
/** 编辑弹窗删除一行至少保留1行 */
editHandleRemoveRow(index) {
@@ -1243,6 +1349,16 @@ export default {
})
this.editDialog.form = form
this.editDialog.visible = true
// 打开弹窗时加载币种列表
this.loadSupportCoins()
// 如果已有币种数据,预加载对应的算法列表
if (form.coinAndAlgoList && form.coinAndAlgoList.length > 0) {
form.coinAndAlgoList.forEach((row, idx) => {
if (row.coin) {
this.editLoadAlgorithmsForCoin(row.coin, idx)
}
})
}
},
/** 保存编辑 */

View File

@@ -16,10 +16,10 @@
{{ getStatusText }}
</span>
<el-button
:type="getButtonType"
type="text"
@click="handleButtonClick"
:loading="loading"
class="security-btn"
class="security-btn two-factor-btn"
>
{{ getButtonText }}
</el-button>
@@ -28,6 +28,56 @@
<div class="security-divider"></div>
</div>
<!-- 修改密码 -->
<div class="security-item-wrapper">
<div class="security-item">
<div class="security-left">
<div class="security-icon">
<i class="el-icon-edit"></i>
</div>
<div class="security-info">
<div class="security-title">修改密码</div>
<p class="security-desc">定期修改密码可以提高账户安全性建议使用强密码并定期更换</p>
</div>
</div>
<div class="security-right">
<el-button
type="text"
@click="handleChangePassword"
class="security-btn change-password-btn"
>
修改
</el-button>
</div>
</div>
<div class="security-divider"></div>
</div>
<!-- 注销账号 -->
<div class="security-item-wrapper">
<div class="security-item">
<div class="security-left">
<div class="security-icon">
<i class="el-icon-warning"></i>
</div>
<div class="security-info">
<div class="security-title">注销账号</div>
<p class="security-desc">注销账号将永久删除您的账户和所有相关数据此操作不可恢复请谨慎操作</p>
</div>
</div>
<div class="security-right">
<el-button
type="text"
@click="handleDeleteAccount"
class="security-btn delete-account-btn"
>
注销
</el-button>
</div>
</div>
<div class="security-divider"></div>
</div>
<!-- 第一步显示二维码和密钥 -->
<el-dialog
title="开启双重验证 - 步骤 1/2"
@@ -263,13 +313,160 @@
</el-button>
</span>
</el-dialog>
<!-- 修改密码弹窗 -->
<el-dialog
title="修改密码"
:visible.sync="changePasswordDialogVisible"
width="500px"
:close-on-click-modal="false"
@close="handleChangePasswordDialogClose"
>
<el-form
ref="changePasswordForm"
:model="changePasswordForm"
:rules="changePasswordRules"
label-position="top"
>
<el-form-item label="用户邮箱">
<el-input
:value="userEmail"
readonly
disabled
class="email-display"
/>
</el-form-item>
<el-form-item label="邮箱验证码" prop="emailCode">
<div class="code-input-group">
<el-input
v-model="changePasswordForm.emailCode"
placeholder="请输入邮箱验证码"
class="code-input"
maxlength="10"
clearable
/>
<el-button
type="primary"
@click="handleSendChangePasswordCode"
:loading="sendingChangePasswordCode"
:disabled="changePasswordCountdown > 0"
>
{{ changePasswordCountdown > 0 ? `${changePasswordCountdown}秒后重试` : '获取验证码' }}
</el-button>
</div>
</el-form-item>
<el-form-item label="新密码" prop="password">
<el-input
v-model="changePasswordForm.password"
type="password"
placeholder="请输入新密码8-32位"
show-password
clearable
/>
<!-- 密码规则提示 -->
<div class="password-tip">
<i class="el-icon-info"></i>
<span>密码需包含大小写字母数字和特殊字符长度8-32</span>
</div>
</el-form-item>
<el-form-item label="确认新密码" prop="confirmPassword">
<el-input
v-model="changePasswordForm.confirmPassword"
type="password"
placeholder="请再次输入新密码"
show-password
clearable
/>
</el-form-item>
<el-form-item label="谷歌验证码" prop="googleCode">
<el-input
v-model="changePasswordForm.googleCode"
placeholder="请输入6位动态口令"
maxlength="6"
@input="handleChangePasswordGoogleCodeInput"
clearable
/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="changePasswordDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmChangePassword" :loading="changingPassword">
确认修改
</el-button>
</span>
</el-dialog>
<!-- 注销账号弹窗 -->
<el-dialog
title="注销账号"
:visible.sync="deleteAccountDialogVisible"
width="500px"
:close-on-click-modal="false"
@close="handleDeleteAccountDialogClose"
>
<el-form
ref="deleteAccountForm"
:model="deleteAccountForm"
:rules="deleteAccountRules"
label-position="top"
>
<el-form-item label="用户邮箱">
<el-input
:value="userEmail"
readonly
disabled
class="email-display"
/>
</el-form-item>
<el-form-item label="邮箱验证码" prop="emailCode">
<div class="code-input-group">
<el-input
v-model="deleteAccountForm.emailCode"
placeholder="请输入邮箱验证码"
class="code-input"
maxlength="10"
clearable
/>
<el-button
type="primary"
@click="handleSendDeleteAccountCode"
:loading="sendingDeleteAccountCode"
:disabled="deleteAccountCountdown > 0"
>
{{ deleteAccountCountdown > 0 ? `${deleteAccountCountdown}秒后重试` : '获取验证码' }}
</el-button>
</div>
</el-form-item>
<el-form-item label="谷歌验证码" prop="googleCode">
<el-input
v-model="deleteAccountForm.googleCode"
placeholder="请输入6位动态口令"
maxlength="6"
@input="handleDeleteAccountGoogleCodeInput"
clearable
/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="deleteAccountDialogVisible = false">取消</el-button>
<el-button type="danger" @click="handleConfirmDeleteAccount" :loading="deletingAccount">
确定注销
</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { getBindInfo, bindGoogle, sendOpenGoogleCode,getGoogleStatus,closeStepTwo,sendCloseGoogleCode,openStepTwo } from '../../api/verification'
import { rsaEncrypt } from '../../utils/rsaEncrypt'
import { closeAccount, sendCloseAccount,sendUpdatePwdCode,updatePasswordInCenter} from '../../api/user'
export default {
name: 'SecuritySettings',
data() {
@@ -294,6 +491,19 @@ export default {
callback()
}
/**
* 确认密码验证
*/
const validateConfirmPassword = (rule, value, callback) => {
if (!value) {
callback(new Error('请再次输入新密码'))
return
}
// 注意:这里需要在 data() 返回后,通过 this.changePasswordForm.password 访问
// 但由于这是在 data() 函数内部,无法访问 this所以需要在 methods 中定义
callback()
}
return {
isEnabled: false, // 是否已开启双重验证
loading: false,
@@ -302,6 +512,9 @@ export default {
step2Visible: false,
closeDialogVisible: false, // 关闭双重验证弹窗
openDialogVisible: false, // 开启双重验证弹窗
deleteAccountDialogVisible: false, // 注销账号弹窗
changePasswordDialogVisible: false, // 修改密码弹窗
userEmail: '', // 用户邮箱
qrCodeUrl: '',
secretKey: '',
sendingCode: false,
@@ -313,6 +526,14 @@ export default {
sendingOpenCode: false, // 发送开启验证码的 loading
openCountdown: 0, // 开启验证码倒计时
openCountdownTimer: null, // 开启验证码倒计时定时器
sendingDeleteAccountCode: false, // 发送注销账号验证码的 loading
deleteAccountCountdown: 0, // 注销账号验证码倒计时
deleteAccountCountdownTimer: null, // 注销账号验证码倒计时定时器
deletingAccount: false, // 注销账号的 loading
sendingChangePasswordCode: false, // 发送修改密码验证码的 loading
changePasswordCountdown: 0, // 修改密码验证码倒计时
changePasswordCountdownTimer: null, // 修改密码验证码倒计时定时器
changingPassword: false, // 修改密码的 loading
closing: false, // 关闭双重验证的 loading
opening: false, // 开启双重验证的 loading
submitting: false,
@@ -329,6 +550,16 @@ export default {
emailCode: '',
googleCode: ''
},
deleteAccountForm: {
emailCode: '',
googleCode: ''
},
changePasswordForm: {
emailCode: '',
password: '',
confirmPassword: '',
googleCode: ''
},
verifyRules: {
password: [
{ required: true, validator: validatePassword, trigger: 'blur' }
@@ -362,6 +593,17 @@ export default {
{ pattern: /^\d{6}$/, message: '请输入6位数字', trigger: 'blur' }
]
},
deleteAccountRules: {
emailCode: [
{ required: true, message: '请输入邮箱验证码', trigger: 'blur' },
{ min: 1, max: 10, message: '验证码长度为1-10位', trigger: 'blur' }
],
googleCode: [
{ required: true, message: '请输入谷歌验证码', trigger: 'blur' },
{ pattern: /^\d{6}$/, message: '请输入6位数字', trigger: 'blur' }
]
},
changePasswordRules: {},
googleStatus: 1 // 谷歌验证状态0 开启1 未绑定2 关闭
}
},
@@ -439,6 +681,7 @@ export default {
},
mounted() {
this.check2FAStatus()
this.loadUserEmail()
},
beforeDestroy() {
if (this.countdownTimer) {
@@ -450,6 +693,9 @@ export default {
if (this.openCountdownTimer) {
clearInterval(this.openCountdownTimer)
}
if (this.deleteAccountCountdownTimer) {
clearInterval(this.deleteAccountCountdownTimer)
}
},
methods: {
/**
@@ -878,6 +1124,260 @@ export default {
*/
handleCannotGetGoogleCode() {
this.$message.info('请确保已正确扫描二维码或输入密钥,并检查时间同步')
},
/**
* 修改密码 - 显示弹窗
*/
handleChangePassword() {
if (!this.userEmail) {
this.$message.warning('无法获取用户邮箱,请重新登录')
return
}
this.changePasswordDialogVisible = true
},
/**
* 发送修改密码的邮箱验证码
*/
async handleSendChangePasswordCode() {
if (this.changePasswordCountdown > 0) return
if (!this.userEmail) {
this.$message.warning('无法获取用户邮箱,请重新登录')
return
}
this.sendingChangePasswordCode = true
try {
const res = await sendUpdatePwdCode({ email: this.userEmail })
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('验证码已发送到您的邮箱')
this.startChangePasswordCountdown()
} else {
this.$message.error(res?.message || res?.msg || '发送验证码失败')
}
} catch (e) {
console.error('发送验证码失败', e)
this.$message.error('发送验证码失败,请稍后重试')
} finally {
this.sendingChangePasswordCode = false
}
},
/**
* 开始修改密码验证码倒计时
*/
startChangePasswordCountdown() {
this.changePasswordCountdown = 60
this.changePasswordCountdownTimer = setInterval(() => {
this.changePasswordCountdown--
if (this.changePasswordCountdown <= 0) {
clearInterval(this.changePasswordCountdownTimer)
this.changePasswordCountdownTimer = null
}
}, 1000)
},
/**
* 修改密码弹窗的谷歌验证码输入(仅数字)
*/
handleChangePasswordGoogleCodeInput(val) {
this.changePasswordForm.googleCode = val.replace(/\D/g, '').slice(0, 6)
},
/**
* 确认密码验证(用于表单验证规则)
*/
validateConfirmPassword(rule, value, callback) {
if (!value) {
callback(new Error('请再次输入新密码'))
return
}
if (value !== this.changePasswordForm.password) {
callback(new Error('两次输入的密码不一致'))
return
}
callback()
},
/**
* 确认修改密码
*/
async handleConfirmChangePassword() {
try {
const valid = await this.$refs.changePasswordForm.validate()
if (!valid) return
this.changingPassword = true
// 对密码进行 RSA 加密
const encryptedPassword = await rsaEncrypt(this.changePasswordForm.password)
if (!encryptedPassword) {
this.$message.error('密码加密失败,请稍后重试')
this.changingPassword = false
return
}
const params = {
code: this.changePasswordForm.emailCode, // 邮箱验证码
email: this.userEmail, // 邮箱
password: encryptedPassword, // RSA 加密后的新密码
gcode: this.changePasswordForm.googleCode // 谷歌验证码
}
const res = await updatePasswordInCenter(params)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('密码修改成功')
this.changePasswordDialogVisible = false
this.handleChangePasswordDialogClose()
} else {
this.$message.error(res?.message || res?.msg || '修改密码失败,请检查输入信息')
}
} catch (e) {
console.error('修改密码失败', e)
this.$message.error('修改密码失败,请稍后重试')
} finally {
this.changingPassword = false
}
},
/**
* 修改密码弹窗关闭时的处理
*/
handleChangePasswordDialogClose() {
this.changePasswordForm = {
emailCode: '',
password: '',
confirmPassword: '',
googleCode: ''
}
this.$refs.changePasswordForm && this.$refs.changePasswordForm.clearValidate()
},
/**
* 加载用户邮箱
*/
loadUserEmail() {
try {
const getVal = (key) => {
const raw = localStorage.getItem(key)
if (raw == null) return null
try { return JSON.parse(raw) } catch (e) { return raw }
}
const val = getVal('leasEmail') || ''
this.userEmail = typeof val === 'string' ? val : String(val)
} catch (e) {
console.error('读取用户邮箱失败', e)
this.userEmail = ''
}
},
/**
* 注销账号 - 显示弹窗
*/
handleDeleteAccount() {
if (!this.userEmail) {
this.$message.warning('无法获取用户邮箱,请重新登录')
return
}
this.deleteAccountDialogVisible = true
},
/**
* 发送注销账号的邮箱验证码
*/
async handleSendDeleteAccountCode() {
if (this.deleteAccountCountdown > 0) return
if (!this.userEmail) {
this.$message.warning('无法获取用户邮箱,请重新登录')
return
}
this.sendingDeleteAccountCode = true
try {
const res = await sendCloseAccount({ email: this.userEmail })
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('验证码已发送到您的邮箱')
this.startDeleteAccountCountdown()
} else {
this.$message.error(res?.message || res?.msg || '发送验证码失败')
}
} catch (e) {
console.error('发送验证码失败', e)
this.$message.error('发送验证码失败,请稍后重试')
} finally {
this.sendingDeleteAccountCode = false
}
},
/**
* 开始注销账号验证码倒计时
*/
startDeleteAccountCountdown() {
this.deleteAccountCountdown = 60
this.deleteAccountCountdownTimer = setInterval(() => {
this.deleteAccountCountdown--
if (this.deleteAccountCountdown <= 0) {
clearInterval(this.deleteAccountCountdownTimer)
this.deleteAccountCountdownTimer = null
}
}, 1000)
},
/**
* 注销账号弹窗的谷歌验证码输入(仅数字)
*/
handleDeleteAccountGoogleCodeInput(val) {
this.deleteAccountForm.googleCode = val.replace(/\D/g, '').slice(0, 6)
},
/**
* 确认注销账号
*/
async handleConfirmDeleteAccount() {
try {
const valid = await this.$refs.deleteAccountForm.validate()
if (!valid) return
// 二次确认,提示用户不能恢复账号及相关信息
this.$confirm(
'注销账号将永久删除您的账户和所有相关数据,包括订单、余额、店铺等所有信息,此操作不可恢复。确定要继续吗?',
'警告',
{
confirmButtonText: '确定注销',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: false
}
).then(async () => {
this.deletingAccount = true
try {
const params = {
eCode: this.deleteAccountForm.emailCode, // 邮箱验证码
gCode: this.deleteAccountForm.googleCode // 谷歌验证码
}
const res = await closeAccount(params)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('账号已成功注销')
this.deleteAccountDialogVisible = false
this.handleDeleteAccountDialogClose()
// 清除登录信息并跳转到登录页
localStorage.removeItem('leasToken')
localStorage.removeItem('leasEmail')
localStorage.removeItem('userInfo')
window.dispatchEvent(new CustomEvent('login-status-changed'))
this.$router.push('/login')
}
} catch (e) {
console.error('注销账号失败', e)
this.$message.error('注销失败,请稍后重试')
} finally {
this.deletingAccount = false
}
}).catch(() => {
// 用户取消
})
} catch (e) {
console.error('表单验证失败', e)
}
},
/**
* 注销账号弹窗关闭时的处理
*/
handleDeleteAccountDialogClose() {
this.deleteAccountForm = {
emailCode: '',
googleCode: ''
}
this.$refs.deleteAccountForm && this.$refs.deleteAccountForm.clearValidate()
}
}
}
@@ -986,6 +1486,48 @@ export default {
box-shadow: 0 4px 12px rgba(245, 108, 108, 0.3);
}
/* 双重验证按钮样式 - 淡紫色背景,紫色字体 */
.two-factor-btn {
background: #f5f7ff !important;
color: #667eea !important;
border: 1px solid #f5f7ff !important;
}
.two-factor-btn:hover {
background: #f5f7ff !important;
color: #667eea !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
/* 修改密码按钮样式 - 淡紫色背景,紫色字体 */
.change-password-btn {
background: #f5f7ff !important;
color: #667eea !important;
border: 1px solid #f5f7ff !important;
}
.change-password-btn:hover {
background: #f5f7ff !important;
color: #667eea !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
/* 注销账号按钮样式 - 淡紫色背景,紫色字体 */
.delete-account-btn {
background: #f5f7ff !important;
color: #667eea !important;
border: 1px solid #f5f7ff !important;
}
.delete-account-btn:hover {
background: #f5f7ff !important;
color: #667eea !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.security-divider {
height: 1px;
background: #e4e7ed;
@@ -1161,5 +1703,9 @@ export default {
justify-content: flex-end;
gap: 12px;
}
.email-display {
background-color: #f5f7fa;
}
</style>

View File

@@ -1011,16 +1011,29 @@ export default {
callback(new Error('请输入有效的金额'))
return
}
// 手续费与总需求按相同精度计算
const feeInt = this.toScaledInt(this.withdrawForm.fee)
const totalRequired = amountInt + feeInt
// 钱包余额转分(使用当前选中钱包的可用余额)
// 钱包余额(可用余额 + 冻结余额)
const availableBalance = this.WalletData && (this.WalletData.walletBalance || this.WalletData.balance) || 0
const balanceInt = this.toScaledInt(availableBalance)
if (totalRequired > balanceInt) {
const blockedBalance = this.WalletData && (this.WalletData.blockedBalance || 0) || 0
const totalBalance = parseFloat(availableBalance) + parseFloat(blockedBalance)
const totalBalanceInt = this.toScaledInt(totalBalance)
// 提现金额可以等于可用余额,但提现金额+手续费不能超过钱包总余额
if (totalRequired > totalBalanceInt) {
const totalText = this.formatDec6FromInt(totalRequired)
callback(new Error(`提现金额加上手续费(${totalText} USDT)不能超过钱包余额`))
callback(new Error(`提现金额加上手续费(${totalText} ${this.displayWithdrawSymbol})不能超过钱包余额`))
return
}
// 可用余额(用于判断提现金额是否超过可用余额)
const availableBalanceInt = this.toScaledInt(availableBalance)
// 允许提现金额等于可用余额,但不能大于
if (amountInt > availableBalanceInt) {
callback(new Error('提现金额不能大于可用余额'))
return
}
@@ -1037,6 +1050,13 @@ export default {
return
}
// 实际到账金额(提现金额 - 手续费必须大于0
const actualInt = amountInt - feeInt
if (actualInt <= 0) {
callback(new Error('提现金额扣除手续费后必须大于0'))
return
}
callback()
},

View File

@@ -278,7 +278,7 @@
}
} catch (error) {
console.error('发送验证码失败:', error)
this.$message.error(error.message || '发送验证码失败,请重试')
} finally {
this.sendingCode = false
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>power_leasing</title><script defer="defer" src="/js/chunk-vendors.4369320b.js"></script><script defer="defer" src="/js/app.cc5f454d.js"></script><link href="/css/chunk-vendors.10dd4e95.css" rel="stylesheet"><link href="/css/app.395f1e08.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but power_leasing doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>power_leasing</title><script defer="defer" src="/js/chunk-vendors.4369320b.js"></script><script defer="defer" src="/js/app.7bd6edb2.js"></script><link href="/css/chunk-vendors.10dd4e95.css" rel="stylesheet"><link href="/css/app.c0e6f336.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but power_leasing doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long