Initial commit

This commit is contained in:
南宁网宿科技
2024-04-24 16:36:46 +08:00
commit 121348e011
991 changed files with 158700 additions and 0 deletions

View File

@@ -0,0 +1,231 @@
<!-- 顶栏消息通知 -->
<template>
<a-dropdown
v-model:visible="visible"
placement="bottom"
:trigger="['click']"
:overlay-style="{ padding: '0 10px' }"
>
<a-badge :count="unreadNum" class="ele-notice-trigger" :offset="[6, 4]">
<bell-outlined style="padding: 8px 0" />
</a-badge>
<template #overlay>
<div class="ant-dropdown-menu ele-notice-pop">
<div @click.stop="">
<a-tabs v-model:active-key="active" :centered="true">
<a-tab-pane key="notice" :tab="noticeTitle">
<a-list item-layout="horizontal" :data-source="notice">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta
@click="openUrl('/user/notice?type=notice')"
:title="item.title"
:description="timeAgo(item.createTime)"
>
<template #avatar>
<a-avatar :style="{ background: item.color }">
<template #icon>
<component :is="item.icon" />
</template>
</a-avatar>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
<div v-if="notice.length" class="ele-cell ele-notice-actions">
<div class="ele-cell-content" @click="clearNotice">
清空通知
</div>
<a-divider type="vertical" />
<router-link
to="/user/notice?type=notice"
class="ele-cell-content"
>
查看更多
</router-link>
</div>
</a-tab-pane>
</a-tabs>
</div>
</div>
</template>
</a-dropdown>
<!-- <chat-message-list-->
<!-- v-model:visible="visibleChatMessageList"-->
<!-- :data="currentChatConversation"-->
<!-- />-->
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { message } from 'ant-design-vue/es';
// import { getUnReadNum, pageNotices, pageTodos } from '@/api/user/message';
import { openUrl } from '@/utils/common';
import { timeAgo } from 'ele-admin-pro';
import { useChatStore } from '@/store/modules/chat';
import { useUserStore } from '@/store/modules/user';
import { storeToRefs } from 'pinia';
// import ChatMessageList from '@/views/love/chat/components/chat-message-list.vue';
import { ChatConversation } from '@/api/system/chat/model';
const chatStore = useChatStore();
const userStore = useUserStore();
// 是否显示
const visible = ref<boolean>(false);
const visibleChatMessageList = ref<boolean>(false);
const currentChatConversation = ref<ChatConversation>();
// 选项卡选中
const active = ref<string>('notice');
// 通知数据
const notice = ref<any>([]);
// 待办数据
const todo = ref<any>([]);
// 通知未读数量
const unReadNotice = ref<any>(0);
// 私信未读数量
const { unReadLetter, unReadConversations } = storeToRefs(chatStore);
chatStore.connectSocketIO(userStore.info?.userId || 0);
// 代办未读数量
const unReadTodo = ref<any>(0);
// 通知标题
const noticeTitle = computed(() => {
return '通知' + (unReadNotice.value > 0 ? `(${unReadNotice.value})` : '');
});
// 未读数量
const unreadNum = computed(() => {
return unReadNotice.value + unReadLetter.value + unReadTodo.value;
});
const openChat = (item: ChatConversation) => {
chatStore.readConversation(item.id);
currentChatConversation.value = item;
visible.value = false;
visibleChatMessageList.value = true;
};
/* 查询数据 */
const query = () => {
// pageNotices({ status: 0 })
// .then((result) => {
// notice.value = result?.list;
// })
// .catch((e) => {
// message.error(e.message);
// });
//
// pageTodos({ status: 0 })
// .then((result) => {
// todo.value = result?.list;
// })
// .catch((e) => {
// message.error(e.message);
// });
};
/* 查询未读数量 */
const queryUnReadNum = () => {
// getUnReadNum()
// .then((result) => {
// unReadNotice.value = result?.notice;
// unReadTodo.value = result?.todo;
// })
// .catch((e) => {
// message.error(e.message);
// });
};
queryUnReadNum();
/* 清空通知 */
const clearNotice = () => {
unReadNotice.value = 0;
};
/* 清空通知 */
const clearLetter = () => {
// unReadLetter = 0;
};
/* 清空通知 */
const clearTodo = () => {
unReadTodo.value = 0;
};
// query();
</script>
<script lang="ts">
import {
BellOutlined,
NotificationFilled,
PushpinFilled,
VideoCameraFilled,
CarryOutFilled,
BellFilled
} from '@ant-design/icons-vue';
export default {
name: 'HeaderNotice',
components: {
BellOutlined,
NotificationFilled,
PushpinFilled,
VideoCameraFilled,
CarryOutFilled,
BellFilled
}
};
</script>
<style lang="less">
.ele-notice-trigger.ant-badge {
color: inherit;
}
.ele-notice-pop {
&.ant-dropdown-menu {
padding: 0;
width: 336px;
max-width: 100%;
margin-top: 11px;
}
// 内容
.ant-list-item {
padding-left: 24px;
padding-right: 24px;
transition: background-color 0.3s;
cursor: pointer;
&:hover {
background: hsla(0, 0%, 60%, 0.05);
}
}
.ant-tag {
margin: 0;
}
// 操作按钮
.ele-notice-actions {
border-top: 1px solid hsla(0, 0%, 60%, 0.15);
& > .ele-cell-content {
line-height: 46px;
text-align: center;
transition: background-color 0.3s;
cursor: pointer;
color: inherit;
&:hover {
background: hsla(0, 0%, 60%, 0.05);
}
}
}
}
</style>

