初始化

This commit is contained in:
2026-04-13 17:32:58 +08:00
commit c6ee0d5243
1342 changed files with 96426 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
<template>
<div class="tool-bar-lf">
<CollapseIcon id="collapseIcon" />
<Breadcrumb v-show="globalStore.breadcrumb" id="breadcrumb" />
</div>
</template>
<script setup lang="ts">
import { useGlobalStore } from "@/stores/modules/global";
import CollapseIcon from "./components/CollapseIcon.vue";
import Breadcrumb from "./components/Breadcrumb.vue";
const globalStore = useGlobalStore();
</script>
<style scoped lang="scss">
.tool-bar-lf {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="tool-bar-ri">
<!-- <div class="header-icon">-->
<!-- <AssemblySize id="assemblySize" />-->
<!-- <Language id="language" />-->
<!-- <SearchMenu id="searchMenu" />-->
<!-- <ThemeSetting id="themeSetting" />-->
<!-- <Message id="message" />-->
<!-- <Fullscreen id="fullscreen" />-->
<!-- </div>-->
<Avatar />
</div>
</template>
<script setup lang="ts">
import Avatar from "./components/Avatar.vue";
</script>
<style scoped lang="scss">
.tool-bar-ri {
display: flex;
align-items: center;
justify-content: center;
// padding-right: 25px;
.header-icon {
display: flex;
align-items: center;
& > * {
margin-left: 21px;
color: var(--el-header-text-color);
}
}
.username {
margin: 0 20px;
font-size: 15px;
color: var(--el-header-text-color);
}
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<el-dropdown trigger="click" @command="setAssemblySize">
<i :class="'iconfont icon-contentright'" class="toolBar-icon"></i>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in assemblySizeList"
:key="item.value"
:command="item.value"
:disabled="assemblySize === item.value"
>
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useGlobalStore } from "@/stores/modules/global";
import { AssemblySizeType } from "@/stores/interface";
const globalStore = useGlobalStore();
const assemblySize = computed(() => globalStore.assemblySize);
const assemblySizeList = [
{ label: "默认", value: "default" },
{ label: "大型", value: "large" },
{ label: "小型", value: "small" }
];
const setAssemblySize = (item: AssemblySizeType) => {
if (assemblySize.value === item) return;
globalStore.setGlobalState("assemblySize", item);
};
</script>

View File

@@ -0,0 +1,104 @@
<template>
<el-dropdown trigger="click">
<div class="userInfo">
<div class="icon">
<Avatar />
</div>
<div class="username">
{{ username }}
</div>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openDialog('themeRef')">
<el-icon><Sunny /></el-icon>
{{ t('header.changeTheme') }}
</el-dropdown-item>
<el-dropdown-item @click="openDialog('passwordRef')">
<el-icon><Edit /></el-icon>
{{ t('header.changePassword') }}
</el-dropdown-item>
<el-dropdown-item @click="openDialog('versionRegisterRef')">
<el-icon><SetUp /></el-icon>
{{ t('header.versionRegister') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div class="avatar">
<img src="@/assets/icons/out_login.svg" alt="avatar" @click="logout" />
</div>
<PasswordDialog ref="passwordRef"></PasswordDialog>
<VersionDialog ref="versionRegisterRef"></VersionDialog>
<ThemeDialog ref="themeRef"></ThemeDialog>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { LOGIN_URL } from '@/config'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/modules/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import PasswordDialog from './PasswordDialog.vue'
import ThemeDialog from './ThemeDialog.vue'
import VersionDialog from '@/views/system/versionRegister/index.vue'
import { Avatar, Sunny } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n'
const userStore = useUserStore()
const username = computed(() => userStore.userInfo.name)
const router = useRouter()
const { t } = useI18n()
const logout = () => {
ElMessageBox.confirm('确认退出登录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
ElMessage.success('退出登录成功')
await userStore.logout()
await router.push(LOGIN_URL)
})
}
const passwordRef = ref<InstanceType<typeof PasswordDialog> | null>(null)
const versionRegisterRef = ref<InstanceType<typeof VersionDialog> | null>(null)
const themeRef = ref<InstanceType<typeof ThemeDialog> | null>(null)
const openDialog = (refName: string) => {
if (refName == 'passwordRef') passwordRef.value?.openDialog()
if (refName == 'versionRegisterRef') versionRegisterRef.value?.openDialog()
if (refName == 'themeRef') themeRef.value?.openDialog()
}
</script>
<style scoped lang="scss">
.userInfo {
min-width: 80px;
display: flex;
justify-content: flex-start;
align-items: center;
cursor: pointer;
.icon {
width: 18px;
height: 18px;
color: #fff !important;
}
.username {
color: #fff;
font-size: 16px;
margin-left: 10px;
}
}
.avatar {
width: 40px;
height: 40px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: flex-end;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div :class="['breadcrumb-box mask-image', !globalStore.breadcrumbIcon && 'no-icon']">
<el-breadcrumb :separator-icon="ArrowRight">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.path">
<div class="el-breadcrumb__inner is-link" @click="onBreadcrumbClick(item, index)">
<el-icon v-show="item.meta.icon && globalStore.breadcrumbIcon" class="breadcrumb-icon">
<component :is="item.meta.icon"></component>
</el-icon>
<span class="breadcrumb-title">{{ item.meta.title }}</span>
</div>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { HOME_URL } from '@/config'
import { useRoute, useRouter } from 'vue-router'
import { ArrowRight } from '@element-plus/icons-vue'
import { useAuthStore } from '@/stores/modules/auth'
import { useGlobalStore } from '@/stores/modules/global'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const globalStore = useGlobalStore()
const homeBreadcrumb = {
path: HOME_URL,
meta: {
icon: 'HomeFilled',
title: '首页'
}
} as Menu.MenuOptions
const breadcrumbList = computed(() => {
const matchedPath = route.matched[route.matched.length - 1]?.path ?? route.path
let breadcrumbData = authStore.breadcrumbListGet[matchedPath] ?? []
if (!breadcrumbData.length) {
return [homeBreadcrumb]
}
if (breadcrumbData[0]?.path !== HOME_URL) {
breadcrumbData = [homeBreadcrumb, ...breadcrumbData]
}
return breadcrumbData
})
const onBreadcrumbClick = (item: Menu.MenuOptions, index: number) => {
if (index !== breadcrumbList.value.length - 1) router.push(item.path)
}
</script>
<style scoped lang="scss">
.breadcrumb-box {
display: flex;
align-items: center;
overflow: hidden;
.el-breadcrumb {
white-space: nowrap;
.el-breadcrumb__item {
position: relative;
display: inline-block;
float: none;
.el-breadcrumb__inner {
display: inline-flex;
&.is-link {
color: var(--el-header-text-color);
&:hover {
color: var(--el-color-primary);
}
}
.breadcrumb-icon {
margin-top: 2px;
margin-right: 6px;
font-size: 16px;
}
.breadcrumb-title {
margin-top: 3px;
}
}
&:last-child .el-breadcrumb__inner,
&:last-child .el-breadcrumb__inner:hover {
color: var(--el-header-text-color-regular);
}
:deep(.el-breadcrumb__separator) {
position: relative;
top: -1px;
}
}
}
}
.no-icon {
.el-breadcrumb {
.el-breadcrumb__item {
top: -2px;
:deep(.el-breadcrumb__separator) {
top: 2px;
}
}
}
}
</style>

View File

@@ -0,0 +1,21 @@
<template>
<el-icon class="collapse-icon" @click="changeCollapse">
<component :is="globalStore.isCollapse ? 'expand' : 'fold'"></component>
</el-icon>
</template>
<script setup lang="ts">
import { useGlobalStore } from "@/stores/modules/global";
const globalStore = useGlobalStore();
const changeCollapse = () => globalStore.setGlobalState("isCollapse", !globalStore.isCollapse);
</script>
<style scoped lang="scss">
.collapse-icon {
margin-right: 20px;
font-size: 22px;
color: var(--el-header-text-color);
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,25 @@
<template>
<div class="fullscreen">
<i :class="['iconfont', isFullscreen ? 'icon-suoxiao' : 'icon-fangda']" class="toolBar-icon" @click="handleFullScreen"></i>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import screenfull from "screenfull";
const isFullscreen = ref(screenfull.isFullscreen);
onMounted(() => {
screenfull.on("change", () => {
if (screenfull.isFullscreen) isFullscreen.value = true;
else isFullscreen.value = false;
});
});
const handleFullScreen = () => {
if (!screenfull.isEnabled) ElMessage.warning("当前您的浏览器不支持全屏 ❌");
screenfull.toggle();
};
</script>

View File

@@ -0,0 +1,38 @@
<template>
<el-dropdown trigger="click" @command="changeLanguage">
<i :class="'iconfont icon-zhongyingwen'" class="toolBar-icon"></i>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in languageList"
:key="item.value"
:command="item.value"
:disabled="language === item.value"
>
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { computed } from "vue";
import { useGlobalStore } from "@/stores/modules/global";
import { LanguageType } from "@/stores/interface";
const i18n = useI18n();
const globalStore = useGlobalStore();
const language = computed(() => globalStore.language);
const languageList = [
{ label: "简体中文", value: "zh" },
{ label: "English", value: "en" }
];
const changeLanguage = (lang: string) => {
i18n.locale.value = lang;
globalStore.setGlobalState("language", lang as LanguageType);
};
</script>

View File

@@ -0,0 +1,109 @@
<template>
<div class="message">
<el-popover placement="bottom" :width="310" trigger="click">
<template #reference>
<el-badge :value="5" class="item">
<i :class="'iconfont icon-xiaoxi'" class="toolBar-icon"></i>
</el-badge>
</template>
<el-tabs v-model="activeName">
<el-tab-pane label="通知(5)" name="first">
<div class="message-list">
<div class="message-item">
<img src="@/assets/images/msg01.png" alt="" class="message-icon" />
<div class="message-content">
<span class="message-title">一键三连 Geeker-Admin 🧡</span>
<span class="message-date">一分钟前</span>
</div>
</div>
<div class="message-item">
<img src="@/assets/images/msg02.png" alt="" class="message-icon" />
<div class="message-content">
<span class="message-title">一键三连 Geeker-Admin 💙</span>
<span class="message-date">一小时前</span>
</div>
</div>
<div class="message-item">
<img src="@/assets/images/msg03.png" alt="" class="message-icon" />
<div class="message-content">
<span class="message-title">一键三连 Geeker-Admin 💚</span>
<span class="message-date">半天前</span>
</div>
</div>
<div class="message-item">
<img src="@/assets/images/msg04.png" alt="" class="message-icon" />
<div class="message-content">
<span class="message-title">一键三连 Geeker-Admin 💜</span>
<span class="message-date">一星期前</span>
</div>
</div>
<div class="message-item">
<img src="@/assets/images/msg05.png" alt="" class="message-icon" />
<div class="message-content">
<span class="message-title">一键三连 Geeker-Admin 💛</span>
<span class="message-date">一个月前</span>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="消息(0)" name="second">
<div class="message-empty">
<img src="@/assets/images/notData.png" alt="notData" />
<div>暂无消息</div>
</div>
</el-tab-pane>
<el-tab-pane label="代办(0)" name="third">
<div class="message-empty">
<img src="@/assets/images/notData.png" alt="notData" />
<div>暂无代办</div>
</div>
</el-tab-pane>
</el-tabs>
</el-popover>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const activeName = ref("first");
</script>
<style scoped lang="scss">
.message-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 260px;
line-height: 45px;
}
.message-list {
display: flex;
flex-direction: column;
.message-item {
display: flex;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid var(--el-border-color-light);
&:last-child {
border: none;
}
.message-icon {
width: 40px;
height: 40px;
margin: 0 20px 0 5px;
}
.message-content {
display: flex;
flex-direction: column;
.message-title {
margin-bottom: 5px;
}
.message-date {
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
}
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<el-dialog v-model="dialogVisible" title="修改密码" width="500px" draggable>
<div>
<el-form :model="formContent" ref="dialogFormRef" :rules="rules">
<el-form-item label="原密码" prop="oldPassword" :label-width="100">
<el-input
type="oldPassword"
v-model="formContent.oldPassword"
show-password
placeholder="请输入原密码"
autocomplete="off"
maxlength="32"
show-word-limit
/>
</el-form-item>
<el-form-item label="新密码" prop="newPassword" :label-width="100">
<el-input
type="newPassword"
v-model="formContent.newPassword"
show-password
placeholder="请输入新密码"
autocomplete="off"
maxlength="32"
show-word-limit
/>
</el-form-item>
<el-form-item label="确认密码" prop="surePassword" :label-width="100">
<el-input
type="surePassword"
v-model="formContent.surePassword"
show-password
placeholder="请再次输入确认密码"
autocomplete="off"
maxlength="32"
show-word-limit
/>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="close()">取消</el-button>
<el-button type="primary" @click="save()">保存</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, type Ref } from 'vue'
import { ElMessage, type FormItemRule } from 'element-plus'
import { updatePassWord } from '@/api/user/user'
import { type User } from '@/api/user/interface/user'
import { useUserStore } from '@/stores/modules/user'
import { LOGIN_URL } from '@/config'
import { useRouter } from 'vue-router'
const userStore = useUserStore()
const router = useRouter()
// 定义弹出组件元信息
const dialogFormRef = ref()
function useMetaInfo() {
const dialogVisible = ref(false)
const formContent = ref<User.ResPassWordUser>({
id: '', //用户ID作为唯一标识
oldPassword: '', //密码
newPassword: '', //
surePassword: ''
})
return { dialogVisible, formContent }
}
const { dialogVisible, formContent } = useMetaInfo()
// 清空formContent
const resetFormContent = () => {
formContent.value = {
id: '', //用户ID作为唯一标识
oldPassword: '', //密码
newPassword: '', //
surePassword: ''
}
}
// 定义规则
const rules: Ref<Record<string, Array<FormItemRule>>> = ref({
oldPassword: [{ required: true, message: '原密码必填!', trigger: 'blur' }],
newPassword: [
{ required: true, message: '新密码必填!', trigger: 'blur' },
{
pattern: /^(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).{8,16}$/,
message: '密码长度为8-16需包含特殊字符',
trigger: 'blur'
}
],
surePassword: [
{ required: true, message: '确认密码必填!', trigger: 'blur' },
{
validator: (rule: FormItemRule, value: string, callback: Function) => {
if (value !== formContent.value.newPassword) {
callback(new Error('两次输入的密码不一致!'))
} else {
callback()
}
},
trigger: 'blur'
}
]
})
// 关闭弹窗
const close = () => {
dialogVisible.value = false
// 清空dialogForm中的值
resetFormContent()
// 重置表单
dialogFormRef.value?.resetFields()
}
// 保存数据
const save = () => {
try {
dialogFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
if (formContent.value.id) {
await updatePassWord(formContent.value)
ElMessage.success('修改密码成功,请重新登录!')
setTimeout(async () => {
await userStore.logout()
await router.push(LOGIN_URL)
}, 2000)
}
close()
}
})
} catch (err) {
console.error('验证过程中出现错误', err)
}
}
// 打开弹窗是编辑
const openDialog = async () => {
// 重置表单
dialogFormRef.value?.resetFields()
dialogVisible.value = true
formContent.value.id = userStore.userInfo.id
}
// 对外映射
defineExpose({ openDialog })
</script>

View File

@@ -0,0 +1,113 @@
<template>
<div class="menu-search-dialog">
<i :class="'iconfont icon-sousuo'" class="toolBar-icon" @click="handleOpen"></i>
<el-dialog v-model="isShowSearch" destroy-on-close :modal="false" :show-close="false" fullscreen @click="closeSearch">
<el-autocomplete
ref="menuInputRef"
v-model="searchMenu"
value-key="path"
placeholder="菜单搜索 :支持菜单名称、路径"
:fetch-suggestions="searchMenuList"
@select="handleClickMenu"
@click.stop
>
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
<template #default="{ item }">
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<span> {{ item.meta.title }} </span>
</template>
</el-autocomplete>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from "vue";
import { Search } from "@element-plus/icons-vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/stores/modules/auth";
const router = useRouter();
const authStore = useAuthStore();
const menuList = computed(() => authStore.flatMenuListGet.filter(item => !item.meta.isHide));
const searchMenuList = (queryString: string, cb: Function) => {
const results = queryString ? menuList.value.filter(filterNodeMethod(queryString)) : menuList.value;
cb(results);
};
// 打开搜索框
const isShowSearch = ref(false);
const menuInputRef = ref();
const searchMenu = ref("");
const handleOpen = () => {
isShowSearch.value = true;
nextTick(() => {
setTimeout(() => {
menuInputRef.value.focus();
});
});
};
// 搜索窗关闭
const closeSearch = () => {
isShowSearch.value = false;
};
// 筛选菜单
const filterNodeMethod = (queryString: string) => {
return (restaurant: Menu.MenuOptions) => {
return (
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
);
};
};
// 点击菜单跳转
const handleClickMenu = (menuItem: Menu.MenuOptions | Record<string, any>) => {
searchMenu.value = "";
if (menuItem.meta.isLink) window.open(menuItem.meta.isLink, "_blank");
else router.push(menuItem.path);
closeSearch();
};
</script>
<style scoped lang="scss">
.menu-search-dialog {
:deep(.el-dialog) {
background-color: rgb(0 0 0 / 50%);
border-radius: 0 !important;
box-shadow: unset !important;
.el-dialog__header {
border-bottom: none !important;
}
}
:deep(.el-autocomplete) {
position: absolute;
top: 100px;
left: 50%;
width: 550px;
transform: translateX(-50%);
.el-input__wrapper {
background-color: var(--el-bg-color);
}
}
}
.el-autocomplete__popper {
.el-icon {
position: relative;
top: 2px;
font-size: 16px;
}
span {
margin: 0 0 0 10px;
font-size: 14px;
}
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<el-dialog v-model="dialogVisible" title="主题切换" width="500px" draggable>
<el-divider content-position="center">主题颜色</el-divider>
<div style="display: flex; justify-content: center;">
<el-color-picker v-model="color" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="sure">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useTheme } from "@/hooks/useTheme";
import { on } from "events";
const color = ref('')
const { changePrimary} = useTheme();
const dialogVisible = ref(false);
const openDialog = () => {
// 修复:使用可选链和空值合并运算符确保不会出现 null 或 undefined
const storedColor = JSON.parse(localStorage.getItem('cn-global') ?? '{}').primary;
color.value = storedColor ?? '#526ADE'; // 默认值为 '#526ADE'
dialogVisible.value = true;
};
const sure = () => {
changePrimary(color.value); // 切换主题
dialogVisible.value = false;
};
// onMounted(() => {
// // 修复:使用可选链和空值合并运算符确保不会出现 null 或 undefined
// const storedColor = JSON.parse(localStorage.getItem('cn-global') ?? '{}').primary;
// console.log('123',storedColor)
// color.value = storedColor ?? '#526ADE'; // 默认值为 '#526ADE'
// })
defineExpose({ openDialog });
</script>

View File

@@ -0,0 +1,12 @@
<template>
<div class="theme-setting">
<i :class="'iconfont icon-zhuti'" class="toolBar-icon" @click="openDrawer"></i>
</div>
</template>
<script setup lang="ts">
import mittBus from "@/utils/mittBus";
const openDrawer = () => {
mittBus.emit("openThemeDrawer");
};
</script>