feat: 增加扫码登录功能

This commit is contained in:
Gsh
2025-08-30 22:28:38 +08:00
parent ba07e2c905
commit 3cae477f3e
13 changed files with 468 additions and 206 deletions

View File

@@ -1,25 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>Hello from Electron renderer!</title>
</head>
<body>
<h1>Hello from Electron renderer!</h1>
<p>👋</p>
<p id="info"></p>
</body>
<script src="./renderer.js"></script>
</html>

View File

@@ -1,26 +0,0 @@
const { app, BrowserWindow } = require('electron/main')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

View File

@@ -1,15 +0,0 @@
{
"name": "file-app",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"electron": "^23.1.3"
}
}

View File

@@ -37,6 +37,7 @@
"@jsonlee_12138/enum": "^1.0.4", "@jsonlee_12138/enum": "^1.0.4",
"@vueuse/core": "^13.5.0", "@vueuse/core": "^13.5.0",
"@vueuse/integrations": "^13.5.0", "@vueuse/integrations": "^13.5.0",
"date-fns": "^2.30.0",
"driver.js": "^1.3.6", "driver.js": "^1.3.6",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"element-plus": "^2.10.4", "element-plus": "^2.10.4",

View File

@@ -32,6 +32,9 @@ importers:
'@vueuse/integrations': '@vueuse/integrations':
specifier: ^13.5.0 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)) 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: driver.js:
specifier: ^1.3.6 specifier: ^1.3.6
version: 1.3.6 version: 1.3.6
@@ -1774,6 +1777,10 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'} 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: dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
@@ -6659,6 +6666,10 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
is-data-view: 1.0.2 is-data-view: 1.0.2
date-fns@2.30.0:
dependencies:
'@babel/runtime': 7.27.6
dayjs@1.11.13: {} dayjs@1.11.13: {}
de-indent@1.0.2: {} de-indent@1.0.2: {}

View File

@@ -2,12 +2,12 @@ import { get, post } from '@/utils/request';
// 获取用户信息 // 获取用户信息
export function getUserInfo() { export function getUserInfo() {
return get<any>('/ai-chat/account').json(); return get<any>('/account/ai').json();
} }
// 获取二维码 LoginOrRegister 登录注册, Bind 绑定 // 获取二维码 LoginOrRegister 登录注册, Bind 绑定
export function getQrCode(data: any) { export function getQrCode(data: any) {
return post<any>('/fuwuhao/qrcode', data).json(); return post<any>(`/fuwuhao/qrcode?sceneType=${data.sceneType}`, data).json();
} }
// 扫码轮询 // 扫码轮询

View File