View File

@@ -0,0 +1,158 @@
<!-- 顶栏右侧区域 -->
<template>
<div class="ele-admin-header-tool">
<!-- 全屏切换 -->
<div
:class="[
'ele-admin-header-tool-item',
{ 'hidden-sm-and-down': styleResponsive }
]"
@click="toggleFullscreen"
>
<fullscreen-exit-outlined v-if="fullscreen" />
<fullscreen-outlined v-else />
</div>
<!-- 语言切换 -->
<!-- <div class="ele-admin-header-tool-item">-->
<!-- <i18n-icon />-->
<!-- </div>-->
<!-- 消息通知 -->
<div class="ele-admin-header-tool-item">
<header-notice />
</div>
<!-- 用户信息 -->
<div class="ele-admin-header-tool-item">
<a-dropdown placement="bottom" :overlay-style="{ minWidth: '120px' }">
<div class="ele-admin-header-avatar">
<a-avatar :src="loginUser.avatar">
<template v-if="!loginUser.avatar" #icon>
<user-outlined />
</template>
</a-avatar>
<span :class="{ 'hidden-sm-and-down': styleResponsive }">
{{ loginUser.nickname }}
</span>
<down-outlined style="margin-left: 6px" />
</div>
<template #overlay>
<a-menu :selectable="false" @click="onUserDropClick">
<a-menu-item key="profile">
<div class="ele-cell">
<user-outlined />
<div class="ele-cell-content">
{{ t('layout.header.profile') }}
</div>
</div>
</a-menu-item>
<a-menu-item key="password">
<div class="ele-cell">
<key-outlined />
<div class="ele-cell-content">
{{ t('layout.header.password') }}
</div>
</div>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout">
<div class="ele-cell">
<logout-outlined />
<div class="ele-cell-content">
{{ t('layout.header.logout') }}
</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<!-- 主题设置 -->
<div class="ele-admin-header-tool-item" @click="openSetting">
<more-outlined />
</div>
</div>
<!-- 修改密码弹窗 -->
<password-modal v-model:visible="passwordVisible" />
<!-- 主题设置抽屉 -->
<setting-drawer v-model:visible="settingVisible" />
</template>
<script lang="ts" setup>
import { computed, createVNode, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { Modal } from 'ant-design-vue/es';
import {
DownOutlined,
MoreOutlined,
UserOutlined,
KeyOutlined,
LogoutOutlined,
ExclamationCircleOutlined,
FullscreenOutlined,
FullscreenExitOutlined
} from '@ant-design/icons-vue';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import HeaderNotice from './header-notice.vue';
import PasswordModal from './password-modal.vue';
import SettingDrawer from './setting-drawer.vue';
// import I18nIcon from './i18n-icon.vue';
import { useUserStore } from '@/store/modules/user';
import { logout } from '@/utils/page-tab-util';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'fullscreen'): void;
}>();
defineProps<{
// 是否是全屏
fullscreen: boolean;
}>();
const { push } = useRouter();
const { t } = useI18n();
const userStore = useUserStore();
// 是否显示修改密码弹窗
const passwordVisible = ref(false);
// 是否显示主题设置抽屉
const settingVisible = ref(false);
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
/* 用户信息下拉点击 */
const onUserDropClick = ({ key }) => {
if (key === 'password') {
passwordVisible.value = true;
} else if (key === 'profile') {
push('/user/profile');
} else if (key === 'logout') {
// 退出登录
Modal.confirm({
title: t('layout.logout.title'),
content: t('layout.logout.message'),
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
logout();
}
});
}
};
/* 切换全屏 */
const toggleFullscreen = () => {
emit('fullscreen');
};
/* 打开主题设置抽屉 */
const openSetting = () => {
settingVisible.value = true;
};
</script>

View File

