feat: 将整体logo视觉替换为ai生成的logo

master
LCJ-MinYa 2 months ago
parent 6825193266
commit 6559b3eff2

@ -1,42 +1,42 @@
// 根据角色动态生成路由 // 根据角色动态生成路由
import { defineFakeRoute } from "vite-plugin-fake-server/client"; import { defineFakeRoute } from 'vite-plugin-fake-server/client';
export default defineFakeRoute([ export default defineFakeRoute([
{ {
url: "/login", url: '/login',
method: "post", method: 'post',
response: ({ body }) => { response: ({ body }) => {
if (body.username === "admin") { if (body.username === 'admin') {
return { return {
success: true, success: true,
data: { data: {
avatar: "https://avatars.githubusercontent.com/u/44761321", avatar: '/user_small.png',
username: "admin", username: 'admin',
nickname: "小铭", nickname: 'Levi',
// 一个用户可能有多个角色 // 一个用户可能有多个角色
roles: ["admin"], roles: ['admin'],
// 按钮级别权限 // 按钮级别权限
permissions: ["*:*:*"], permissions: ['*:*:*'],
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin", accessToken: 'eyJhbGciOiJIUzUxMiJ9.admin',
refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh", refreshToken: 'eyJhbGciOiJIUzUxMiJ9.adminRefresh',
expires: "2030/10/30 00:00:00" expires: '2030/10/30 00:00:00',
} },
}; };
} else { } else {
return { return {
success: true, success: true,
data: { data: {
avatar: "https://avatars.githubusercontent.com/u/52823142", avatar: 'https://avatars.githubusercontent.com/u/52823142',
username: "common", username: 'common',
nickname: "小林", nickname: '小林',
roles: ["common"], roles: ['common'],
permissions: ["permission:btn:add", "permission:btn:edit"], permissions: ['permission:btn:add', 'permission:btn:edit'],
accessToken: "eyJhbGciOiJIUzUxMiJ9.common", accessToken: 'eyJhbGciOiJIUzUxMiJ9.common',
refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh", refreshToken: 'eyJhbGciOiJIUzUxMiJ9.commonRefresh',
expires: "2030/10/30 00:00:00" expires: '2030/10/30 00:00:00',
} },
}; };
} }
} },
} },
]); ]);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 96 KiB