@@ -2,16 +2,32 @@
import { Check, Picture as IconPicture, Refresh } from '@element-plus/icons-vue'; import { Check, Picture as IconPicture, Refresh } from '@element-plus/icons-vue';
import { useCountdown } from '@vueuse/core'; import { useCountdown } from '@vueuse/core';
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'; 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 urlText = shallowRef('');
const qrCodeUrl = ref(''); const qrCodeUrl = ref('');
const isExpired = ref(false); const isExpired = ref(false);
const isScanned = ref(false); const isScanned = ref(false);
const isConfirming = ref(false); const isConfirming = ref(false);
const isAuthorization = ref(false);
const confirmCountdownSeconds = shallowRef(180); const confirmCountdownSeconds = shallowRef(180);
const sceneStr = ref(''); // 场景值,用于标识二维码 const sceneStr = ref(''); // 场景值,用于标识二维码
const userStore = useUserStore();
const router = useRouter();
const sessionStore = useSessionStore();
// 二维码倒计时实例 // 二维码倒计时实例
const { start: qrStart, stop: qrStop } = useCountdown(shallowRef(600), { const { start: qrStart, stop: qrStop } = useCountdown(shallowRef(600), {
@@ -39,18 +55,9 @@ let statusPolling: number | null = null;
async function fetchQRCodeInfo() { async function fetchQRCodeInfo() {
try { try {
const param = { const param = {
sceneType: 'LoginOrRegister', sceneType: QrCodeType,
}; };
const response = await getQrCode(param); 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) { if (response && response.data.qrCodeUrl && response.data.scene) {
qrCodeUrl.value = response.data.qrCodeUrl; qrCodeUrl.value = response.data.qrCodeUrl;
sceneStr.value = response.data.scene; sceneStr.value = response.data.scene;
@@ -68,30 +75,27 @@ async function fetchQRCodeInfo() {
async function checkQRCodeStatus() { async function checkQRCodeStatus() {
if (!sceneStr.value) if (!sceneStr.value)
return; return;
try { try {
console.log('startStatusPolling---');
const param = { const param = {
scene: sceneStr.value, scene: sceneStr.value,
}; };
const response = await getQrCodeResult(param); const response = await getQrCodeResult(param);
console.log('response.data.sceneResult---', response.data);
console.log('response.data.sceneResult---', response.data.sceneResult);
switch (response.data.sceneResult) { switch (response.data.sceneResult) {
case 'Wait': // Wait case 'Wait': // Wait
// 继续等待 // 继续等待
break; break;
case 'Login': // Login case 'Login': // Login
// 登录成功 // 登录成功
handleLoginSuccess(response.data.token, response.refreshToken); await handleLoginSuccess(response.data.token, response.data.refreshToken);
break; break;
case 'Register': // Register case 'Register': // Register
// 需要注册 // 需要注册
handleRegister(); handleRegister();
break; break;
case 'Bind': // Bind case 'Bind': // Bind
// 需要绑定 // 绑定成功
handleBind(response.data.token); handleBind();
break; break;
case 'Expired': // Expired case 'Expired': // Expired
// 二维码过期 // 二维码过期
@@ -111,65 +115,35 @@ async function checkQRCodeStatus() {
} }
// 处理登录成功 // 处理登录成功
function handleLoginSuccess(token: string, refreshToken: string) { async function handleLoginSuccess(token: string, refreshToken: string) {
// 停止轮询 // 停止轮询
stopPolling(); stopPolling();
userStore.setToken(token, refreshToken);
// 存储token const resUserInfo = await getUserInfo();
localStorage.setItem('access_token', token); userStore.setUserInfo(resUserInfo.data);
localStorage.setItem('refresh_token', refreshToken || '');
// 提示用户 // 提示用户
ElMessage.success('登录成功'); ElMessage.success('登录成功');
// 刷新页面或跳转到首页 await router.replace('/');
setTimeout(() => { await sessionStore.requestSessionList(1, true);
window.location.reload(); userStore.closeLoginDialog();
}, 1000);
} }
// 处理注册授权 // 处理注册授权
function handleRegister() { function handleRegister() {
console.log('需要注册授权');
ElMessage.info('请在微信授权'); 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.success('微信绑定成功');
ElMessage.info('请绑定您的账号'); const resUserInfo = await getUserInfo();
} userStore.setUserInfo(resUserInfo.data);
// 调用父组件方法
// 完成注册 emit('bind-wechat');
async function completeRegistration() {
try {
// 调用后端API完成注册
// const response = await completeRegister({ token });
// ElMessage.success('注册成功,正在登录...');
// 模拟登录成功
// handleLoginSuccess();
}
catch (error) {
console.error('注册失败:', error);
ElMessage.error('注册失败,请重试');
}
} }
// 更新UI状态 // 更新UI状态
@@ -180,11 +154,14 @@ function updateUIStatus(status: string) {
isConfirming.value = false; isConfirming.value = false;
break; break;
case 'Login': // Login - 已扫码并确认 case 'Login': // Login - 已扫码并确认
case 'Register': // Register - 已扫码并确认
case 'Bind': // Bind - 已扫码并确认 case 'Bind': // Bind - 已扫码并确认
isScanned.value = true; isScanned.value = true;
isConfirming.value = false; isConfirming.value = false;
break; break;
case 'Register': // Register - 已扫码并确认
isScanned.value = true;
isAuthorization.value = true;
break;
case 'Expired': // Expired case 'Expired': // Expired
isExpired.value = true; isExpired.value = true;
isScanned.value = false; 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() { function stopPolling() {
if (statusPolling) { if (statusPolling) {
@@ -247,7 +201,6 @@ async function handleRefresh() {
/** 启动状态轮询 */ /** 启动状态轮询 */
function startStatusPolling() { function startStatusPolling() {
console.log('1111----');
stopPolling(); // 先停止之前的轮询 stopPolling(); // 先停止之前的轮询
statusPolling = setInterval(async () => { statusPolling = setInterval(async () => {
@@ -260,7 +213,6 @@ function startStatusPolling() {
/** 组件初始化 */ /** 组件初始化 */
onMounted(async () => { onMounted(async () => {
const success = await fetchQRCodeInfo(); const success = await fetchQRCodeInfo();
console.log('qrCodeUrl---', success);
if (success) { if (success) {
qrStart(); qrStart();
startStatusPolling(); startStatusPolling();
@@ -277,23 +229,15 @@ onBeforeUnmount(() => {
stopPolling(); stopPolling();
}); });
// 监听URL参数中的微信code适用于微信授权回调
onMounted(() => { onMounted(() => {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
if (code) {
// 处理微信授权回调
handleWechatAuth(code);
}
}); });
</script> </script>
<template> <template>
<div class="qr-wrapper"> <div class="qr-wrapper">
<div class="tip"> <div class="tip">
请使用手机微信扫码登录 {{ QrCodeType === WECHAT_QRCODE_TYPE.Bind ? '请使用手机微信扫码绑定' : '请使用手机微信扫码登录/注册' }}
</div> </div>
<div class="qr-img-wrapper"> <div class="qr-img-wrapper">
@@ -329,6 +273,10 @@ onMounted(() => {
<p v-if="isConfirming" class="scanned-text"> <p v-if="isConfirming" class="scanned-text">
请在手机端确认登录 请在手机端确认登录
</p> </p>
<p v-if="isAuthorization" class="scanned-text">
请在手机端微信继续操作<br>
请关注微信服务号并授权
</p>
<p v-else class="scanned-text"> <p v-else class="scanned-text">
处理中... 处理中...
@@ -338,12 +286,76 @@ onMounted(() => {
</div> </div>
<div class="help-text"> <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>
</div> </div>
</template> </template>
<style scoped lang="scss"> <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 { .qr-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -142,11 +142,12 @@ function handleLoginAgainYi() {
userStore.setToken(token, refreshToken); userStore.setToken(token, refreshToken);
const resUserInfo = await getUserInfo(); const resUserInfo = await getUserInfo();
userStore.setUserInfo(resUserInfo.data); userStore.setUserInfo(resUserInfo.data);
ElMessage.success('登录成功');
// 关闭弹窗 // 关闭弹窗
if (popup && !popup.closed) if (popup && !popup.closed)
popup.close(); popup.close();
// 后续逻辑 // 后续逻辑
ElMessage.success('登录成功');
userStore.closeLoginDialog(); userStore.closeLoginDialog();
await sessionStore.requestSessionList(1, true); await sessionStore.requestSessionList(1, true);
await router.replace('/'); await router.replace('/');

View File

@@ -1,24 +1,338 @@
<script lang="ts" setup> <script lang="ts" setup>
interface User { import { Camera, Edit, SuccessFilled } from '@element-plus/icons-vue';
name: string; import { format } from 'date-fns';
email: string; import { computed, ref } from 'vue';
role: string; 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: '管理员' }, function getSexText(sex: string | null) {
{ name: '李四', email: 'lisi@example.com', role: '编辑' }, const sexMap: Record<string, string> = {
{ name: '王五', email: 'wangwu@example.com', role: '查看者' }, 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> </script>
<template> <template>
<div class="user-management"> <div class="user-profile">
<h3>用户管理</h3> <el-card class="profile-card">
<el-table :data="users" style="width: 100%"> <template #header>
<el-table-column prop="name" label="姓名" /> <div class="card-header">
<el-table-column prop="email" label="邮箱" /> <h3>个人信息</h3>
<el-table-column prop="role" label="角色" /> <el-button v-if="false" type="primary" size="small" @click="handleEdit">
</el-table> <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> </div>
</template> </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>

View File

@@ -58,9 +58,10 @@ const popoverList = ref([
const dialogVisible = ref(false); const dialogVisible = ref(false);
const navItems = [ const navItems = [
// { name: 'user', label: '用户管理', icon: 'User' }, { name: 'user', label: '用户信息', icon: 'User' },
// { name: 'role', label: '角色管理', icon: 'Avatar' }, // { name: 'role', label: '角色管理', icon: 'Avatar' },
// { name: 'permission', label: '权限管理', icon: 'Key' }, // { name: 'permission', label: '权限管理', icon: 'Key' },
// { name: 'userInfo', label: '用户信息', icon: 'User' },
{ name: 'apiKey', label: 'API密钥', icon: 'Key' }, { name: 'apiKey', label: 'API密钥', icon: 'Key' },
{ name: 'rechargeLog', label: '充值记录', icon: 'Document' }, { name: 'rechargeLog', label: '充值记录', icon: 'Document' },
{ name: 'usageStatistics', label: '用量统计', icon: 'Histogram' }, { name: 'usageStatistics', label: '用量统计', icon: 'Histogram' },

View File

@@ -18,3 +18,10 @@ export function getUserProfilePicture(): string {
// 系统头像(可以常量) // 系统头像(可以常量)
export const systemProfilePicture = `/images/logo.png`; export const systemProfilePicture = `/images/logo.png`;
// 获取微信二维码类型
// 获取二维码 LoginOrRegister 登录注册, Bind 绑定
export const WECHAT_QRCODE_TYPE = {
LoginOrRegister: 'LoginOrRegister',
Bind: 'Bind',
};

View File

@@ -19,6 +19,8 @@ declare module 'vue' {
ElCollapse: typeof import('element-plus/es')['ElCollapse'] ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElContainer: typeof import('element-plus/es')['ElContainer'] 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'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider'] ElDivider: typeof import('element-plus/es')['ElDivider']
ElEmpty: typeof import('element-plus/es')['ElEmpty'] ElEmpty: typeof import('element-plus/es')['ElEmpty']