@@ -0,0 +1,325 @@
<!-- 顶栏右侧区域 -->
<template>
<div class="ele-admin-header-tool">
<div class="ele-admin-header-tool-item">
<a-tree-select
show-search
tree-node-filter-prop="title"
treeDefaultExpandAll
:bordered="bordered"
:tree-data="menuTree"
:placeholder="`搜索...`"
:value="parentId || undefined"
style="width: 180px"
:dropdown-style="{ maxHeight: '560px', overflow: 'auto' }"
@change="onChange"
>
<template #suffixIcon><SearchOutlined /></template>
</a-tree-select>
</div>
<!-- <div-->
<!-- class="ele-admin-header-tool-item"-->
<!-- @click="openUrl('https://b.gxwebsoft.com')"-->
<!-- >-->
<!-- 旧版-->
<!-- </div>-->
<!-- 消息通知 -->
<div class="ele-admin-header-tool-item">
<header-notice />
</div>
<!-- 全屏切换 -->
<div
:class="[
'ele-admin-header-tool-item',
{ 'hidden-sm-and-down': styleResponsive }
]"
@click="toggleFullscreen"
>
<fullscreen-exit-outlined v-if="fullscreen" />
<fullscreen-outlined v-else />
</div>
<!-- 语言切换 -->
<!-- <div class="ele-admin-header-tool-item">-->
<!-- <i18n-icon />-->
<!-- </div>-->
<!-- 用户信息 -->
<div class="ele-admin-header-tool-item">
<a-dropdown placement="bottom" :overlay-style="{ minWidth: '280px' }">
<div class="ele-admin-header-avatar">
<a-avatar :src="loginUser.avatar">
<template v-if="!loginUser.avatar" #icon>
<user-outlined />
</template>
</a-avatar>
<span :class="{ 'hidden-sm-and-down': styleResponsive }">
{{ loginUser.nickname }}
</span>
<down-outlined style="margin-left: 6px" />
</div>
<template #overlay>
<a-menu :selectable="false" @click="onUserDropClick">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<div class="user-profile">
<div class="user-info">
<div class="nickname">
<span class="ele-text-heading">
{{ loginUser.nickname }}
</span>
<div class="ele-text-placeholder">
用户ID<span class="ele-text-secondary">{{
loginUser.userId
}}</span>
<copy-outlined
style="margin-left: 10px"
@click="copyText(loginUser.userId)"
/>
</div>
<div class="role" style="margin-top: 10px">
<template
v-for="(item, index) in loginUser.roles"
:key="item.roleId"
>
<a-tag color="blue" v-if="index === 0">
<div class="role-name">
<span>{{ item.roleName }}</span>
<span v-if="index === 1"></span>
</div>
</a-tag>
</template>
</div>
</div>
</div>
</div>
</a-card>
<a-menu-divider />
<a-menu-item key="profile">
<div class="ele-cell">
<div class="ele-cell-content"> 基本资料 </div>
</div>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="password">
<div class="ele-cell">
<div class="ele-cell-content"> 修改密码 </div>
</div>
</a-menu-item>
<template v-if="loginUser.username == 'admin'">
<a-menu-divider />
<a-menu-item key="accessKey" v-if="loginUser.username == 'admin'">
<div class="ele-cell">
<div class="ele-cell-content"> 秘钥管理 </div>
</div>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="skin" v-if="loginUser.username == 'admin'">
<div class="ele-cell">
<div class="ele-cell-content"> 偏好设置 </div>
</div>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="system" v-if="loginUser.username == 'admin'">
<div class="ele-cell">
<div class="ele-cell-content"> 系统设置 </div>
</div>
</a-menu-item>
</template>
<a-menu-divider />
<a-menu-item key="logout">
<div class="ele-cell" align="center">
<div class="ele-cell-content">
<logout-outlined />
{{ t('layout.header.logout') }}
</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<!-- 主题设置 -->
<!-- <div class="ele-admin-header-tool-item" @click="openSetting">-->
<!-- <more-outlined />-->
<!-- </div>-->
</div>
<!-- 修改密码弹窗 -->
<password-modal v-model:visible="passwordVisible" />
<!-- 主题设置抽屉 -->
<setting-drawer v-model:visible="settingVisible" />
</template>
<script lang="ts" setup>
import { computed, createVNode, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { Modal } from 'ant-design-vue/es';
import {
DownOutlined,
CopyOutlined,
UserOutlined,
MoreOutlined,
LogoutOutlined,
ExclamationCircleOutlined,
FullscreenOutlined,
FullscreenExitOutlined,
SearchOutlined
} from '@ant-design/icons-vue';
import { storeToRefs } from 'pinia';
import { copyText, openNew, openUrl } from '@/utils/common';
import { useThemeStore } from '@/store/modules/theme';
import HeaderNotice from './header-notice.vue';
import PasswordModal from './password-modal.vue';
import SettingDrawer from './setting-drawer.vue';
import { useUserStore } from '@/store/modules/user';
import { logout } from '@/utils/page-tab-util';
import type { Menu } from '@/api/system/menu/model';
import { listMenus } from '@/api/system/menu';
import { isExternalLink, toTreeData } from 'ele-admin-pro';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
// const TENANT_ID = localStorage.getItem('TenantId');
// const TENANT_NAME = localStorage.getItem('TenantName');
const emit = defineEmits<{
(e: 'fullscreen'): void;
}>();
defineProps<{
// 是否是全屏
fullscreen: boolean;
}>();
const { push } = useRouter();
const { t } = useI18n();
const userStore = useUserStore();
// 是否显示修改密码弹窗
const passwordVisible = ref(false);
// 是否显示主题设置抽屉
const settingVisible = ref(false);
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
const menuList = ref<Menu[]>([]);
const menuTree = ref<Menu[]>([]);
const parentId = ref<number>();
const bordered = ref<boolean>(false);
/* 用户信息下拉点击 */
const onUserDropClick = ({ key }) => {
if (key === 'password') {
passwordVisible.value = true;
} else if (key === 'profile') {
push('/system/profile');
} else if (key === 'accessKey') {
push('/system/access-key');
} else if (key === 'taskAdd') {
push('/user/task/add');
} else if (key === 'myTask') {
push('/user/task/index');
} else if (key === 'skin') {
settingVisible.value = true;
} else if (key === 'system') {
push('/system/setting');
} else if (key === 'logout') {
// 退出登录
Modal.confirm({
title: t('layout.logout.title'),
content: t('layout.logout.message'),
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
logout();
}
});
}
};
const myOrder = () => {
push('/user/my-balance-log');
};
const onChange = (index: number) => {
const item = menuList.value.find((d) => d.menuId == index);
const isExternal = isExternalLink(item?.path);
if (isExternal) {
return openNew(`${item?.path}`);
}
if (item?.parentId == 0) {
return push(item.path);
}
if (item?.component) {
return push(item.path);
}
// bordered.value = true;
};
/* 切换全屏 */
const toggleFullscreen = () => {
emit('fullscreen');
};
/* 打开主题设置抽屉 */
const openSetting = () => {
settingVisible.value = true;
};
listMenus({ menuType: 0, hide: 0 }).then((data) => {
if (data) {
menuList.value = data;
menuTree.value = toTreeData({
data: data.map((d) => {
return { ...d, key: d.menuId, value: d.menuId };
}),
idField: 'menuId',
parentIdField: 'parentId'
});
}
});
</script>
<script lang="ts">
import * as icons from '@/layout/menu-icons';
export default {
components: icons,
data() {
return {
iconData: [
{
title: '已引入的图标',
icons: Object.keys(icons)
}
]
};
}
};
</script>
<style lang="less" scoped>
.ant-select {
color: #bfbfbf;
}
.user-profile {
display: flex;
.user-info {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.nickname {
display: flex;
flex-direction: column;
}
}
}
.ele-admin-header-tool-item {
:deep(.ant-select-tree-switcher) {
background-color: #ff00fe;
}
:deep(.ant-select-tree-indent) {
display: none;
}
}
</style>

