feat: 增加扫码登录功能
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
"@jsonlee_12138/enum": "^1.0.4",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"@vueuse/integrations": "^13.5.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"driver.js": "^1.3.6",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.10.4",
|
||||
|
||||
11
Yi.Ai.Vue3/pnpm-lock.yaml
generated
11
Yi.Ai.Vue3/pnpm-lock.yaml
generated
@@ -32,6 +32,9 @@ importers:
|
||||
'@vueuse/integrations':
|
||||
specifier: ^13.5.0
|
||||
version: 13.5.0(async-validator@4.2.5)(nprogress@0.2.0)(qrcode@1.5.4)(vue@3.5.17(typescript@5.8.3))
|
||||
date-fns:
|
||||
specifier: ^2.30.0
|
||||
version: 2.30.0
|
||||
driver.js:
|
||||
specifier: ^1.3.6
|
||||
version: 1.3.6
|
||||
@@ -1774,6 +1777,10 @@ packages:
|
||||
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
date-fns@2.30.0:
|
||||
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
|
||||
engines: {node: '>=0.11'}
|
||||
|
||||
dayjs@1.11.13:
|
||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||
|
||||
@@ -6659,6 +6666,10 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-data-view: 1.0.2
|
||||
|
||||
date-fns@2.30.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
|
||||
dayjs@1.11.13: {}
|
||||
|
||||
de-indent@1.0.2: {}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { get, post } from '@/utils/request';
|
||||
|
||||
// 获取用户信息
|
||||
export function getUserInfo() {
|
||||
return get<any>('/ai-chat/account').json();
|
||||
return get<any>('/account/ai').json();
|
||||
}
|
||||
|
||||
// 获取二维码 LoginOrRegister 登录注册, Bind 绑定
|
||||
export function getQrCode(data: any) {
|
||||
return post<any>('/fuwuhao/qrcode', data).json();
|
||||
return post<any>(`/fuwuhao/qrcode?sceneType=${data.sceneType}`, data).json();
|
||||
}
|
||||
|
||||
// 扫码轮询
|
||||
|
||||
@@ -2,16 +2,32 @@
|
||||
import { Check, Picture as IconPicture, Refresh } from '@element-plus/icons-vue';
|
||||
import { useCountdown } from '@vueuse/core';
|
||||
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
||||
import { getQrCode, getQrCodeResult, getWechatAuth } from '@/api';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getQrCode, getQrCodeResult, getUserInfo } from '@/api';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session.ts';
|
||||
import { WECHAT_QRCODE_TYPE } from '@/utils/user.ts';
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: WECHAT_QRCODE_TYPE.LoginOrRegister,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['bind-wechat']);
|
||||
const QrCodeType = props.type || WECHAT_QRCODE_TYPE.LoginOrRegister;
|
||||
// 响应式状态
|
||||
// const urlText = shallowRef('');
|
||||
const qrCodeUrl = ref('');
|
||||
const isExpired = ref(false);
|
||||
const isScanned = ref(false);
|
||||
const isConfirming = ref(false);
|
||||
const isAuthorization = ref(false);
|
||||
const confirmCountdownSeconds = shallowRef(180);
|
||||
const sceneStr = ref(''); // 场景值,用于标识二维码
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
// 二维码倒计时实例
|
||||
const { start: qrStart, stop: qrStop } = useCountdown(shallowRef(600), {
|
||||
@@ -39,18 +55,9 @@ let statusPolling: number | null = null;
|
||||
async function fetchQRCodeInfo() {
|
||||
try {
|
||||
const param = {
|
||||
sceneType: 'LoginOrRegister',
|
||||
sceneType: QrCodeType,
|
||||
};
|
||||
const response = await getQrCode(param);
|
||||
console.log('response---', response);
|
||||
// {
|
||||
// "code": 200,
|
||||
// "data": {
|
||||
// "qrCodeUrl": "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=gQG17zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAySnR6NzBJcGxhTC0xQmR4TXhFY1UAAgT1XrBoAwRYAgAA",
|
||||
// "scene": "8baf623f79dc452880c6832415deb26a"
|
||||
// },
|
||||
// "msg": "success"
|
||||
// }
|
||||
if (response && response.data.qrCodeUrl && response.data.scene) {
|
||||
qrCodeUrl.value = response.data.qrCodeUrl;
|
||||
sceneStr.value = response.data.scene;
|
||||
@@ -68,30 +75,27 @@ async function fetchQRCodeInfo() {
|
||||
async function checkQRCodeStatus() {
|
||||
if (!sceneStr.value)
|
||||
return;
|
||||
|
||||
try {
|
||||
console.log('startStatusPolling---');
|
||||
const param = {
|
||||
scene: sceneStr.value,
|
||||
};
|
||||
const response = await getQrCodeResult(param);
|
||||
console.log('response.data.sceneResult---', response.data);
|
||||
console.log('response.data.sceneResult---', response.data.sceneResult);
|
||||
|
||||
switch (response.data.sceneResult) {
|
||||
case 'Wait': // Wait
|
||||
// 继续等待
|
||||
break;
|
||||
case 'Login': // Login
|
||||
// 登录成功
|
||||
handleLoginSuccess(response.data.token, response.refreshToken);
|
||||
await handleLoginSuccess(response.data.token, response.data.refreshToken);
|
||||
break;
|
||||
case 'Register': // Register
|
||||
// 需要注册
|
||||
handleRegister();
|
||||
break;
|
||||
case 'Bind': // Bind
|
||||
// 需要绑定
|
||||
handleBind(response.data.token);
|
||||
// 绑定成功
|
||||
handleBind();
|
||||
break;
|
||||
case 'Expired': // Expired
|
||||
// 二维码过期
|
||||
@@ -111,65 +115,35 @@ async function checkQRCodeStatus() {
|
||||
}
|
||||
|
||||
// 处理登录成功
|
||||
function handleLoginSuccess(token: string, refreshToken: string) {
|
||||
async function handleLoginSuccess(token: string, refreshToken: string) {
|
||||
// 停止轮询
|
||||
stopPolling();
|
||||
|
||||
// 存储token
|
||||
localStorage.setItem('access_token', token);
|
||||
localStorage.setItem('refresh_token', refreshToken || '');
|
||||
|
||||
userStore.setToken(token, refreshToken);
|
||||
const resUserInfo = await getUserInfo();
|
||||
userStore.setUserInfo(resUserInfo.data);
|
||||
// 提示用户
|
||||
ElMessage.success('登录成功');
|
||||
|
||||
// 刷新页面或跳转到首页
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
await router.replace('/');
|
||||
await sessionStore.requestSessionList(1, true);
|
||||
userStore.closeLoginDialog();
|
||||
}
|
||||
|
||||
// 处理注册授权
|
||||
function handleRegister() {
|
||||
console.log('需要注册授权');
|
||||
ElMessage.info('请在微信授权');
|
||||
// const appId = 'wx373eb3ecd65bdac4';
|
||||
// const redirectUri = encodeURIComponent('http://localhost:17001/wechat/callback');
|
||||
// const state = 'register'; // 用于防止CSRF,可根据业务生成随机字符串
|
||||
|
||||
// scope 可以选 snsapi_base(静默,只拿 openid) 或 snsapi_userinfo(需要用户确认,可拿昵称头像)
|
||||
// const scope = 'snsapi_userinfo';
|
||||
|
||||
// 拼接授权链接
|
||||
// const wechatAuthUrl
|
||||
// = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}`
|
||||
// + `&redirect_uri=${redirectUri}`
|
||||
// + `&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
|
||||
// console.log('wechatAuthUrl---', wechatAuthUrl);
|
||||
// 打开授权页(推荐用 location.href 替换,直接跳转)
|
||||
// window.location.href = wechatAuthUrl;
|
||||
}
|
||||
|
||||
// 处理绑定
|
||||
function handleBind(token: string) {
|
||||
async function handleBind() {
|
||||
// 停止轮询
|
||||
stopPolling();
|
||||
// 处理账号绑定逻辑
|
||||
console.log('需要绑定,临时token:', token);
|
||||
ElMessage.info('请绑定您的账号');
|
||||
}
|
||||
|
||||
// 完成注册
|
||||
async function completeRegistration() {
|
||||
try {
|
||||
|
||||
// 调用后端API完成注册
|
||||
// const response = await completeRegister({ token });
|
||||
// ElMessage.success('注册成功,正在登录...');
|
||||
// 模拟登录成功
|
||||
// handleLoginSuccess();
|
||||
}
|
||||
catch (error) {
|
||||
console.error('注册失败:', error);
|
||||
ElMessage.error('注册失败,请重试');
|
||||
}
|
||||
ElMessage.success('微信绑定成功');
|
||||
const resUserInfo = await getUserInfo();
|
||||
userStore.setUserInfo(resUserInfo.data);
|
||||
// 调用父组件方法
|
||||
emit('bind-wechat');
|
||||
}
|
||||
|
||||
// 更新UI状态
|
||||
@@ -180,11 +154,14 @@ function updateUIStatus(status: string) {
|
||||
isConfirming.value = false;
|
||||
break;
|
||||
case 'Login': // Login - 已扫码并确认
|
||||
case 'Register': // Register - 已扫码并确认
|
||||
case 'Bind': // Bind - 已扫码并确认
|
||||
isScanned.value = true;
|
||||
isConfirming.value = false;
|
||||
break;
|
||||
case 'Register': // Register - 已扫码并确认
|
||||
isScanned.value = true;
|
||||
isAuthorization.value = true;
|
||||
break;
|
||||
case 'Expired': // Expired
|
||||
isExpired.value = true;
|
||||
isScanned.value = false;
|
||||
@@ -197,29 +174,6 @@ function updateUIStatus(status: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 在微信授权页面获取到微信code后发给服务端
|
||||
async function handleWechatAuth(code: string) {
|
||||
try {
|
||||
const param = {
|
||||
code,
|
||||
};
|
||||
const response = await getWechatAuth(param);
|
||||
|
||||
if (response.success) {
|
||||
// 授权成功,可以获取用户信息或完成登录
|
||||
console.log('微信授权成功', response);
|
||||
}
|
||||
else {
|
||||
console.error('微信授权失败:', response.message);
|
||||
ElMessage.error(`微信授权失败: ${response.message}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('处理微信授权失败:', error);
|
||||
ElMessage.error('处理微信授权时出错');
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止所有轮询 */
|
||||
function stopPolling() {
|
||||
if (statusPolling) {
|
||||
@@ -247,7 +201,6 @@ async function handleRefresh() {
|
||||
|
||||
/** 启动状态轮询 */
|
||||
function startStatusPolling() {
|
||||
console.log('1111----');
|
||||
stopPolling(); // 先停止之前的轮询
|
||||
|
||||
statusPolling = setInterval(async () => {
|
||||
@@ -260,7 +213,6 @@ function startStatusPolling() {
|
||||
/** 组件初始化 */
|
||||
onMounted(async () => {
|
||||
const success = await fetchQRCodeInfo();
|
||||
console.log('qrCodeUrl---', success);
|
||||
if (success) {
|
||||
qrStart();
|
||||
startStatusPolling();
|
||||
@@ -277,23 +229,15 @@ onBeforeUnmount(() => {
|
||||
stopPolling();
|
||||
});
|
||||
|
||||
// 监听URL参数中的微信code(适用于微信授权回调)
|
||||
onMounted(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const state = urlParams.get('state');
|
||||
|
||||
if (code) {
|
||||
// 处理微信授权回调
|
||||
handleWechatAuth(code);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="qr-wrapper">
|
||||
<div class="tip">
|
||||
请使用手机微信扫码登录
|
||||
{{ QrCodeType === WECHAT_QRCODE_TYPE.Bind ? '请使用手机微信扫码绑定' : '请使用手机微信扫码登录/注册' }}
|
||||
</div>
|
||||
|
||||
<div class="qr-img-wrapper">
|
||||
@@ -329,6 +273,10 @@ onMounted(() => {
|
||||
<p v-if="isConfirming" class="scanned-text">
|
||||
请在手机端确认登录
|
||||
</p>
|
||||
<p v-if="isAuthorization" class="scanned-text">
|
||||
请在手机端微信继续操作<br>
|
||||
请关注微信服务号并授权
|
||||
</p>
|
||||
|
||||
<p v-else class="scanned-text">
|
||||
处理中...
|
||||
@@ -338,12 +286,76 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="help-text">
|
||||
扫码后请在微信中确认登录
|
||||
{{ QrCodeType === WECHAT_QRCODE_TYPE.Bind ? '扫码后请在微信服务号中授权' : '扫码后请在微信服务号中授权' }}
|
||||
</div>
|
||||
|
||||
<div v-if="QrCodeType === WECHAT_QRCODE_TYPE.LoginOrRegister" class="tip-old-user">
|
||||
提示:<br>
|
||||
意社区老用户可返回登录<br>
|
||||
登录后直接绑定微信
|
||||
</div>
|
||||
<div v-if="QrCodeType === WECHAT_QRCODE_TYPE.Bind" class="tip-old-user-bind">
|
||||
提示:<br>
|
||||
若该微信已注册意社区<br>
|
||||
将直接解绑到该账号
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tip-old-user{
|
||||
margin: 5px 0;
|
||||
padding: 6px 12px;
|
||||
color: #F56C6C;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 4rem;
|
||||
border-radius: var(--r);
|
||||
border: none;
|
||||
background: rgba(#fff, 0.1);
|
||||
backdrop-filter: blur(2px);
|
||||
text-shadow: 0.25em 0.25em 1px #00000010;
|
||||
--shadow-color: 0deg 0% 64%;
|
||||
--shadow-elevation-high: 0.5px 1px 1.1px hsl(var(--shadow-color) / 0.28), 1.4px 3.1px 3.4px -0.4px hsl(var(--shadow-color) / 0.27), 2.5px 5.3px 5.9px -0.7px hsl(var(--shadow-color) / 0.25), 3.9px 8.4px 9.3px -1.1px hsl(var(--shadow-color) / 0.24), 6px 12.9px 14.3px -1.5px hsl(var(--shadow-color) / 0.23), 9px 19.5px 21.6px -1.8px hsl(var(--shadow-color) / 0.21), 13.4px 28.9px 32px -2.2px hsl(var(--shadow-color) / 0.2), 19.3px 41.7px 46.2px -2.6px hsl(var(--shadow-color) / 0.19), 27.1px 58.5px 64.8px -2.9px hsl(var(--shadow-color) / 0.17), 37.1px 80px 88.6px -3.3px hsl(var(--shadow-color) / 0.16);
|
||||
--inner-light: inset 0 -6px 2px -5px #ffffff24, inset 0 -8px 3px -5px #ffffff3b, inset 0 -20px 10px -15px #ffffff5c, inset 7px 25px 10px -20px #ffffff5c;
|
||||
--inner-shadow: inset -20px 5px 10px -20px #00000021, inset -40px 50px 7px -55px #00000021;
|
||||
--external-light: 5px -30px 30px -20px #ffffff70, 5px 10px 30px -20px #ffffff70;
|
||||
--default: var(--external-light), var(--shadow-elevation-high), var(--inner-light), var(--inner-shadow);
|
||||
box-shadow: var(--default);
|
||||
background-position: center;
|
||||
animation: gradient 10s linear infinite;
|
||||
background: linear-gradient(45deg, #85d5e757, #7a9ed254, #ba6ac93d, #de54c217, #f86b2d4f);
|
||||
background: #00DB73;
|
||||
|
||||
}
|
||||
.tip-old-user-bind{
|
||||
line-height: 1.5;
|
||||
margin: 5px 0;
|
||||
padding: 6px 12px;
|
||||
color: #ffffff;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 4rem;
|
||||
border-radius: var(--r);
|
||||
border: none;
|
||||
background: rgba(#fff, 0.1);
|
||||
backdrop-filter: blur(2px);
|
||||
text-shadow: 0.25em 0.25em 1px #00000010;
|
||||
--shadow-color: 0deg 0% 64%;
|
||||
--shadow-elevation-high: 0.5px 1px 1.1px hsl(var(--shadow-color) / 0.28), 1.4px 3.1px 3.4px -0.4px hsl(var(--shadow-color) / 0.27), 2.5px 5.3px 5.9px -0.7px hsl(var(--shadow-color) / 0.25), 3.9px 8.4px 9.3px -1.1px hsl(var(--shadow-color) / 0.24), 6px 12.9px 14.3px -1.5px hsl(var(--shadow-color) / 0.23), 9px 19.5px 21.6px -1.8px hsl(var(--shadow-color) / 0.21), 13.4px 28.9px 32px -2.2px hsl(var(--shadow-color) / 0.2), 19.3px 41.7px 46.2px -2.6px hsl(var(--shadow-color) / 0.19), 27.1px 58.5px 64.8px -2.9px hsl(var(--shadow-color) / 0.17), 37.1px 80px 88.6px -3.3px hsl(var(--shadow-color) / 0.16);
|
||||
--inner-light: inset 0 -6px 2px -5px #ffffff24, inset 0 -8px 3px -5px #ffffff3b, inset 0 -20px 10px -15px #ffffff5c, inset 7px 25px 10px -20px #ffffff5c;
|
||||
--inner-shadow: inset -20px 5px 10px -20px #00000021, inset -40px 50px 7px -55px #00000021;
|
||||
--external-light: 5px -30px 30px -20px #ffffff70, 5px 10px 30px -20px #ffffff70;
|
||||
--default: var(--external-light), var(--shadow-elevation-high), var(--inner-light), var(--inner-shadow);
|
||||
box-shadow: var(--default);
|
||||
background-position: center;
|
||||
animation: gradient 10s linear infinite;
|
||||
background: linear-gradient(45deg, #85d5e757, #7a9ed254, #ba6ac93d, #de54c217, #f86b2d4f);
|
||||
background: #ff0000;
|
||||
|
||||
}
|
||||
.qr-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -142,11 +142,12 @@ function handleLoginAgainYi() {
|
||||
userStore.setToken(token, refreshToken);
|
||||
const resUserInfo = await getUserInfo();
|
||||
userStore.setUserInfo(resUserInfo.data);
|
||||
ElMessage.success('登录成功');
|
||||
|
||||
// 关闭弹窗
|
||||
if (popup && !popup.closed)
|
||||
popup.close();
|
||||
// 后续逻辑
|
||||
ElMessage.success('登录成功');
|
||||
userStore.closeLoginDialog();
|
||||
await sessionStore.requestSessionList(1, true);
|
||||
await router.replace('/');
|
||||
|
||||
@@ -1,24 +1,338 @@
|
||||
<script lang="ts" setup>
|
||||
interface User {
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
import { Camera, Edit, SuccessFilled } from '@element-plus/icons-vue';
|
||||
import { format } from 'date-fns';
|
||||
import { computed, ref } from 'vue';
|
||||
import QrCodeLogin from '@/components/LoginDialog/components/QrCodeLogin/index.vue';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { getUserProfilePicture, WECHAT_QRCODE_TYPE } from '@/utils/user.ts';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const user = computed(() => userStore.userInfo.user || {});
|
||||
const wechatDialogVisible = ref(false);
|
||||
|
||||
// 计算属性
|
||||
const userIcon = computed(() => {
|
||||
return getUserProfilePicture() || `https://your-cdn.com/${user.value.icon}`;
|
||||
});
|
||||
|
||||
const userNick = computed(() => {
|
||||
return user.value.nick || user.value.userName || '未知用户';
|
||||
});
|
||||
// 是否绑定了微信
|
||||
const isWechatBound = computed(() => {
|
||||
return userStore.userInfo.isBindFuwuhao || false;
|
||||
});
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateString: string | null) {
|
||||
if (!dateString)
|
||||
return '-';
|
||||
try {
|
||||
return format(new Date(dateString), 'yyyy-MM-dd HH:mm:ss');
|
||||
}
|
||||
catch {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
const users: User[] = [
|
||||
{ name: '张三', email: 'zhangsan@example.com', role: '管理员' },
|
||||
{ name: '李四', email: 'lisi@example.com', role: '编辑' },
|
||||
{ name: '王五', email: 'wangwu@example.com', role: '查看者' },
|
||||
];
|
||||
// 性别显示
|
||||
function getSexText(sex: string | null) {
|
||||
const sexMap: Record<string, string> = {
|
||||
Male: '男',
|
||||
Female: '女',
|
||||
Unknown: '未知',
|
||||
};
|
||||
return sexMap[sex || 'Unknown'] || '未知';
|
||||
}
|
||||
|
||||
function getSexTagType(sex: string | null) {
|
||||
const typeMap: Record<string, string> = {
|
||||
Male: 'primary',
|
||||
Female: 'danger',
|
||||
Unknown: 'info',
|
||||
};
|
||||
return typeMap[sex || 'Unknown'] || 'info';
|
||||
}
|
||||
|
||||
// 敏感信息脱敏
|
||||
function maskEmail(email: string) {
|
||||
if (!email)
|
||||
return '';
|
||||
const [name, domain] = email.split('@');
|
||||
if (name.length <= 2)
|
||||
return email;
|
||||
return `${name.substring(0, 2)}****@${domain}`;
|
||||
}
|
||||
|
||||
function maskPhone(phone: string) {
|
||||
if (!phone)
|
||||
return '';
|
||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
|
||||
}
|
||||
|
||||
// 操作处理
|
||||
function handleEdit() {
|
||||
ElMessage.info('编辑功能开发中');
|
||||
}
|
||||
|
||||
function changeAvatar() {
|
||||
ElMessage.info('更换头像功能开发中');
|
||||
}
|
||||
|
||||
function handleWechatBind() {
|
||||
wechatDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 微信绑定成功
|
||||
function bindWechat() {
|
||||
wechatDialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<h3>用户管理</h3>
|
||||
<el-table :data="users" style="width: 100%">
|
||||
<el-table-column prop="name" label="姓名" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="role" label="角色" />
|
||||
</el-table>
|
||||
<div class="user-profile">
|
||||
<el-card class="profile-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h3>个人信息</h3>
|
||||
<el-button v-if="false" type="primary" size="small" @click="handleEdit">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑信息
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="profile-content">
|
||||
<!-- 头像区域 -->
|
||||
<div class="avatar-section">
|
||||
<el-avatar :size="100" :src="userIcon" class="user-avatar">
|
||||
{{ userNick.charAt(0) }}
|
||||
</el-avatar>
|
||||
<div v-if="false" class="avatar-actions">
|
||||
<el-button size="small" @click="changeAvatar">
|
||||
<el-icon><Camera /></el-icon>
|
||||
更换头像
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div class="info-section">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="用户名">
|
||||
{{ user.userName || '-' }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="昵称">
|
||||
{{ userNick }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="性别">
|
||||
<el-tag :type="getSexTagType(user.sex)">
|
||||
{{ getSexText(user.sex) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="注册时间">
|
||||
{{ formatDate(user.creationTime) }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="邮箱">
|
||||
<span v-if="user.email">
|
||||
{{ maskEmail(user.email) }}
|
||||
<el-tooltip content="已验证" placement="top">
|
||||
<el-icon color="#67C23A"><SuccessFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span v-else class="unset-text">未设置</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="手机号">
|
||||
<span v-if="user.phone">
|
||||
{{ maskPhone(user.phone) }}
|
||||
</span>
|
||||
<span v-else class="unset-text">未设置</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="微信绑定">
|
||||
<div class="wechat-binding">
|
||||
<span v-if="isWechatBound">
|
||||
<el-icon color="#07C160"><SuccessFilled /></el-icon>
|
||||
已绑定
|
||||
<!-- <span class="wechat-id">({{ maskWechat(wechatInfo) }})</span> -->
|
||||
</span>
|
||||
<span v-else class="unset-text">
|
||||
未绑定
|
||||
</span>
|
||||
<el-button
|
||||
v-if="!isWechatBound"
|
||||
type="text"
|
||||
size="small"
|
||||
class="bind-btn"
|
||||
@click="handleWechatBind"
|
||||
>
|
||||
绑定
|
||||
</el-button>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="个人简介">
|
||||
<span v-if="user.introduction">
|
||||
{{ user.introduction }}
|
||||
</span>
|
||||
<span v-else class="unset-text">暂无简介</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 微信绑定对话框 -->
|
||||
<el-dialog
|
||||
v-model="wechatDialogVisible"
|
||||
title="微信绑定"
|
||||
width="400px"
|
||||
>
|
||||
<div class="wechat-dialog">
|
||||
<QrCodeLogin :type="WECHAT_QRCODE_TYPE.Bind" @bind-wechat="bindWechat()" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="wechatDialogVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="wechatDialogVisible = false"
|
||||
>
|
||||
关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.user-profile {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.profile-content {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
margin-bottom: 15px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.avatar-actions {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__body) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__label) {
|
||||
font-weight: 600;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.unset-text {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.wechat-binding {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.wechat-id {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.bind-btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.wechat-dialog {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qrcode-section {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.qrcode-placeholder {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.qrcode-placeholder .el-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wechat-tip {
|
||||
color: #666;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.wechat-info {
|
||||
color: #07C160;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-content {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,9 +58,10 @@ const popoverList = ref([
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const navItems = [
|
||||
// { name: 'user', label: '用户管理', icon: 'User' },
|
||||
{ name: 'user', label: '用户信息', icon: 'User' },
|
||||
// { name: 'role', label: '角色管理', icon: 'Avatar' },
|
||||
// { name: 'permission', label: '权限管理', icon: 'Key' },
|
||||
// { name: 'userInfo', label: '用户信息', icon: 'User' },
|
||||
{ name: 'apiKey', label: 'API密钥', icon: 'Key' },
|
||||
{ name: 'rechargeLog', label: '充值记录', icon: 'Document' },
|
||||
{ name: 'usageStatistics', label: '用量统计', icon: 'Histogram' },
|
||||
|
||||
@@ -18,3 +18,10 @@ export function getUserProfilePicture(): string {
|
||||
|
||||
// 系统头像(可以常量)
|
||||
export const systemProfilePicture = `/images/logo.png`;
|
||||
|
||||
// 获取微信二维码类型
|
||||
// 获取二维码 LoginOrRegister 登录注册, Bind 绑定
|
||||
export const WECHAT_QRCODE_TYPE = {
|
||||
LoginOrRegister: 'LoginOrRegister',
|
||||
Bind: 'Bind',
|
||||
};
|
||||
|
||||
2
Yi.Ai.Vue3/types/components.d.ts
vendored
2
Yi.Ai.Vue3/types/components.d.ts
vendored
@@ -19,6 +19,8 @@ declare module 'vue' {
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
|
||||
Reference in New Issue
Block a user