wip:个人中心&代理用户详情页初版

This commit is contained in:
2026-05-09 09:47:52 +08:00
parent 0e195a5434
commit 3a4f3f1df1
7 changed files with 962 additions and 18 deletions

View File

@ -0,0 +1,40 @@
import { request } from '@/api';
// 获取用户信息
export async function getUserInfoApi() {
return request.get('/user/getUserInfo');
}
// 修改语言类型
export async function editLangTypeApi(lang_type: number) {
return request.post('/user/editLangType', { lang_type });
}
// 绑定手机号
export async function bindMobileApi(data: { mobile: string; code: string }) {
return request.post('/user/bindMobile', data);
}
// 绑定邮箱
export async function bindEmailApi(data: { email: string; code: string }) {
return request.post('/user/bindEmail', data);
}
// 修改用户名
export async function editUsernameApi(data: { user_id: number; username: string }) {
return request.post('/user/editUser', data);
}
// 短信验证码修改密码
// 验证码修改密码(统一接口)
export async function updatePwdByCaptchaApi(data: {
reg_type: number;
email?: string;
mobile?: string;
code: string;
login_password1: string;
login_password2: string;
}) {
return request.post('/user/editLoginPass', data);
}

View File

@ -176,6 +176,7 @@ return {
sideMenu,
// 方法(箭头函数)
login,
setUserInfo,
register,
logout,
forgotPassword,

View File

@ -0,0 +1,322 @@
<template>
<el-drawer v-model="innerVisible" :title="drawerTitle" direction="rtl" size="80%" header-align="center">
<div class="drawer-content">
<!-- 顶部客户信息卡片 -->
<div class="user-top" v-loading="loading">
<div class="left-card">
<div class="avatar-wrapper">
<el-icon class="avatar-icon">
<UserFilled />
</el-icon>
</div>
<div class="left-text">
<p>{{ userInfo.name }}</p>
<p>ID{{ userInfo.id }}</p>
<p>{{ userInfo.role }}</p>
</div>
</div>
<div class="right-card">
<div class="item">
<div class="item-label">
<el-icon>
<Phone />
</el-icon>
<span>手机号</span>
</div>
<div class="item-value">{{ userInfo.phone || '-' }}</div>
</div>
<div class="item">
<div class="item-label">
<el-icon>
<CreditCard />
</el-icon>
<span>账户余额</span>
</div>
<div class="item-value text-highlight">{{ userInfo.balance || '0' }}</div>
</div>
<div class="item">
<div class="item-label">
<el-icon>
<Money />
</el-icon>
<span>存款金额</span>
</div>
<div class="item-value text-highlight">{{ userInfo.deposit || '0' }}</div>
</div>
<div class="item">
<div class="item-label">
<el-icon>
<Message />
</el-icon>
<span>邮箱</span>
</div>
<div class="item-value">{{ userInfo.email || '-' }}</div>
</div>
<div class="item">
<div class="item-label">
<el-icon>
<Clock />
</el-icon>
<span>注册时间</span>
</div>
<div class="item-value">{{ userInfo.registerTime || '-' }}</div>
</div>
<div class="item">
<div class="item-label">
<el-icon>
<Wallet />
</el-icon>
<span>历史交易金额</span>
</div>
<div class="item-value">{{ userInfo.hisMeny || '-' }}</div>
</div>
</div>
</div>
<!-- 账户表格 -->
<div class="table-container" v-loading="tableLoading">
<CustomerListTable :table-data="tableData" :columns="tableColumnsOptions" row-key="tradeAccount"
:default-sort="{ prop: 'addTime', order: 'descending' }">
</CustomerListTable>
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { ElAvatar, ElIcon, ElDrawer, ElButton, ElMessage } from 'element-plus'
import { UserFilled, Phone, Message, Money, Wallet, Clock, CreditCard } from '@element-plus/icons-vue'
import CustomerListTable from '@/components/common/Table.vue'
import { getCustomerDetailApi } from '@/api/modules/agent/customerDetail'
import dayjs from 'dayjs'
const roleMap: { [key: number]: string } = {
1: '平台',
2: '承包商',
3: '特殊做市商',
4: '普通做市商',
5: '代理',
6: '用户',
}
//
const props = defineProps<{
drawerVisible: boolean
queryId: string | number
drawerTitle: string
}>()
//
const emit = defineEmits<{
'update:drawerVisible': [value: boolean]
}>()
// =============== computed v-model ===============
const innerVisible = computed({
get() {
return props.drawerVisible
},
set(val) {
emit('update:drawerVisible', val)
}
})
//
const loading = ref(false)
const tableLoading = ref(false)
//
const userInfo = ref({
name: '',
id: '',
role: '',
phone: '',
email: '',
deposit: '',
balance: '',
registerTime: '',
hisMeny: 122,
})
//
const tableData = ref()
const tableColumnsOptions = ref([
{ label: '交易账号', prop: 'tradeAccount' },
{ label: '账户名称', prop: 'accountName' },
{ label: '余额', prop: 'balance', sortable: true },
{ label: '交易杠杆', prop: 'leverage' },
{ label: '历史交易量', prop: 'tradeVolume' },
{ label: '添加时间', prop: 'addTime', width: 180 }
])
// ===================== =====================
const loadDetailData = async (id: string | number) => {
loading.value = true
tableLoading.value = true
try {
//
getCustomerDetailApi({ user_id: id }).then((res) => {
console.log('res:', res);
const { accounts, user } = res;
userInfo.value = {
name: user.email || user.mobile,
id: user.id,
role: roleMap[user.role_id],
phone: user.mobile || '-',
email: user.email || '-',
balance: user.capital_amount,
deposit: user.total_recharge_amount,
registerTime: dayjs(user.created_at).format('YYYY-MM-DD HH:mm:ss'),
hisMeny: user.history_transfer_amount,
}
//
tableData.value = accounts.map((item: any) => ({
tradeAccount: item.account_id,
accountName: item.account_name,
balance: item.wallets_trade.amount,
leverage: item.wallets_trade.lever,
tradeVolume: item.collect_data,
addTime: dayjs(item.created_at).format('YYYY-MM-DD HH:mm:ss'),
}))
})
} catch (err) {
ElMessage.error('加载详情失败')
} finally {
loading.value = false
tableLoading.value = false
}
}
// ===================== ID =====================
watch(
() => props.queryId,
async (id) => {
if (!id) return
await loadDetailData(id)
},
{ immediate: true }
)
//
const handleAccount = (row: any) => {
ElMessage.success('操作账户:' + row.tradeAccount)
}
</script>
<style scoped lang="scss">
.drawer-content {
padding: 0px 32px 28px 32px;
}
.user-top {
display: flex;
gap: 24px;
margin-bottom: 32px;
flex-wrap: wrap;
}
.left-card {
display: flex;
align-items: center;
gap: 20px;
padding: 20px 32px;
background: #fafbfc;
border-radius: 16px;
border: 1px solid #f0f2f5;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
min-width: 300px;
.avatar-wrapper {
width: 80px;
height: 80px;
border-radius: 50%;
background: #f2f3f5;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-icon {
font-size: 48px;
color: #909399;
}
}
}
.left-text {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 15px;
font-weight: 500;
color: #1d2129;
}
.left-text p {
margin: 0;
line-height: 1.4;
}
.right-card {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 28px 36px;
padding: 24px 32px;
background: #fafbfc;
border-radius: 16px;
border: 1px solid #f0f2f5;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
align-items: flex-start;
}
.item {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 140px;
}
.item-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #86909c;
}
.item-label :deep(.el-icon) {
font-size: 16px;
color: #86909c;
}
.item-value {
font-size: 14px;
font-weight: 500;
color: #1d2129;
}
.text-highlight {
color: #1677ff;
font-weight: 600;
}
.table-container {
width: 100%;
height: auto;
margin-top: 10px;
}
</style>