View File

@@ -0,0 +1,33 @@
<!-- 顶栏消息通知 -->
<template>
<a-popover>
<template #content>
<p class="ele-text-placeholder" style="text-align: center">
请使用微信扫描二维码
</p>
<a-image
:preview="false"
:width="200"
:height="200"
src="https://file.wsdns.cn/20230518/6b3c88423e0e437f81b3adbf63d30cba.png"
/>
<p class="ele-text-placeholder" style="text-align: center">
客服电话0771-5386339
</p>
</template>
<div class="ele-admin-header-tool-item">
<wechat-outlined style="padding: 8px 0" />
</div>
</a-popover>
</template>
<script lang="ts" setup>
import { WechatOutlined } from '@ant-design/icons-vue';
</script>
<style lang="less">
.wechat-box {
.title {
line-height: 40px;
}
}
</style>

View File

@@ -0,0 +1,52 @@
<!-- 国际化语言切换组件 -->
<template>
<a-dropdown
:placement="placement"
:overlay-style="{ minWidth: '120px', paddingTop: '17px' }"
>
<slot>
<global-outlined :style="style" />
</slot>
<template #overlay>
<a-menu :selected-keys="language" @click="changeLanguage">
<a-menu-item key="en">English</a-menu-item>
<a-menu-item key="zh_CN">简体中文</a-menu-item>
<a-menu-item key="zh_TW">繁體中文</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<script lang="ts" setup>
import type { CSSProperties } from 'vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { GlobalOutlined } from '@ant-design/icons-vue';
import { I18N_CACHE_NAME } from '@/config/setting';
withDefaults(
defineProps<{
// dropdown placement
placement?: any;
// 自定义样式
style?: CSSProperties;
}>(),
{
placement: 'bottom',
style: () => {
return { transform: 'scale(1.08)' };
}
}
);
const { locale } = useI18n();
// 当前显示语言
const language = computed(() => [locale.value]);
/* 切换语言 */
const changeLanguage = ({ key }) => {
locale.value = key;
localStorage.setItem(I18N_CACHE_NAME, key);
};
</script>

View File

