每周更新
This commit is contained in:
@@ -7,8 +7,8 @@ NODE_ENV = production
|
|||||||
ENV = 'staging'
|
ENV = 'staging'
|
||||||
|
|
||||||
# 测试环境
|
# 测试环境
|
||||||
# VUE_APP_BASE_API = 'http://10.168.2.220:8888'
|
VUE_APP_BASE_API = 'http://10.168.2.220:8888'
|
||||||
VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
|
# VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
|
||||||
VUE_APP_BASE_URL = 'https://test.m2pool.com/'
|
VUE_APP_BASE_URL = 'https://test.m2pool.com/'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,21 @@ export function updateShop(data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除店铺
|
// 删除店铺(兼容:deleteShop(id) / deleteShop({ id, gCode }) / deleteShop(id, gCode))
|
||||||
export function deleteShop(id) {
|
export function deleteShop(id, gCode) {
|
||||||
|
// 兼容对象入参
|
||||||
|
if (id && typeof id === 'object') {
|
||||||
|
const payload = id
|
||||||
|
return request({
|
||||||
|
url: `/lease/shop/deleteShop`,
|
||||||
|
method: 'post',
|
||||||
|
data: payload
|
||||||
|
})
|
||||||
|
}
|
||||||
return request({
|
return request({
|
||||||
url: `/lease/shop/deleteShop`,
|
url: `/lease/shop/deleteShop`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: { id }
|
data: gCode != null ? { id, gCode } : { id }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,34 +18,34 @@
|
|||||||
* @type {Product[]}
|
* @type {Product[]}
|
||||||
*/
|
*/
|
||||||
const products = [
|
const products = [
|
||||||
{
|
// {
|
||||||
id: 'p1001',
|
// id: 'p1001',
|
||||||
title: '新能源充电桩(家用)',
|
// title: '新能源充电桩(家用)',
|
||||||
description: '7kW 单相,智能预约,支持远程监控。',
|
// description: '7kW 单相,智能预约,支持远程监控。',
|
||||||
price: 1299,
|
// price: 1299,
|
||||||
image: 'https://via.placeholder.com/300x200?text=%E5%85%85%E7%94%B5%E6%A1%A9'
|
// image: 'https://via.placeholder.com/300x200?text=%E5%85%85%E7%94%B5%E6%A1%A9'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: 'p1002',
|
// id: 'p1002',
|
||||||
title: '工业电能表',
|
// title: '工业电能表',
|
||||||
description: '三相四线,远程抄表,Modbus 通信。',
|
// description: '三相四线,远程抄表,Modbus 通信。',
|
||||||
price: 899,
|
// price: 899,
|
||||||
image: 'https://via.placeholder.com/300x200?text=%E7%94%B5%E8%83%BD%E8%A1%A8'
|
// image: 'https://via.placeholder.com/300x200?text=%E7%94%B5%E8%83%BD%E8%A1%A8'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: 'p1003',
|
// id: 'p1003',
|
||||||
title: '配电柜(入门版)',
|
// title: '配电柜(入门版)',
|
||||||
description: 'IP54 防护,内置断路器与防雷模块。',
|
// description: 'IP54 防护,内置断路器与防雷模块。',
|
||||||
price: 5599,
|
// price: 5599,
|
||||||
image: 'https://via.placeholder.com/300x200?text=%E9%85%8D%E7%94%B5%E6%9F%9C'
|
// image: 'https://via.placeholder.com/300x200?text=%E9%85%8D%E7%94%B5%E6%9F%9C'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: 'p1004',
|
// id: 'p1004',
|
||||||
title: '工矿照明灯',
|
// title: '工矿照明灯',
|
||||||
description: '120W 高亮,耐腐蚀,适配多场景。',
|
// description: '120W 高亮,耐腐蚀,适配多场景。',
|
||||||
price: 329,
|
// price: 329,
|
||||||
image: 'https://via.placeholder.com/300x200?text=%E7%85%A7%E6%98%8E%E7%81%AF'
|
// image: 'https://via.placeholder.com/300x200?text=%E7%85%A7%E6%98%8E%E7%81%AF'
|
||||||
}
|
// }
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 加密密钥(从环境变量或固定字符串派生)
|
* 加密密钥(从环境变量或固定字符串派生)
|
||||||
* 注意:实际生产环境应该使用更安全的密钥管理方案
|
*
|
||||||
*/
|
*/
|
||||||
const ENCRYPTION_KEY_SOURCE = 'power-leasing-2024-secure-key-v1';
|
const ENCRYPTION_KEY_SOURCE = 'power-leasing-2024-secure-key-v1';
|
||||||
|
|
||||||
|
|||||||
61
power_leasing/src/utils/validators/password.js
Normal file
61
power_leasing/src/utils/validators/password.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 密码校验工具(与登录页体验对齐:分步校验,返回最具体的错误提示)
|
||||||
|
*
|
||||||
|
* 规则:
|
||||||
|
* - 长度 8-32 位
|
||||||
|
* - 必须包含:小写字母 / 大写字母 / 数字 / 特殊字符
|
||||||
|
* - 不能包含中文字符
|
||||||
|
*
|
||||||
|
* @param {{ emptyMessage?: string }} [options]
|
||||||
|
* @returns {(rule: any, value: any, callback: Function) => void}
|
||||||
|
*/
|
||||||
|
export const createPasswordValidator = (options = {}) => {
|
||||||
|
const emptyMessage = options.emptyMessage || '请输入密码'
|
||||||
|
|
||||||
|
return (rule, value, callback) => {
|
||||||
|
if (!value) {
|
||||||
|
callback(new Error(emptyMessage))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const v = String(value)
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
{
|
||||||
|
test: (s) => s.length >= 8 && s.length <= 32,
|
||||||
|
msg: '密码长度应为8-32位'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: (s) => /[a-z]/.test(s),
|
||||||
|
msg: '密码应包含小写字母'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: (s) => /[A-Z]/.test(s),
|
||||||
|
msg: '密码应包含大写字母'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: (s) => /\d/.test(s),
|
||||||
|
msg: '密码应包含数字'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: (s) => /[\W_]/.test(s),
|
||||||
|
msg: '密码应包含特殊字符(如 !@#$%^&*)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: (s) => !/[\u4e00-\u9fa5]/.test(s),
|
||||||
|
msg: '密码不能包含中文字符'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const check of checks) {
|
||||||
|
if (!check.test(v)) {
|
||||||
|
callback(new Error(check.msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="订单完成时间" width="160">
|
<el-table-column v-if="showEndTime" label="订单完成时间" width="160">
|
||||||
<template #default="scope">{{ formatDateTime(scope.row && scope.row.endTime) }}</template>
|
<template #default="scope">{{ formatDateTime(scope.row && scope.row.endTime) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" min-width="60" fixed="right">
|
<el-table-column label="操作" min-width="60" fixed="right">
|
||||||
@@ -208,8 +208,17 @@ export default {
|
|||||||
items: { type: Array, default: () => [] },
|
items: { type: Array, default: () => [] },
|
||||||
emptyText: { type: String, default: '暂无数据' },
|
emptyText: { type: String, default: '暂无数据' },
|
||||||
showCheckout: { type: Boolean, default: false },
|
showCheckout: { type: Boolean, default: false },
|
||||||
onCancel: { type: Function, default: null },
|
/**
|
||||||
isSeller: { type: Boolean, default: false } // 标识是否是卖家订单
|
* 是否为卖家侧订单列表(用于详情页跳转后左侧导航分组高亮)
|
||||||
|
*/
|
||||||
|
isSeller: { type: Boolean, default: false },
|
||||||
|
/**
|
||||||
|
* 是否展示“订单完成时间”列
|
||||||
|
* - 订单进行中:false
|
||||||
|
* - 订单已完成:true
|
||||||
|
*/
|
||||||
|
showEndTime: { type: Boolean, default: false },
|
||||||
|
onCancel: { type: Function, default: null }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -311,18 +320,13 @@ export default {
|
|||||||
});
|
});
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 记录来源:用于详情页决定左侧导航分组(买家/卖家)
|
||||||
|
const from = this.isSeller ? 'seller' : 'buyer'
|
||||||
|
try { sessionStorage.setItem('orderDetailFrom', from) } catch (e) { /* noop */ }
|
||||||
try {
|
try {
|
||||||
// 判断是买家还是卖家订单,传递 from 参数
|
|
||||||
const from = this.isSeller ? 'seller' : 'buyer'
|
|
||||||
// 保存到 sessionStorage,以便详情页可以读取
|
|
||||||
try {
|
|
||||||
sessionStorage.setItem('orderDetailFrom', from)
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('保存订单来源失败', e)
|
|
||||||
}
|
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: `/account/order-detail/${id}`,
|
path: `/account/order-detail/${id}`,
|
||||||
query: { from: from }
|
query: { from }
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$message({
|
this.$message({
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
<h2 class="title">已售出订单</h2>
|
<h2 class="title">已售出订单</h2>
|
||||||
<el-tabs v-model="active" @tab-click="handleTabClick">
|
<el-tabs v-model="active" @tab-click="handleTabClick">
|
||||||
<el-tab-pane label="订单进行中" name="7">
|
<el-tab-pane label="订单进行中" name="7">
|
||||||
<order-list :items="orders[7]" :show-checkout="false" :is-seller="true" empty-text="暂无进行中的订单" />
|
<order-list :items="orders[7]" :show-checkout="false" :show-end-time="false" :is-seller="true" empty-text="暂无进行中的订单" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="订单已完成" name="8">
|
<el-tab-pane label="订单已完成" name="8">
|
||||||
<order-list :items="orders[8]" :show-checkout="false" :is-seller="true" empty-text="暂无已完成的订单" />
|
<order-list :items="orders[8]" :show-checkout="false" :show-end-time="true" :is-seller="true" empty-text="暂无已完成的订单" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -222,6 +222,15 @@
|
|||||||
@input="handleEditFeeRateInput"
|
@input="handleEditFeeRateInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<span slot="footer" class="dialog-footer">
|
||||||
<el-button @click="visibleEdit=false">取消</el-button>
|
<el-button @click="visibleEdit=false">取消</el-button>
|
||||||
@@ -263,7 +272,7 @@ import { getMyShop, updateShop, deleteShop, queryShop, closeShop ,updateShopConf
|
|||||||
import { coinList } from '@/utils/coinList'
|
import { coinList } from '@/utils/coinList'
|
||||||
import { getShopConfig,getShopConfigV2 ,withdrawBalanceForSeller,updateShopConfigV2} from '@/api/wallet'
|
import { getShopConfig,getShopConfigV2 ,withdrawBalanceForSeller,updateShopConfigV2} from '@/api/wallet'
|
||||||
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
|
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
|
||||||
|
import { getGoogleStatus } from '@/api/verification'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AccountMyShops',
|
name: 'AccountMyShops',
|
||||||
@@ -281,7 +290,7 @@ export default {
|
|||||||
state: 0
|
state: 0
|
||||||
},
|
},
|
||||||
visibleEdit: false,
|
visibleEdit: false,
|
||||||
editForm: { id: '', name: '', image: '', description: '', feeRate: '' },
|
editForm: { id: '', name: '', image: '', description: '', feeRate: '', gCode: '' },
|
||||||
// 店铺配置列表
|
// 店铺配置列表
|
||||||
shopConfigs: [],
|
shopConfigs: [],
|
||||||
visibleConfigEdit: false,
|
visibleConfigEdit: false,
|
||||||
@@ -370,6 +379,58 @@ export default {
|
|||||||
this.fetchMyShop()
|
this.fetchMyShop()
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
formatBalance(row) {
|
||||||
try {
|
try {
|
||||||
@@ -399,6 +460,9 @@ export default {
|
|||||||
},
|
},
|
||||||
/** 打开提现对话框(行数据驱动) */
|
/** 打开提现对话框(行数据驱动) */
|
||||||
async handleWithdraw(row) {
|
async handleWithdraw(row) {
|
||||||
|
const ok = await this.ensureGoogleStatusEnabledForWalletOp('提现')
|
||||||
|
if (!ok) return
|
||||||
|
|
||||||
this.currentWithdrawRow = row || {}
|
this.currentWithdrawRow = row || {}
|
||||||
const fee = Number(row && (row.serviceCharge != null ? row.serviceCharge : row.charge))
|
const fee = Number(row && (row.serviceCharge != null ? row.serviceCharge : row.charge))
|
||||||
this.withdrawForm.fee = Number.isFinite(fee) ? this.formatDec6(fee) : '0.00'
|
this.withdrawForm.fee = Number.isFinite(fee) ? this.formatDec6(fee) : '0.00'
|
||||||
@@ -413,7 +477,10 @@ export default {
|
|||||||
{ required: true, message: '请输入提现金额', trigger: 'blur' },
|
{ required: true, message: '请输入提现金额', trigger: 'blur' },
|
||||||
{ validator: this.validateWithdrawAmount, trigger: 'blur' }
|
{ validator: this.validateWithdrawAmount, trigger: 'blur' }
|
||||||
],
|
],
|
||||||
// 地址为只读已填,不再要求用户输入
|
toAddress: [
|
||||||
|
{ required: true, message: '请输入收款钱包地址', trigger: 'blur' },
|
||||||
|
{ validator: this.validateWithdrawToAddress, trigger: 'blur' }
|
||||||
|
],
|
||||||
googleCode: [
|
googleCode: [
|
||||||
{ required: true, message: '请输入谷歌验证码', trigger: 'blur' },
|
{ required: true, message: '请输入谷歌验证码', trigger: 'blur' },
|
||||||
{ validator: this.validateGoogleCode, trigger: 'blur' }
|
{ validator: this.validateGoogleCode, trigger: 'blur' }
|
||||||
@@ -584,6 +651,17 @@ export default {
|
|||||||
if (!/^\d{6}$/.test(v)) { callback(new Error('谷歌验证码必须是6位数字')); return }
|
if (!/^\d{6}$/.test(v)) { callback(new Error('谷歌验证码必须是6位数字')); return }
|
||||||
callback()
|
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;空值显示为 '-'
|
* 手续费率显示:最多6位小数,去除多余的0;空值显示为 '-'
|
||||||
*/
|
*/
|
||||||
@@ -707,6 +785,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async handleEditConfig(row) {
|
async handleEditConfig(row) {
|
||||||
|
const ok = await this.ensureGoogleStatusEnabledForWalletOp('修改')
|
||||||
|
if (!ok) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getChainAndCoin({ id: row.id })
|
const res = await getChainAndCoin({ id: row.id })
|
||||||
if (res && (res.code === 0 || res.code === 200) && res.data) {
|
if (res && (res.code === 0 || res.code === 200) && res.data) {
|
||||||
@@ -745,7 +826,31 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async handleDeleteConfig(row) {
|
async handleDeleteConfig(row) {
|
||||||
this.deleteShopConfig({id:row.id})
|
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) {
|
||||||
|
// 用户取消或弹窗关闭
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -828,6 +933,8 @@ export default {
|
|||||||
this.configForm.payCoins = (this.configForm.payCoins || []).filter(v => String(v) !== String(value))
|
this.configForm.payCoins = (this.configForm.payCoins || []).filter(v => String(v) !== String(value))
|
||||||
},
|
},
|
||||||
async handleOpenEdit() {
|
async handleOpenEdit() {
|
||||||
|
const ok = await this.ensureGoogleStatusEnabledForWalletOp('修改店铺')
|
||||||
|
if (!ok) return
|
||||||
try {
|
try {
|
||||||
// 先打开弹窗,提供更快的视觉反馈
|
// 先打开弹窗,提供更快的视觉反馈
|
||||||
this.visibleEdit = true
|
this.visibleEdit = true
|
||||||
@@ -839,7 +946,8 @@ export default {
|
|||||||
name: res.data.name,
|
name: res.data.name,
|
||||||
image: res.data.image,
|
image: res.data.image,
|
||||||
description: res.data.description,
|
description: res.data.description,
|
||||||
feeRate: res.data.feeRate
|
feeRate: res.data.feeRate,
|
||||||
|
gCode: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -850,7 +958,8 @@ export default {
|
|||||||
name: this.shop.name,
|
name: this.shop.name,
|
||||||
image: this.shop.image,
|
image: this.shop.image,
|
||||||
description: this.shop.description,
|
description: this.shop.description,
|
||||||
feeRate: this.shop.feeRate
|
feeRate: this.shop.feeRate,
|
||||||
|
gCode: ''
|
||||||
}
|
}
|
||||||
this.$message.warning(res && res.msg ? res.msg : '未获取到店铺详情')
|
this.$message.warning(res && res.msg ? res.msg : '未获取到店铺详情')
|
||||||
}
|
}
|
||||||
@@ -861,7 +970,8 @@ export default {
|
|||||||
name: this.shop.name,
|
name: this.shop.name,
|
||||||
image: this.shop.image,
|
image: this.shop.image,
|
||||||
description: this.shop.description,
|
description: this.shop.description,
|
||||||
feeRate: this.shop.feeRate
|
feeRate: this.shop.feeRate,
|
||||||
|
gCode: ''
|
||||||
}
|
}
|
||||||
console.error('查询店铺详情失败:', error)
|
console.error('查询店铺详情失败:', error)
|
||||||
|
|
||||||
@@ -918,7 +1028,14 @@ export default {
|
|||||||
}
|
}
|
||||||
this.editForm.feeRate = rateNum.toString()
|
this.editForm.feeRate = rateNum.toString()
|
||||||
|
|
||||||
const payload = { ...this.editForm }
|
// 谷歌验证码:必填 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)
|
const res = await updateShop(payload)
|
||||||
if (res && (res.code === 0 || res.code === 200)) {
|
if (res && (res.code === 0 || res.code === 200)) {
|
||||||
this.$message({
|
this.$message({
|
||||||
@@ -941,9 +1058,27 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async handleDelete() {
|
async handleDelete() {
|
||||||
|
const ok = await this.ensureGoogleStatusEnabledForWalletOp('删除店铺')
|
||||||
|
if (!ok) return
|
||||||
try {
|
try {
|
||||||
await this.$confirm('确定删除该店铺吗?此操作不可恢复', '提示', { type: 'warning' })
|
const { value } = await this.$prompt(
|
||||||
const res = await deleteShop(this.shop.id)
|
'删除店铺将不可恢复,请输入 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)) {
|
if (res && (res.code === 0 || res.code === 200)) {
|
||||||
this.$message({
|
this.$message({
|
||||||
message: '删除成功',
|
message: '删除成功',
|
||||||
@@ -1074,6 +1209,51 @@ export default {
|
|||||||
/* 全局弹窗宽度微调(仅当前页面生效)*/
|
/* 全局弹窗宽度微调(仅当前页面生效)*/
|
||||||
.el-dialog__body .row { margin-bottom: 12px; }
|
.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 {
|
.el-dialog__body .row {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="row"><span class="label">店铺:</span><span class="value">{{ order.shopName || '—' }}</span></div>
|
<div class="row"><span class="label">店铺:</span><span class="value">{{ order.shopName || '—' }}</span></div>
|
||||||
<div class="row"><span class="label">金额(USDT):</span><span class="value strong">{{ order.totalPrice }}</span></div>
|
<div class="row"><span class="label">金额(USDT):</span><span class="value strong">{{ order.totalPrice }}</span></div>
|
||||||
<div class="row"><span class="label">创建时间:</span><span class="value">{{ formatDateTime(order.createTime) }}</span></div>
|
<div class="row"><span class="label">创建时间:</span><span class="value">{{ formatDateTime(order.createTime) }}</span></div>
|
||||||
<div class="row"><span class="label">订单完成时间:</span><span class="value">{{ formatDateTime(order.endTime) }}</span></div>
|
<div v-if="Number(order.status) === 8" class="row"><span class="label">订单完成时间:</span><span class="value">{{ formatDateTime(order.endTime) }}</span></div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card class="section" style="margin-top:12px;">
|
<el-card class="section" style="margin-top:12px;">
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
<h2 class="title">订单列表</h2>
|
<h2 class="title">订单列表</h2>
|
||||||
<el-tabs v-model="active" @tab-click="handleTabClick">
|
<el-tabs v-model="active" @tab-click="handleTabClick">
|
||||||
<el-tab-pane label="订单进行中" name="7">
|
<el-tab-pane label="订单进行中" name="7">
|
||||||
<order-list :items="orders[7]" :show-checkout="true" :on-cancel="handleCancelOrder" empty-text="暂无进行中的订单" />
|
<order-list :items="orders[7]" :show-checkout="true" :show-end-time="false" :on-cancel="handleCancelOrder" empty-text="暂无进行中的订单" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="订单已完成" name="8">
|
<el-tab-pane label="订单已完成" name="8">
|
||||||
<order-list :items="orders[8]" :show-checkout="false" empty-text="暂无已完成的订单" />
|
<order-list :items="orders[8]" :show-checkout="false" :show-end-time="true" empty-text="暂无已完成的订单" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<!-- <el-tab-pane label="余额不足,订单已取消" name="9">
|
<!-- <el-tab-pane label="余额不足,订单已取消" name="9">
|
||||||
<order-list :items="orders[9]" :show-checkout="false" empty-text="暂无因余额不足取消的订单" />
|
<order-list :items="orders[9]" :show-checkout="false" empty-text="暂无因余额不足取消的订单" />
|
||||||
|
|||||||
@@ -223,7 +223,7 @@
|
|||||||
>
|
>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.sellCount"
|
v-model="form.sellCount"
|
||||||
placeholder="0 - 9999"
|
placeholder="1 - 9999"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
style="width: 50%"
|
style="width: 50%"
|
||||||
@input="handleSellCountInput"
|
@input="handleSellCountInput"
|
||||||
@@ -625,9 +625,43 @@ export default {
|
|||||||
this.getPayTypes();
|
this.getPayTypes();
|
||||||
this.loadSupportCoins();
|
this.loadSupportCoins();
|
||||||
|
|
||||||
this.userEmail = JSON.parse(localStorage.getItem("leasEmail")) || "";
|
this.userEmail = this.getLeasEmailFromStorage();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* 安全获取本地缓存的邮箱(兼容:纯字符串 / JSON 字符串)
|
||||||
|
*
|
||||||
|
* 背景:项目里 `leasEmail` 存储方式不一致,有的地方直接 `setItem('leasEmail', email)` 存纯字符串,
|
||||||
|
* 如果这里强行 JSON.parse 会导致 created hook 直接报错并中断页面逻辑。
|
||||||
|
*
|
||||||
|
* @returns {string} 邮箱;获取失败返回空字符串
|
||||||
|
*/
|
||||||
|
getLeasEmailFromStorage() {
|
||||||
|
const raw = localStorage.getItem("leasEmail");
|
||||||
|
if (!raw) return "";
|
||||||
|
|
||||||
|
const trimmed = String(raw).trim();
|
||||||
|
if (!trimmed) return "";
|
||||||
|
|
||||||
|
// 常见情况:直接存的纯字符串邮箱
|
||||||
|
if (!trimmed.startsWith("{") && !trimmed.startsWith("[") && !trimmed.startsWith('"')) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容情况:存了 JSON(例如 '"a@b.com"' 或 { email: '...' })
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (typeof parsed === "string") return parsed.trim();
|
||||||
|
if (parsed && typeof parsed === "object") {
|
||||||
|
const v = parsed.email || parsed.leasEmail || parsed.userEmail;
|
||||||
|
return v ? String(v).trim() : "";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
} catch (e) {
|
||||||
|
// JSON 解析失败兜底:按纯字符串处理,避免抛错
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
},
|
||||||
/** ASIC 行校验:币种/算法/理论算力/单位 */
|
/** ASIC 行校验:币种/算法/理论算力/单位 */
|
||||||
validateCoinAlgoRows(rule, value, callback) {
|
validateCoinAlgoRows(rule, value, callback) {
|
||||||
try {
|
try {
|
||||||
@@ -954,6 +988,12 @@ export default {
|
|||||||
if (n > 9999) v = "9999";
|
if (n > 9999) v = "9999";
|
||||||
}
|
}
|
||||||
this.form.sellCount = v;
|
this.form.sellCount = v;
|
||||||
|
// 当输入框为空时,清除验证错误
|
||||||
|
if (!v || v === "") {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.machineForm && this.$refs.machineForm.clearValidate('sellCount');
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
handleSellCountBlur() {
|
handleSellCountBlur() {
|
||||||
const raw = String(this.form.sellCount ?? "");
|
const raw = String(this.form.sellCount ?? "");
|
||||||
@@ -976,17 +1016,17 @@ export default {
|
|||||||
*/
|
*/
|
||||||
handleDownloadClient(types) {
|
handleDownloadClient(types) {
|
||||||
// 走后端接口下载客户端程序
|
// 走后端接口下载客户端程序
|
||||||
let userEmail = "";
|
const userEmail = this.getLeasEmailFromStorage();
|
||||||
try {
|
console.log(userEmail, "userEmail");
|
||||||
const email = localStorage.getItem("leasEmail");
|
|
||||||
if (email) {
|
if (!userEmail) {
|
||||||
userEmail = JSON.parse(email);
|
this.$message.warning("未获取到登录邮箱,无法下载客户端,请重新登录后再试");
|
||||||
}
|
return;
|
||||||
} catch (e) {
|
|
||||||
// 忽略解析错误,userEmail 保持为空字符串
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.downloadUrl = `${request.defaults.baseURL}/lease/user/downloadClient?userEmail=${userEmail || ""}&type=${types}`;
|
this.downloadUrl = `${request.defaults.baseURL}/lease/user/downloadClient?userEmail=${encodeURIComponent(
|
||||||
|
userEmail
|
||||||
|
)}&type=${encodeURIComponent(types || "")}`;
|
||||||
let a = document.createElement(`a`);
|
let a = document.createElement(`a`);
|
||||||
a.href = this.downloadUrl;
|
a.href = this.downloadUrl;
|
||||||
a.click();
|
a.click();
|
||||||
@@ -1179,6 +1219,12 @@ export default {
|
|||||||
if (typeof this.form.type === "string" && this.form.type.length > 20) {
|
if (typeof this.form.type === "string" && this.form.type.length > 20) {
|
||||||
this.form.type = this.form.type.slice(0, 20);
|
this.form.type = this.form.type.slice(0, 20);
|
||||||
}
|
}
|
||||||
|
// 当输入框为空时,清除验证错误
|
||||||
|
if (!this.form.type || this.form.type.trim() === "") {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.machineForm && this.$refs.machineForm.clearValidate('type');
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
syncCostToRows() {
|
syncCostToRows() {
|
||||||
const newCost = Number(this.form.cost);
|
const newCost = Number(this.form.cost);
|
||||||
|
|||||||
@@ -388,8 +388,12 @@
|
|||||||
<el-form-item label="功耗(kw/h)">
|
<el-form-item label="功耗(kw/h)">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="editDialog.form.powerDissipation"
|
v-model="editDialog.form.powerDissipation"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.00000001"
|
||||||
|
inputmode="decimal"
|
||||||
placeholder="功耗"
|
placeholder="功耗"
|
||||||
@input="editDialog.form.powerDissipation = (String(editDialog.form.powerDissipation||'').replace(/[^\\d.]/g,''))"
|
@input="editDialog.form.powerDissipation = normalizePowerInput(editDialog.form.powerDissipation)"
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -398,7 +402,7 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="editDialog.form.saleNumbers"
|
v-model="editDialog.form.saleNumbers"
|
||||||
placeholder="整数"
|
placeholder="整数"
|
||||||
@input="editDialog.form.saleNumbers = (String(editDialog.form.saleNumbers||'').replace(/[^\d]/g,''))"
|
@input="editDialog.form.saleNumbers = normalizeSaleNumbersInput(editDialog.form.saleNumbers)"
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -563,6 +567,42 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* 规范化功耗输入:仅允许非负数字,最多 8 位小数
|
||||||
|
* @param {string|number} val - 输入值
|
||||||
|
* @returns {string} 规范化后的字符串
|
||||||
|
*/
|
||||||
|
normalizePowerInput(val) {
|
||||||
|
if (val === null || val === undefined) return ''
|
||||||
|
let v = String(val).replace(/[^\d.]/g, '')
|
||||||
|
const firstDot = v.indexOf('.')
|
||||||
|
if (firstDot !== -1) {
|
||||||
|
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '')
|
||||||
|
}
|
||||||
|
const endsWithDot = v.endsWith('.')
|
||||||
|
let [intPart, decPart = ''] = v.split('.')
|
||||||
|
if (intPart.length > 1) intPart = intPart.replace(/^0+/, '') || '0'
|
||||||
|
if (decPart.length > 8) decPart = decPart.slice(0, 8)
|
||||||
|
if (decPart.length) return `${intPart}.${decPart}`
|
||||||
|
return endsWithDot ? `${intPart}.` : intPart
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 规范化出售数量输入:仅允许 1-9999 的整数
|
||||||
|
* @param {string|number} val - 输入值
|
||||||
|
* @returns {string} 规范化后的字符串(允许空串表示未填)
|
||||||
|
*/
|
||||||
|
normalizeSaleNumbersInput(val) {
|
||||||
|
if (val === null || val === undefined) return ''
|
||||||
|
let v = String(val).replace(/[^\d]/g, '')
|
||||||
|
if (!v) return ''
|
||||||
|
// 去掉前导 0
|
||||||
|
v = v.replace(/^0+/, '')
|
||||||
|
if (!v) return ''
|
||||||
|
const n = parseInt(v, 10)
|
||||||
|
if (!Number.isFinite(n) || n < 1) return ''
|
||||||
|
if (n > 9999) return '9999'
|
||||||
|
return String(n)
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 加载支持的币种列表
|
* 加载支持的币种列表
|
||||||
*/
|
*/
|
||||||
@@ -1373,6 +1413,12 @@ export default {
|
|||||||
const f = this.editDialog.form
|
const f = this.editDialog.form
|
||||||
// 基础校验
|
// 基础校验
|
||||||
if (!String(f.name || '').trim()) { this.$message.warning('矿机型号不能为空'); return }
|
if (!String(f.name || '').trim()) { this.$message.warning('矿机型号不能为空'); return }
|
||||||
|
// 功耗必填校验(为空时禁止提交并提示用户)
|
||||||
|
const powerStrRaw = String(f.powerDissipation ?? '').trim()
|
||||||
|
if (!powerStrRaw) { this.$message.warning('请填写功耗后提交'); return }
|
||||||
|
const powerNum = Number(String(powerStrRaw).replace(/[^\d.]/g, ''))
|
||||||
|
if (!Number.isFinite(powerNum)) { this.$message.warning('请填写功耗后提交'); return }
|
||||||
|
if (!(powerNum > 0)) { this.$message.warning('功耗必须大于0'); return }
|
||||||
// 校验 coinAndAlgoList
|
// 校验 coinAndAlgoList
|
||||||
const list = Array.isArray(f.coinAndAlgoList) ? f.coinAndAlgoList : []
|
const list = Array.isArray(f.coinAndAlgoList) ? f.coinAndAlgoList : []
|
||||||
if (!list.length) { this.$message.warning('请至少添加一行币种/算法/算力/单位'); return }
|
if (!list.length) { this.$message.warning('请至少添加一行币种/算法/算力/单位'); return }
|
||||||
@@ -1396,8 +1442,9 @@ export default {
|
|||||||
}
|
}
|
||||||
const days = parseInt(String(f.maxLeaseDays || '').replace(/[^\d]/g, ''), 10)
|
const days = parseInt(String(f.maxLeaseDays || '').replace(/[^\d]/g, ''), 10)
|
||||||
if (!(Number.isInteger(days) && days >= 1 && days <= 365)) { this.$message.warning('最大租赁天数需为1-365的整数'); return }
|
if (!(Number.isInteger(days) && days >= 1 && days <= 365)) { this.$message.warning('最大租赁天数需为1-365的整数'); return }
|
||||||
const sale = parseInt(String(f.saleNumbers || '').replace(/[^\d]/g, ''), 10)
|
const saleStr = String(f.saleNumbers ?? '').trim()
|
||||||
if (!Number.isInteger(sale) || sale < 0) { this.$message.warning('出售数量应为非负整数'); return }
|
const sale = parseInt(saleStr.replace(/[^\d]/g, ''), 10)
|
||||||
|
if (!Number.isInteger(sale) || sale < 1 || sale > 9999) { this.$message.warning('出售数量需为1-9999的整数'); return }
|
||||||
const payload = {
|
const payload = {
|
||||||
id: f.id,
|
id: f.id,
|
||||||
coinAndAlgoList: (f.coinAndAlgoList || []).map(it => ({
|
coinAndAlgoList: (f.coinAndAlgoList || []).map(it => ({
|
||||||
@@ -1409,7 +1456,7 @@ export default {
|
|||||||
})),
|
})),
|
||||||
maxLeaseDays: days,
|
maxLeaseDays: days,
|
||||||
name: String(f.name || '').trim(),
|
name: String(f.name || '').trim(),
|
||||||
powerDissipation: Number(String(f.powerDissipation || '0').replace(/[^\d.]/g, '')) || 0,
|
powerDissipation: powerNum,
|
||||||
priceList: (this.editDialog.priceList || []).map(p => ({
|
priceList: (this.editDialog.priceList || []).map(p => ({
|
||||||
chain: p.chain,
|
chain: p.chain,
|
||||||
coin: p.coin,
|
coin: p.coin,
|
||||||
@@ -1660,5 +1707,19 @@ export default {
|
|||||||
line-height: 0px;
|
line-height: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 编辑弹窗:隐藏 number 输入框右侧上下箭头(spinner),避免样式突兀 */
|
||||||
|
.edit-form :deep(input[type="number"]),
|
||||||
|
.edit-form ::v-deep input[type="number"] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
appearance: textfield;
|
||||||
|
}
|
||||||
|
.edit-form :deep(input[type="number"]::-webkit-outer-spin-button),
|
||||||
|
.edit-form :deep(input[type="number"]::-webkit-inner-spin-button),
|
||||||
|
.edit-form ::v-deep input[type="number"]::-webkit-outer-spin-button,
|
||||||
|
.edit-form ::v-deep input[type="number"]::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -698,6 +698,56 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* 安全设置页敏感操作前置校验:必须已开启双重验证(Google Authenticator)
|
||||||
|
* getGoogleStatus 返回值:0 开启;1 未绑定;2 关闭
|
||||||
|
*
|
||||||
|
* - status=0:允许继续操作
|
||||||
|
* - status=1/2:弹窗提示并阻止操作;在本页直接引导开启/绑定
|
||||||
|
*
|
||||||
|
* @param {string} actionLabel - 操作名称(用于文案:如“修改密码/注销账号”)
|
||||||
|
* @returns {Promise<boolean>} 是否允许继续
|
||||||
|
*/
|
||||||
|
async ensureGoogleStatusEnabledForSensitiveOp(actionLabel) {
|
||||||
|
try {
|
||||||
|
const res = await getGoogleStatus()
|
||||||
|
if (!res || !(res.code === 0 || res.code === 200)) {
|
||||||
|
this.$message.error('获取双重验证状态失败,请稍后重试')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = res.data?.status ?? res.data ?? 1
|
||||||
|
// 同步本页状态,避免 UI 与实际不一致
|
||||||
|
this.googleStatus = status
|
||||||
|
this.isEnabled = Number(status) === 0
|
||||||
|
|
||||||
|
if (Number(status) === 0) return true
|
||||||
|
|
||||||
|
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, '安全提示', {
|
||||||
|
confirmButtonText: '去开启双重验证',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
customClass: 'google-2fa-guard-dialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 本页即是安全设置页:直接触发开启流程
|
||||||
|
await this.handleEnable2FA()
|
||||||
|
return false
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 检查双重验证状态
|
* 检查双重验证状态
|
||||||
* 返回值:0 开启;1 未绑定;2 关闭
|
* 返回值:0 开启;1 未绑定;2 关闭
|
||||||
@@ -1133,11 +1183,13 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* 修改密码 - 显示弹窗
|
* 修改密码 - 显示弹窗
|
||||||
*/
|
*/
|
||||||
handleChangePassword() {
|
async handleChangePassword() {
|
||||||
if (!this.userEmail) {
|
if (!this.userEmail) {
|
||||||
this.$message.warning('无法获取用户邮箱,请重新登录')
|
this.$message.warning('无法获取用户邮箱,请重新登录')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const ok = await this.ensureGoogleStatusEnabledForSensitiveOp('修改密码')
|
||||||
|
if (!ok) return
|
||||||
this.changePasswordDialogVisible = true
|
this.changePasswordDialogVisible = true
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -1279,11 +1331,13 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* 注销账号 - 显示弹窗
|
* 注销账号 - 显示弹窗
|
||||||
*/
|
*/
|
||||||
handleDeleteAccount() {
|
async handleDeleteAccount() {
|
||||||
if (!this.userEmail) {
|
if (!this.userEmail) {
|
||||||
this.$message.warning('无法获取用户邮箱,请重新登录')
|
this.$message.warning('无法获取用户邮箱,请重新登录')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const ok = await this.ensureGoogleStatusEnabledForSensitiveOp('注销账号')
|
||||||
|
if (!ok) return
|
||||||
this.deleteAccountDialogVisible = true
|
this.deleteAccountDialogVisible = true
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -1722,3 +1776,48 @@ export default {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 双重验证拦截弹窗 - 与导航同色系淡紫渐变(参考钱包绑定页) */
|
||||||
|
.google-2fa-guard-dialog {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.google-2fa-guard-dialog .el-message-box__header {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -118,3 +118,4 @@ export default {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ import { getWalletInfo, withdrawBalance ,balanceRechargeList,balanceWithdrawList
|
|||||||
|
|
||||||
import { getChainAndList,bindWallet } from "../../api/wallet";
|
import { getChainAndList,bindWallet } from "../../api/wallet";
|
||||||
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
|
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
|
||||||
|
import { getGoogleStatus } from '@/api/verification'
|
||||||
export default {
|
export default {
|
||||||
name: 'WalletPage',
|
name: 'WalletPage',
|
||||||
data() {
|
data() {
|
||||||
@@ -473,6 +474,50 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* 钱包相关敏感操作前置校验:必须已开启双重验证(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.data?.status ?? res.data ?? 1
|
||||||
|
if (Number(status) === 0) return true
|
||||||
|
|
||||||
|
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, '安全提示', {
|
||||||
|
confirmButtonText: '去安全设置',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
customClass: 'google-2fa-guard-dialog'
|
||||||
|
})
|
||||||
|
this.$router.push('/account/security-settings')
|
||||||
|
return false
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 统一获取钱包展示单位优先级:fromSymbol > toSymbol > coin > 'USDT'
|
* 统一获取钱包展示单位优先级:fromSymbol > toSymbol > coin > 'USDT'
|
||||||
*/
|
*/
|
||||||
@@ -747,7 +792,10 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* 打开提现对话框
|
* 打开提现对话框
|
||||||
*/
|
*/
|
||||||
handleWithdraw(wallet) {
|
async handleWithdraw(wallet) {
|
||||||
|
const ok = await this.ensureGoogleStatusEnabledForWalletOp('提现')
|
||||||
|
if (!ok) return
|
||||||
|
|
||||||
// 若需要也可根据点击卡片切换默认链/币种
|
// 若需要也可根据点击卡片切换默认链/币种
|
||||||
if (wallet) {
|
if (wallet) {
|
||||||
// 同步当前选中的钱包,驱动只读展示链与币种
|
// 同步当前选中的钱包,驱动只读展示链与币种
|
||||||
@@ -1665,3 +1713,48 @@ export default {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 双重验证拦截弹窗 - 与导航同色系淡紫渐变(本组件加载时生效) */
|
||||||
|
.google-2fa-guard-dialog {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.google-2fa-guard-dialog .el-message-box__header {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,7 @@
|
|||||||
import { getLogin, sendLoginCode } from '@/api/user'
|
import { getLogin, sendLoginCode } from '@/api/user'
|
||||||
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
|
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
|
||||||
import { updateToken } from '@/utils/request'
|
import { updateToken } from '@/utils/request'
|
||||||
|
import { createPasswordValidator } from '@/utils/validators/password'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'LoginPage',
|
name: 'LoginPage',
|
||||||
@@ -148,54 +149,8 @@
|
|||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 密码格式验证:复用通用校验器(与登录页体验对齐:分步校验,返回最具体提示) */
|
||||||
* 密码格式验证(分步验证,提供详细提示)
|
const validatePassword = createPasswordValidator({ emptyMessage: '请输入密码' })
|
||||||
* 8-32位,包含大小写字母、数字和特殊字符
|
|
||||||
*/
|
|
||||||
const validatePassword = (rule, value, callback) => {
|
|
||||||
if (!value) {
|
|
||||||
callback(new Error('请输入密码'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分步验证密码规则,给出具体提示
|
|
||||||
const checks = [
|
|
||||||
{
|
|
||||||
test: (v) => v.length >= 8 && v.length <= 32,
|
|
||||||
msg: '密码长度应为8-32位'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: (v) => /[a-z]/.test(v),
|
|
||||||
msg: '密码应包含小写字母'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: (v) => /[A-Z]/.test(v),
|
|
||||||
msg: '密码应包含大写字母'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: (v) => /\d/.test(v),
|
|
||||||
msg: '密码应包含数字'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: (v) => /[\W_]/.test(v),
|
|
||||||
msg: '密码应包含特殊字符(如 !@#$%^&*)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: (v) => !/[\u4e00-\u9fa5]/.test(v),
|
|
||||||
msg: '密码不能包含中文字符'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 逐个检查,返回第一个不满足的条件
|
|
||||||
for (const check of checks) {
|
|
||||||
if (!check.test(value)) {
|
|
||||||
callback(new Error(check.msg))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 登录表单数据
|
// 登录表单数据
|
||||||
|
|||||||
@@ -145,6 +145,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { register, sendEmailCode } from '../../api/user'
|
import { register, sendEmailCode } from '../../api/user'
|
||||||
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
|
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
|
||||||
|
import { createPasswordValidator } from '@/utils/validators/password'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RegisterPage',
|
name: 'RegisterPage',
|
||||||
@@ -168,24 +169,9 @@ export default {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 密码格式验证
|
* 密码格式验证
|
||||||
* 8-32位,包含大小写字母、数字和特殊字符
|
* 8-32位,包含大小写字母、数字和特殊字符(与登录页体验对齐:分步校验,返回最具体提示)
|
||||||
*/
|
*/
|
||||||
const validatePassword = (rule, value, callback) => {
|
const validatePassword = createPasswordValidator({ emptyMessage: '请输入密码' })
|
||||||
if (!value) {
|
|
||||||
callback(new Error('请输入密码'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 密码验证正则:8-32位,包含大小写字母、数字和特殊字符(!@#¥%……&.*)
|
|
||||||
const regexPassword = /^(?!.*[\u4e00-\u9fa5])(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_]+$)(?![a-z0-9]+$)(?![a-z\W_]+$)(?![0-9\W_]+$)[a-zA-Z0-9\W_]{8,32}$/
|
|
||||||
|
|
||||||
if (!regexPassword.test(value)) {
|
|
||||||
callback(new Error('密码应包含大小写字母、数字和特殊字符,长度8-32位'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确认密码验证
|
* 确认密码验证
|
||||||
|
|||||||
@@ -134,6 +134,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { updatePassword, sendUpdatePwdCode } from '@/api/user'
|
import { updatePassword, sendUpdatePwdCode } from '@/api/user'
|
||||||
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
|
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
|
||||||
|
import { createPasswordValidator } from '@/utils/validators/password'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ResetPasswordPage',
|
name: 'ResetPasswordPage',
|
||||||
@@ -157,28 +158,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 密码格式验证
|
* 密码格式验证(与登录页体验对齐:分步校验,返回最具体提示)
|
||||||
*/
|
*/
|
||||||
/**
|
const validatePassword = createPasswordValidator({ emptyMessage: '请输入新密码' })
|
||||||
* 密码格式验证
|
|
||||||
* 8-32位,包含大小写字母、数字和特殊字符
|
|
||||||
*/
|
|
||||||
const validatePassword = (rule, value, callback) => {
|
|
||||||
if (!value) {
|
|
||||||
callback(new Error('请输入新密码'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 密码验证正则:8-32位,包含大小写字母、数字和特殊字符(!@#¥%……&.*)
|
|
||||||
const regexPassword = /^(?!.*[\u4e00-\u9fa5])(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_]+$)(?![a-z0-9]+$)(?![a-z\W_]+$)(?![0-9\W_]+$)[a-zA-Z0-9\W_]{8,32}$/
|
|
||||||
|
|
||||||
if (!regexPassword.test(value)) {
|
|
||||||
callback(new Error('密码应包含大小写字母、数字和特殊字符,长度8-32位'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确认密码验证
|
* 确认密码验证
|
||||||
|
|||||||
@@ -722,6 +722,7 @@ import { getGoodsList, deleteBatchGoods as apiDeleteBatchGoods ,deleteBatchGoods
|
|||||||
import { addOrders,cancelOrder,getOrdersByIds,getOrdersByStatus , getMachineSupportPool,getChainAndListForSeller,getCoinPrice,getMachineSupportCoinAndAlgorithm, addOrdersV2} from '../../api/order'
|
import { addOrders,cancelOrder,getOrdersByIds,getOrdersByStatus , getMachineSupportPool,getChainAndListForSeller,getCoinPrice,getMachineSupportCoinAndAlgorithm, addOrdersV2} from '../../api/order'
|
||||||
import { truncateAmountByCoin, truncateTo6 } from '../../utils/amount'
|
import { truncateAmountByCoin, truncateTo6 } from '../../utils/amount'
|
||||||
import { rsaEncrypt, rsaEncryptSync } from '../../utils/rsaEncrypt'
|
import { rsaEncrypt, rsaEncryptSync } from '../../utils/rsaEncrypt'
|
||||||
|
import { getGoogleStatus } from '@/api/verification'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Cart',
|
name: 'Cart',
|
||||||
@@ -1056,6 +1057,47 @@ export default {
|
|||||||
this.noticeTimer = null
|
this.noticeTimer = null
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* 结算相关敏感操作前置校验:必须已开启双重验证(Google Authenticator)
|
||||||
|
* getGoogleStatus 返回值:0 开启;1 未绑定;2 关闭
|
||||||
|
*
|
||||||
|
* @param {string} actionLabel - 操作名称(用于提示文案)
|
||||||
|
* @returns {Promise<boolean>} 是否允许继续
|
||||||
|
*/
|
||||||
|
async ensureGoogleStatusEnabledForCheckout(actionLabel) {
|
||||||
|
try {
|
||||||
|
const res = await getGoogleStatus()
|
||||||
|
if (!res || !(res.code === 0 || res.code === 200)) {
|
||||||
|
this.$message.error('获取双重验证状态失败,请稍后重试')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = res.data?.status ?? res.data ?? 1
|
||||||
|
if (Number(status) === 0) return true
|
||||||
|
|
||||||
|
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, '安全提示', {
|
||||||
|
confirmButtonText: '去安全设置',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
customClass: 'google-2fa-guard-dialog'
|
||||||
|
})
|
||||||
|
this.$router.push('/account/security-settings')
|
||||||
|
return false
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
// 获取支持的矿池/模型列表,按币种和算法
|
// 获取支持的矿池/模型列表,按币种和算法
|
||||||
async fetchGetMachineSupportPool(coin, algorithm) {
|
async fetchGetMachineSupportPool(coin, algorithm) {
|
||||||
const payload = { coin: coin || '', algorithm: algorithm || '' }
|
const payload = { coin: coin || '', algorithm: algorithm || '' }
|
||||||
@@ -1994,6 +2036,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async handleCheckoutSelected() {
|
async handleCheckoutSelected() {
|
||||||
|
const ok = await this.ensureGoogleStatusEnabledForCheckout('结算')
|
||||||
|
if (!ok) return
|
||||||
if (!this.selectedMachineCount) {
|
if (!this.selectedMachineCount) {
|
||||||
this.$message({ message: '请先勾选要结算的机器', type: 'warning', showClose: true })
|
this.$message({ message: '请先勾选要结算的机器', type: 'warning', showClose: true })
|
||||||
return
|
return
|
||||||
@@ -3500,3 +3544,48 @@ export default {
|
|||||||
background-color: #e8e8e8 !important;
|
background-color: #e8e8e8 !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 双重验证拦截弹窗 - 与“我的店铺/钱包”一致的淡紫渐变风格 */
|
||||||
|
.google-2fa-guard-dialog {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.google-2fa-guard-dialog .el-message-box__header {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Binary file not shown.
1
power_leasing/test/css/app.9ce7bea6.css
Normal file
1
power_leasing/test/css/app.9ce7bea6.css
Normal file
File diff suppressed because one or more lines are too long
@@ -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.92ffcf12.js"></script><script defer="defer" src="/js/app.69d68908.js"></script><link href="/css/chunk-vendors.10dd4e95.css" rel="stylesheet"><link href="/css/app.6cf7dde7.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.92ffcf12.js"></script><script defer="defer" src="/js/app.b471bca6.js"></script><link href="/css/chunk-vendors.10dd4e95.css" rel="stylesheet"><link href="/css/app.9ce7bea6.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>
|
||||||
2
power_leasing/test/js/app.b471bca6.js
Normal file
2
power_leasing/test/js/app.b471bca6.js
Normal file
File diff suppressed because one or more lines are too long
1
power_leasing/test/js/app.b471bca6.js.map
Normal file
1
power_leasing/test/js/app.b471bca6.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -1,104 +0,0 @@
|
|||||||
# ✅ Token 存储降级方案 - 修复说明
|
|
||||||
|
|
||||||
## 📌 问题描述
|
|
||||||
|
|
||||||
**现象:** 本地调试正常,但在其他电脑上登录后 `localStorage.leasToken` 没有存储成功。
|
|
||||||
|
|
||||||
**根本原因:** 项目使用了 AES-GCM 加密存储,依赖 Web Crypto API,在 HTTP 环境或旧版浏览器中不可用。
|
|
||||||
|
|
||||||
## ✅ 修复方案
|
|
||||||
|
|
||||||
实现了**三层降级存储策略**,确保在任何环境下都能正常存储 token:
|
|
||||||
|
|
||||||
```
|
|
||||||
优先: AES-GCM 加密存储 (HTTPS + 现代浏览器)
|
|
||||||
↓ 失败自动降级
|
|
||||||
降级: 明文 JSON 存储 (HTTP 环境/旧浏览器)
|
|
||||||
↓ 失败保底
|
|
||||||
保底: 内存缓存 (至少当前会话可用)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 修改的文件
|
|
||||||
|
|
||||||
**核心修复:**
|
|
||||||
- [src/utils/request.js](src/utils/request.js) - Token 管理逻辑
|
|
||||||
- `initTokenCache()` - 智能读取(加密→明文→内存)
|
|
||||||
- `updateToken()` - 智能存储(加密→明文→内存)
|
|
||||||
- `clearToken()` - 完全清除(加密+明文+内存)
|
|
||||||
|
|
||||||
## 📊 兼容性
|
|
||||||
|
|
||||||
| 环境 | 存储方式 | 状态 |
|
|
||||||
|------|----------|------|
|
|
||||||
| HTTPS + 现代浏览器 | ✅ AES-GCM 加密 | 最安全 |
|
|
||||||
| HTTP 环境 | ⚠️ 明文 JSON | 自动降级 |
|
|
||||||
| 旧版浏览器 (IE11/360兼容模式) | ⚠️ 明文 JSON | 自动降级 |
|
|
||||||
| 隐私模式 | 💾 内存缓存 | 会话内可用 |
|
|
||||||
|
|
||||||
## 🧪 验证方法
|
|
||||||
|
|
||||||
### 开发环境测试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run serve
|
|
||||||
```
|
|
||||||
|
|
||||||
登录后在浏览器控制台检查:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 检查是否存储成功
|
|
||||||
console.log('Token 已存储:', !!localStorage.getItem('leasToken'));
|
|
||||||
|
|
||||||
// 查看存储模式(开发环境会有日志)
|
|
||||||
// HTTPS: [Token缓存] ✅ 已保存到加密存储
|
|
||||||
// HTTP: [Token缓存] ✅ 已保存到明文存储(降级模式)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 测试登录持久化
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 1. 登录后刷新页面
|
|
||||||
location.reload();
|
|
||||||
|
|
||||||
// 2. 应该保持登录状态,不需要重新登录
|
|
||||||
```
|
|
||||||
|
|
||||||
### 在问题电脑上验证
|
|
||||||
|
|
||||||
1. 清除旧数据: `localStorage.clear()`
|
|
||||||
2. 重新登录
|
|
||||||
3. 检查 localStorage: `console.log('Token:', !!localStorage.getItem('leasToken'))`
|
|
||||||
4. 刷新页面验证登录状态保持
|
|
||||||
|
|
||||||
## 🔍 控制台日志说明
|
|
||||||
|
|
||||||
### HTTPS 环境(加密存储)
|
|
||||||
```
|
|
||||||
[Token缓存] ✅ 已保存到加密存储
|
|
||||||
[Token缓存] ✅ 从加密存储加载成功
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTTP 环境(自动降级)
|
|
||||||
```
|
|
||||||
[Token缓存] ⚠️ 加密存储失败,降级为明文存储
|
|
||||||
[Token缓存] ✅ 已保存到明文存储(降级模式)
|
|
||||||
[Token缓存] ✅ 从明文存储加载成功(降级模式)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔒 安全说明
|
|
||||||
|
|
||||||
- **HTTPS 环境**: 自动使用 AES-GCM 加密,安全性高 ✅
|
|
||||||
- **HTTP 环境**: 降级为明文存储,建议尽快升级到 HTTPS ⚠️
|
|
||||||
- **生产环境**: 强烈推荐使用 HTTPS 部署
|
|
||||||
|
|
||||||
## ✅ 向后兼容
|
|
||||||
|
|
||||||
- ✅ 完全向后兼容,无需数据迁移
|
|
||||||
- ✅ 自动识别旧的明文 token
|
|
||||||
- ✅ 支持自动升级到加密存储(如果环境支持)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**修复时间:** 2026-01-08
|
|
||||||
**影响范围:** 所有用户的登录和会话管理
|
|
||||||
**紧急程度:** 建议尽快部署测试
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
# ✅ 项目优化任务完成报告
|
|
||||||
|
|
||||||
## 📋 任务执行概况
|
|
||||||
|
|
||||||
**执行日期:** 2026-01-06
|
|
||||||
**任务数量:** 7 项
|
|
||||||
**完成状态:** 7/7 ✅ 全部完成
|
|
||||||
**修改文件:** 5 个
|
|
||||||
**新增文件:** 2 个
|
|
||||||
**优化代码行数:** +450 / -80
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ 已完成的优化任务
|
|
||||||
|
|
||||||
### 1. ✅ Token 加密存储
|
|
||||||
|
|
||||||
**完成度:** 100%
|
|
||||||
**核心改进:**
|
|
||||||
- ✅ 创建 `src/utils/secureStorage.js` 加密存储工具类
|
|
||||||
- ✅ 使用 AES-GCM 加密算法(Web Crypto API)
|
|
||||||
- ✅ 实现双层缓存机制(加密 localStorage + 内存缓存)
|
|
||||||
- ✅ 修改 `src/utils/request.js` 集成 Token 管理
|
|
||||||
- ✅ 修改 `src/views/auth/login.vue` 使用加密存储
|
|
||||||
- ⚠️ 剩余 2 个文件需手动修改(header.vue, securitySettings.vue)
|
|
||||||
|
|
||||||
**安全提升:**
|
|
||||||
- 防止 XSS 攻击窃取 Token
|
|
||||||
- 加密密钥使用 PBKDF2 派生
|
|
||||||
- 随机 IV(初始化向量)确保每次加密结果不同
|
|
||||||
|
|
||||||
### 2. ✅ 删除无意义调试代码
|
|
||||||
|
|
||||||
**完成度:** 100%
|
|
||||||
**具体操作:**
|
|
||||||
- ✅ 删除 `src/utils/request.js:203` 行的 `console.log(token,"if就覅飞机飞机")`
|
|
||||||
- ✅ 优化 console.log 全局禁用逻辑(仅生产环境)
|
|
||||||
- ✅ 添加详细注释说明
|
|
||||||
|
|
||||||
**代码质量提升:**
|
|
||||||
- 清理无意义变量名和注释
|
|
||||||
- 避免敏感信息泄露到控制台
|
|
||||||
|
|
||||||
### 3. ✅ 卸载 Vuex 依赖
|
|
||||||
|
|
||||||
**完成度:** 95%
|
|
||||||
**具体操作:**
|
|
||||||
- ✅ 执行 `npm uninstall vuex`(成功卸载)
|
|
||||||
- ✅ 修改 `src/main.js` 删除 store 引用
|
|
||||||
- ⚠️ 需手动删除 `src/store` 目录
|
|
||||||
|
|
||||||
**Bundle 大小优化:**
|
|
||||||
- 减少约 50KB 的打包体积
|
|
||||||
- 移除未使用的依赖,降低维护成本
|
|
||||||
|
|
||||||
### 4. ✅ 请求并发控制机制
|
|
||||||
|
|
||||||
**完成度:** 100%
|
|
||||||
**核心改进:**
|
|
||||||
- ✅ 添加 `MAX_CONCURRENT_RETRIES = 3` 常量
|
|
||||||
- ✅ 实现 `retryWithConcurrencyLimit()` 函数
|
|
||||||
- ✅ 网络恢复时限制并发重试数量
|
|
||||||
- ✅ 防止请求风暴导致服务器压力
|
|
||||||
|
|
||||||
**性能提升:**
|
|
||||||
- 网络恢复时最多 3 个并发重试
|
|
||||||
- 智能队列管理,超时请求自动清理
|
|
||||||
- 详细的日志记录(开发环境)
|
|
||||||
|
|
||||||
### 5. ✅ 全局事件监听器清理
|
|
||||||
|
|
||||||
**完成度:** 100%
|
|
||||||
**核心改进:**
|
|
||||||
- ✅ 导出 `cleanupRequestListeners()` 函数
|
|
||||||
- ✅ 在 `src/main.js` 的 `beforeDestroy` 钩子中调用
|
|
||||||
- ✅ 清理 `online` 和 `offline` 事件监听器
|
|
||||||
- ✅ 添加详细注释说明
|
|
||||||
|
|
||||||
**内存管理提升:**
|
|
||||||
- 防止内存泄漏
|
|
||||||
- 应用卸载时自动清理资源
|
|
||||||
- 遵循最佳实践规范
|
|
||||||
|
|
||||||
### 6. ✅ 密码验证分步验证
|
|
||||||
|
|
||||||
**完成度:** 100%
|
|
||||||
**核心改进:**
|
|
||||||
- ✅ 将复杂正则表达式拆分为 6 个独立检查
|
|
||||||
- ✅ 提供详细的错误提示信息
|
|
||||||
- ✅ 修改 `src/views/auth/login.vue` 的 `validatePassword` 函数
|
|
||||||
|
|
||||||
**用户体验提升:**
|
|
||||||
- 密码长度应为8-32位 ✓
|
|
||||||
- 密码应包含小写字母 ✓
|
|
||||||
- 密码应包含大写字母 ✓
|
|
||||||
- 密码应包含数字 ✓
|
|
||||||
- 密码应包含特殊字符(如 !@#$%^&*) ✓
|
|
||||||
- 密码不能包含中文字符 ✓
|
|
||||||
|
|
||||||
### 7. ✅ 删除未使用的脚手架文件
|
|
||||||
|
|
||||||
**完成度:** 100%
|
|
||||||
**具体操作:**
|
|
||||||
- ✅ 检查并确认以下文件已不存在:
|
|
||||||
- `src/views/HomeView.vue`
|
|
||||||
- `src/views/AboutView.vue`
|
|
||||||
- `src/components/HelloWorld.vue`
|
|
||||||
- ⚠️ 需手动删除 `src/store` 目录
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📂 新增/修改文件清单
|
|
||||||
|
|
||||||
### 新增文件(2 个)
|
|
||||||
1. ✅ `src/utils/secureStorage.js` - Token 加密存储工具类(175 行)
|
|
||||||
2. ✅ `优化完成总结.md` - 优化总结文档
|
|
||||||
3. ✅ `🔴 最后手动步骤.md` - 手动步骤指南
|
|
||||||
|
|
||||||
### 修改文件(5 个)
|
|
||||||
1. ✅ `src/utils/request.js` - Token 管理、并发控制、事件清理(+150 行)
|
|
||||||
2. ✅ `src/views/auth/login.vue` - 密码验证优化、Token 加密存储(+30 行)
|
|
||||||
3. ✅ `src/main.js` - 删除 Vuex、添加事件清理(+5 / -3 行)
|
|
||||||
4. ⚠️ `src/components/header.vue` - 需手动修改
|
|
||||||
5. ⚠️ `src/views/account/securitySettings.vue` - 需手动修改
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ 待手动完成的步骤
|
|
||||||
|
|
||||||
### 必须完成(2 个文件修改 + 1 个目录删除)
|
|
||||||
|
|
||||||
#### 1. 删除 `src/store` 目录
|
|
||||||
```powershell
|
|
||||||
# PowerShell
|
|
||||||
Remove-Item -Recurse -Force "E:\myProject\computing-power-leasing\power_leasing\src\store"
|
|
||||||
|
|
||||||
# 或者使用文件资源管理器手动删除
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 修改 `src/components/header.vue`
|
|
||||||
请参考《优化完成总结.md》中的详细代码示例:
|
|
||||||
- 修改 `updateLoginStatus()` 方法
|
|
||||||
- 修改 `handleLogout()` 方法
|
|
||||||
|
|
||||||
#### 3. 修改 `src/views/account/securitySettings.vue`
|
|
||||||
- 修改账户注销逻辑中的 Token 清除代码
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 优化效果对比
|
|
||||||
|
|
||||||
| 优化项目 | 优化前 | 优化后 | 改进幅度 |
|
|
||||||
|---------|--------|--------|----------|
|
|
||||||
| **Token 安全性** | 明文存储 | AES-GCM 加密 | ⬆️ 90% |
|
|
||||||
| **请求并发控制** | 无限制(潜在风暴) | 最多 3 并发 | ⬆️ 服务器负载 -70% |
|
|
||||||
| **密码验证体验** | 1 个模糊提示 | 6 个详细提示 | ⬆️ 用户满意度 +50% |
|
|
||||||
| **代码质量** | 99+ console.log | 已清理 | ⬆️ 可维护性 +30% |
|
|
||||||
| **内存管理** | 未清理监听器 | 自动清理 | ⬆️ 无内存泄漏 |
|
|
||||||
| **Bundle 大小** | 包含 Vuex | 已移除 | ⬇️ 约 50KB |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 技术亮点
|
|
||||||
|
|
||||||
### 1. Web Crypto API 应用
|
|
||||||
- 浏览器原生加密,无需第三方库
|
|
||||||
- AES-GCM 认证加密,防篡改
|
|
||||||
- PBKDF2 密钥派生,安全性高
|
|
||||||
|
|
||||||
### 2. 双层缓存设计
|
|
||||||
- 加密 localStorage:持久化存储
|
|
||||||
- 内存缓存:同步读取性能
|
|
||||||
- 自动同步机制
|
|
||||||
|
|
||||||
### 3. 并发控制算法
|
|
||||||
- 信号量模式限制并发
|
|
||||||
- 智能队列管理
|
|
||||||
- 超时自动清理
|
|
||||||
|
|
||||||
### 4. 用户体验优化
|
|
||||||
- 分步密码验证
|
|
||||||
- 详细错误提示
|
|
||||||
- 渐进式引导
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 后续建议
|
|
||||||
|
|
||||||
### 立即执行(今天)
|
|
||||||
1. ✅ 完成手动修改的 2 个文件
|
|
||||||
2. ✅ 删除 `src/store` 目录
|
|
||||||
3. ✅ 运行 `npm run serve` 测试
|
|
||||||
4. ✅ 执行完整的功能测试
|
|
||||||
|
|
||||||
### 本周完成
|
|
||||||
1. 添加 Token 过期自动刷新机制
|
|
||||||
2. 实施 CSP(Content Security Policy)
|
|
||||||
3. 添加前端错误监控
|
|
||||||
4. 优化路由代码分割
|
|
||||||
|
|
||||||
### 本月完成
|
|
||||||
1. 完善单元测试覆盖率
|
|
||||||
2. 性能监控和优化
|
|
||||||
3. SEO 优化
|
|
||||||
4. PWA 支持
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 支持与反馈
|
|
||||||
|
|
||||||
如遇到问题,请:
|
|
||||||
1. 查阅《优化完成总结.md》中的常见问题
|
|
||||||
2. 检查《🔴 最后手动步骤.md》是否完成
|
|
||||||
3. 查看浏览器控制台的错误信息
|
|
||||||
4. 回滚到之前的 Git 提交
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 总结
|
|
||||||
|
|
||||||
本次优化覆盖了**安全性、性能、用户体验、代码质量**四个维度,显著提升了项目的整体水平。
|
|
||||||
|
|
||||||
**核心成果:**
|
|
||||||
- ✅ 安全性:Token 加密存储,防 XSS 攻击
|
|
||||||
- ✅ 性能:请求并发控制,防服务器压力
|
|
||||||
- ✅ 体验:密码验证优化,清晰错误提示
|
|
||||||
- ✅ 质量:清理冗余代码,规范注释
|
|
||||||
- ✅ 维护:移除未使用依赖,减少技术债
|
|
||||||
|
|
||||||
**下一步:**
|
|
||||||
请按照《🔴 最后手动步骤.md》完成剩余的手动操作,然后进行全面测试。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**优化执行者:** Claude Code
|
|
||||||
**完成时间:** 2026-01-06
|
|
||||||
**项目路径:** E:\myProject\computing-power-leasing\power_leasing
|
|
||||||
**文档版本:** 1.0
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
# 项目优化完成总结
|
|
||||||
|
|
||||||
## ✅ 已完成的优化
|
|
||||||
|
|
||||||
### 1. Token 加密存储 ✅
|
|
||||||
|
|
||||||
**新增文件:**
|
|
||||||
- `src/utils/secureStorage.js` - AES-GCM 加密存储工具类
|
|
||||||
|
|
||||||
**修改文件:**
|
|
||||||
- `src/utils/request.js` - 添加 Token 内存缓存机制和加密存储支持
|
|
||||||
- `src/views/auth/login.vue` - 使用 `updateToken()` 函数加密存储
|
|
||||||
|
|
||||||
**关键改进:**
|
|
||||||
- 使用 Web Crypto API (AES-GCM) 加密 Token
|
|
||||||
- 双层缓存:加密的 localStorage + 内存缓存
|
|
||||||
- 同步/异步接口兼容性
|
|
||||||
|
|
||||||
**使用方法:**
|
|
||||||
```javascript
|
|
||||||
// 导入 Token 管理函数
|
|
||||||
import { updateToken, clearToken, getToken } from '@/utils/request'
|
|
||||||
|
|
||||||
// 存储 Token(加密)
|
|
||||||
await updateToken(accessToken)
|
|
||||||
|
|
||||||
// 清除 Token
|
|
||||||
await clearToken()
|
|
||||||
|
|
||||||
// 获取 Token(同步,从内存缓存)
|
|
||||||
const token = getToken()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 删除无意义调试代码 ✅
|
|
||||||
|
|
||||||
**修改文件:**
|
|
||||||
- `src/utils/request.js:203` - 已删除 `console.log(token,"if就覅飞机飞机")`
|
|
||||||
|
|
||||||
### 3. 请求并发控制机制 ✅
|
|
||||||
|
|
||||||
**修改文件:**
|
|
||||||
- `src/utils/request.js`
|
|
||||||
|
|
||||||
**新增功能:**
|
|
||||||
- 添加 `MAX_CONCURRENT_RETRIES = 3` 常量
|
|
||||||
- 实现 `retryWithConcurrencyLimit()` 函数
|
|
||||||
- 网络恢复时限制并发重试请求数量,防止请求风暴
|
|
||||||
|
|
||||||
**关键代码:**
|
|
||||||
```javascript
|
|
||||||
// 带并发控制的请求重试
|
|
||||||
async function retryWithConcurrencyLimit(request) {
|
|
||||||
while (activeRetries >= MAX_CONCURRENT_RETRIES) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
activeRetries++;
|
|
||||||
try {
|
|
||||||
return await service(request.config);
|
|
||||||
} finally {
|
|
||||||
activeRetries--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 全局事件监听器清理 ✅
|
|
||||||
|
|
||||||
**修改文件:**
|
|
||||||
- `src/utils/request.js`
|
|
||||||
|
|
||||||
**新增功能:**
|
|
||||||
- 导出 `cleanupRequestListeners()` 函数
|
|
||||||
- 可在应用卸载时清理网络状态监听器
|
|
||||||
|
|
||||||
**使用方法:**
|
|
||||||
```javascript
|
|
||||||
// 在 App.vue 或 main.js 的 beforeDestroy/unmount 中调用
|
|
||||||
import { cleanupRequestListeners } from '@/utils/request'
|
|
||||||
|
|
||||||
beforeDestroy() {
|
|
||||||
cleanupRequestListeners()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 密码验证分步验证 ✅
|
|
||||||
|
|
||||||
**修改文件:**
|
|
||||||
- `src/views/auth/login.vue`
|
|
||||||
|
|
||||||
**改进内容:**
|
|
||||||
- 将复杂正则替换为分步验证
|
|
||||||
- 提供具体的错误提示(长度、大小写、数字、特殊字符、中文)
|
|
||||||
- 用户体验大幅提升
|
|
||||||
|
|
||||||
**验证规则:**
|
|
||||||
1. 密码长度应为8-32位
|
|
||||||
2. 密码应包含小写字母
|
|
||||||
3. 密码应包含大写字母
|
|
||||||
4. 密码应包含数字
|
|
||||||
5. 密码应包含特殊字符(如 !@#$%^&*)
|
|
||||||
6. 密码不能包含中文字符
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⏳ 待手动完成的任务
|
|
||||||
|
|
||||||
由于时间和篇幅限制,以下任务需要手动完成:
|
|
||||||
|
|
||||||
### 6. 更新所有 Token 操作(剩余文件)
|
|
||||||
|
|
||||||
**需要修改的文件:**
|
|
||||||
|
|
||||||
#### `src/components/header.vue`
|
|
||||||
```javascript
|
|
||||||
// 导入清除函数
|
|
||||||
import { clearToken, getToken } from '../utils/request'
|
|
||||||
import secureStorage from '../utils/secureStorage'
|
|
||||||
|
|
||||||
// 修改 updateLoginStatus 方法 (第 204 行)
|
|
||||||
async updateLoginStatus() {
|
|
||||||
try {
|
|
||||||
// 从加密存储读取 token
|
|
||||||
const encryptedToken = await secureStorage.getItem('leasToken')
|
|
||||||
const token = encryptedToken ? JSON.parse(encryptedToken) : null
|
|
||||||
this.isLoggedIn = !!token
|
|
||||||
|
|
||||||
// ...剩余代码保持不变
|
|
||||||
} catch (e) {
|
|
||||||
console.error('更新登录状态失败:', e)
|
|
||||||
this.isLoggedIn = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改 handleLogout 方法 (第 258 行)
|
|
||||||
async handleLogout() {
|
|
||||||
// 清除 Token(包括加密存储和内存缓存)
|
|
||||||
await clearToken()
|
|
||||||
localStorage.removeItem('userInfo')
|
|
||||||
localStorage.removeItem('leasEmail')
|
|
||||||
|
|
||||||
// 触发登录状态变化事件
|
|
||||||
window.dispatchEvent(new CustomEvent('login-status-changed'))
|
|
||||||
|
|
||||||
// ...剩余代码保持不变
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `src/views/account/securitySettings.vue`
|
|
||||||
```javascript
|
|
||||||
// 在顶部导入
|
|
||||||
import { clearToken } from '@/utils/request'
|
|
||||||
|
|
||||||
// 修改账户注销逻辑 (第 1353 行)
|
|
||||||
// 将 localStorage.removeItem('leasToken') 替换为:
|
|
||||||
await clearToken()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. 卸载 Vuex 依赖
|
|
||||||
|
|
||||||
**步骤:**
|
|
||||||
|
|
||||||
1. **卸载依赖:**
|
|
||||||
```bash
|
|
||||||
npm uninstall vuex
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **修改 `src/main.js`:**
|
|
||||||
```javascript
|
|
||||||
import Vue from 'vue'
|
|
||||||
import App from './App.vue'
|
|
||||||
import router from './router'
|
|
||||||
// import store from './store' // 删除这行
|
|
||||||
import ElementUI from 'element-ui';
|
|
||||||
import 'element-ui/lib/theme-chalk/index.css';
|
|
||||||
import { initNoEmojiGuard } from './utils/noEmojiGuard.js';
|
|
||||||
|
|
||||||
console.log = ()=>{} // 全局关闭打印(仅生产环境建议)
|
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
|
||||||
Vue.use(ElementUI);
|
|
||||||
initNoEmojiGuard();
|
|
||||||
|
|
||||||
const vm = new Vue({
|
|
||||||
router,
|
|
||||||
// store, // 删除这行
|
|
||||||
render: h => h(App)
|
|
||||||
}).$mount('#app')
|
|
||||||
|
|
||||||
window.vm = vm
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **删除 `src/store` 目录:**
|
|
||||||
```bash
|
|
||||||
# Windows
|
|
||||||
rmdir /s /q src\store
|
|
||||||
|
|
||||||
# Linux/Mac
|
|
||||||
rm -rf src/store
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. 删除未使用的脚手架文件
|
|
||||||
|
|
||||||
**删除以下文件:**
|
|
||||||
```bash
|
|
||||||
# 删除未使用的视图组件
|
|
||||||
rm src/views/HomeView.vue
|
|
||||||
rm src/views/AboutView.vue
|
|
||||||
|
|
||||||
# 删除未使用的组件
|
|
||||||
rm src/components/HelloWorld.vue
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 运行时注意事项
|
|
||||||
|
|
||||||
### 1. Token 迁移
|
|
||||||
首次运行时,旧的明文 Token 会被自动迁移到加密存储。但建议用户重新登录以确保安全。
|
|
||||||
|
|
||||||
### 2. 开发环境 console.log
|
|
||||||
当前 `console.log = ()=>{}` 在所有环境生效。建议修改为:
|
|
||||||
```javascript
|
|
||||||
// src/main.js
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
console.log = ()=>{}
|
|
||||||
console.debug = ()=>{}
|
|
||||||
console.info = ()=>{}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 测试清单
|
|
||||||
- [ ] 用户登录功能(Token 加密存储)
|
|
||||||
- [ ] 用户退出功能(Token 清除)
|
|
||||||
- [ ] Token 过期处理(421 错误)
|
|
||||||
- [ ] 网络断线重连(并发控制)
|
|
||||||
- [ ] 密码验证提示(分步验证)
|
|
||||||
- [ ] 购物车功能(不受 Token 影响)
|
|
||||||
- [ ] 页面路由跳转(不受影响)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 优化效果对比
|
|
||||||
|
|
||||||
| 项目 | 优化前 | 优化后 | 改进 |
|
|
||||||
|------|--------|--------|------|
|
|
||||||
| Token 安全性 | 明文存储 | AES-GCM 加密 | ⬆️ 显著提升 |
|
|
||||||
| 网络重连并发 | 无限制 | 最多3个并发 | ⬆️ 防止服务器压力 |
|
|
||||||
| 密码验证提示 | 模糊提示 | 6个详细提示 | ⬆️ 用户体验提升 |
|
|
||||||
| 调试代码 | 99+ console.log | 已清理 | ⬆️ 代码质量提升 |
|
|
||||||
| 事件监听器 | 未清理(内存泄漏) | 导出清理函数 | ⬆️ 内存管理 |
|
|
||||||
| Vuex 依赖 | 未使用但引入 | 可移除 | ⬇️ Bundle 大小 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 后续建议
|
|
||||||
|
|
||||||
### 短期优化(1-2周)
|
|
||||||
1. 实施 CSP(Content Security Policy)策略
|
|
||||||
2. 添加 Token 过期自动刷新机制
|
|
||||||
3. 实施 API 请求签名防篡改
|
|
||||||
4. 添加前端日志上报系统
|
|
||||||
|
|
||||||
### 中期优化(1-2月)
|
|
||||||
1. 考虑迁移到 Vue 3(更好的性能和类型支持)
|
|
||||||
2. 实施代码分割和懒加载优化
|
|
||||||
3. 添加 PWA 支持
|
|
||||||
4. 实施服务端渲染(SSR)或静态生成(SSG)
|
|
||||||
|
|
||||||
### 长期优化(3-6月)
|
|
||||||
1. 微前端架构重构
|
|
||||||
2. GraphQL API 迁移
|
|
||||||
3. 自动化测试覆盖率达到 80%+
|
|
||||||
4. 性能监控和错误追踪系统
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ❓ 常见问题
|
|
||||||
|
|
||||||
**Q: 旧用户的 Token 会失效吗?**
|
|
||||||
A: 是的。由于存储方式变更,建议所有用户重新登录。可以在登录页添加提示。
|
|
||||||
|
|
||||||
**Q: 如何查看加密后的 Token?**
|
|
||||||
A: 在浏览器开发者工具中:
|
|
||||||
```javascript
|
|
||||||
import secureStorage from './utils/secureStorage'
|
|
||||||
const token = await secureStorage.getItem('leasToken')
|
|
||||||
console.log(token) // 解密后的 Token
|
|
||||||
```
|
|
||||||
|
|
||||||
**Q: 性能影响如何?**
|
|
||||||
A: AES-GCM 加密/解密性能优异,单次操作 <1ms,对用户体验无影响。
|
|
||||||
|
|
||||||
**Q: 是否支持 IE11?**
|
|
||||||
A: Web Crypto API 不支持 IE11。如需支持,需改用 crypto-js 库。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**优化完成时间:** 2026-01-06
|
|
||||||
**修改文件数量:** 5 个
|
|
||||||
**新增文件数量:** 2 个
|
|
||||||
**删除文件数量:** 待定(Vuex + 脚手架文件)
|
|
||||||
**代码行数变化:** +400 / -100
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# 最后手动步骤(请立即执行)
|
|
||||||
|
|
||||||
## ⚠️ 必须手动删除 Store 目录
|
|
||||||
|
|
||||||
由于命令行路径问题,请手动删除以下目录:
|
|
||||||
|
|
||||||
```
|
|
||||||
📁 src/store/
|
|
||||||
└── index.js
|
|
||||||
```
|
|
||||||
|
|
||||||
**删除方法(任选一种):**
|
|
||||||
|
|
||||||
### 方法 1:使用文件资源管理器
|
|
||||||
1. 打开项目目录:`E:\myProject\computing-power-leasing\power_leasing`
|
|
||||||
2. 进入 `src` 文件夹
|
|
||||||
3. 找到 `store` 文件夹
|
|
||||||
4. 右键 → 删除
|
|
||||||
|
|
||||||
### 方法 2:使用命令行(PowerShell)
|
|
||||||
```powershell
|
|
||||||
Remove-Item -Recurse -Force "E:\myProject\computing-power-leasing\power_leasing\src\store"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 方法 3:使用命令行(CMD)
|
|
||||||
```cmd
|
|
||||||
rd /s /q "E:\myProject\computing-power-leasing\power_leasing\src\store"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ 验证删除是否成功
|
|
||||||
|
|
||||||
删除后,运行以下命令检查:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run serve
|
|
||||||
```
|
|
||||||
|
|
||||||
如果没有报错,说明成功!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 其他需要手动检查的文件
|
|
||||||
|
|
||||||
以下文件需要手动更新 Token 操作(参考《优化完成总结.md》):
|
|
||||||
|
|
||||||
### 1. `src/components/header.vue`
|
|
||||||
|
|
||||||
需要修改 2 个方法:
|
|
||||||
- `updateLoginStatus()` 方法(第 202-215 行)
|
|
||||||
- `handleLogout()` 方法(第 256-265 行)
|
|
||||||
|
|
||||||
### 2. `src/views/account/securitySettings.vue`
|
|
||||||
|
|
||||||
需要修改 1 处:
|
|
||||||
- 账户注销逻辑(第 1353 行)
|
|
||||||
|
|
||||||
**修改方法:**
|
|
||||||
请参考《优化完成总结.md》中的"⏳ 待手动完成的任务"章节。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 完成后测试清单
|
|
||||||
|
|
||||||
- [ ] 运行 `npm run serve` 检查是否有编译错误
|
|
||||||
- [ ] 测试用户登录功能
|
|
||||||
- [ ] 测试用户退出功能
|
|
||||||
- [ ] 测试 Token 过期处理(421 错误)
|
|
||||||
- [ ] 测试网络断线重连
|
|
||||||
- [ ] 测试密码验证提示
|
|
||||||
- [ ] 检查购物车功能是否正常
|
|
||||||
- [ ] 检查路由跳转是否正常
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**删除此文件前请确保已完成所有步骤!**
|
|
||||||
Reference in New Issue
Block a user