View File

@ -7,6 +7,7 @@
@search="handleSearch"
@reset="handleReset"
></CustomerListSearch>
<!-- 表格 -->
<CustomerListTable
:table-data="tableTree"
@ -38,11 +39,13 @@
</el-button>
</template>
<template #action="{ row }">
<el-button link type="primary" size="small" @click="handleDetail(row)" v-if="row.userId">
<!-- 👇 这里改成打开抽屉不再跳转路由 -->
<el-button link type="primary" size="small" @click="openDetailDrawer(row)" v-if="row.userId">
查看详情
</el-button>
</template>
</CustomerListTable>
<!-- 分页 -->
<CustomerListPagination
v-model="currentPage"
@ -50,6 +53,7 @@
:total="total"
@pagination-change="handlePaginationChange"
/>
<!-- dialog -->
<CustomerListDialog
:visible="dialogVisible"
@ -88,7 +92,14 @@
v-if="dialogType === 'editCommissionRatio'"
></CustomerListForm>
</CustomerListDialog>
<!-- dialog表格 -->
<!-- ===================== 新增客户详情抽屉 ===================== -->
<CustomerDetailDrawer
v-model:drawer-visible="detailDrawerVisible"
:query-id="detailQueryId"
:drawer-title="detailDrawerTitle"
/>
</div>
</template>
@ -99,6 +110,10 @@ import CustomerListTable from '@/components/common/Table.vue'
import CustomerListPagination from '@/components/common/Pagination.vue'
import CustomerListDialog from '@/components/common/Dialog.vue'
import CustomerListForm from '@/components/common/Form.vue'
// 👇 default.vue
import CustomerDetailDrawer from './default.vue'
//
import { search, tableColumns, marginRatioTableColumns, commissionRatioFormItem } from './options'
@ -112,6 +127,7 @@ import {
//
import { getStorage } from '@/utils/storage'
const router = useRouter()
/**
* @description: 获取用户id
*/
@ -151,6 +167,11 @@ const dialogTitle = ref<string>('')
const dialogType = ref<string>('')
const dialogFullscreen = ref(false)
// ===================== =====================
const detailDrawerVisible = ref(false)
const detailQueryId = ref('') // ID
const detailDrawerTitle = ref('') //
/**
* @description: 定义点差表格数据
*/
@ -309,9 +330,18 @@ const handleReset = () => {
/**
* @description: 定义表格方法
*/
// ===================== ID =====================
const openDetailDrawer = (row: any) => {
detailQueryId.value = row.userId
detailDrawerTitle.value = `${row.accountName} - 客户详情`
detailDrawerVisible.value = true
}
// 👇
const handleDetail = (row: any) => {
console.log('查看详情', row)
router.push(`/agent/customerDetail/${row.userId}`)
console.log('查看详情(旧路由)', row)
// router.push(`/agent/customerDetail/${row.userId}`)
}
//
@ -443,15 +473,13 @@ const handleCancel = () => {
const handleClose = () => {
dialogVisible.value = false
}
/**
* @description: 定义弹窗表格方法
*/
</script>
<style scoped lang="scss">
:deep(.placeholder-row) {
text-align: center;
color: #909399;
/* 灰色和Element空状态颜色一致 */
}
</style>

View File

@ -111,6 +111,7 @@
placeholder="请输入验证码"
>
</el-input>
<el-button type="primary" @click="handleSendCode" :disabled="sendState.isCounting">{{
sendState.isCounting ? `${sendState.countdown}s后重新获取` : '发送验证码'
}}</el-button>

View File

@ -0,0 +1,552 @@
<template>
<div class="personal-center">
<div class="page-header">
<h2>账户信息</h2>
</div>
<div class="content-wrapper">
<el-card class="card avatar-card">
<div class="avatar-box">
<div class="avatar-wrapper">
<img v-if="userInfo.avatar" :src="userInfo.avatar" class="avatar-image" alt="用户头像" />
<el-icon v-else class="avatar-icon">
<UserFilled />
</el-icon>
</div>
<div class="info-text">
<div class="username-row">
<span class="username-text">{{ userInfo.username }}</span>
<el-button link type="primary" size="small" @click="showEditDialog('username')">修改</el-button>
</div>
<div class="login-time">最后登录{{ userInfo.last_login }}</div>
</div>
</div>
</el-card>
<el-card class="card info-card">
<div class="info-list">
<div class="info-item">
<span class="label">邮箱</span>
<span class="value">{{ userInfo.email || '未设置' }}</span>
<template v-if="!userInfo.email">
<el-button link type="primary" @click="showEditDialog('email')">去绑定</el-button>
</template>
<template v-else>
<el-tag type="success" size="small">已绑定</el-tag>
</template>
</div>
<div class="info-item">
<span class="label">手机号</span>
<span class="value">{{ userInfo.mobile || '未绑定' }}</span>
<el-button link type="primary" @click="showEditDialog('mobile')">
{{ userInfo.mobile ? '重新绑定' : '绑定' }}
</el-button>
</div>
<div class="info-item">
<span class="label">密码</span>
<el-button link type="primary" @click="showPasswordDialog">修改</el-button>
</div>
<div class="info-item">
<span class="label">语言</span>
<span class="value">选择您偏好的通讯语言</span>
<el-select v-model="userInfo.lang_type" @change="handleLanguageChange" style="width: 130px" size="small">
<el-option label="简体中文" :value="1" />
<el-option label="English" :value="2" />
</el-select>
</div>
</div>
</el-card>
</div>
<!-- 编辑弹窗 -->
<Dialog
:visible="dialogVisible"
:title="dialogTitle"
width="500px"
@confirm="handleEditConfirm"
@cancel="handleEditDialogReset"
@close="handleEditDialogReset"
>
<el-form
ref="editFormRef"
:model="editForm"
:rules="getEditRules()"
label-width="80px"
label-position="top"
>
<el-form-item :label="editForm.label" prop="value">
<el-input
v-if="editField !== 'language'"
v-model="editForm.value"
:placeholder="`请输入${editForm.label}`"
size="large"
/>
</el-form-item>
<UserCaptcha
v-if="['email', 'mobile'].includes(editField)"
v-model="editForm.code"
@sendCode="handleSendCaptcha"
:show-send-button="showSendButton"
:send-state="sendState"
/>
</el-form>
</Dialog>
<!-- 密码修改弹窗 验证码模式 -->
<Dialog
:visible="passwordDialogVisible"
title="修改密码"
width="500px"
@confirm="handlePasswordConfirm"
@cancel="handlePasswordDialogReset"
@close="handlePasswordDialogReset"
>
<el-form
ref="passwordFormRef"
:model="passwordForm"
:rules="passwordFormRules"
label-width="100px"
label-position="top"
>
<!-- 选择验证方式 -->
<el-form-item label="验证方式" prop="verifyType">
<el-radio-group v-model="passwordForm.verifyType" @change="onVerifyTypeChange">
<el-radio label="mobile" :disabled="!userInfo.mobile">手机验证</el-radio>
<el-radio label="email" :disabled="!userInfo.email">邮箱验证</el-radio>
</el-radio-group>
</el-form-item>
<!-- 验证账号 -->
<el-form-item label="验证账号">
<span style="font-weight:500;color:#333">
{{ passwordForm.verifyType === 'mobile' ? userInfo.mobile : userInfo.email }}
</span>
</el-form-item>
<!-- 验证码 -->
<UserCaptcha
prop="code"
v-model="passwordForm.code"
@sendCode="handleSendPwdCaptcha"
:show-send-button="showPwdSendButton"
:send-state="pwdSendState"
/>
<!-- 新密码1 -->
<el-form-item label="新密码" prop="login_password1">
<el-input
v-model="passwordForm.login_password1"
type="password"
show-password
placeholder="6-12位含大小写+特殊字符"
size="large"
/>
</el-form-item>
<!-- 新密码2 -->
<el-form-item label="确认密码" prop="login_password2">
<el-input
v-model="passwordForm.login_password2"
type="password"
show-password
placeholder="请再次输入新密码"
size="large"
/>
</el-form-item>
</el-form>
</Dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { UserFilled } from '@element-plus/icons-vue';
import Dialog from '@/components/common/Dialog.vue';
import UserCaptcha from '@/views/user/components/Captcha.vue';
import { useUserStore } from '@/stores/modules/user';
import { useLanguageStore } from '@/stores/modules/language';
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha';
import { codeRules } from '@/views/user/config/rules';
//
import {
getUserInfoApi,
editLangTypeApi,
bindMobileApi,
bindEmailApi,
editUsernameApi,
updatePwdByCaptchaApi //
} from '@/api/modules/profile/personalCenter';
const userInfo = reactive({
id: 0,
username: '',
email: '',
mobile: '',
lang_type: 1,
avatar: '',
last_login: ''
});
const dialogVisible = ref(false);
const passwordDialogVisible = ref(false);
const editFormRef = ref<FormInstance>();
const passwordFormRef = ref<FormInstance>();
const editField = ref('');
const dialogTitle = ref('');
const editForm = reactive({ label: '', value: '', code: '' });
//
const passwordForm = reactive({
verifyType: 'mobile' as 'mobile' | 'email',
code: '',
login_password1: '',
login_password2: ''
});
//
const showSendButton = ref(true);
const sendState = ref({ isCounting: false, countdown: 0 });
const phoneCaptchaState = initCaptchaState('phone');
const emailCaptchaState = initCaptchaState('email');
const showPwdSendButton = ref(true);
const pwdSendState = ref({ isCounting: false, countdown: 0 });
const pwdMobileCaptchaState = initCaptchaState('phone');
const pwdEmailCaptchaState = initCaptchaState('email');
const userStore = useUserStore();
const languageStore = useLanguageStore();
//
const getEditRules = (): FormRules => {
const rules: FormRules = {};
if (['email', 'mobile'].includes(editField.value)) {
rules.code = codeRules().code;
}
switch (editField.value) {
case 'email':
rules.value = [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
];
return rules;
case 'mobile':
rules.value = [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^\+?\d{5,20}$/, message: '手机号格式不正确', trigger: 'blur' }
];
return rules;
case 'username':
rules.value = [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 20, message: '用户名长度 2-20 位', trigger: 'blur' }
];
return rules;
default:
return {};
}
};
//
const passwordFormRules = ref<FormRules>({
verifyType: [{ required: true, message: '请选择验证方式', trigger: 'change' }],
code: codeRules().code,
login_password1: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 12, message: '长度 6-12 位', trigger: 'blur' },
{ pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{6,12}$/,
message: '必须包含大小写+特殊字符', trigger: 'blur' }
],
login_password2: [
{ required: true, message: '请确认新密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== passwordForm.login_password1) callback(new Error('两次密码不一致'));
callback();
}, trigger: 'blur'
}
]
});
//
const fetchUserProfile = async () => {
const res = await getUserInfoApi();
Object.assign(userInfo, res);
languageStore.changeLang(res.lang_type === 1 ? 'zh-CN' : 'en-US');
};
//
const showEditDialog = (field: string) => {
editField.value = field;
editForm.code = '';
switch (field) {
case 'email':
editForm.label = '邮箱';
editForm.value = userInfo.email;
dialogTitle.value = '绑定邮箱';
break;
case 'mobile':
editForm.label = '手机号';
editForm.value = userInfo.mobile;
dialogTitle.value = userInfo.mobile ? '重新绑定手机号' : '绑定手机号';
break;
case 'username':
editForm.label = '用户名';
editForm.value = userInfo.username;
dialogTitle.value = '修改用户名';
break;
}
dialogVisible.value = true;
updateSendState();
};
//
const handleSendCaptcha = async () => {
const type = editField.value === 'email' ? 'email' : 'phone';
const receiver = editForm.value?.trim();
const currentState = type === 'email' ? emailCaptchaState : phoneCaptchaState;
await sendCaptcha(type, receiver, currentState, () => {
return userStore.sendCode({
reg_type: type === 'email' ? 1 : 2,
[type === 'email' ? 'email' : 'mobile']: receiver,
type: type === 'email' ? 'transfer_code' : 'bindMobile_code'
});
});
updateSendState();
};
const updateSendState = () => {
const t = editField.value === 'email' ? emailCaptchaState : phoneCaptchaState;
sendState.value = { isCounting: t.isCounting, countdown: t.countdown };
};
//
const handleLanguageChange = async (val: number) => {
await editLangTypeApi(val);
userInfo.lang_type = val;
languageStore.changeLang(val === 1 ? 'zh-CN' : 'en-US');
ElMessage.success('语言切换成功');
};
//
const handleEditConfirm = async () => {
if (!editFormRef.value) return;
await editFormRef.value.validate();
try {
if (editField.value === 'username') {
await editUsernameApi({ username: editForm.value, user_id: userInfo.id });
userInfo.username = editForm.value;
}
if (editField.value === 'mobile') {
await bindMobileApi({ mobile: editForm.value, code: editForm.code });
userInfo.mobile = editForm.value;
}
if (editField.value === 'email') {
await bindEmailApi({ email: editForm.value, code: editForm.code });
userInfo.email = editForm.value;
}
ElMessage.success('操作成功');
dialogVisible.value = false;
editFormRef.value.resetFields();
} catch (err) {
ElMessage.error('操作失败');
}
};
// ==================== ====================
const showPasswordDialog = () => {
if (!userInfo.mobile && !userInfo.email) {
ElMessage.warning('请先绑定手机或邮箱');
return;
}
passwordForm.verifyType = userInfo.mobile ? 'mobile' : 'email';
passwordForm.code = '';
passwordForm.login_password1 = '';
passwordForm.login_password2 = '';
passwordDialogVisible.value = true;
updatePwdSendState();
};
const onVerifyTypeChange = () => {
passwordForm.code = '';
updatePwdSendState();
};
//
const handleSendPwdCaptcha = async () => {
const type = passwordForm.verifyType === 'email' ? 'email' : 'phone';
const receiver = passwordForm.verifyType === 'email' ? userInfo.email : userInfo.mobile;
const currentState = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
await sendCaptcha(type, receiver, currentState, () => {
return userStore.sendCode({
reg_type: passwordForm.verifyType === 'email' ? 1 : 2,
[passwordForm.verifyType]: receiver,
type: 'resetPass_code'
});
});
updatePwdSendState();
};
const updatePwdSendState = () => {
const t = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
pwdSendState.value = { isCounting: t.isCounting, countdown: t.countdown };
};
// 100%
const handlePasswordConfirm = async () => {
if (!passwordFormRef.value) return;
await passwordFormRef.value.validate();
try {
const params = {
reg_type: passwordForm.verifyType === 'email' ? 1 : 2,
email: passwordForm.verifyType === 'email' ? userInfo.email : undefined,
mobile: passwordForm.verifyType === 'mobile' ? userInfo.mobile : undefined,
code: passwordForm.code,
login_password1: passwordForm.login_password1,
login_password2: passwordForm.login_password2
};
await updatePwdByCaptchaApi(params);
ElMessage.success('密码修改成功');
passwordDialogVisible.value = false;
passwordFormRef.value.resetFields();
} catch (err) {
ElMessage.error('密码修改失败');
}
};
const handleEditDialogReset = () => {
dialogVisible.value = false;
editFormRef.value?.resetFields();
};
const handlePasswordDialogReset = () => {
passwordDialogVisible.value = false;
passwordFormRef.value?.resetFields();
};
onMounted(() => {
fetchUserProfile();
});
onUnmounted(() => {
clearCaptchaTimer(phoneCaptchaState);
clearCaptchaTimer(emailCaptchaState);
clearCaptchaTimer(pwdMobileCaptchaState);
clearCaptchaTimer(pwdEmailCaptchaState);
});
</script>
<style scoped lang="scss">
.personal-center {
padding: 24px;
.page-header {
margin-bottom: 20px;
h2 {
font-size: 24px;
font-weight: 600;
color: #1f2329;
margin: 0;
}
}
}
.content-wrapper {
display: flex;
flex-direction: column;
gap: 16px;
}
.card {
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
border: none;
}
.avatar-card {
.avatar-box {
display: flex;
align-items: center;
gap: 20px;
.avatar-wrapper {
width: 80px;
height: 80px;
border-radius: 50%;
background: #f2f3f5;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-icon {
font-size: 48px;
color: #909399;
}
}
.info-text {
display: flex;
flex-direction: column;
gap: 6px;
.username-row {
display: flex;
align-items: flex-end;
gap: 8px;
.username-text {
font-size: 20px;
font-weight: 600;
color: #1f2329;
line-height: 1.2;
}
}
.login-time {
font-size: 14px;
color: #909399;
}
}
}
}
.info-card {
.info-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.info-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #f2f3f5;
&:last-child {
border-bottom: none;
}
.label {
font-size: 14px;
font-weight: 500;
color: #606266;
min-width: 70px;
}
.value {
flex: 1;
padding: 0 12px;
color: #303133;
font-size: 14px;
}
}
}
</style>

