每周更新

This commit is contained in:
2026-01-16 15:03:50 +08:00
parent cb0a715f4a
commit da223f8935
28 changed files with 738 additions and 888 deletions

View File

@@ -7,8 +7,8 @@ NODE_ENV = production
ENV = 'staging'
# 测试环境
# VUE_APP_BASE_API = 'http://10.168.2.220:8888'
VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
VUE_APP_BASE_API = 'http://10.168.2.220:8888'
# VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
VUE_APP_BASE_URL = 'https://test.m2pool.com/'

View File

@@ -27,12 +27,21 @@ export function updateShop(data) {
})
}
// 删除店铺
export function deleteShop(id) {
// 删除店铺兼容deleteShop(id) / deleteShop({ id, gCode }) / deleteShop(id, gCode)
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({
url: `/lease/shop/deleteShop`,
method: 'post',
data: { id }
data: gCode != null ? { id, gCode } : { id }
})
}

View File

@@ -18,34 +18,34 @@
* @type {Product[]}
*/
const products = [
{
id: 'p1001',
title: '新能源充电桩(家用)',
description: '7kW 单相,智能预约,支持远程监控。',
price: 1299,
image: 'https://via.placeholder.com/300x200?text=%E5%85%85%E7%94%B5%E6%A1%A9'
},
{
id: 'p1002',
title: '工业电能表',
description: '三相四线远程抄表Modbus 通信。',
price: 899,
image: 'https://via.placeholder.com/300x200?text=%E7%94%B5%E8%83%BD%E8%A1%A8'
},
{
id: 'p1003',
title: '配电柜(入门版)',
description: 'IP54 防护,内置断路器与防雷模块。',
price: 5599,
image: 'https://via.placeholder.com/300x200?text=%E9%85%8D%E7%94%B5%E6%9F%9C'
},
{
id: 'p1004',
title: '工矿照明灯',
description: '120W 高亮,耐腐蚀,适配多场景。',
price: 329,
image: 'https://via.placeholder.com/300x200?text=%E7%85%A7%E6%98%8E%E7%81%AF'
}
// {
// id: 'p1001',
// title: '新能源充电桩(家用)',
// description: '7kW 单相,智能预约,支持远程监控。',
// price: 1299,
// image: 'https://via.placeholder.com/300x200?text=%E5%85%85%E7%94%B5%E6%A1%A9'
// },
// {
// id: 'p1002',
// title: '工业电能表',
// description: '三相四线远程抄表Modbus 通信。',
// price: 899,
// image: 'https://via.placeholder.com/300x200?text=%E7%94%B5%E8%83%BD%E8%A1%A8'
// },
// {
// id: 'p1003',
// title: '配电柜(入门版)',
// description: 'IP54 防护,内置断路器与防雷模块。',
// price: 5599,
// image: 'https://via.placeholder.com/300x200?text=%E9%85%8D%E7%94%B5%E6%9F%9C'
// },
// {
// id: 'p1004',
// title: '工矿照明灯',
// description: '120W 高亮,耐腐蚀,适配多场景。',
// price: 329,
// image: 'https://via.placeholder.com/300x200?text=%E7%85%A7%E6%98%8E%E7%81%AF'
// }
]
/**

View File

@@ -6,7 +6,7 @@
/**
* 加密密钥(从环境变量或固定字符串派生)
* 注意:实际生产环境应该使用更安全的密钥管理方案
*
*/
const ENCRYPTION_KEY_SOURCE = 'power-leasing-2024-secure-key-v1';

View 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()
}
}

View File

