修改用户中心,修改默认主题

This commit is contained in:
lj124 2026-05-13 00:08:04 +08:00
parent 7890bca4e2
commit 3cb3fb5063
5 changed files with 497 additions and 164 deletions

View File

@ -89,7 +89,7 @@ i {
}
:root {
--header-bg-color: #242f42;
--header-bg-color: #00bcd4;
--header-text-color: #fff;
--active-color: var(--el-color-primary);
}

View File

@ -4,8 +4,8 @@ export const useSidebarStore = defineStore('sidebar', {
state: () => {
return {
collapse: false,
bgColor: localStorage.getItem('sidebar-bg-color') || '#324157',
textColor: localStorage.getItem('sidebar-text-color') || '#bfcbd9'
bgColor: localStorage.getItem('sidebar-bg-color') || '#a6d3df',
textColor: localStorage.getItem('sidebar-text-color') || '#5b6e88'
};
},
getters: {},

View File

@ -4,12 +4,12 @@ import { defineStore } from 'pinia';
export const useThemeStore = defineStore('theme', {
state: () => {
return {
primary: '',
primary: '#00bcd4',
success: '',
warning: '',
danger: '',
info: '',
headerBgColor: '#242f42',
headerBgColor: '#00bcd4',
headerTextColor: '#ffff',
};
},
@ -20,6 +20,9 @@ export const useThemeStore = defineStore('theme', {
const color = localStorage.getItem(`theme-${type}`) || '';
if (color) {
this.setPropertyColor(color, type); // 设置主题色
} else if (type === 'primary') {
// 没有保存的主题色时,使用默认的宁静主题色
this.setPropertyColor(this.primary, type);
}
});
const headerBgColor = localStorage.getItem('header-bg-color');

View File

@ -77,7 +77,7 @@ const themeStore = useThemeStore();
const sidebar = useSidebarStore();
const color = reactive({
primary: localStorage.getItem('theme-primary') || '#409eff',
primary: localStorage.getItem('theme-primary') || '#00bcd4',
success: localStorage.getItem('theme-success') || '#67c23a',
warning: localStorage.getItem('theme-warning') || '#e6a23c',
danger: localStorage.getItem('theme-danger') || '#f56c6c',

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="ucenter-page">
<div class="user-container">
<el-card class="user-profile" shadow="hover" :body-style="{ padding: '0px' }">
<div class="user-profile-bg"></div>
@ -27,13 +27,11 @@
<el-card
class="user-content"
shadow="hover"
:body-style="{ padding: '20px 50px', height: '100%', boxSizing: 'border-box' }"
:body-style="{ padding: '20px 30px', height: '100%', boxSizing: 'border-box' }"
>
<el-tabs tab-position="left" v-model="activeName">
<!-- <el-tab-pane name="label1" label="消息通知" class="user-tabpane">
<TabsComp />
</el-tab-pane> -->
<el-tabs tab-position="left" v-model="activeName" class="user-tabs">
<el-tab-pane name="label2" label="我的头像" class="user-tabpane">
<div class="avatar-section">
<div class="crop-wrap" v-if="activeName === 'label2'">
<vueCropper
ref="cropper"
@ -45,25 +43,34 @@
>
</vueCropper>
</div>
<div class="avatar-actions">
<el-button class="crop-demo-btn" type="primary"
>选择图片
<input class="crop-input" type="file" name="image" accept="image/*" @change="setImage" />
</el-button>
<el-button type="success" @click="saveAvatar">上传并保存</el-button>
</div>
</div>
</el-tab-pane>
<el-tab-pane name="label3" label="修改密码" class="user-tabpane">
<el-form class="w500" label-position="top">
<el-form-item label="旧密码:">
<el-input type="password" v-model="form.old"></el-input>
<el-form class="password-form" label-position="top" :model="form" ref="passwordForm">
<el-form-item label="旧密码:" prop="old" :rules="[{ required: true, message: '请输入旧密码', trigger: 'blur' }]">
<el-input type="password" v-model="form.old" placeholder="请输入旧密码" show-password />
</el-form-item>
<el-form-item label="新密码:">
<el-input type="password" v-model="form.new"></el-input>
<el-form-item label="新密码:" prop="new" :rules="[
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
]">
<el-input type="password" v-model="form.new" placeholder="请输入新密码" show-password />
</el-form-item>
<el-form-item label="确认新密码:">
<el-input type="password" v-model="form.new1"></el-input>
<el-form-item label="确认新密码:" prop="new1" :rules="[
{ required: true, message: '请确认新密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]">
<el-input type="password" v-model="form.new1" placeholder="请再次输入新密码" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPassword ">保存</el-button>
<el-button type="primary" @click="resetPassword">保存</el-button>
</el-form-item>
<el-link type="primary" @click="reset_password()">忘记密码使用验证码重置</el-link>
</el-form>
@ -72,78 +79,116 @@
<TableEdit :form-data="userInfo" :options="options_edit" :edit="true" :update="updateUserInfo" />
</el-tab-pane>
<el-tab-pane name="label5" label="第三方账号" class="user-tabpane">
<div>
<el-select v-model="activePlatformName" placeholder="请选择第三方平台" size="small" style="width: 200px;">
<!-- <el-option label="QQ" value="qq"></el-option>
<el-option label="Github" value="github"></el-option>
<el-option label="gitee" value="gitee"></el-option>
<el-option label="google" value="google"></el-option> -->
<!-- thirdPartyPlatform -->
<template v-for="(item, index) in thirdPartyPlatform" :key="index">
<el-option :label="item.label" :value="item.value"></el-option>
<div class="third-party-section">
<div class="bind-section">
<el-select v-model="activePlatformName" placeholder="请选择第三方平台" size="default" style="width: 200px;">
<template v-for="(item, index) in availablePlatforms" :key="index">
<el-option :label="item.label" :value="item.value">
<span class="platform-option">{{ item.label }}</span>
</el-option>
</template>
</el-select>
<el-button type="primary" @click="thirdLogin(activePlatformName)">绑定</el-button>
<el-button type="primary" @click="thirdLogin(activePlatformName)" :disabled="!activePlatformName">绑定</el-button>
</div>
<div>
<p>已绑定的第三方登录账号</p>
<div class="bound-section" v-if="thirdPartyUserInfo.length > 0">
<h4>已绑定的第三方登录账号</h4>
<div class="bound-accounts">
<template v-for="(item, index) in thirdPartyUserInfo" :key="index">
<el-row :gutter="20" class="user-tabpane">
<el-col :span="12">
<el-tag>{{ item.third_party_platform }}</el-tag>
</el-col>
<el-col :span="12">
<el-avatar :src="item.third_party_user_avatar" size="small"></el-avatar>
</el-col>
<el-col :span="12">
<el-tag>{{ item.third_party_user_name }}</el-tag>
</el-col>
<el-col :span="12">
<el-button type="danger" @click="unBindThirdParty(item.ID)">解绑</el-button>
</el-col>
<!-- <el-tag>{{ item.third_party_platform }}</el-tag>
<el-avatar :src="item.third_party_user_avatar" size="small"></el-avatar>
<el-tag>{{ item.third_party_user_name }}</el-tag>
<el-button type="danger" @click="unBindThirdParty(item.ID)">解绑</el-button> -->
</el-row>
<el-card class="account-card" shadow="hover">
<div class="account-info">
<el-avatar :src="item.third_party_user_avatar" size="medium" class="account-avatar"></el-avatar>
<div class="account-details">
<el-tag type="primary" class="platform-tag">{{ item.third_party_platform }}</el-tag>
<span class="account-name">{{ item.third_party_user_name }}</span>
</div>
</div>
<el-button type="danger" size="small" @click="unBindThirdParty(item.ID)">解绑</el-button>
</el-card>
</template>
</div>
</div>
<div class="empty-state" v-else>
<el-empty description="暂无绑定的第三方账号" />
</div>
</div>
</el-tab-pane>
<el-tab-pane name="label6" label="二次认证" class="user-tabpane">
<br />
<el-switch v-model="second_auth.password_need_second_auth" active-text="打开" inactive-text="密码登录需进行二次认证" />
<br />
<br />
<el-switch v-model="second_auth.code_need_second_auth" active-text="打开" inactive-text="验证码登录需进行二次认证" />
<br />
<br />
<el-switch v-model="second_auth.third_party_need_second_auth" active-text="打开" inactive-text="第三方登录需进行二次认证" />
<br />
<br />
<el-switch v-model="second_auth.ai_second_auth" active-text="打开" inactive-text="智能增强认证" />
<br />
<el-button type="primary" @click="updateUserSecondAuthInfo(userInfo)">保存二次认证设置</el-button>
<div>
<p>TOTP密钥</p>
<el-button type="primary" @click="GenTOTPSecret" :disabled="gen_totp_secret">生成TOTP密钥</el-button>
<div v-if="totp_secret || totp_url" style="margin-top: 20px;">
<!-- 提示 -->
<p>使用支持TOTP的应用如Google AuthenticatorAuthy等扫描二维码或手动输入密钥进行绑定</p>
<p>请妥善保存您的TOTP密钥只能查看一次若丢失请解绑后重新生成</p>
<el-button type="primary" @click="onCopyTOTPSecret" v-if="totp_secret">点击复制密钥</el-button>
<vueQr v-if="totp_url" :text="totp_url" :size="200" :margin="20" :background="'#ffffff'" :foreground="'#000000'" />
<div class="auth-section">
<div class="auth-switches">
<div class="switch-item">
<el-switch v-model="second_auth.password_need_second_auth" active-text="开启" inactive-text="关闭" />
<span class="switch-label">密码登录需进行二次认证</span>
</div>
<div class="switch-item">
<el-switch v-model="second_auth.code_need_second_auth" active-text="开启" inactive-text="关闭" />
<span class="switch-label">验证码登录需进行二次认证</span>
</div>
<div class="switch-item">
<el-switch v-model="second_auth.third_party_need_second_auth" active-text="开启" inactive-text="关闭" />
<span class="switch-label">第三方登录需进行二次认证</span>
</div>
<div class="switch-item">
<el-switch v-model="second_auth.ai_second_auth" active-text="开启" inactive-text="关闭" />
<span class="switch-label">智能增强认证</span>
</div>
<el-row :gutter="20" class="user-tabpane">
<el-col :span="12" v-if="totp_secret_created_at">
<el-tag>密钥创建时间{{ totp_secret_created_at }}</el-tag>
</el-col>
<el-col :span="12" v-if="totp_secret_created_at">
<el-button type="danger" @click="unBindTOTPSecret">解绑</el-button>
</el-col>
</el-row>
</div>
<el-button type="primary" @click="updateUserSecondAuthInfo(userInfo)" class="save-auth-btn">保存二次认证设置</el-button>
<div class="totp-section" v-if="!totp_secret_created_at">
<h4>TOTP 动态口令</h4>
<el-button type="primary" @click="GenTOTPSecret" :disabled="gen_totp_secret">生成 TOTP 密钥</el-button>
<div v-if="totp_secret || totp_url" class="totp-result">
<el-alert
title="重要提示"
type="warning"
:closable="false"
show-icon
style="margin-bottom: 20px;"
>
<template #default>
<p>使用支持 TOTP 的应用 Google AuthenticatorAuthy 扫描二维码或手动输入密钥进行绑定</p>
<p>请妥善保存您的 TOTP 密钥只能查看一次若丢失请解绑后重新生成</p>
</template>
</el-alert>
<div class="totp-content">
<div class="totp-qrcode" v-if="totp_url">
<vueQr :text="totp_url" :size="200" :margin="20" :background="'#ffffff'" :foreground="'#000000'" />
</div>
<div class="totp-info">
<el-input
v-if="totp_secret"
v-model="totp_secret"
readonly
style="margin-bottom: 10px;"
>
<template #append>
<el-button @click="onCopyTOTPSecret" icon="Copy">复制</el-button>
</template>
</el-input>
</div>
</div>
</div>
</div>
<div class="totp-bound" v-if="totp_secret_created_at">
<el-alert
title="TOTP 已绑定"
type="success"
:closable="false"
show-icon
>
<template #default>
<p>密钥创建时间{{ totp_secret_created_at }}</p>
<el-button type="danger" @click="unBindTOTPSecret" style="margin-top: 10px;">解绑 TOTP</el-button>
</template>
</el-alert>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
@ -152,18 +197,17 @@
</template>
<script setup lang="ts" name="ucenter">
import { reactive, ref,inject } from 'vue';
import { reactive, ref, inject, computed } from 'vue';
import { VueCropper } from 'vue-cropper';
import 'vue-cropper/dist/index.css';
import avatar from '@/assets/img/img.jpg';
import TabsComp from '../element/tabs.vue';
import {GetUserInfoService, genTOTPSecret, unBindTOTPSecretService, getTOTPSecretInfo} from "@/api/user";
import { GetUserStatisticService } from "@/api/user";
import { UploadFileService } from "@/api/tool";
import { UserInfo } from '@/types/user';
import { ThirdPartyUserInfo } from '@/types/user';
import { FormOption, FormOptionList } from '@/types/form-option';
import { avatarEmits, ElMessage } from 'element-plus';
import { FormOption } from '@/types/form-option';
import { ElMessage, FormInstance } from 'element-plus';
import TableEdit from '@/components/table-edit.vue';
import {genResetPassword} from "@/api/user";
import {updateUserInfoService} from "@/api/user";
@ -172,7 +216,6 @@ import {getThirdPartyUUID,getThirdPartyLoginStatus,getThirdPartyLoginUrl, getThi
import vueQr from 'vue-qr/src/packages/vue-qr.vue'
const name = localStorage.getItem('ms_username');
const qqButtonBgImage = ref('https://wiki.connect.qq.com/wp-content/uploads/2016/12/Connect_logo_4.png');
const form = reactive({
new1: '',
new: '',
@ -190,7 +233,7 @@ const isUserInfoLoaded = ref(false);
const globalData = inject("globalData");
const activeName = ref('label2');
const activePlatformName = ref('github');
const activePlatformName = ref('');
const router = useRouter();
const avatarImg = ref('');
const imgSrc = ref('');
@ -201,6 +244,7 @@ const currentLoginRequest = ref(0); //当前请求次数
const totp_secret = ref('');
const totp_url = ref('');
const totp_secret_created_at = ref('');
const passwordForm = ref<FormInstance | null>(null);
const second_auth = ref({
password_need_second_auth: true,
@ -222,6 +266,21 @@ const thirdPartyPlatform = ref([
{ label: "Gitea自建", value: "my_gitea"},
{ label: "Microsoft", value: "microsoft"}
]);
//
const availablePlatforms = computed(() => {
const boundPlatforms = thirdPartyUserInfo.value.map(item => item.third_party_platform);
return thirdPartyPlatform.value.filter(platform => !boundPlatforms.includes(platform.value));
});
//
const validateConfirmPassword = (rule: any, value: string, callback: any) => {
if (value !== form.new) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
};
const onCopyTOTPSecret = () => {
navigator.clipboard.writeText(totp_secret.value).then(() => {
ElMessage.success('复制成功!');
@ -370,13 +429,17 @@ const updateUserSecondAuthInfo = async (data: any) => {
};
const resetPassword = async () =>{
if (!passwordForm.value) return;
try {
await passwordForm.value.validate();
let req={
old_password: form.old,
new_password: form.new1,
email: userInfo.value.Email,
email: userInfo.value?.Email,
type:1
}
try{
let result = await genResetPassword(req);
if (result["code"] === 0) {
//token
@ -384,6 +447,8 @@ const resetPassword = async () =>{
localStorage.setItem('token', result.data.token);
globalData["token"] = result.data.token;
ElMessage.success('重置密码成功');
//
passwordForm.value.resetFields();
}
} else {
ElMessage.error(result["msg"]);
@ -662,91 +727,123 @@ const unBindThirdParty = async (id) => {
</script>
<style scoped>
.ucenter-page {
padding: 20px;
min-height: 100vh;
background: #f5f7fa;
}
.user-container {
display: flex;
gap: 20px;
max-width: 1600px;
margin: 0 auto;
}
.user-profile {
position: relative;
width: 420px;
flex: 0 0 auto;
align-self: flex-start;
transition: all 0.3s ease;
}
.user-profile:hover {
transform: translateY(-2px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
.user-profile-bg {
width: 100%;
height: 200px;
height: 220px;
background-image: url('../../assets/img/ucenter-bg.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.user-profile {
width: 500px;
margin-right: 20px;
flex: 0 0 auto;
align-self: flex-start;
border-radius: 4px 4px 0 0;
}
.user-avatar-wrap {
position: absolute;
top: 135px;
top: 155px;
width: 100%;
text-align: center;
}
.user-avatar {
border: 5px solid #fff;
border: 6px solid #fff;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 7px 12px 0 rgba(62, 57, 107, 0.16);
box-shadow: 0 8px 16px rgba(62, 57, 107, 0.16);
transition: transform 0.3s ease;
}
.user-avatar:hover {
transform: scale(1.05);
}
.user-info {
text-align: center;
padding: 80px 0 30px;
padding: 90px 0 30px;
}
.info-name {
margin: 0 0 20px;
font-size: 22px;
font-weight: 500;
color: #373a3c;
}
.info-desc {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 5px;
}
.info-desc,
.info-desc a {
font-size: 18px;
color: #55595c;
}
.info-icon {
margin-top: 10px;
}
.info-icon i {
font-size: 30px;
margin: 0 10px;
color: #343434;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.user-content {
flex: 1;
min-height: calc(100vh - 40px);
transition: all 0.3s ease;
}
.user-content:hover {
transform: translateY(-2px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
.user-tabs {
height: 100%;
}
.user-tabpane {
padding: 10px 20px;
padding: 20px;
min-height: 600px;
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateX(10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.avatar-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.crop-wrap {
width: 600px;
height: 350px;
margin-bottom: 20px;
width: 100%;
max-width: 600px;
height: 400px;
border: 1px dashed #dcdfe6;
border-radius: 4px;
background: #fafafa;
}
.avatar-actions {
display: flex;
gap: 12px;
}
.crop-demo-btn {
@ -755,41 +852,274 @@ const unBindThirdParty = async (id) => {
.crop-input {
position: absolute;
width: 100px;
height: 40px;
width: 100%;
height: 100%;
left: 0;
top: 0;
opacity: 0;
cursor: pointer;
}
.w500 {
width: 500px;
.password-form {
max-width: 500px;
}
.user-footer {
display: flex;
border-top: 1px solid rgba(83, 70, 134, 0.1);
background: #fafafa;
border-radius: 0 0 4px 4px;
}
.user-footer-item {
padding: 20px 0;
width: 33.3333333333%;
padding: 24px 0;
flex: 1;
text-align: center;
transition: all 0.3s ease;
}
.user-footer-item:hover {
background: #f0f2f5;
}
.user-footer > div + div {
border-left: 1px solid rgba(83, 70, 134, 0.1);
}
.user-footer-item :deep(.el-statistic__label) {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.user-footer-item :deep(.el-statistic__number) {
font-size: 24px;
font-weight: 600;
color: #409eff;
}
/* 第三方账号样式 */
.third-party-section {
display: flex;
flex-direction: column;
gap: 30px;
}
.bind-section {
display: flex;
gap: 12px;
align-items: center;
}
.bound-section h4 {
margin: 0 0 20px 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.bound-accounts {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
}
.account-card {
padding: 16px;
transition: all 0.3s ease;
}
.account-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.account-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.account-avatar {
flex-shrink: 0;
}
.account-details {
display: flex;
flex-direction: column;
gap: 8px;
}
.platform-tag {
width: fit-content;
}
.account-name {
font-size: 14px;
color: #606266;
}
.empty-state {
padding: 40px 0;
}
/* 二次认证样式 */
.auth-section {
display: flex;
flex-direction: column;
gap: 30px;
}
.auth-switches {
display: flex;
flex-direction: column;
gap: 24px;
padding: 20px;
background: #fafafa;
border-radius: 4px;
}
.switch-item {
display: flex;
align-items: center;
gap: 16px;
}
.switch-label {
font-size: 14px;
color: #303133;
}
.save-auth-btn {
align-self: flex-start;
}
.totp-section h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.totp-result {
margin-top: 20px;
}
.totp-content {
display: flex;
gap: 30px;
align-items: flex-start;
flex-wrap: wrap;
}
.totp-qrcode {
flex-shrink: 0;
padding: 20px;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
}
.totp-info {
flex: 1;
min-width: 300px;
}
.totp-bound {
margin-top: 20px;
}
.login-btn {
display: block;
width: 100%;
}
/* 响应式适配 */
@media (max-width: 1200px) {
.user-container {
flex-direction: column;
}
.user-profile {
width: 100%;
margin-right: 0;
}
.user-content {
width: 100%;
}
.crop-wrap {
max-width: 100%;
height: 350px;
}
.password-form {
max-width: 100%;
}
}
@media (max-width: 768px) {
.ucenter-page {
padding: 10px;
}
.user-tabpane {
padding: 10px;
}
.bound-accounts {
grid-template-columns: 1fr;
}
.totp-content {
flex-direction: column;
align-items: center;
}
.avatar-actions {
flex-direction: column;
}
.bind-section {
flex-direction: column;
align-items: stretch;
}
}
</style>
<style>
.el-tabs.el-tabs--left {
height: 100%;
}
.el-tabs.el-tabs--left .el-tabs__header {
margin: 0;
border-right: 1px solid #e4e7ed;
background: #fafafa;
}
.el-tabs.el-tabs--left .el-tabs__item {
height: 50px;
line-height: 50px;
padding: 0 20px;
transition: all 0.3s ease;
}
.el-tabs.el-tabs--left .el-tabs__item.is-active {
background: #409eff;
color: #fff;
border-right-color: #409eff;
}
.el-tabs.el-tabs--left .el-tabs__item:hover:not(.is-active) {
background: #ecf5ff;
color: #409eff;
}
.el-tabs.el-tabs--left .el-tabs__nav-wrap::after {
display: none;
}
</style>