View File

@ -1,5 +1,6 @@
import { fileURLToPath, URL } from 'node:url'
import { loadEnv, defineConfig, ConfigEnv, ViteConfig } from 'vite'
import { loadEnv, defineConfig, ConfigEnv } from 'vite'
import type { UserConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
@ -11,10 +12,16 @@ import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig(({ mode }: ConfigEnv): ViteConfig => {
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
const isProduction = mode === 'production'
const env = loadEnv(mode, process.cwd())
return {
esbuild: {
drop: isProduction ? ['console', 'debugger'] : [],
// minifyIdentifiers: true,
minifySyntax: true,
minifyWhitespace: true,
},
plugins: [
vue(),
// 生产环境移除 devTools 插件(减少产物体积)
@ -63,12 +70,6 @@ export default defineConfig(({ mode }: ConfigEnv): ViteConfig => {
// 压缩配置
minify: 'esbuild',
esbuild: {
drop: isProduction ? ['console', 'debugger'] : [],
minifyIdentifiers: true,
minifySyntax: true,
minifyWhitespace: true,
},
// 大文件警告阈值默认500KB可调整
chunkSizeWarningLimit: 1000,
@ -82,7 +83,6 @@ export default defineConfig(({ mode }: ConfigEnv): ViteConfig => {
server: {
host: true,
port: 9999,
https: false,
open: true,
proxy: {
'/api': {