@@ -96,7 +96,7 @@
</span>
</template>
</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>
</el-table-column>
<el-table-column label="操作" min-width="60" fixed="right">
@@ -208,8 +208,17 @@ export default {
items: { type: Array, default: () => [] },
emptyText: { type: String, default: '暂无数据' },
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() {
return {
@@ -311,18 +320,13 @@ export default {
});
return
}
// 记录来源:用于详情页决定左侧导航分组(买家/卖家)
const from = this.isSeller ? 'seller' : 'buyer'
try { sessionStorage.setItem('orderDetailFrom', from) } catch (e) { /* noop */ }
try {
// 判断是买家还是卖家订单,传递 from 参数
const from = this.isSeller ? 'seller' : 'buyer'
// 保存到 sessionStorage以便详情页可以读取
try {
sessionStorage.setItem('orderDetailFrom', from)
} catch (e) {
console.warn('保存订单来源失败', e)
}
this.$router.push({
path: `/account/order-detail/${id}`,
query: { from: from }
query: { from }
})
} catch (e) {
this.$message({

View File

@@ -3,10 +3,10 @@
<h2 class="title">已售出订单</h2>
<el-tabs v-model="active" @tab-click="handleTabClick">
<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 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-tabs>
</div>

View File

@@ -222,6 +222,15 @@
@input="handleEditFeeRateInput"
/>
</div>
<div class="row">
<label class="label">谷歌验证码</label>
<el-input
v-model="editForm.gCode"
placeholder="请输入6位谷歌验证码"
maxlength="6"
@input="handleEditShopGoogleCodeInput"
/>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="visibleEdit=false">取消</el-button>
@@ -263,7 +272,7 @@ import { getMyShop, updateShop, deleteShop, queryShop, closeShop ,updateShopConf
import { coinList } from '@/utils/coinList'
import { getShopConfig,getShopConfigV2 ,withdrawBalanceForSeller,updateShopConfigV2} from '@/api/wallet'
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
import { getGoogleStatus } from '@/api/verification'
export default {
name: 'AccountMyShops',
@@ -281,7 +290,7 @@ export default {
state: 0
},
visibleEdit: false,
editForm: { id: '', name: '', image: '', description: '', feeRate: '' },
editForm: { id: '', name: '', image: '', description: '', feeRate: '', gCode: '' },
// 店铺配置列表
shopConfigs: [],
visibleConfigEdit: false,
@@ -370,6 +379,58 @@ export default {
this.fetchMyShop()
},
methods: {
/**
* 修改店铺谷歌验证码输入仅数字最多6位
*/
handleEditShopGoogleCodeInput(v) {
this.editForm.gCode = String(v || '').replace(/\D/g, '').slice(0, 6)
},
/**
* 钱包相关敏感操作前置校验必须已开启双重验证Google Authenticator
* getGoogleStatus 返回值0 开启1 未绑定2 关闭
*
* - status=0允许继续操作
* - status=1/2弹窗提示并阻止操作可跳转到“安全设置”页面开启/绑定
*
* @param {string} actionLabel - 操作名称(用于文案:如“提现/修改/删除”)
* @returns {Promise<boolean>} 是否允许继续
*/
async ensureGoogleStatusEnabledForWalletOp(actionLabel) {
try {
const res = await getGoogleStatus()
if (!res || !(res.code === 0 || res.code === 200)) {
this.$message.error('获取双重验证状态失败,请稍后重试')
return false
}
const status = (res && res.data && res.data.status != null) ? res.data.status : (res.data ?? 1)
if (Number(status) === 0) return true
const title = '安全提示'
const reason = Number(status) === 1 ? '您尚未绑定双重验证' : '您已关闭双重验证'
const message = `
<div class="google-2fa-guard__content">
<div class="google-2fa-guard__title">${reason}</div>
<div class="google-2fa-guard__desc">
请先在<strong>安全设置</strong>中绑定并开启双重验证后,才可以进行<strong>${actionLabel || '该'}操作</strong>。
</div>
</div>
`
await this.$confirm(message, title, {
confirmButtonText: '去安全设置',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true,
customClass: 'google-2fa-guard-dialog'
})
this.$router.push('/account/security-settings')
return false
} catch (e) {
// 用户取消弹窗或接口异常都视为不允许继续
return false
}
},
/** 余额展示:带币种单位 */
formatBalance(row) {
try {
@@ -399,6 +460,9 @@ export default {
},
/** 打开提现对话框(行数据驱动) */
async handleWithdraw(row) {
const ok = await this.ensureGoogleStatusEnabledForWalletOp('提现')
if (!ok) return
this.currentWithdrawRow = row || {}
const fee = Number(row && (row.serviceCharge != null ? row.serviceCharge : row.charge))
this.withdrawForm.fee = Number.isFinite(fee) ? this.formatDec6(fee) : '0.00'
@@ -413,7 +477,10 @@ export default {
{ required: true, message: '请输入提现金额', trigger: 'blur' },
{ validator: this.validateWithdrawAmount, trigger: 'blur' }
],
// 地址为只读已填,不再要求用户输入
toAddress: [
{ required: true, message: '请输入收款钱包地址', trigger: 'blur' },
{ validator: this.validateWithdrawToAddress, trigger: 'blur' }
],
googleCode: [
{ required: true, message: '请输入谷歌验证码', trigger: 'blur' },
{ validator: this.validateGoogleCode, trigger: 'blur' }
@@ -584,6 +651,17 @@ export default {
if (!/^\d{6}$/.test(v)) { callback(new Error('谷歌验证码必须是6位数字')); return }
callback()
},
/**
* 校验:收款地址不能为空
* @param {any} rule
* @param {string} value
* @param {(err?: Error) => void} callback
*/
validateWithdrawToAddress(rule, value, callback) {
const v = String(value || '').trim()
if (!v) { callback(new Error('请输入收款钱包地址')); return }
callback()
},
/**
* 手续费率显示最多6位小数去除多余的0空值显示为 '-'
*/
@@ -707,6 +785,9 @@ export default {
}
},
async handleEditConfig(row) {
const ok = await this.ensureGoogleStatusEnabledForWalletOp('修改')
if (!ok) return
try {
const res = await getChainAndCoin({ id: row.id })
if (res && (res.code === 0 || res.code === 200) && res.data) {
@@ -745,7 +826,31 @@ export default {
}
},
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))
},
async handleOpenEdit() {
const ok = await this.ensureGoogleStatusEnabledForWalletOp('修改店铺')
if (!ok) return
try {
// 先打开弹窗,提供更快的视觉反馈
this.visibleEdit = true
@@ -839,7 +946,8 @@ export default {
name: res.data.name,
image: res.data.image,
description: res.data.description,
feeRate: res.data.feeRate
feeRate: res.data.feeRate,
gCode: ''
}
@@ -850,7 +958,8 @@ export default {
name: this.shop.name,
image: this.shop.image,
description: this.shop.description,
feeRate: this.shop.feeRate
feeRate: this.shop.feeRate,
gCode: ''
}
this.$message.warning(res && res.msg ? res.msg : '未获取到店铺详情')
}
@@ -861,7 +970,8 @@ export default {
name: this.shop.name,
image: this.shop.image,
description: this.shop.description,
feeRate: this.shop.feeRate
feeRate: this.shop.feeRate,
gCode: ''
}
console.error('查询店铺详情失败:', error)
@@ -918,7 +1028,14 @@ export default {
}
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)
if (res && (res.code === 0 || res.code === 200)) {
this.$message({
@@ -941,9 +1058,27 @@ export default {
}
},
async handleDelete() {
const ok = await this.ensureGoogleStatusEnabledForWalletOp('删除店铺')
if (!ok) return
try {
await this.$confirm('确定删除该店铺吗?此操作不可恢复', '提示', { type: 'warning' })
const res = await deleteShop(this.shop.id)
const { value } = await this.$prompt(
'删除店铺将不可恢复,请输入 6 位谷歌验证码确认删除',
'安全验证',
{
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'warning',
inputPlaceholder: '6位数字验证码',
inputPattern: /^\d{6}$/,
inputErrorMessage: '谷歌验证码必须是6位数字'
}
)
const gCode = String(value || '').trim()
if (!/^\d{6}$/.test(gCode)) {
this.$message.warning('谷歌验证码必须是6位数字')
return
}
const res = await deleteShop(this.shop.id, gCode)
if (res && (res.code === 0 || res.code === 200)) {
this.$message({
message: '删除成功',
@@ -1074,6 +1209,51 @@ export default {
/* 全局弹窗宽度微调(仅当前页面生效)*/
.el-dialog__body .row { margin-bottom: 12px; }
/* 双重验证拦截弹窗 - 轻渐变美化(仅当前页面生效) */
.google-2fa-guard-dialog {
border-radius: 12px;
overflow: hidden;
}
.google-2fa-guard-dialog .el-message-box__header {
/* 与导航按钮同色系:#667eea -> #764ba2降低透明度让其更淡 */
background: linear-gradient(135deg, rgba(102, 126, 234, 0.16), rgba(118, 75, 162, 0.10));
border-bottom: 1px solid rgba(102, 126, 234, 0.10);
padding: 14px 16px 10px;
}
.google-2fa-guard-dialog .el-message-box__title {
font-weight: 700;
color: #1f2d3d;
}
.google-2fa-guard-dialog .el-message-box__content {
padding: 14px 18px 6px;
}
.google-2fa-guard-dialog .el-message-box__message {
color: #374151;
line-height: 1.7;
}
.google-2fa-guard-dialog .google-2fa-guard__title {
font-size: 15px;
font-weight: 700;
color: #111827;
margin-bottom: 6px;
}
.google-2fa-guard-dialog .google-2fa-guard__desc {
font-size: 13px;
color: #4b5563;
}
.google-2fa-guard-dialog .el-message-box__btns {
padding: 10px 16px 14px;
}
.google-2fa-guard-dialog .el-button--primary {
border: none;
/* 与导航按钮一致的紫色渐变 */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.google-2fa-guard-dialog .el-button--primary:hover,
.google-2fa-guard-dialog .el-button--primary:focus {
filter: brightness(1.02);
}
/* 弹窗表单统一对齐与留白优化 */
.el-dialog__body .row {
display: grid;

View File

@@ -10,7 +10,7 @@
<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">创建时间</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 class="section" style="margin-top:12px;">

View File

@@ -3,10 +3,10 @@
<h2 class="title">订单列表</h2>
<el-tabs v-model="active" @tab-click="handleTabClick">
<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 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 label="余额不足,订单已取消" name="9">
<order-list :items="orders[9]" :show-checkout="false" empty-text="暂无因余额不足取消的订单" />

View File

@@ -223,7 +223,7 @@
>
<el-input
v-model="form.sellCount"
placeholder="0 - 9999"
placeholder="1 - 9999"
inputmode="numeric"
style="width: 50%"
@input="handleSellCountInput"
@@ -625,9 +625,43 @@ export default {
this.getPayTypes();
this.loadSupportCoins();
this.userEmail = JSON.parse(localStorage.getItem("leasEmail")) || "";
this.userEmail = this.getLeasEmailFromStorage();
},
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 行校验:币种/算法/理论算力/单位 */
validateCoinAlgoRows(rule, value, callback) {
try {
@@ -954,6 +988,12 @@ export default {
if (n > 9999) v = "9999";
}
this.form.sellCount = v;
// 当输入框为空时,清除验证错误
if (!v || v === "") {
this.$nextTick(() => {
this.$refs.machineForm && this.$refs.machineForm.clearValidate('sellCount');
});
}
},
handleSellCountBlur() {
const raw = String(this.form.sellCount ?? "");
@@ -976,17 +1016,17 @@ export default {
*/
handleDownloadClient(types) {
// 走后端接口下载客户端程序
let userEmail = "";
try {
const email = localStorage.getItem("leasEmail");
if (email) {
userEmail = JSON.parse(email);
}
} catch (e) {
// 忽略解析错误userEmail 保持为空字符串
const userEmail = this.getLeasEmailFromStorage();
console.log(userEmail, "userEmail");
if (!userEmail) {
this.$message.warning("未获取到登录邮箱,无法下载客户端,请重新登录后再试");
return;
}
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`);
a.href = this.downloadUrl;
a.click();
@@ -1179,6 +1219,12 @@ export default {
if (typeof this.form.type === "string" && this.form.type.length > 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() {
const newCost = Number(this.form.cost);

View File

@@ -388,8 +388,12 @@
<el-form-item label="功耗(kw/h)">
<el-input
v-model="editDialog.form.powerDissipation"
type="number"
min="0"
step="0.00000001"
inputmode="decimal"
placeholder="功耗"
@input="editDialog.form.powerDissipation = (String(editDialog.form.powerDissipation||'').replace(/[^\\d.]/g,''))"
@input="editDialog.form.powerDissipation = normalizePowerInput(editDialog.form.powerDissipation)"
style="width: 200px"
/>
</el-form-item>
@@ -398,7 +402,7 @@
<el-input
v-model="editDialog.form.saleNumbers"
placeholder="整数"
@input="editDialog.form.saleNumbers = (String(editDialog.form.saleNumbers||'').replace(/[^\d]/g,''))"
@input="editDialog.form.saleNumbers = normalizeSaleNumbersInput(editDialog.form.saleNumbers)"
style="width: 200px"
/>
</el-form-item>
@@ -563,6 +567,42 @@ export default {
}
},
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
// 基础校验
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
const list = Array.isArray(f.coinAndAlgoList) ? f.coinAndAlgoList : []
if (!list.length) { this.$message.warning('请至少添加一行币种/算法/算力/单位'); return }
@@ -1396,8 +1442,9 @@ export default {
}
const days = parseInt(String(f.maxLeaseDays || '').replace(/[^\d]/g, ''), 10)
if (!(Number.isInteger(days) && days >= 1 && days <= 365)) { this.$message.warning('最大租赁天数需为1-365的整数'); return }
const sale = parseInt(String(f.saleNumbers || '').replace(/[^\d]/g, ''), 10)
if (!Number.isInteger(sale) || sale < 0) { this.$message.warning('出售数量应为非负整数'); return }
const saleStr = String(f.saleNumbers ?? '').trim()
const sale = parseInt(saleStr.replace(/[^\d]/g, ''), 10)
if (!Number.isInteger(sale) || sale < 1 || sale > 9999) { this.$message.warning('出售数量需为1-9999的整数'); return }
const payload = {
id: f.id,
coinAndAlgoList: (f.coinAndAlgoList || []).map(it => ({
@@ -1409,7 +1456,7 @@ export default {
})),
maxLeaseDays: days,
name: String(f.name || '').trim(),
powerDissipation: Number(String(f.powerDissipation || '0').replace(/[^\d.]/g, '')) || 0,
powerDissipation: powerNum,
priceList: (this.editDialog.priceList || []).map(p => ({
chain: p.chain,
coin: p.coin,
@@ -1660,5 +1707,19 @@ export default {
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>

View File

@@ -698,6 +698,56 @@ export default {
}
},
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 关闭
@@ -1133,11 +1183,13 @@ export default {
/**
* 修改密码 - 显示弹窗
*/
handleChangePassword() {
async handleChangePassword() {
if (!this.userEmail) {
this.$message.warning('无法获取用户邮箱,请重新登录')
return
}
const ok = await this.ensureGoogleStatusEnabledForSensitiveOp('修改密码')
if (!ok) return
this.changePasswordDialogVisible = true
},
/**
@@ -1279,11 +1331,13 @@ export default {
/**
* 注销账号 - 显示弹窗
*/
handleDeleteAccount() {
async handleDeleteAccount() {
if (!this.userEmail) {
this.$message.warning('无法获取用户邮箱,请重新登录')
return
}
const ok = await this.ensureGoogleStatusEnabledForSensitiveOp('注销账号')
if (!ok) return
this.deleteAccountDialogVisible = true
},
/**
@@ -1722,3 +1776,48 @@ export default {
}
</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>

View File

@@ -118,3 +118,4 @@ export default {

View File

@@ -307,6 +307,7 @@ import { getWalletInfo, withdrawBalance ,balanceRechargeList,balanceWithdrawList
import { getChainAndList,bindWallet } from "../../api/wallet";
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
import { getGoogleStatus } from '@/api/verification'
export default {
name: 'WalletPage',
data() {
@@ -473,6 +474,50 @@ export default {
},
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'
*/
@@ -747,7 +792,10 @@ export default {
/**
* 打开提现对话框
*/
handleWithdraw(wallet) {
async handleWithdraw(wallet) {
const ok = await this.ensureGoogleStatusEnabledForWalletOp('提现')
if (!ok) return
// 若需要也可根据点击卡片切换默认链/币种
if (wallet) {
// 同步当前选中的钱包,驱动只读展示链与币种
@@ -1665,3 +1713,48 @@ export default {
}
</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>

View File

@@ -126,6 +126,7 @@
import { getLogin, sendLoginCode } from '@/api/user'
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
import { updateToken } from '@/utils/request'
import { createPasswordValidator } from '@/utils/validators/password'
export default {
name: 'LoginPage',
@@ -148,54 +149,8 @@
callback()
}
/**
* 密码格式验证(分步验证,提供详细提示)
* 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()
}
/** 密码格式验证:复用通用校验器(与登录页体验对齐:分步校验,返回最具体提示) */
const validatePassword = createPasswordValidator({ emptyMessage: '请输入密码' })
return {
// 登录表单数据

View File

@@ -145,6 +145,7 @@
<script>
import { register, sendEmailCode } from '../../api/user'
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
import { createPasswordValidator } from '@/utils/validators/password'
export default {
name: 'RegisterPage',
@@ -168,24 +169,9 @@ export default {
/**
* 密码格式验证
* 8-32位包含大小写字母、数字和特殊字符
* 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()
}
const validatePassword = createPasswordValidator({ emptyMessage: '请输入密码' })
/**
* 确认密码验证

View File

@@ -134,6 +134,7 @@
<script>
import { updatePassword, sendUpdatePwdCode } from '@/api/user'
import { rsaEncrypt, rsaEncryptSync } from '@/utils/rsaEncrypt'
import { createPasswordValidator } from '@/utils/validators/password'
export default {
name: 'ResetPasswordPage',
@@ -157,28 +158,9 @@ export default {
}
/**
* 密码格式验证
* 密码格式验证(与登录页体验对齐:分步校验,返回最具体提示)
*/
/**
* 密码格式验证
* 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()
}
const validatePassword = createPasswordValidator({ emptyMessage: '请输入新密码' })
/**
* 确认密码验证

View File

@@ -722,6 +722,7 @@ import { getGoodsList, deleteBatchGoods as apiDeleteBatchGoods ,deleteBatchGoods
import { addOrders,cancelOrder,getOrdersByIds,getOrdersByStatus , getMachineSupportPool,getChainAndListForSeller,getCoinPrice,getMachineSupportCoinAndAlgorithm, addOrdersV2} from '../../api/order'
import { truncateAmountByCoin, truncateTo6 } from '../../utils/amount'
import { rsaEncrypt, rsaEncryptSync } from '../../utils/rsaEncrypt'
import { getGoogleStatus } from '@/api/verification'
export default {
name: 'Cart',
@@ -1056,6 +1057,47 @@ export default {
this.noticeTimer = null
},
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) {
const payload = { coin: coin || '', algorithm: algorithm || '' }
@@ -1994,6 +2036,8 @@ export default {
}
},
async handleCheckoutSelected() {
const ok = await this.ensureGoogleStatusEnabledForCheckout('结算')
if (!ok) return
if (!this.selectedMachineCount) {
this.$message({ message: '请先勾选要结算的机器', type: 'warning', showClose: true })
return
@@ -3500,3 +3544,48 @@ export default {
background-color: #e8e8e8 !important;
}
</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.

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.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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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
**影响范围:** 所有用户的登录和会话管理
**紧急程度:** 建议尽快部署测试

View File

@@ -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. 实施 CSPContent 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

View File

@@ -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. 实施 CSPContent 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

View File

@@ -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 错误)
- [ ] 测试网络断线重连
- [ ] 测试密码验证提示
- [ ] 检查购物车功能是否正常
- [ ] 检查路由跳转是否正常
---
**删除此文件前请确保已完成所有步骤!**