wip:个人中心&代理用户详情页调整

This commit is contained in:
2026-05-09 10:59:13 +08:00
parent 3a4f3f1df1
commit b694fb3461
7 changed files with 416 additions and 160 deletions

View File

@ -1,6 +1,7 @@
//获取代理客户列表接口 // 获取代理客户详情接口
import { request } from '@/api'; import { request } from '@/api';
import type { GetCustomerDetailParams, CustomerDetailResponse } from '@/api/types/agent';
export const getCustomerDetailApi = (params: any) => { export const getCustomerDetailApi = (params: GetCustomerDetailParams): Promise<CustomerDetailResponse> => {
return request.get('/agent/getAgentUserDetail', {...params}) return request.get<CustomerDetailResponse>('/agent/getAgentUserDetail', params);
} }

View File

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

83
src/api/types/agent.d.ts vendored Normal file
View File

@ -0,0 +1,83 @@
// 代理客户详情相关类型定义
// 角色映射类型
export type RoleType = 1 | 2 | 3 | 4 | 5 | 6
// 角色名称映射
export const ROLE_MAP: Record<RoleType, string> = {
1: '平台',
2: '承包商',
3: '特殊做市商',
4: '普通做市商',
5: '代理',
6: '用户',
}
// 用户信息类型
export interface CustomerUserInfo {
id: string | number
email: string
mobile: string
role_id: RoleType
capital_amount: string | number
total_recharge_amount: string | number
history_transfer_amount: string | number
created_at: string
}
// 交易账户钱包信息类型
export interface AccountWallet {
amount: string | number
lever: string | number
}
// 交易账户类型
export interface Account {
account_id: string | number
account_name: string
wallets_trade: AccountWallet
collect_data: string | number
created_at: string
}
// 客户详情响应类型
export interface CustomerDetailResponse {
user: CustomerUserInfo
accounts: Account[]
}
// 获取客户详情参数类型
export interface GetCustomerDetailParams {
user_id: string | number
}
// 表格列配置类型
export interface TableColumn {
label: string
prop: string
width?: number
sortable?: boolean
}
// 用户展示信息类型
export interface UserDisplayInfo {
name: string
id: string | number
role: string
phone: string
email: string
deposit: string | number
balance: string | number
registerTime: string
hisMeny: string | number
}
// 表格数据行类型
export interface TableDataRow {
tradeAccount: string | number
accountName: string
balance: string | number
leverage: string | number
tradeVolume: string | number
addTime: string
}

60
src/api/types/profile.d.ts vendored Normal file
View File

@ -0,0 +1,60 @@
// 个人中心相关类型定义
// 用户信息类型
export interface UserInfo {
id: number
username: string
email: string
mobile: string
lang_type: number
avatar: string
last_login: string
}
// 编辑表单类型
export interface EditForm {
label: string
value: string
code: string
}
// 密码表单类型
export interface PasswordForm {
verifyType: 'mobile' | 'email'
code: string
login_password1: string
login_password2: string
}
// 修改语言参数类型
export interface EditLangTypeParams {
lang_type: number
}
// 绑定手机号参数类型
export interface BindMobileParams {
mobile: string
code: string
}
// 绑定邮箱参数类型
export interface BindEmailParams {
email: string
code: string
}
// 修改用户名参数类型
export interface EditUsernameParams {
user_id: number
username: string
}
// 验证码修改密码参数类型
export interface UpdatePwdByCaptchaParams {
reg_type: number
email?: string
mobile?: string
code: string
login_password1: string
login_password2: string
}

View File