@ -1,96 +1,102 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue"; import { ref, computed } from 'vue';
import { noticesData } from "./data"; import { noticesData } from './data';
import NoticeList from "./components/NoticeList.vue"; import NoticeList from './components/NoticeList.vue';
import BellIcon from "@iconify-icons/ep/bell"; import BellIcon from '@iconify-icons/ep/bell';
const noticesNum = ref(0); const noticesNum = ref(0);
const notices = ref(noticesData); const notices = ref([]);
// const notices = ref(noticesData);
const activeKey = ref(noticesData[0]?.key); const activeKey = ref(noticesData[0]?.key);
notices.value.map(v => (noticesNum.value += v.list.length)); notices.value.map((v) => (noticesNum.value += v.list.length));
const getLabel = computed( const getLabel = computed(() => (item) => item.name + (item.list.length > 0 ? `(${item.list.length})` : ''));
() => item =>
item.name + (item.list.length > 0 ? `(${item.list.length})` : "")
);
</script> </script>
<template> <template>
<el-dropdown trigger="click" placement="bottom-end"> <el-dropdown
<span trigger="click"
:class="[ placement="bottom-end"
'dropdown-badge',
'navbar-bg-hover',
'select-none',
Number(noticesNum) !== 0 && 'mr-[10px]'
]"
> >
<el-badge :value="Number(noticesNum) === 0 ? '' : noticesNum" :max="99"> <span :class="['dropdown-badge', 'navbar-bg-hover', 'select-none', Number(noticesNum) !== 0 && 'mr-[10px]']">
<span class="header-notice-icon"> <el-badge
<IconifyIconOffline :icon="BellIcon" /> :value="Number(noticesNum) === 0 ? '' : noticesNum"
:max="99"
>
<span class="header-notice-icon">
<IconifyIconOffline :icon="BellIcon" />
</span>
</el-badge>
</span> </span>
</el-badge> <template #dropdown>
</span> <el-dropdown-menu>
<template #dropdown> <el-tabs
<el-dropdown-menu> v-model="activeKey"
<el-tabs :stretch="true"
v-model="activeKey" class="dropdown-tabs"
:stretch="true" :style="{ width: notices.length === 0 ? '200px' : '330px' }"
class="dropdown-tabs" >
:style="{ width: notices.length === 0 ? '200px' : '330px' }" <el-empty
> v-if="notices.length === 0"
<el-empty description="暂无消息"
v-if="notices.length === 0" :image-size="60"
description="暂无消息" />
:image-size="60" <span v-else>
/> <template
<span v-else> v-for="item in notices"
<template v-for="item in notices" :key="item.key"> :key="item.key"
<el-tab-pane :label="getLabel(item)" :name="`${item.key}`"> >
<el-scrollbar max-height="330px"> <el-tab-pane
<div class="noticeList-container"> :label="getLabel(item)"
<NoticeList :list="item.list" :emptyText="item.emptyText" /> :name="`${item.key}`"
</div> >
</el-scrollbar> <el-scrollbar max-height="330px">
</el-tab-pane> <div class="noticeList-container">
</template> <NoticeList
</span> :list="item.list"
</el-tabs> :emptyText="item.emptyText"
</el-dropdown-menu> />
</template> </div>
</el-dropdown> </el-scrollbar>
</el-tab-pane>
</template>
</span>
</el-tabs>
</el-dropdown-menu>
</template>
</el-dropdown>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.dropdown-badge { .dropdown-badge {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 40px; width: 40px;
height: 48px; height: 48px;
cursor: pointer; cursor: pointer;
.header-notice-icon { .header-notice-icon {
font-size: 18px; font-size: 18px;
} }
} }
.dropdown-tabs { .dropdown-tabs {
.noticeList-container { .noticeList-container {
padding: 15px 24px 0; padding: 15px 24px 0;
} }
:deep(.el-tabs__header) { :deep(.el-tabs__header) {
margin: 0; margin: 0;
} }
:deep(.el-tabs__nav-wrap)::after { :deep(.el-tabs__nav-wrap)::after {
height: 1px; height: 1px;
} }
:deep(.el-tabs__nav-wrap) { :deep(.el-tabs__nav-wrap) {
padding: 0 36px; padding: 0 36px;
} }
} }
</style> </style>

