web登录注册修改
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
import { api } from '@/apis/index';
|
||||
import type { ResultDomain } from '@/types';
|
||||
import type { ConfigItem, SaveConfigParam } from '@/types/system/config';
|
||||
import type { SystemBaseInfo } from '@/types/system/baseinfo';
|
||||
/**
|
||||
* 系统配置API服务
|
||||
*/
|
||||
@@ -58,5 +59,14 @@ export const configApi = {
|
||||
async deleteConfig(configKey: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.delete<boolean>(`/system/config/${configKey}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取系统基础信息(公开接口)
|
||||
* @returns Promise<ResultDomain<SystemBaseInfo>>
|
||||
*/
|
||||
async getBaseInfo(): Promise<ResultDomain<SystemBaseInfo>> {
|
||||
const response = await api.get<SystemBaseInfo>('/system/config/baseinfo');
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
<!-- 已登录状态 -->
|
||||
<div class="user-info" v-else>
|
||||
<div class="user-avatar">
|
||||
<img :src="userAvatar" :alt="user?.username" v-if="userAvatar">
|
||||
<img :src="userAvatar" :alt="currentUser?.username" v-if="userAvatar">
|
||||
<span class="avatar-placeholder" v-else>{{ avatarText }}</span>
|
||||
</div>
|
||||
<div class="user-details" v-if="!collapsed">
|
||||
<div class="user-name">{{ user?.username }}</div>
|
||||
<div class="user-name">{{ currentUser?.username }}</div>
|
||||
<div class="user-role">{{ primaryRole }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +103,9 @@ interface Props {
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const user = computed(() => store.getters['auth/user']);
|
||||
|
||||
// 使用传入的 userinfo 或从 store 获取
|
||||
const currentUser = computed(() => props.userinfo || store.getters['auth/user']);
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
@@ -125,11 +127,12 @@ const isLoggedIn = computed(() => {
|
||||
});
|
||||
|
||||
const userAvatar = computed(() => {
|
||||
return props.userinfo?.avatar && props.userinfo?.avatar!='default' ? FILE_DOWNLOAD_URL + props.userinfo?.avatar : defaultAvatarImg;
|
||||
const avatar = currentUser.value?.avatar;
|
||||
return avatar && avatar !== 'default' ? FILE_DOWNLOAD_URL + avatar : defaultAvatarImg;
|
||||
});
|
||||
|
||||
const avatarText = computed(() => {
|
||||
const name = props.userinfo?.fullName || props.userinfo?.username || '';
|
||||
const name = currentUser.value?.fullName || currentUser.value?.username || '';
|
||||
return name.charAt(0).toUpperCase();
|
||||
});
|
||||
|
||||
|
||||
@@ -21,37 +21,16 @@
|
||||
<nav class="sidebar-nav">
|
||||
<MenuSidebar :menus="sidebarMenus" :collapsed="false" @menu-click="handleMenuClick" />
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-footer-contain">
|
||||
<!-- 用户头像+名称 -->
|
||||
<div class="user-info">
|
||||
<img :src="getUserAvatar()" alt="用户头像" class="user-avatar" />
|
||||
<span>{{ user?.username }}</span>
|
||||
|
||||
</div>
|
||||
<div class="dropdown-item" @click="toggleDropdown">
|
||||
<img src="@/assets/imgs/else.svg" alt="其他" class="dropdown-icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="dropdown-up">
|
||||
<div v-if="showDropdown" class="dropdown-menu" @mouseleave="showDropdown = false">
|
||||
<div class="dropdown-item">
|
||||
<img src="@/assets/imgs/home.svg" alt="用户界面" class="dropdown-icon" />
|
||||
<ChangeHome />
|
||||
</div>
|
||||
<div class="dropdown-item" @click="handleLogout">
|
||||
<img src="@/assets/imgs/logout.svg" alt="退出登录" class="dropdown-icon" />
|
||||
<span>退出登录</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
<main class="main-wrapper">
|
||||
<nav class="top-nav">
|
||||
<UserDropdown :userinfo="user" @logout="handleLogout" />
|
||||
</nav>
|
||||
<div class="main-content">
|
||||
<router-view />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -66,14 +45,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import type { SysMenu } from '@/types';
|
||||
import { MenuType } from '@/types/enums';
|
||||
import { MenuSidebar, ChangeHome } from '@/components';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
import defaultAvatar from '@/assets/imgs/default-avatar.png';
|
||||
import { MenuSidebar, UserDropdown } from '@/components';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -81,8 +58,6 @@ const store = useStore();
|
||||
|
||||
const user = computed(() => store.getters['auth/user']);
|
||||
|
||||
const showDropdown = ref(false);
|
||||
|
||||
// 获取所有菜单
|
||||
const allMenus = computed(() => store.getters['auth/menuTree']);
|
||||
|
||||
@@ -106,17 +81,9 @@ function handleMenuClick(menu: SysMenu) {
|
||||
}
|
||||
}
|
||||
|
||||
function getUserAvatar() {
|
||||
return user.value?.avatar ? `${FILE_DOWNLOAD_URL}/${user.value?.avatar}` : defaultAvatar;
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
store.dispatch('auth/logout');
|
||||
}
|
||||
|
||||
function toggleDropdown() {
|
||||
showDropdown.value = !showDropdown.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -153,9 +120,12 @@ function toggleDropdown() {
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 24px 24px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
height: 80px;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
height: 76px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
@@ -215,122 +185,32 @@ function toggleDropdown() {
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
height: 70px;
|
||||
.main-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #F9FAFB;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.top-nav {
|
||||
display: flex;
|
||||
height: 76px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
.sidebar-footer-contain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
border-radius: 8px;
|
||||
background-color: #F6F6F6;
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #1f2933;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
/* width: 20px; */
|
||||
height: 80%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
bottom: 52px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
border-radius: 6px;
|
||||
padding: 8px 0;
|
||||
min-width: 180px;
|
||||
z-index: 10;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
span,
|
||||
.change-home-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-up-enter-active,
|
||||
.dropdown-up-leave-active {
|
||||
transition: all 0.15s ease-out;
|
||||
}
|
||||
|
||||
.dropdown-up-enter-from,
|
||||
.dropdown-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 6px);
|
||||
}
|
||||
|
||||
.dropdown-up-enter-to,
|
||||
.dropdown-up-leave-from {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
padding: 12px 20px;
|
||||
background: #FFFFFF;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
flex-shrink: 0;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
background: #F9FAFB;
|
||||
overflow-y: auto;
|
||||
/* min-width: 0; */
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
height: 100vh;
|
||||
|
||||
// 美化滚动条
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
@@ -22,6 +22,14 @@ async function initApp() {
|
||||
app.use(ElementPlus);
|
||||
app.use(store);
|
||||
|
||||
// 加载系统基础信息(Logo、系统名称、登录开关等)
|
||||
try {
|
||||
await store.dispatch('system/loadBaseInfo');
|
||||
console.log('[应用初始化] 系统基础信息加载成功');
|
||||
} catch (error) {
|
||||
console.error('[应用初始化] 系统基础信息加载失败:', error);
|
||||
}
|
||||
|
||||
// 在路由初始化前,尝试恢复登录状态并生成动态路由
|
||||
const authState = (store.state as any).auth;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createStore } from "vuex";
|
||||
import authModule from './modules/auth';
|
||||
import systemModule from './modules/system';
|
||||
|
||||
export default createStore({
|
||||
state: {},
|
||||
@@ -7,6 +8,7 @@ export default createStore({
|
||||
mutations: {},
|
||||
actions: {},
|
||||
modules: {
|
||||
auth: authModule
|
||||
auth: authModule,
|
||||
system: systemModule
|
||||
},
|
||||
});
|
||||
|
||||
93
schoolNewsWeb/src/store/modules/system.ts
Normal file
93
schoolNewsWeb/src/store/modules/system.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { configApi } from '@/apis/system';
|
||||
import type { SystemBaseInfo } from '@/types/system/baseinfo';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
|
||||
interface SystemState {
|
||||
baseInfo: SystemBaseInfo | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: (): SystemState => ({
|
||||
baseInfo: null,
|
||||
loading: false
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 短信登录是否启用
|
||||
smsLoginEnabled: (state: SystemState) => state.baseInfo?.smsLoginEnabled || false,
|
||||
|
||||
// 邮箱登录是否启用
|
||||
emailLoginEnabled: (state: SystemState) => state.baseInfo?.emailLoginEnabled || false,
|
||||
|
||||
// 系统名称
|
||||
systemName: (state: SystemState) => state.baseInfo?.systemName || '红色思政学习平台',
|
||||
|
||||
// 系统简称
|
||||
systemShortName: (state: SystemState) => state.baseInfo?.systemShortName || '思政平台',
|
||||
|
||||
// Logo URL(自动拼接fileId,为空时使用默认图片)
|
||||
loginLogoUrl: (state: SystemState) => {
|
||||
const fileId = state.baseInfo?.loginLogo;
|
||||
return fileId ? `${FILE_DOWNLOAD_URL}/${fileId}` : '/logo-icon.svg';
|
||||
},
|
||||
|
||||
homeLogoUrl: (state: SystemState) => {
|
||||
const fileId = state.baseInfo?.homeLogo;
|
||||
return fileId ? `${FILE_DOWNLOAD_URL}/${fileId}` : '/logo-icon.svg';
|
||||
},
|
||||
|
||||
adminLogoUrl: (state: SystemState) => {
|
||||
const fileId = state.baseInfo?.adminLogo;
|
||||
return fileId ? `${FILE_DOWNLOAD_URL}/${fileId}` : '/logo-icon.svg';
|
||||
},
|
||||
|
||||
faviconUrl: (state: SystemState) => {
|
||||
const fileId = state.baseInfo?.favicon;
|
||||
return fileId ? `${FILE_DOWNLOAD_URL}/${fileId}` : '/favicon.ico';
|
||||
}
|
||||
},
|
||||
|
||||
mutations: {
|
||||
SET_BASE_INFO(state: SystemState, info: SystemBaseInfo) {
|
||||
state.baseInfo = info;
|
||||
},
|
||||
|
||||
SET_LOADING(state: SystemState, loading: boolean) {
|
||||
state.loading = loading;
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
async loadBaseInfo({ commit, getters }: any) {
|
||||
commit('SET_LOADING', true);
|
||||
try {
|
||||
const result = await configApi.getBaseInfo();
|
||||
if (result.success && result.data) {
|
||||
commit('SET_BASE_INFO', result.data);
|
||||
// 动态设置favicon
|
||||
const faviconUrl = getters.faviconUrl;
|
||||
if (faviconUrl && faviconUrl !== '/favicon.ico') {
|
||||
setFavicon(faviconUrl);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载系统基础信息失败:', error);
|
||||
} finally {
|
||||
commit('SET_LOADING', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 动态设置网站图标
|
||||
*/
|
||||
function setFavicon(faviconUrl: string) {
|
||||
const link = document.querySelector("link[rel*='icon']") as HTMLLinkElement || document.createElement('link');
|
||||
link.type = 'image/x-icon';
|
||||
link.rel = 'shortcut icon';
|
||||
link.href = faviconUrl;
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
}
|
||||
16
schoolNewsWeb/src/types/system/baseinfo.ts
Normal file
16
schoolNewsWeb/src/types/system/baseinfo.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 系统基础信息接口
|
||||
*/
|
||||
export interface SystemBaseInfo {
|
||||
// 基础信息
|
||||
systemName: string; // 系统名称
|
||||
systemShortName: string; // 系统简称
|
||||
loginLogo: string; // 登录页Logo(fileId)
|
||||
homeLogo: string; // 首页Logo(fileId)
|
||||
adminLogo: string; // 管理后台Logo(fileId)
|
||||
favicon: string; // 网站图标(fileId)
|
||||
|
||||
// 登录开关(来自不同分组)
|
||||
smsLoginEnabled: boolean; // 短信登录开关(sms分组)
|
||||
emailLoginEnabled: boolean; // 邮箱登录开关(email分组)
|
||||
}
|
||||
@@ -104,6 +104,27 @@
|
||||
/>
|
||||
<span v-if="item.remark" class="form-item-remark">{{ item.remark }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<el-form-item
|
||||
v-else-if="getRenderType(item) === 'imgupload'"
|
||||
:label="item.configName || item.configKey"
|
||||
:prop="item.configKey"
|
||||
>
|
||||
<el-upload
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="true"
|
||||
:limit="1"
|
||||
list-type="picture-card"
|
||||
:on-success="(response: any) => handleUploadSuccess(response, group.groupKey, item.configKey)"
|
||||
:on-remove="() => handleRemove(group.groupKey, item.configKey)"
|
||||
:file-list="getFileList(group.groupKey, item.configKey)"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<span v-if="item.remark" class="form-item-remark">{{ item.remark }}</span>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
@@ -125,11 +146,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { AdminLayout } from '@/views/admin';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { configApi } from '@/apis/system';
|
||||
import type { ConfigItem } from '@/types/system/config';
|
||||
import { APP_CONFIG, FILE_DOWNLOAD_URL } from '@/config';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
defineOptions({
|
||||
name: 'SystemConfigView'
|
||||
@@ -146,6 +170,13 @@ interface ConfigGroup {
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const activeTab = ref('');
|
||||
const store = useStore();
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = APP_CONFIG.file.uploadUrl;
|
||||
const uploadHeaders = computed(() => ({
|
||||
Authorization: `Bearer ${store.state.auth.token}`
|
||||
}));
|
||||
|
||||
// 配置分组列表
|
||||
const configGroups = ref<ConfigGroup[]>([]);
|
||||
@@ -256,10 +287,13 @@ async function loadConfigs() {
|
||||
|
||||
// 定义分组名称映射
|
||||
const groupNames: Record<string, string> = {
|
||||
basic: '基本配置',
|
||||
email: '邮件配置',
|
||||
storage: '存储配置',
|
||||
system: '系统参数'
|
||||
'基础配置': '基础配置',
|
||||
'爬虫配置': '爬虫配置',
|
||||
'Dify配置': 'Dify配置',
|
||||
'邮件配置': '邮件配置',
|
||||
'短信配置': '短信配置',
|
||||
'存储配置': '存储配置',
|
||||
'系统参数': '系统参数'
|
||||
};
|
||||
|
||||
// 按分组组织配置
|
||||
@@ -329,6 +363,41 @@ async function loadConfigs() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件列表(用于显示已上传的图片)
|
||||
*/
|
||||
function getFileList(groupKey: string, configKey: string): any[] {
|
||||
const fileId = configData[groupKey]?.[configKey];
|
||||
if (fileId) {
|
||||
return [{
|
||||
name: 'image',
|
||||
url: `${FILE_DOWNLOAD_URL}${fileId}`
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理上传成功
|
||||
*/
|
||||
function handleUploadSuccess(response: any, groupKey: string, configKey: string) {
|
||||
if (response.success && response.data) {
|
||||
// 后端返回的是fileId
|
||||
configData[groupKey][configKey] = response.data;
|
||||
ElMessage.success('图片上传成功');
|
||||
} else {
|
||||
ElMessage.error(response.message || '图片上传失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理删除图片
|
||||
*/
|
||||
function handleRemove(groupKey: string, configKey: string) {
|
||||
configData[groupKey][configKey] = '';
|
||||
ElMessage.success('图片已删除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存指定分组的配置
|
||||
*/
|
||||
|
||||
@@ -26,15 +26,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 重置密码方式切换 -->
|
||||
<div class="reset-type-tabs">
|
||||
<!-- 不支持密码找回提示 -->
|
||||
<div v-if="!canResetPassword" class="reset-disabled-notice">
|
||||
<div class="notice-icon">
|
||||
<el-icon :size="48" color="#909399"><WarningFilled /></el-icon>
|
||||
</div>
|
||||
<h3 class="notice-title">不支持密码找回</h3>
|
||||
<p class="notice-text">系统管理员尚未开启手机号或邮箱验证码找回功能</p>
|
||||
<p class="notice-text">请联系管理员重置密码</p>
|
||||
<el-button type="primary" @click="$router.push('/login')" class="back-button">返回登录</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 重置密码表单 -->
|
||||
<template v-else>
|
||||
<!-- 重置密码方式切换 -->
|
||||
<div class="reset-type-tabs" v-if="showResetTypeTabs">
|
||||
<div
|
||||
v-if="smsLoginEnabled"
|
||||
:class="['tab-item', { active: resetType === 'phone' }]"
|
||||
@click="switchResetType('phone')"
|
||||
>
|
||||
手机号重置
|
||||
</div>
|
||||
<div
|
||||
v-if="emailLoginEnabled"
|
||||
:class="['tab-item', { active: resetType === 'email' }]"
|
||||
@click="switchResetType('email')"
|
||||
>
|
||||
@@ -140,6 +155,7 @@
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
@@ -156,9 +172,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onUnmounted } from 'vue';
|
||||
import { ref, reactive, onBeforeUnmount, onUnmounted, computed, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { WarningFilled } from '@element-plus/icons-vue';
|
||||
import { authApi } from '@/apis/system/auth';
|
||||
|
||||
// 响应式引用
|
||||
@@ -171,9 +189,35 @@ let emailTimer: number | null = null;
|
||||
|
||||
// Composition API
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
// 获取系统配置
|
||||
const smsLoginEnabled = computed(() => store.getters['system/smsLoginEnabled']);
|
||||
const emailLoginEnabled = computed(() => store.getters['system/emailLoginEnabled']);
|
||||
|
||||
// 是否支持密码找回
|
||||
const canResetPassword = computed(() => smsLoginEnabled.value || emailLoginEnabled.value);
|
||||
|
||||
// 是否显示重置方式切换标签
|
||||
const showResetTypeTabs = computed(() => smsLoginEnabled.value && emailLoginEnabled.value);
|
||||
|
||||
// 重置方式:phone-手机号,email-邮箱
|
||||
const resetType = ref<'phone' | 'email'>('phone');
|
||||
const getDefaultResetType = (): 'phone' | 'email' => {
|
||||
if (!smsLoginEnabled.value && emailLoginEnabled.value) {
|
||||
return 'email';
|
||||
}
|
||||
return 'phone';
|
||||
};
|
||||
const resetType = ref<'phone' | 'email'>(getDefaultResetType());
|
||||
|
||||
// 监听配置变化
|
||||
watch([smsLoginEnabled, emailLoginEnabled], () => {
|
||||
if (resetType.value === 'phone' && !smsLoginEnabled.value && emailLoginEnabled.value) {
|
||||
resetType.value = 'email';
|
||||
} else if (resetType.value === 'email' && !emailLoginEnabled.value && smsLoginEnabled.value) {
|
||||
resetType.value = 'phone';
|
||||
}
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const forgotForm = reactive({
|
||||
@@ -690,9 +734,40 @@ onUnmounted(() => {
|
||||
line-height: 2;
|
||||
color: #D9D9D9;
|
||||
text-align: center;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-disabled-notice {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
|
||||
.notice-icon {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.notice-text {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
margin-top: 32px;
|
||||
width: 200px;
|
||||
background-color: #C62828;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Element Plus 组件样式覆盖
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
|
||||
@@ -27,23 +27,38 @@
|
||||
<h2 class="forgot-password-title">找回密码</h2>
|
||||
</div>
|
||||
|
||||
<!-- 重置密码方式切换 -->
|
||||
<div class="reset-type-tabs">
|
||||
<div
|
||||
:class="['tab-item', { active: resetType === 'phone' }]"
|
||||
@click="switchResetType('phone')"
|
||||
>
|
||||
手机号重置
|
||||
</div>
|
||||
<div
|
||||
:class="['tab-item', { active: resetType === 'email' }]"
|
||||
@click="switchResetType('email')"
|
||||
>
|
||||
邮箱重置
|
||||
<!-- 不支持密码找回提示 -->
|
||||
<div v-if="!canResetPassword" class="reset-disabled-notice">
|
||||
<div class="notice-icon">
|
||||
<el-icon :size="48" color="#909399"><WarningFilled /></el-icon>
|
||||
</div>
|
||||
<h3 class="notice-title">不支持密码找回</h3>
|
||||
<p class="notice-text">系统管理员尚未开启手机号或邮箱验证码找回功能</p>
|
||||
<p class="notice-text">请联系管理员重置密码</p>
|
||||
<el-button type="primary" @click="goToLogin" class="back-button">返回登录</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 忘记密码表单 -->
|
||||
<!-- 重置密码表单 -->
|
||||
<template v-else>
|
||||
<!-- 重置密码方式切换 -->
|
||||
<div class="reset-type-tabs" v-if="showResetTypeTabs">
|
||||
<div
|
||||
v-if="smsLoginEnabled"
|
||||
:class="['tab-item', { active: resetType === 'phone' }]"
|
||||
@click="switchResetType('phone')"
|
||||
>
|
||||
手机号重置
|
||||
</div>
|
||||
<div
|
||||
v-if="emailLoginEnabled"
|
||||
:class="['tab-item', { active: resetType === 'email' }]"
|
||||
@click="switchResetType('email')"
|
||||
>
|
||||
邮箱重置
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 忘记密码表单 -->
|
||||
<el-form
|
||||
ref="forgotFormRef"
|
||||
:model="forgotForm"
|
||||
@@ -139,6 +154,7 @@
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
<!-- 底部信息 -->
|
||||
@@ -155,9 +171,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ref, reactive, onBeforeUnmount, computed, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { WarningFilled } from '@element-plus/icons-vue';
|
||||
import { authApi } from '@/apis/system/auth';
|
||||
|
||||
// 响应式引用
|
||||
@@ -170,9 +188,36 @@ let emailTimer: number | null = null;
|
||||
|
||||
// Composition API
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
// 获取系统配置
|
||||
const smsLoginEnabled = computed(() => store.getters['system/smsLoginEnabled']);
|
||||
const emailLoginEnabled = computed(() => store.getters['system/emailLoginEnabled']);
|
||||
|
||||
// 是否支持密码找回(至少一种方式启用)
|
||||
const canResetPassword = computed(() => smsLoginEnabled.value || emailLoginEnabled.value);
|
||||
|
||||
// 是否显示重置方式切换标签(两种方式都启用)
|
||||
const showResetTypeTabs = computed(() => smsLoginEnabled.value && emailLoginEnabled.value);
|
||||
|
||||
// 重置方式:phone-手机号,email-邮箱
|
||||
const resetType = ref<'phone' | 'email'>('phone');
|
||||
// 根据配置设置默认值
|
||||
const getDefaultResetType = (): 'phone' | 'email' => {
|
||||
if (!smsLoginEnabled.value && emailLoginEnabled.value) {
|
||||
return 'email';
|
||||
}
|
||||
return 'phone';
|
||||
};
|
||||
const resetType = ref<'phone' | 'email'>(getDefaultResetType());
|
||||
|
||||
// 监听配置变化,自动调整重置方式
|
||||
watch([smsLoginEnabled, emailLoginEnabled], () => {
|
||||
if (resetType.value === 'phone' && !smsLoginEnabled.value && emailLoginEnabled.value) {
|
||||
resetType.value = 'email';
|
||||
} else if (resetType.value === 'email' && !emailLoginEnabled.value && smsLoginEnabled.value) {
|
||||
resetType.value = 'phone';
|
||||
}
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const forgotForm = reactive({
|
||||
@@ -747,4 +792,34 @@ onUnmounted(() => {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-disabled-notice {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
|
||||
.notice-icon {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.notice-text {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
margin-top: 32px;
|
||||
width: 200px;
|
||||
background-color: #C62828;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
密码登录
|
||||
</div>
|
||||
<div
|
||||
v-if="showCaptchaLoginTab"
|
||||
:class="['tab-item', { active: loginMode === 'captcha' }]"
|
||||
@click="switchLoginMode('captcha')"
|
||||
>
|
||||
@@ -74,14 +75,16 @@
|
||||
<!-- 验证码登录模式 -->
|
||||
<template v-else>
|
||||
<!-- 登录方式选择 -->
|
||||
<div class="captcha-type-tabs">
|
||||
<div class="captcha-type-tabs" v-if="showCaptchaTypeTabs">
|
||||
<div
|
||||
v-if="smsLoginEnabled"
|
||||
:class="['captcha-tab-item', { active: captchaType === 'phone' }]"
|
||||
@click="switchCaptchaType('phone')"
|
||||
>
|
||||
手机号
|
||||
</div>
|
||||
<div
|
||||
v-if="emailLoginEnabled"
|
||||
:class="['captcha-tab-item', { active: captchaType === 'email' }]"
|
||||
@click="switchCaptchaType('email')"
|
||||
>
|
||||
@@ -196,7 +199,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
@@ -210,19 +213,45 @@ const loginLoading = ref(false);
|
||||
const showCaptcha = ref(false);
|
||||
const captchaImage = ref('');
|
||||
|
||||
// Composition API
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
|
||||
// 获取系统配置
|
||||
const smsLoginEnabled = computed(() => store.getters['system/smsLoginEnabled']);
|
||||
const emailLoginEnabled = computed(() => store.getters['system/emailLoginEnabled']);
|
||||
|
||||
// 是否显示验证码登录选项卡
|
||||
const showCaptchaLoginTab = computed(() => smsLoginEnabled.value || emailLoginEnabled.value);
|
||||
|
||||
// 是否显示验证码类型切换选项卡
|
||||
const showCaptchaTypeTabs = computed(() => smsLoginEnabled.value && emailLoginEnabled.value);
|
||||
|
||||
// 登录模式:password-密码登录,captcha-验证码登录
|
||||
const loginMode = ref<'password' | 'captcha'>('password');
|
||||
|
||||
// 验证码类型:phone-手机号,email-邮箱
|
||||
const captchaType = ref<'phone' | 'email'>('phone');
|
||||
const getDefaultCaptchaType = (): 'phone' | 'email' => {
|
||||
if (!smsLoginEnabled.value && emailLoginEnabled.value) {
|
||||
return 'email';
|
||||
}
|
||||
return 'phone';
|
||||
};
|
||||
const captchaType = ref<'phone' | 'email'>(getDefaultCaptchaType());
|
||||
|
||||
// 倒计时
|
||||
const smsCountdown = ref(0);
|
||||
const emailCountdown = ref(0);
|
||||
|
||||
// Composition API
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
// 监听配置变化
|
||||
watch([smsLoginEnabled, emailLoginEnabled], () => {
|
||||
if (captchaType.value === 'phone' && !smsLoginEnabled.value && emailLoginEnabled.value) {
|
||||
captchaType.value = 'email';
|
||||
} else if (captchaType.value === 'email' && !emailLoginEnabled.value && smsLoginEnabled.value) {
|
||||
captchaType.value = 'phone';
|
||||
}
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const loginForm = reactive<LoginParam>({
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
密码登录
|
||||
</div>
|
||||
<div
|
||||
v-if="showCaptchaLoginTab"
|
||||
:class="['tab-item', { active: loginMode === 'captcha' }]"
|
||||
@click="switchLoginMode('captcha')"
|
||||
>
|
||||
@@ -74,14 +75,16 @@
|
||||
<!-- 验证码登录模式 -->
|
||||
<template v-else>
|
||||
<!-- 登录方式选择 -->
|
||||
<div class="captcha-type-tabs">
|
||||
<div class="captcha-type-tabs" v-if="showCaptchaTypeTabs">
|
||||
<div
|
||||
v-if="smsLoginEnabled"
|
||||
:class="['captcha-tab-item', { active: captchaType === 'phone' }]"
|
||||
@click="switchCaptchaType('phone')"
|
||||
>
|
||||
手机号
|
||||
</div>
|
||||
<div
|
||||
v-if="emailLoginEnabled"
|
||||
:class="['captcha-tab-item', { active: captchaType === 'email' }]"
|
||||
@click="switchCaptchaType('email')"
|
||||
>
|
||||
@@ -196,7 +199,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
@@ -210,19 +213,39 @@ const loginLoading = ref(false);
|
||||
const showCaptcha = ref(false);
|
||||
const captchaImage = ref('');
|
||||
|
||||
// Composition API
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
|
||||
// 获取系统配置(必须先定义,因为后续初始化会用到)
|
||||
const smsLoginEnabled = computed(() => store.getters['system/smsLoginEnabled']);
|
||||
const emailLoginEnabled = computed(() => store.getters['system/emailLoginEnabled']);
|
||||
|
||||
// 登录模式:password-密码登录,captcha-验证码登录
|
||||
const loginMode = ref<'password' | 'captcha'>('password');
|
||||
|
||||
// 验证码类型:phone-手机号,email-邮箱
|
||||
const captchaType = ref<'phone' | 'email'>('phone');
|
||||
// 根据配置设置默认值
|
||||
const getDefaultCaptchaType = (): 'phone' | 'email' => {
|
||||
// 如果只启用了邮箱登录,默认使用邮箱
|
||||
if (!smsLoginEnabled.value && emailLoginEnabled.value) {
|
||||
return 'email';
|
||||
}
|
||||
// 其他情况默认使用手机号
|
||||
return 'phone';
|
||||
};
|
||||
const captchaType = ref<'phone' | 'email'>(getDefaultCaptchaType());
|
||||
|
||||
// 倒计时
|
||||
const smsCountdown = ref(0);
|
||||
const emailCountdown = ref(0);
|
||||
|
||||
// Composition API
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
// 是否显示验证码登录选项卡(至少一个验证码登录方式启用)
|
||||
const showCaptchaLoginTab = computed(() => smsLoginEnabled.value || emailLoginEnabled.value);
|
||||
|
||||
// 是否显示验证码类型切换选项卡(两种验证码方式都启用)
|
||||
const showCaptchaTypeTabs = computed(() => smsLoginEnabled.value && emailLoginEnabled.value);
|
||||
|
||||
// 表单数据
|
||||
const loginForm = reactive<LoginParam>({
|
||||
@@ -334,15 +357,22 @@ const switchCaptchaType = (type: 'phone' | 'email') => {
|
||||
if (captchaType.value === type) return;
|
||||
|
||||
captchaType.value = type;
|
||||
|
||||
// 清空相关表单数据和验证
|
||||
loginFormRef.value?.clearValidate();
|
||||
loginForm.phone = '';
|
||||
loginForm.email = '';
|
||||
loginForm.captcha = '';
|
||||
loginForm.captchaId = '';
|
||||
loginFormRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
// 监听配置变化,动态调整验证码类型
|
||||
watch([smsLoginEnabled, emailLoginEnabled], () => {
|
||||
// 如果当前选中的验证码类型未启用,切换到可用的类型
|
||||
if (captchaType.value === 'phone' && !smsLoginEnabled.value && emailLoginEnabled.value) {
|
||||
captchaType.value = 'email';
|
||||
} else if (captchaType.value === 'email' && !emailLoginEnabled.value && smsLoginEnabled.value) {
|
||||
captchaType.value = 'phone';
|
||||
}
|
||||
});
|
||||
|
||||
// 发送短信验证码
|
||||
const handleSendSmsCode = async () => {
|
||||
// 验证手机号
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
用户名
|
||||
</div>
|
||||
<div
|
||||
v-if="smsLoginEnabled"
|
||||
class="tab-item"
|
||||
:class="{ active: registerType === RegisterType.PHONE }"
|
||||
@click="switchRegisterType(RegisterType.PHONE)"
|
||||
@@ -43,6 +44,7 @@
|
||||
手机号
|
||||
</div>
|
||||
<div
|
||||
v-if="emailLoginEnabled"
|
||||
class="tab-item"
|
||||
:class="{ active: registerType === RegisterType.EMAIL }"
|
||||
@click="switchRegisterType(RegisterType.EMAIL)"
|
||||
@@ -183,8 +185,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onUnmounted } from 'vue';
|
||||
import { ref, reactive, computed, onUnmounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import type { RegisterParam } from '@/types';
|
||||
import { RegisterType } from '@/types';
|
||||
@@ -200,10 +203,24 @@ let emailTimer: number | null = null;
|
||||
|
||||
// Composition API
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
// 获取系统配置
|
||||
const smsLoginEnabled = computed(() => store.getters['system/smsLoginEnabled']);
|
||||
const emailLoginEnabled = computed(() => store.getters['system/emailLoginEnabled']);
|
||||
|
||||
// 注册方式
|
||||
const registerType = ref<RegisterType>(RegisterType.USERNAME);
|
||||
|
||||
// 监听配置变化
|
||||
watch([smsLoginEnabled, emailLoginEnabled], () => {
|
||||
if (registerType.value === RegisterType.PHONE && !smsLoginEnabled.value) {
|
||||
registerType.value = RegisterType.USERNAME;
|
||||
} else if (registerType.value === RegisterType.EMAIL && !emailLoginEnabled.value) {
|
||||
registerType.value = RegisterType.USERNAME;
|
||||
}
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const registerForm = reactive<RegisterParam>({
|
||||
registerType: RegisterType.USERNAME,
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
用户名
|
||||
</div>
|
||||
<div
|
||||
v-if="smsLoginEnabled"
|
||||
class="tab-item"
|
||||
:class="{ active: registerType === RegisterType.PHONE }"
|
||||
@click="switchRegisterType(RegisterType.PHONE)"
|
||||
@@ -43,6 +44,7 @@
|
||||
手机号
|
||||
</div>
|
||||
<div
|
||||
v-if="emailLoginEnabled"
|
||||
class="tab-item"
|
||||
:class="{ active: registerType === RegisterType.EMAIL }"
|
||||
@click="switchRegisterType(RegisterType.EMAIL)"
|
||||
@@ -190,8 +192,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { ref, reactive, computed, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import type { RegisterParam } from '@/types';
|
||||
import { RegisterType } from '@/types';
|
||||
@@ -207,10 +210,25 @@ let emailTimer: number | null = null;
|
||||
|
||||
// Composition API
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
// 获取系统配置
|
||||
const smsLoginEnabled = computed(() => store.getters['system/smsLoginEnabled']);
|
||||
const emailLoginEnabled = computed(() => store.getters['system/emailLoginEnabled']);
|
||||
|
||||
// 注册方式
|
||||
const registerType = ref<RegisterType>(RegisterType.USERNAME);
|
||||
|
||||
// 监听配置变化,自动调整注册方式
|
||||
watch([smsLoginEnabled, emailLoginEnabled], () => {
|
||||
// 如果当前选中的注册方式未启用,切换到用户名注册
|
||||
if (registerType.value === RegisterType.PHONE && !smsLoginEnabled.value) {
|
||||
registerType.value = RegisterType.USERNAME;
|
||||
} else if (registerType.value === RegisterType.EMAIL && !emailLoginEnabled.value) {
|
||||
registerType.value = RegisterType.USERNAME;
|
||||
}
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const registerForm = reactive<RegisterParam>({
|
||||
registerType: RegisterType.USERNAME,
|
||||
|
||||
@@ -25,23 +25,19 @@
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="header-search">
|
||||
<el-input
|
||||
v-model="localSearchKeyword"
|
||||
placeholder="搜索文章和课程内容"
|
||||
class="search-input"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="Search"
|
||||
@click="handleSearch"
|
||||
class="search-button"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="localSearchKeyword"
|
||||
type="text"
|
||||
placeholder="搜索文章和课程内容"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<div class="search-button" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -438,19 +434,20 @@ onMounted(() => {
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: #f5f7fa;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: #F9FAFB;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #4A5565;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #e4e7ed;
|
||||
color: #409eff;
|
||||
background: #F3F4F6;
|
||||
color: #E7000B;
|
||||
border-color: #E7000B;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,21 +483,55 @@ onMounted(() => {
|
||||
|
||||
.header-search {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 24px;
|
||||
padding-right: 0;
|
||||
}
|
||||
.search-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid rgba(186, 192, 204, 0.5);
|
||||
border-radius: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
margin-right: 4px;
|
||||
input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding: 0 90px 0 20px;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: "Source Han Sans SC";
|
||||
font-size: 14px;
|
||||
color: #141F38;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.search-button {
|
||||
border-radius: 20px;
|
||||
padding: 8px 24px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 60px;
|
||||
height: 100%;
|
||||
background: #C62828;
|
||||
border-radius: 0 30px 30px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #B71C1C;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user