@ -0,0 +1,75 @@
<template>
<span>{{ animatedValue }}</span>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue';
interface Props {
value: number;
duration?: number;
decimals?: number;
separator?: string;
}
const props = withDefaults(defineProps<Props>(), {
duration: 1000,
decimals: 2,
separator: ','
});
const animatedValue = ref('0.00');
let animationFrameId: number | null = null;
let startTime: number | null = null;
const startValue = ref(0);
//
const formatNumber = (num: number): string => {
const fixed = num.toFixed(props.decimals);
const parts = fixed.split('.');
parts[0] && (parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, props.separator));
return parts.join('.');
};
//
const animate = (timestamp: number) => {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / props.duration, 1);
// 使 easeOutQuart
const easeProgress = 1 - Math.pow(1 - progress, 4);
const currentValue = startValue.value + (props.value - startValue.value) * easeProgress;
animatedValue.value = formatNumber(currentValue);
if (progress < 1) {
animationFrameId = requestAnimationFrame(animate);
}
};
//
const startAnimation = () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
startTime = null;
startValue.value = parseFloat(animatedValue.value.replace(/,/g, '')) || 0;
animationFrameId = requestAnimationFrame(animate);
};
//
watch(() => props.value, () => {
startAnimation();
}, { immediate: false });
onMounted(() => {
startAnimation();
});
onUnmounted(() => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
});
</script>

View File