@ -1,157 +1,152 @@
import { storeToRefs } from "pinia"; import { storeToRefs } from 'pinia';
import { getConfig } from "@/config"; import { getConfig } from '@/config';
import { emitter } from "@/utils/mitt"; import { emitter } from '@/utils/mitt';
import Avatar from "@/assets/user.jpg"; import Avatar from '@/assets/user.jpg';
import { getTopMenu } from "@/router/utils"; import { getTopMenu } from '@/router/utils';
import { useFullscreen } from "@vueuse/core"; import { useFullscreen } from '@vueuse/core';
import type { routeMetaType } from "../types"; import type { routeMetaType } from '../types';
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from 'vue-router';
import { router, remainingPaths } from "@/router"; import { router, remainingPaths } from '@/router';
import { computed, type CSSProperties } from "vue"; import { computed, type CSSProperties } from 'vue';
import { useAppStoreHook } from "@/store/modules/app"; import { useAppStoreHook } from '@/store/modules/app';
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from '@/store/modules/user';
import { useGlobal, isAllEmpty } from "@pureadmin/utils"; import { useGlobal, isAllEmpty } from '@pureadmin/utils';
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from '@/store/modules/permission';
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill"; import ExitFullscreen from '@iconify-icons/ri/fullscreen-exit-fill';
import Fullscreen from "@iconify-icons/ri/fullscreen-fill"; import Fullscreen from '@iconify-icons/ri/fullscreen-fill';
const errorInfo = const errorInfo = 'The current routing configuration is incorrect, please check the configuration';
"The current routing configuration is incorrect, please check the configuration";
export function useNav() { export function useNav() {
const route = useRoute(); const route = useRoute();
const pureApp = useAppStoreHook(); const pureApp = useAppStoreHook();
const routers = useRouter().options.routes; const routers = useRouter().options.routes;
const { isFullscreen, toggle } = useFullscreen(); const { isFullscreen, toggle } = useFullscreen();
const { wholeMenus } = storeToRefs(usePermissionStoreHook()); const { wholeMenus } = storeToRefs(usePermissionStoreHook());
/** 平台`layout`中所有`el-tooltip`的`effect`配置,默认`light` */ /** 平台`layout`中所有`el-tooltip`的`effect`配置,默认`light` */
const tooltipEffect = getConfig()?.TooltipEffect ?? "light"; const tooltipEffect = getConfig()?.TooltipEffect ?? 'light';
const getDivStyle = computed((): CSSProperties => { const getDivStyle = computed((): CSSProperties => {
return {
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
overflow: 'hidden',
};
});
/** 头像(如果头像为空则使用 src/assets/user.jpg */
const userAvatar = computed(() => {
return isAllEmpty(useUserStoreHook()?.avatar) ? Avatar : useUserStoreHook()?.avatar;
});
/** 昵称(如果昵称为空则显示用户名) */
const username = computed(() => {
return isAllEmpty(useUserStoreHook()?.nickname) ? useUserStoreHook()?.username : useUserStoreHook()?.nickname;
});
const avatarsStyle = computed(() => {
return username.value ? { marginRight: '10px' } : '';
});
const isCollapse = computed(() => {
return !pureApp.getSidebarStatus;
});
const device = computed(() => {
return pureApp.getDevice;
});
const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
const layout = computed(() => {
return $storage?.layout?.layout;
});
const title = computed(() => {
return $config.Title;
});
/** 动态title */
function changeTitle(meta: routeMetaType) {
const Title = getConfig().Title;
if (Title) document.title = `${meta.title} | ${Title}`;
else document.title = meta.title;
}
/** 退出登录 */
function logout() {
useUserStoreHook().logOut();
}
function backTopMenu() {
router.push(getTopMenu()?.path);
}
function onPanel() {
emitter.emit('openPanel');
}
function toggleSideBar() {
pureApp.toggleSideBar();
}
function handleResize(menuRef) {
menuRef?.handleResize();
}
function resolvePath(route) {
if (!route.children) return console.error(errorInfo);
const httpReg = /^http(s?):\/\//;
const routeChildPath = route.children[0]?.path;
if (httpReg.test(routeChildPath)) {
return route.path + '/' + routeChildPath;
} else {
return routeChildPath;
}
}
function menuSelect(indexPath: string) {
if (wholeMenus.value.length === 0 || isRemaining(indexPath)) return;
emitter.emit('changLayoutRoute', indexPath);
}
/** 判断路径是否参与菜单 */
function isRemaining(path: string) {
return remainingPaths.includes(path);
}
/** 获取`logo` */
function getLogo() {
return new URL('/user_small.png', import.meta.url).href;
}
return { return {
width: "100%", route,
display: "flex", title,
alignItems: "center", device,
justifyContent: "space-between", layout,
overflow: "hidden" logout,
routers,
$storage,
isFullscreen,
Fullscreen,
ExitFullscreen,
toggle,
backTopMenu,
onPanel,
getDivStyle,
changeTitle,
toggleSideBar,
menuSelect,
handleResize,
resolvePath,
getLogo,
isCollapse,
pureApp,
username,
userAvatar,
avatarsStyle,
tooltipEffect,
}; };
});
/** 头像(如果头像为空则使用 src/assets/user.jpg */
const userAvatar = computed(() => {
return isAllEmpty(useUserStoreHook()?.avatar)
? Avatar
: useUserStoreHook()?.avatar;
});
/** 昵称(如果昵称为空则显示用户名) */
const username = computed(() => {
return isAllEmpty(useUserStoreHook()?.nickname)
? useUserStoreHook()?.username
: useUserStoreHook()?.nickname;
});
const avatarsStyle = computed(() => {
return username.value ? { marginRight: "10px" } : "";
});
const isCollapse = computed(() => {
return !pureApp.getSidebarStatus;
});
const device = computed(() => {
return pureApp.getDevice;
});
const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
const layout = computed(() => {
return $storage?.layout?.layout;
});
const title = computed(() => {
return $config.Title;
});
/** 动态title */
function changeTitle(meta: routeMetaType) {
const Title = getConfig().Title;
if (Title) document.title = `${meta.title} | ${Title}`;
else document.title = meta.title;
}
/** 退出登录 */
function logout() {
useUserStoreHook().logOut();
}
function backTopMenu() {
router.push(getTopMenu()?.path);
}
function onPanel() {
emitter.emit("openPanel");
}
function toggleSideBar() {
pureApp.toggleSideBar();
}
function handleResize(menuRef) {
menuRef?.handleResize();
}
function resolvePath(route) {
if (!route.children) return console.error(errorInfo);
const httpReg = /^http(s?):\/\//;
const routeChildPath = route.children[0]?.path;
if (httpReg.test(routeChildPath)) {
return route.path + "/" + routeChildPath;
} else {
return routeChildPath;
}
}
function menuSelect(indexPath: string) {
if (wholeMenus.value.length === 0 || isRemaining(indexPath)) return;
emitter.emit("changLayoutRoute", indexPath);
}
/** 判断路径是否参与菜单 */
function isRemaining(path: string) {
return remainingPaths.includes(path);
}
/** 获取`logo` */
function getLogo() {
return new URL("/logo.svg", import.meta.url).href;
}
return {
route,
title,
device,
layout,
logout,
routers,
$storage,
isFullscreen,
Fullscreen,
ExitFullscreen,
toggle,
backTopMenu,
onPanel,
getDivStyle,
changeTitle,
toggleSideBar,
menuSelect,
handleResize,
resolvePath,
getLogo,
isCollapse,
pureApp,
username,
userAvatar,
avatarsStyle,
tooltipEffect
};
} }