@@ -0,0 +1,17 @@
<template>
<span>{{ item.meta.title }}</span>
<div v-if="item.meta && item.meta.badge" class="ele-menu-badge">
<a-badge
:count="item.meta.badge"
:number-style="{ background: item.meta.badgeColor as string }"
/>
</div>
</template>
<script lang="ts" setup>
import type { MenuItemType } from 'ele-admin-pro/es';
defineProps<{
item: MenuItemType;
}>();
</script>

View File

@@ -0,0 +1,14 @@
<!-- 全局页脚 -->
<template>
<div class="ele-text-center" style="padding: 16px 0">
<div class="ele-text-secondary" style="margin-top: 8px">
{{ t('layout.footer.copyright') }}
</div>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>

View File

@@ -0,0 +1,146 @@
<!-- 修改密码弹窗 -->
<template>
<ele-modal
:width="420"
title="修改密码"
:visible="visible"
:confirm-loading="loading"
:body-style="{ paddingBottom: '16px' }"
@update:visible="updateVisible"
@cancel="onCancel"
@ok="onOk"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { sm: 6 } : { flex: '90px' }"
:wrapper-col="styleResponsive ? { sm: 18 } : { flex: '1' }"
>
<a-form-item label="旧密码" name="oldPassword">
<a-input-password
v-model:value="form.oldPassword"
placeholder="请输入旧密码"
/>
</a-form-item>
<a-form-item label="新密码" name="password">
<a-input-password
v-model:value="form.password"
placeholder="请输入新密码"
/>
</a-form-item>
<a-form-item label="确认密码" name="password2">
<a-input-password
v-model:value="form.password2"
placeholder="请再次输入新密码"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { updatePassword } from '@/api/layout';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
}>();
defineProps<{
visible: boolean;
}>();
//
const formRef = ref<FormInstance | null>(null);
// 提交loading
const loading = ref<boolean>(false);
// 表单数据
const { form, resetFields } = useFormData({
oldPassword: '',
password: '',
password2: ''
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
oldPassword: [
{
required: true,
type: 'string',
message: '请输入旧密码',
trigger: 'blur'
}
],
password: [
{
required: true,
type: 'string',
message: '请输入新密码',
trigger: 'blur'
}
],
password2: [
{
required: true,
type: 'string',
validator: async (_rule: Rule, value: string) => {
if (!value) {
return Promise.reject('请再次输入新密码');
}
if (value !== form.password) {
return Promise.reject('两次输入密码不一致');
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
/* 修改visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 保存修改 */
const onOk = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
updatePassword(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 关闭回调 */
const onCancel = () => {
resetFields();
formRef.value?.clearValidate();
loading.value = false;
};
</script>

View File

@@ -0,0 +1,745 @@
<!-- 主题设置抽屉 -->
<template>
<a-drawer
:width="280"
:visible="visible"
:body-style="{ padding: 0 }"
:header-style="{
position: 'absolute',
top: '16px',
right: 0,
padding: 0,
background: 'none'
}"
:z-index="1001"
@update:visible="updateVisible"
>
<div :class="['ele-setting-wrapper', { 'ele-setting-dark': darkMode }]">
<div class="ele-setting-title">{{ t('layout.setting.title') }}</div>
<!-- 侧栏风格 -->
<div
v-if="layoutStyle !== 'top'"
class="ele-setting-theme ele-text-primary"
>
<a-tooltip :title="t('layout.setting.sideStyles.dark')">
<div
class="ele-bg-base ele-side-dark"
@click="updateSideStyle('dark')"
>
<check-outlined v-if="sideStyle === 'dark'" />
</div>
</a-tooltip>
<a-tooltip :title="t('layout.setting.sideStyles.light')">
<div class="ele-bg-base" @click="updateSideStyle('light')">
<check-outlined v-if="sideStyle === 'light'" />
</div>
</a-tooltip>
</div>
<!-- 顶栏风格 -->
<div class="ele-setting-theme ele-text-primary">
<a-tooltip :title="t('layout.setting.headStyles.light')">
<div
class="ele-bg-base ele-head-light"
@click="updateHeadStyle('light')"
>
<check-outlined v-if="headStyle === 'light'" />
</div>
</a-tooltip>
<a-tooltip :title="t('layout.setting.headStyles.dark')">
<div
class="ele-bg-base ele-head-dark"
@click="updateHeadStyle('dark')"
>
<check-outlined v-if="headStyle === 'dark'" />
</div>
</a-tooltip>
<a-tooltip :title="t('layout.setting.headStyles.primary')">
<div
class="ele-bg-base ele-head-primary"
@click="updateHeadStyle('primary')"
>
<div class="ele-bg-primary"></div>
<check-outlined v-if="headStyle === 'primary'" />
</div>
</a-tooltip>
</div>
<!-- 主题色 -->
<div class="ele-setting-colors">
<div
v-for="item in themes"
:key="item.name"
:style="{ 'background-color': item.color || item.value }"
class="ele-setting-color-item"
@click="updateColor(item.value)"
>
<check-outlined v-if="item.value ? item.value === color : !color" />
<a-tooltip :title="t('layout.setting.colors.' + item.name)">
<div class="ele-setting-color-tooltip"></div>
</a-tooltip>
</div>
<!-- 颜色选择器 -->
<ele-color-picker
v-model:value="colorValue"
:predefine="predefineColors"
custom-class="ele-setting-color-picker"
@change="updateColor"
/>
</div>
<!-- 暗黑模式 -->
<div class="ele-setting-item">
<div class="setting-item-title">{{ t('layout.setting.darkMode') }}</div>
<div class="setting-item-control">
<a-switch size="small" :checked="darkMode" @change="updateDarkMode" />
</div>
</div>
<a-divider />
<!-- 导航布局 -->
<div
:class="[
'ele-setting-title ele-text-secondary',
{ 'hidden-xs-only': styleResponsive }
]"
>
{{ t('layout.setting.layoutStyle') }}
</div>
<div
:class="[
'ele-setting-theme ele-text-primary',
{ 'hidden-xs-only': styleResponsive }
]"
>
<a-tooltip :title="t('layout.setting.layoutStyles.side')">
<div
class="ele-bg-base ele-side-dark"
@click="updateLayoutStyle('side')"
>
<check-outlined v-if="layoutStyle === 'side'" />
</div>
</a-tooltip>
<a-tooltip :title="t('layout.setting.layoutStyles.top')">
<div
class="ele-bg-base ele-head-dark ele-layout-top"
@click="updateLayoutStyle('top')"
>
<check-outlined v-if="layoutStyle === 'top'" />
</div>
</a-tooltip>
<a-tooltip :title="t('layout.setting.layoutStyles.mix')">
<div
class="ele-bg-base ele-layout-mix"
@click="updateLayoutStyle('mix')"
>
<check-outlined v-if="layoutStyle === 'mix'" />
</div>
</a-tooltip>
</div>
<!-- 侧栏菜单布局 -->
<div
v-if="layoutStyle !== 'top'"
:class="['ele-setting-item', { 'hidden-xs-only': styleResponsive }]"
>
<div class="setting-item-title">
{{ t('layout.setting.sideMenuStyle') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="sideMenuStyle === 'mix'"
@change="updateSideMenuStyle"
/>
</div>
</div>
<!-- 内容区域定宽 -->
<div :class="['ele-setting-item', { 'hidden-xs-only': styleResponsive }]">
<div class="setting-item-title">
{{ t('layout.setting.bodyFull') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="!bodyFull"
@change="updateBodyFull"
/>
</div>
</div>
<a-divider :class="{ 'hidden-xs-only': styleResponsive }" />
<div class="ele-setting-title ele-text-secondary">
{{ t('layout.setting.other') }}
</div>
<!-- 固定主体 -->
<div class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.fixedBody') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="fixedBody"
@change="updateFixedBody"
/>
</div>
</div>
<!-- 固定顶栏 -->
<div class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.fixedHeader') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:disabled="fixedBody"
:checked="fixedHeader"
@change="updateFixedHeader"
/>
</div>
</div>
<!-- 固定侧栏 -->
<div
v-if="layoutStyle !== 'top'"
:class="['ele-setting-item', { 'hidden-xs-only': styleResponsive }]"
>
<div class="setting-item-title">
{{ t('layout.setting.fixedSidebar') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:disabled="fixedBody"
:checked="fixedSidebar"
@change="updateFixedSidebar"
/>
</div>
</div>
<!-- logo 置于顶栏 -->
<div
v-if="layoutStyle !== 'top'"
:class="['ele-setting-item', { 'hidden-xs-only': styleResponsive }]"
>
<div class="setting-item-title">
{{ t('layout.setting.logoAutoSize') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="logoAutoSize"
@change="updateLogoAutoSize"
/>
</div>
</div>
<!-- 移动端响应式 -->
<div class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.styleResponsive') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="styleResponsive"
@change="updateStyleResponsive"
/>
</div>
</div>
<!-- 侧栏彩色图标 -->
<div v-if="layoutStyle !== 'top'" class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.colorfulIcon') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="colorfulIcon"
@change="updateColorfulIcon"
/>
</div>
</div>
<!-- 侧栏排他展开 -->
<div v-if="layoutStyle !== 'top'" class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.sideUniqueOpen') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="sideUniqueOpen"
@change="updateSideUniqueOpen"
/>
</div>
</div>
<!-- 全局页脚 -->
<div class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.showFooter') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="showFooter"
@change="updateShowFooter"
/>
</div>
</div>
<!-- 色弱模式 -->
<div class="ele-setting-item">
<div class="setting-item-title">{{ t('layout.setting.weakMode') }}</div>
<div class="setting-item-control">
<a-switch size="small" :checked="weakMode" @change="updateWeakMode" />
</div>
</div>
<!-- 页签 -->
<div class="ele-setting-item">
<div class="setting-item-title">{{ t('layout.setting.showTabs') }}</div>
<div class="setting-item-control">
<a-switch size="small" :checked="showTabs" @change="updateShowTabs" />
</div>
</div>
<!-- 页签风格 -->
<div v-if="showTabs" class="ele-setting-item">
<div class="setting-item-title">{{ t('layout.setting.tabStyle') }}</div>
<div class="setting-item-control">
<a-select
size="small"
:value="tabStyle"
style="width: 80px"
@change="updateTabStyle"
>
<a-select-option value="default">
{{ t('layout.setting.tabStyles.default') }}
</a-select-option>
<a-select-option value="dot">
{{ t('layout.setting.tabStyles.dot') }}
</a-select-option>
<a-select-option value="card">
{{ t('layout.setting.tabStyles.card') }}
</a-select-option>
</a-select>
</div>
</div>
<!-- 切换动画 -->
<div class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.transitionName') }}
</div>
<div class="setting-item-control">
<a-select
size="small"
:value="transitionName"
style="width: 100px"
@change="updateTransitionName"
>
<a-select-option value="slide-right">
{{ t('layout.setting.transitions.slideRight') }}
</a-select-option>
<a-select-option value="slide-bottom">
{{ t('layout.setting.transitions.slideBottom') }}
</a-select-option>
<a-select-option value="zoom-in">
{{ t('layout.setting.transitions.zoomIn') }}
</a-select-option>
<a-select-option value="zoom-out">
{{ t('layout.setting.transitions.zoomOut') }}
</a-select-option>
<a-select-option value="fade">
{{ t('layout.setting.transitions.fade') }}
</a-select-option>
</a-select>
</div>
</div>
<!-- 提示 -->
<a-divider />
<!-- <a-alert show-icon type="warning" :message="t('layout.setting.tips')">-->
<!-- <template #icon>-->
<!-- <sound-outlined />-->
<!-- </template>-->
<!-- </a-alert>-->
<!-- 重置 -->
<a-button block type="dashed" @click="resetSetting">
{{ t('layout.setting.reset') }}
</a-button>
</div>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { message } from 'ant-design-vue/es';
import { CheckOutlined, SoundOutlined } from '@ant-design/icons-vue';
import { messageLoading } from 'ele-admin-pro/es';
import type {
ThemeItem,
HeadStyleType,
SideStyleType,
LayoutStyleType,
TabStyleType
} from 'ele-admin-pro/es';
import { useThemeStore } from '@/store/modules/theme';
defineProps<{
// drawer 是否显示, v-model
visible: boolean;
}>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
}>();
const { t } = useI18n();
const themeStore = useThemeStore();
const {
showTabs,
showFooter,
headStyle,
sideStyle,
layoutStyle,
sideMenuStyle,
tabStyle,
transitionName,
fixedHeader,
fixedSidebar,
fixedBody,
bodyFull,
logoAutoSize,
colorfulIcon,
sideUniqueOpen,
styleResponsive,
weakMode,
darkMode,
color
} = storeToRefs(themeStore);
// 主题列表
const themes = ref<ThemeItem[]>([
{
name: 'default',
color: '#1890ff'
},
{
name: 'dust',
value: '#5f80c7'
},
{
name: 'sunset',
value: '#faad14'
},
{
name: 'volcano',
value: '#f5686f'
},
{
name: 'purple',
value: '#9266f9'
},
{
name: 'green',
value: '#33cc99'
},
{
name: 'geekblue',
value: '#32a2d4'
}
]);
// 颜色选择器预设颜色
const predefineColors = ref<string[]>([
'#f5222d',
'#fa541c',
'#fa8c16',
'#faad14',
'#a0d911',
'#52c41a',
'#13c2c2',
'#2f54eb',
'#722ed1',
'#eb2f96'
]);
// 颜色选择器选中颜色
const colorValue = ref<string | undefined>(void 0);
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const updateShowTabs = (value: boolean) => {
themeStore.setShowTabs(value);
};
const updateShowFooter = (value: boolean) => {
themeStore.setShowFooter(value);
};
const updateHeadStyle = (value: HeadStyleType) => {
themeStore.setHeadStyle(value);
};
const updateSideStyle = (value: SideStyleType) => {
themeStore.setSideStyle(value);
};
const updateLayoutStyle = (value: LayoutStyleType) => {
themeStore.setLayoutStyle(value);
};
const updateSideMenuStyle = (value: boolean) => {
themeStore.setSideMenuStyle(value ? 'mix' : 'default');
};
const updateTabStyle = (value: TabStyleType) => {
themeStore.setTabStyle(value);
};
const updateTransitionName = (value: string) => {
themeStore.setTransitionName(value);
};
const updateFixedHeader = (value: boolean) => {
themeStore.setFixedHeader(value);
};
const updateFixedSidebar = (value: boolean) => {
themeStore.setFixedSidebar(value);
};
const updateFixedBody = (value: boolean) => {
themeStore.setFixedBody(value);
};
const updateBodyFull = (value: boolean) => {
themeStore.setBodyFull(!value);
};
const updateLogoAutoSize = (value: boolean) => {
themeStore.setLogoAutoSize(value);
};
const updateStyleResponsive = (value: boolean) => {
themeStore.setStyleResponsive(value);
updateVisible(false);
};
const updateColorfulIcon = (value: boolean) => {
themeStore.setColorfulIcon(value);
};
const updateSideUniqueOpen = (value: boolean) => {
themeStore.setSideUniqueOpen(value);
};
const updateWeakMode = (value: boolean) => {
themeStore.setWeakMode(value);
};
const updateDarkMode = (value: boolean) => {
doWithLoading(() => themeStore.setDarkMode(value));
};
const updateColor = (value?: string) => {
doWithLoading(() => themeStore.setColor(value));
};
const resetSetting = () => {
doWithLoading(() => themeStore.resetSetting());
};
const doWithLoading = (fun: () => Promise<void>) => {
const hide = messageLoading('正在加载主题..', 0);
setTimeout(() => {
fun()
.then(() => {
hide();
initColorValue();
})
.catch((e) => {
hide();
console.error(e);
message.error('主题加载失败');
});
}, 0);
};
const initColorValue = () => {
if (color?.value && !themes.value.some((t) => t.value === color.value)) {
colorValue.value = color.value;
} else {
colorValue.value = void 0;
}
};
initColorValue();
</script>
<style lang="less">
.ele-setting-wrapper {
padding: 20px 18px;
.ele-setting-title {
font-size: 13px;
margin-bottom: 15px;
}
/* 主题风格 */
.ele-setting-theme > div {
width: 52px;
height: 36px;
line-height: 1;
border-radius: 3px;
margin: 0 20px 30px 0;
padding: 16px 0 0 26px;
box-sizing: border-box;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
display: inline-block;
vertical-align: top;
position: relative;
overflow: hidden;
cursor: pointer;
transition: background-color 0.2s;
&:before,
&:after,
& > .ele-bg-primary {
content: '';
width: 100%;
height: 10px;
background: #fff;
position: absolute;
left: 0;
top: 0;
transition: background-color 0.2s;
}
&:after {
width: 14px;
height: 100%;
}
&.ele-side-dark:after,
&.ele-head-dark:before,
&.ele-layout-mix:before,
&.ele-layout-mix:after {
background: #001529;
}
&.ele-head-light:before,
&.ele-head-dark:before,
& > .ele-bg-primary {
z-index: 1;
}
&.ele-layout-top {
padding-left: 19px;
&:after {
display: none;
}
}
}
/* 主题色选择 */
.ele-setting-colors {
color: #fff;
margin-bottom: 20px;
}
.ele-setting-color-item {
width: 20px;
height: 20px;
line-height: 20px;
border-radius: 2px;
margin: 8px 8px 0 0;
display: inline-block;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
vertical-align: top;
position: relative;
text-align: center;
cursor: pointer;
.ele-setting-color-tooltip {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
/* 主题配置项 */
.ele-setting-item {
display: flex;
align-items: center;
margin-bottom: 20px;
.setting-item-title {
flex: 1;
line-height: 28px;
}
.setting-item-control {
line-height: 1;
}
}
.ant-divider {
margin-bottom: 20px;
}
.ant-alert + .ant-btn {
margin-top: 12px;
}
/* 暗黑模式 */
&.ele-setting-dark .ele-setting-theme > div {
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
&:before,
&:after,
& > .ele-bg-primary {
background: #1f1f1f;
}
&.ele-side-dark:after,
&.ele-head-dark:before,
&.ele-layout-mix:before,
&.ele-layout-mix:after {
background: #262626;
}
}
}
/* 颜色选择器 */
.ele-setting-color-picker.ele-color-picker-trigger {
padding: 0;
width: 20px;
height: 20px;
margin-top: 8px;
border: none !important;
background: none !important;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
& > .ele-color-picker-trigger-inner {
background: none;
&.is-empty {
background: conic-gradient(
from 90deg at 50% 50%,
rgb(255, 0, 0) -19.41deg,
rgb(255, 0, 0) 18.76deg,
rgb(255, 138, 0) 59.32deg,
rgb(255, 230, 0) 99.87deg,
rgb(20, 255, 0) 141.65deg,
rgb(0, 163, 255) 177.72deg,
rgb(5, 0, 255) 220.23deg,
rgb(173, 0, 255) 260.13deg,
rgb(255, 0, 199) 300.69deg,
rgb(255, 0, 0) 340.59deg,
rgb(255, 0, 0) 378.76deg
);
& + .ele-color-picker-trigger-arrow {
display: none;
}
}
}
}
</style>