@ -34,7 +34,9 @@
</el-icon> </el-icon>
<span>账户余额</span> <span>账户余额</span>
</div> </div>
<div class="item-value text-highlight">{{ userInfo.balance || '0' }}</div> <div class="item-value text-highlight">
<AnimatedNumber :value="Number(userInfo.balance) || 0" />
</div>
</div> </div>
<div class="item"> <div class="item">
<div class="item-label"> <div class="item-label">
@ -43,7 +45,9 @@
</el-icon> </el-icon>
<span>存款金额</span> <span>存款金额</span>
</div> </div>
<div class="item-value text-highlight">{{ userInfo.deposit || '0' }}</div> <div class="item-value text-highlight">
<AnimatedNumber :value="Number(userInfo.deposit) || 0" />
</div>
</div> </div>
<div class="item"> <div class="item">
<div class="item-label"> <div class="item-label">
@ -70,7 +74,9 @@
</el-icon> </el-icon>
<span>历史交易金额</span> <span>历史交易金额</span>
</div> </div>
<div class="item-value">{{ userInfo.hisMeny || '-' }}</div> <div class="item-value">
<AnimatedNumber :value="Number(userInfo.hisMeny) || 0" />
</div>
</div> </div>
</div> </div>
</div> </div>
@ -92,8 +98,20 @@ import { UserFilled, Phone, Message, Money, Wallet, Clock, CreditCard } from '@e
import CustomerListTable from '@/components/common/Table.vue' import CustomerListTable from '@/components/common/Table.vue'
import { getCustomerDetailApi } from '@/api/modules/agent/customerDetail' import { getCustomerDetailApi } from '@/api/modules/agent/customerDetail'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import AnimatedNumber from './AnimatedNumber.vue'
const roleMap: { [key: number]: string } = { //
import type {
RoleType,
CustomerUserInfo,
Account,
CustomerDetailResponse,
UserDisplayInfo,
TableDataRow,
TableColumn
} from '@/api/types/agent'
const roleMap: Record<RoleType, string> = {
1: '平台', 1: '平台',
2: '承包商', 2: '承包商',
3: '特殊做市商', 3: '特殊做市商',
@ -129,21 +147,21 @@ const loading = ref(false)
const tableLoading = ref(false) const tableLoading = ref(false)
// //
const userInfo = ref({ const userInfo = ref<UserDisplayInfo>({
name: '', name: '',
id: '', id: '',
role: '', role: '',
phone: '', phone: '',
email: '', email: '',
deposit: '', deposit: 0,
balance: '', balance: 0,
registerTime: '', registerTime: '',
hisMeny: 122, hisMeny: 0,
}) })
// //
const tableData = ref() const tableData = ref<TableDataRow[]>([])
const tableColumnsOptions = ref([ const tableColumnsOptions = ref<TableColumn[]>([
{ label: '交易账号', prop: 'tradeAccount' }, { label: '交易账号', prop: 'tradeAccount' },
{ label: '账户名称', prop: 'accountName' }, { label: '账户名称', prop: 'accountName' },
{ label: '余额', prop: 'balance', sortable: true }, { label: '余额', prop: 'balance', sortable: true },
@ -153,40 +171,38 @@ const tableColumnsOptions = ref([
]) ])
// ===================== ===================== // ===================== =====================
const loadDetailData = async (id: string | number) => { const loadDetailData = async (id: string | number) => {
loading.value = true loading.value = true
tableLoading.value = true tableLoading.value = true
try { try {
// const res: CustomerDetailResponse = await getCustomerDetailApi({ user_id: id });
getCustomerDetailApi({ user_id: id }).then((res) => { const { accounts, user } = res;
console.log('res:', res);
const { accounts, user } = res; userInfo.value = {
userInfo.value = { name: user.email || user.mobile || '-',
name: user.email || user.mobile, id: user.id,
id: user.id, role: roleMap[user.role_id] || '-',
role: roleMap[user.role_id], phone: user.mobile || '-',
phone: user.mobile || '-', email: user.email || '-',
email: user.email || '-', balance: Number(user.capital_amount) || 0,
balance: user.capital_amount, deposit: Number(user.total_recharge_amount) || 0,
deposit: user.total_recharge_amount, registerTime: dayjs(user.created_at).format('YYYY-MM-DD HH:mm:ss'),
registerTime: dayjs(user.created_at).format('YYYY-MM-DD HH:mm:ss'), hisMeny: Number(user.history_transfer_amount) || 0,
}
hisMeny: user.history_transfer_amount, //
} tableData.value = accounts.map((item: Account): TableDataRow => ({
tradeAccount: item.account_id,
// accountName: item.account_name,
tableData.value = accounts.map((item: any) => ({ balance: Number(item.wallets_trade.amount) || 0,
tradeAccount: item.account_id, leverage: Number(item.wallets_trade.lever) || 0,
accountName: item.account_name, tradeVolume: Number(item.collect_data) || 0,
balance: item.wallets_trade.amount, addTime: dayjs(item.created_at).format('YYYY-MM-DD HH:mm:ss'),
leverage: item.wallets_trade.lever, }))
tradeVolume: item.collect_data,
addTime: dayjs(item.created_at).format('YYYY-MM-DD HH:mm:ss'),
}))
})
} catch (err) { } catch (err) {
console.error('加载详情失败:', err);
ElMessage.error('加载详情失败') ElMessage.error('加载详情失败')
} finally { } finally {
loading.value = false loading.value = false
@ -203,8 +219,9 @@ watch(
}, },
{ immediate: true } { immediate: true }
) )
// //
const handleAccount = (row: any) => { const handleAccount = (row: TableDataRow) => {
ElMessage.success('操作账户:' + row.tradeAccount) ElMessage.success('操作账户:' + row.tradeAccount)
} }
</script> </script>

View File

@ -40,9 +40,12 @@
<div class="info-item"> <div class="info-item">
<span class="label">手机号</span> <span class="label">手机号</span>
<span class="value">{{ userInfo.mobile || '未绑定' }}</span> <span class="value">{{ userInfo.mobile || '未绑定' }}</span>
<el-button link type="primary" @click="showEditDialog('mobile')"> <el-button v-if="!userInfo.mobile" link type="primary" @click="showEditDialog('mobile')">
{{ userInfo.mobile ? '重新绑定' : '绑定' }} 绑定
</el-button> </el-button>
<template v-else>
<el-tag type="success" size="small">已绑定</el-tag>
</template>
</div> </div>
<div class="info-item"> <div class="info-item">
@ -63,56 +66,24 @@
</div> </div>
<!-- 编辑弹窗 --> <!-- 编辑弹窗 -->
<Dialog <Dialog :visible="dialogVisible" :title="dialogTitle" width="500px" @confirm="handleEditConfirm"
:visible="dialogVisible" @cancel="handleEditDialogReset" @close="handleEditDialogReset">
:title="dialogTitle" <el-form ref="editFormRef" :model="editForm" :rules="getEditRules()" label-width="80px" label-position="top">
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-form-item :label="editForm.label" prop="value">
<el-input <el-input v-if="editField !== 'language'" v-model="editForm.value" :placeholder="`请输入${editForm.label}`"
v-if="editField !== 'language'" size="large" />
v-model="editForm.value"
:placeholder="`请输入${editForm.label}`"
size="large"
/>
</el-form-item> </el-form-item>
<UserCaptcha <UserCaptcha v-if="['email', 'mobile'].includes(editField)" v-model="editForm.code"
v-if="['email', 'mobile'].includes(editField)" @sendCode="handleSendCaptcha" :show-send-button="showSendButton" :send-state="sendState" />
v-model="editForm.code"
@sendCode="handleSendCaptcha"
:show-send-button="showSendButton"
:send-state="sendState"
/>
</el-form> </el-form>
</Dialog> </Dialog>
<!-- 密码修改弹窗 验证码模式 --> <!-- 密码修改弹窗 验证码模式 -->
<Dialog <Dialog :visible="passwordDialogVisible" title="修改密码" width="500px" @confirm="handlePasswordConfirm"
:visible="passwordDialogVisible" @cancel="handlePasswordDialogReset" @close="handlePasswordDialogReset">
title="修改密码" <el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordFormRules" label-width="100px"
width="500px" label-position="top">
@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-form-item label="验证方式" prop="verifyType">
<el-radio-group v-model="passwordForm.verifyType" @change="onVerifyTypeChange"> <el-radio-group v-model="passwordForm.verifyType" @change="onVerifyTypeChange">
@ -128,35 +99,20 @@
</span> </span>
</el-form-item> </el-form-item>
<!-- 验证码 --> <!-- 验证码 -->
<UserCaptcha <UserCaptcha prop="code" v-model="passwordForm.code" @sendCode="handleSendPwdCaptcha"
prop="code" :show-send-button="showPwdSendButton" :send-state="pwdSendState" />
v-model="passwordForm.code"
@sendCode="handleSendPwdCaptcha"
:show-send-button="showPwdSendButton"
:send-state="pwdSendState"
/>
<!-- 新密码1 --> <!-- 新密码1 -->
<el-form-item label="新密码" prop="login_password1"> <el-form-item label="新密码" prop="login_password1">
<el-input <el-input v-model="passwordForm.login_password1" type="password" show-password placeholder="6-12位含大小写+特殊字符"
v-model="passwordForm.login_password1" size="large" />
type="password"
show-password
placeholder="6-12位含大小写+特殊字符"
size="large"
/>
</el-form-item> </el-form-item>
<!-- 新密码2 --> <!-- 新密码2 -->
<el-form-item label="确认密码" prop="login_password2"> <el-form-item label="确认密码" prop="login_password2">
<el-input <el-input v-model="passwordForm.login_password2" type="password" show-password placeholder="请再次输入新密码"
v-model="passwordForm.login_password2" size="large" />
type="password"
show-password
placeholder="请再次输入新密码"
size="large"
/>
</el-form-item> </el-form-item>
</el-form> </el-form>
</Dialog> </Dialog>
@ -164,7 +120,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'; import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'; import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { UserFilled } from '@element-plus/icons-vue'; import { UserFilled } from '@element-plus/icons-vue';
import Dialog from '@/components/common/Dialog.vue'; import Dialog from '@/components/common/Dialog.vue';
@ -174,6 +130,18 @@ import { useLanguageStore } from '@/stores/modules/language';
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha'; import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha';
import { codeRules } from '@/views/user/config/rules'; import { codeRules } from '@/views/user/config/rules';
//
import type {
UserInfo,
EditForm,
PasswordForm,
EditLangTypeParams,
BindMobileParams,
BindEmailParams,
EditUsernameParams,
UpdatePwdByCaptchaParams
} from '@/api/types/profile';
// //
import { import {
getUserInfoApi, getUserInfoApi,
@ -181,10 +149,10 @@ import {
bindMobileApi, bindMobileApi,
bindEmailApi, bindEmailApi,
editUsernameApi, editUsernameApi,
updatePwdByCaptchaApi // updatePwdByCaptchaApi
} from '@/api/modules/profile/personalCenter'; } from '@/api/modules/profile/personalCenter';
const userInfo = reactive({ const userInfo = reactive<UserInfo>({
id: 0, id: 0,
username: '', username: '',
email: '', email: '',
@ -198,20 +166,20 @@ const dialogVisible = ref(false);
const passwordDialogVisible = ref(false); const passwordDialogVisible = ref(false);
const editFormRef = ref<FormInstance>(); const editFormRef = ref<FormInstance>();
const passwordFormRef = ref<FormInstance>(); const passwordFormRef = ref<FormInstance>();
const editField = ref(''); const editField = ref<string>('');
const dialogTitle = ref(''); const dialogTitle = ref('');
const editForm = reactive({ label: '', value: '', code: '' }); const editForm = reactive<EditForm>({ label: '', value: '', code: '' });
// //
const passwordForm = reactive({ const passwordForm = reactive<PasswordForm>({
verifyType: 'mobile' as 'mobile' | 'email', verifyType: 'mobile',
code: '', code: '',
login_password1: '', login_password1: '',
login_password2: '' login_password2: ''
}); });
// //
const showSendButton = ref(true); const showSendButton = ref(true);
const sendState = ref({ isCounting: false, countdown: 0 }); const sendState = ref({ isCounting: false, countdown: 0 });
const phoneCaptchaState = initCaptchaState('phone'); const phoneCaptchaState = initCaptchaState('phone');
@ -262,29 +230,40 @@ const passwordFormRules = ref<FormRules>({
login_password1: [ login_password1: [
{ required: true, message: '请输入新密码', trigger: 'blur' }, { required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 12, message: '长度 6-12 位', 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' } pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{6,12}$/,
message: '必须包含大小写+特殊字符', trigger: 'blur'
}
], ],
login_password2: [ login_password2: [
{ required: true, message: '请确认新密码', trigger: 'blur' }, { required: true, message: '请确认新密码', trigger: 'blur' },
{ {
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
if (value !== passwordForm.login_password1) callback(new Error('两次密码不一致')); if (value !== passwordForm.login_password1) {
callback(); callback(new Error('两次密码不一致'));
}, trigger: 'blur' } else {
callback();
}
},
trigger: 'blur'
} }
] ]
}); });
// //
const fetchUserProfile = async () => { const fetchUserProfile = async () => {
const res = await getUserInfoApi(); try {
Object.assign(userInfo, res); const res = await getUserInfoApi();
languageStore.changeLang(res.lang_type === 1 ? 'zh-CN' : 'en-US'); Object.assign(userInfo, res);
languageStore.changeLang(res.lang_type === 1 ? 'zh-CN' : 'en-US');
} catch (error) {
ElMessage.error('获取用户信息失败');
console.error('获取用户信息失败:', error);
}
}; };
// //
const showEditDialog = (field: string) => { const showEditDialog = (field: 'email' | 'mobile' | 'username') => {
editField.value = field; editField.value = field;
editForm.code = ''; editForm.code = '';
switch (field) { switch (field) {
@ -312,6 +291,10 @@ const showEditDialog = (field: string) => {
const handleSendCaptcha = async () => { const handleSendCaptcha = async () => {
const type = editField.value === 'email' ? 'email' : 'phone'; const type = editField.value === 'email' ? 'email' : 'phone';
const receiver = editForm.value?.trim(); const receiver = editForm.value?.trim();
if (!receiver) {
ElMessage.warning('请输入接收验证码的账号');
return;
}
const currentState = type === 'email' ? emailCaptchaState : phoneCaptchaState; const currentState = type === 'email' ? emailCaptchaState : phoneCaptchaState;
await sendCaptcha(type, receiver, currentState, () => { await sendCaptcha(type, receiver, currentState, () => {
return userStore.sendCode({ return userStore.sendCode({
@ -325,43 +308,53 @@ const handleSendCaptcha = async () => {
const updateSendState = () => { const updateSendState = () => {
const t = editField.value === 'email' ? emailCaptchaState : phoneCaptchaState; const t = editField.value === 'email' ? emailCaptchaState : phoneCaptchaState;
sendState.value = { isCounting: t.isCounting, countdown: t.countdown }; sendState.value = { isCounting: t.isCounting.value, countdown: t.countdown.value };
}; };
// //
const handleLanguageChange = async (val: number) => { const handleLanguageChange = async (val: number) => {
await editLangTypeApi(val); try {
userInfo.lang_type = val; await editLangTypeApi({ lang_type: val });
languageStore.changeLang(val === 1 ? 'zh-CN' : 'en-US'); userInfo.lang_type = val;
ElMessage.success('语言切换成功'); languageStore.changeLang(val === 1 ? 'zh-CN' : 'en-US');
ElMessage.success('语言切换成功');
} catch (error) {
ElMessage.error('语言切换失败');
console.error('语言切换失败:', error);
}
}; };
// //
const handleEditConfirm = async () => { const handleEditConfirm = async () => {
if (!editFormRef.value) return; if (!editFormRef.value) return;
await editFormRef.value.validate();
try { try {
await editFormRef.value.validate();
if (editField.value === 'username') { if (editField.value === 'username') {
await editUsernameApi({ username: editForm.value, user_id: userInfo.id }); const params: EditUsernameParams = { username: editForm.value, user_id: userInfo.id };
await editUsernameApi(params);
userInfo.username = editForm.value; userInfo.username = editForm.value;
} }
if (editField.value === 'mobile') { if (editField.value === 'mobile') {
await bindMobileApi({ mobile: editForm.value, code: editForm.code }); const params: BindMobileParams = { mobile: editForm.value, code: editForm.code };
await bindMobileApi(params);
userInfo.mobile = editForm.value; userInfo.mobile = editForm.value;
} }
if (editField.value === 'email') { if (editField.value === 'email') {
await bindEmailApi({ email: editForm.value, code: editForm.code }); const params: BindEmailParams = { email: editForm.value, code: editForm.code };
await bindEmailApi(params);
userInfo.email = editForm.value; userInfo.email = editForm.value;
} }
ElMessage.success('操作成功'); ElMessage.success('操作成功');
dialogVisible.value = false; dialogVisible.value = false;
editFormRef.value.resetFields(); editFormRef.value.resetFields();
} catch (err) { } catch (err) {
ElMessage.error('操作失败'); // APIElMessage
console.error('操作失败:', err);
} }
}; };
// ==================== ==================== // ==================== ====================
const showPasswordDialog = () => { const showPasswordDialog = () => {
if (!userInfo.mobile && !userInfo.email) { if (!userInfo.mobile && !userInfo.email) {
ElMessage.warning('请先绑定手机或邮箱'); ElMessage.warning('请先绑定手机或邮箱');
@ -373,17 +366,31 @@ const showPasswordDialog = () => {
passwordForm.login_password2 = ''; passwordForm.login_password2 = '';
passwordDialogVisible.value = true; passwordDialogVisible.value = true;
updatePwdSendState(); updatePwdSendState();
//
nextTick(() => {
passwordFormRef.value?.clearValidate();
});
}; };
const onVerifyTypeChange = () => { const onVerifyTypeChange = () => {
passwordForm.code = ''; passwordForm.code = '';
updatePwdSendState(); updatePwdSendState();
//
nextTick(() => {
passwordFormRef.value?.clearValidate(['code']);
});
}; };
// //
const handleSendPwdCaptcha = async () => { const handleSendPwdCaptcha = async () => {
const type = passwordForm.verifyType === 'email' ? 'email' : 'phone'; const type = passwordForm.verifyType === 'email' ? 'email' : 'phone';
const receiver = passwordForm.verifyType === 'email' ? userInfo.email : userInfo.mobile; const receiver = passwordForm.verifyType === 'email' ? userInfo.email : userInfo.mobile;
if (!receiver) {
ElMessage.warning('请先绑定验证账号');
return;
}
const currentState = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState; const currentState = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
await sendCaptcha(type, receiver, currentState, () => { await sendCaptcha(type, receiver, currentState, () => {
@ -398,16 +405,16 @@ const handleSendPwdCaptcha = async () => {
const updatePwdSendState = () => { const updatePwdSendState = () => {
const t = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState; const t = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
pwdSendState.value = { isCounting: t.isCounting, countdown: t.countdown }; pwdSendState.value = { isCounting: t.isCounting.value, countdown: t.countdown.value };
}; };
// 100% //
const handlePasswordConfirm = async () => { const handlePasswordConfirm = async () => {
if (!passwordFormRef.value) return; if (!passwordFormRef.value) return;
await passwordFormRef.value.validate();
try { try {
const params = { await passwordFormRef.value.validate();
const params: UpdatePwdByCaptchaParams = {
reg_type: passwordForm.verifyType === 'email' ? 1 : 2, reg_type: passwordForm.verifyType === 'email' ? 1 : 2,
email: passwordForm.verifyType === 'email' ? userInfo.email : undefined, email: passwordForm.verifyType === 'email' ? userInfo.email : undefined,
mobile: passwordForm.verifyType === 'mobile' ? userInfo.mobile : undefined, mobile: passwordForm.verifyType === 'mobile' ? userInfo.mobile : undefined,
@ -421,7 +428,8 @@ const handlePasswordConfirm = async () => {
passwordDialogVisible.value = false; passwordDialogVisible.value = false;
passwordFormRef.value.resetFields(); passwordFormRef.value.resetFields();
} catch (err) { } catch (err) {
ElMessage.error('密码修改失败'); // APIElMessage
console.error('密码修改失败:', err);
} }
}; };
@ -450,8 +458,10 @@ onUnmounted(() => {
<style scoped lang="scss"> <style scoped lang="scss">
.personal-center { .personal-center {
padding: 24px; padding: 24px;
.page-header { .page-header {
margin-bottom: 20px; margin-bottom: 20px;
h2 { h2 {
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
@ -478,6 +488,7 @@ onUnmounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20px; gap: 20px;
.avatar-wrapper { .avatar-wrapper {
width: 80px; width: 80px;
height: 80px; height: 80px;
@ -487,24 +498,29 @@ onUnmounted(() => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
.avatar-image { .avatar-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
.avatar-icon { .avatar-icon {
font-size: 48px; font-size: 48px;
color: #909399; color: #909399;
} }
} }
.info-text { .info-text {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
.username-row { .username-row {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
gap: 8px; gap: 8px;
.username-text { .username-text {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
@ -512,6 +528,7 @@ onUnmounted(() => {
line-height: 1.2; line-height: 1.2;
} }
} }
.login-time { .login-time {
font-size: 14px; font-size: 14px;
color: #909399; color: #909399;
@ -526,21 +543,25 @@ onUnmounted(() => {
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
} }
.info-item { .info-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 10px 0; padding: 10px 0;
border-bottom: 1px solid #f2f3f5; border-bottom: 1px solid #f2f3f5;
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
} }
.label { .label {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #606266; color: #606266;
min-width: 70px; min-width: 70px;
} }
.value { .value {
flex: 1; flex: 1;
padding: 0 12px; padding: 0 12px;