@ -1,25 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import Motion from "./utils/motion"; import Motion from './utils/motion';
import { useRouter } from "vue-router"; import { useRouter } from 'vue-router';
import { message } from "@/utils/message"; import { message } from '@/utils/message';
import { loginRules } from "./utils/rule"; import { loginRules } from './utils/rule';
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from '@/layout/hooks/useNav';
import type { FormInstance } from "element-plus"; import type { FormInstance } from 'element-plus';
import { useLayout } from "@/layout/hooks/useLayout"; import { useLayout } from '@/layout/hooks/useLayout';
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from '@/store/modules/user';
import { initRouter, getTopMenu } from "@/router/utils"; import { initRouter, getTopMenu } from '@/router/utils';
import { bg, avatar, illustration } from "./utils/static"; import { bg, avatar, illustration } from './utils/static';
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from '@/components/ReIcon/src/hooks';
import { ref, reactive, toRaw, onMounted, onBeforeUnmount } from "vue"; import { ref, reactive, toRaw, onMounted, onBeforeUnmount } from 'vue';
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange"; import { useDataThemeChange } from '@/layout/hooks/useDataThemeChange';
import dayIcon from "@/assets/svg/day.svg?component"; import dayIcon from '@/assets/svg/day.svg?component';
import darkIcon from "@/assets/svg/dark.svg?component"; import darkIcon from '@/assets/svg/dark.svg?component';
import Lock from "@iconify-icons/ri/lock-fill"; import Lock from '@iconify-icons/ri/lock-fill';
import User from "@iconify-icons/ri/user-3-fill"; import User from '@iconify-icons/ri/user-3-fill';
defineOptions({ defineOptions({
name: "Login" name: 'Login',
}); });
const router = useRouter(); const router = useRouter();
const loading = ref(false); const loading = ref(false);
@ -33,136 +33,139 @@ dataThemeChange(overallStyle.value);
const { title } = useNav(); const { title } = useNav();
const ruleForm = reactive({ const ruleForm = reactive({
username: "admin", username: 'admin',
password: "admin123" password: 'admin123',
}); });
const onLogin = async (formEl: FormInstance | undefined) => { const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
await formEl.validate((valid, fields) => { await formEl.validate((valid, fields) => {
if (valid) { if (valid) {
loading.value = true; loading.value = true;
useUserStoreHook() useUserStoreHook()
.loginByUsername({ username: ruleForm.username, password: "admin123" }) .loginByUsername({ username: ruleForm.username, password: 'admin123' })
.then(res => { .then((res) => {
if (res.success) { if (res.success) {
// //
return initRouter().then(() => { return initRouter().then(() => {
router.push(getTopMenu(true).path).then(() => { router.push(getTopMenu(true).path).then(() => {
message("登录成功", { type: "success" }); message('登录成功', { type: 'success' });
}); });
}); });
} else { } else {
message("登录失败", { type: "error" }); message('登录失败', { type: 'error' });
} }
}) })
.finally(() => (loading.value = false)); .finally(() => (loading.value = false));
} }
}); });
}; };
/** 使用公共函数,避免`removeEventListener`失效 */ /** 使用公共函数,避免`removeEventListener`失效 */
function onkeypress({ code }: KeyboardEvent) { function onkeypress({ code }: KeyboardEvent) {
if (["Enter", "NumpadEnter"].includes(code)) { if (['Enter', 'NumpadEnter'].includes(code)) {
onLogin(ruleFormRef.value); onLogin(ruleFormRef.value);
} }
} }
onMounted(() => { onMounted(() => {
window.document.addEventListener("keypress", onkeypress); window.document.addEventListener('keypress', onkeypress);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.document.removeEventListener("keypress", onkeypress); window.document.removeEventListener('keypress', onkeypress);
}); });
</script> </script>
<template> <template>
<div class="select-none"> <div class="select-none">
<img :src="bg" class="wave" /> <img
<div class="flex-c absolute right-5 top-3"> :src="bg"
<!-- 主题 --> class="wave"
<el-switch />
v-model="dataTheme" <div class="flex-c absolute right-5 top-3">
inline-prompt <!-- 主题 -->
:active-icon="dayIcon" <el-switch
:inactive-icon="darkIcon" v-model="dataTheme"
@change="dataThemeChange" inline-prompt
/> :active-icon="dayIcon"
</div> :inactive-icon="darkIcon"
<div class="login-container"> @change="dataThemeChange"
<div class="img"> />
<component :is="toRaw(illustration)" /> </div>
</div> <div class="login-container">
<div class="login-box"> <div class="img">
<div class="login-form"> <component :is="toRaw(illustration)" />
<avatar class="avatar" /> </div>
<Motion> <div class="login-box">
<h2 class="outline-none">{{ title }}</h2> <div class="login-form">
</Motion> <!-- <avatar class="avatar" /> -->
<Motion>
<h2 class="outline-none">{{ title }}</h2>
</Motion>
<el-form <el-form
ref="ruleFormRef" ref="ruleFormRef"
:model="ruleForm" :model="ruleForm"
:rules="loginRules" :rules="loginRules"
size="large" size="large"
> >
<Motion :delay="100"> <Motion :delay="100">
<el-form-item <el-form-item
:rules="[ :rules="[
{ {
required: true, required: true,
message: '请输入账号', message: '请输入账号',
trigger: 'blur' trigger: 'blur',
} },
]" ]"
prop="username" prop="username"
> >
<el-input <el-input
v-model="ruleForm.username" v-model="ruleForm.username"
clearable clearable
placeholder="账号" placeholder="账号"
:prefix-icon="useRenderIcon(User)" :prefix-icon="useRenderIcon(User)"
/> />
</el-form-item> </el-form-item>
</Motion> </Motion>
<Motion :delay="150"> <Motion :delay="150">
<el-form-item prop="password"> <el-form-item prop="password">
<el-input <el-input
v-model="ruleForm.password" v-model="ruleForm.password"
clearable clearable
show-password show-password
placeholder="密码" placeholder="密码"
:prefix-icon="useRenderIcon(Lock)" :prefix-icon="useRenderIcon(Lock)"
/> />
</el-form-item> </el-form-item>
</Motion> </Motion>
<Motion :delay="250"> <Motion :delay="250">
<el-button <el-button
class="w-full mt-4" class="w-full mt-4"
size="default" size="default"
type="primary" type="primary"
:loading="loading" :loading="loading"
@click="onLogin(ruleFormRef)" @click="onLogin(ruleFormRef)"
> >
登录 登录
</el-button> </el-button>
</Motion> </Motion>
</el-form> </el-form>
</div>
</div>
</div> </div>
</div>
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped>
@import url("@/style/login.css"); @import url('@/style/login.css');
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-input-group__append, .el-input-group__prepend) { :deep(.el-input-group__append, .el-input-group__prepend) {
padding: 0; padding: 0;
} }
</style> </style>

Loading…
Cancel
Save