release: update 5.2.0

This commit is contained in:
xiaoxian521 2024-03-22 20:47:17 +08:00
parent 51a4a6d246
commit 03d68a24d9
30 changed files with 1411 additions and 1196 deletions

View File

@ -7,5 +7,5 @@ eslint.config.js
.prettierrc.js .prettierrc.js
commitlint.config.js commitlint.config.js
postcss.config.js postcss.config.js
tailwind.config.js tailwind.config.ts
stylelint.config.js stylelint.config.js

View File

@ -7,10 +7,14 @@
"prettier --cache --write--parser json" "prettier --cache --write--parser json"
], ],
"package.json": ["prettier --cache --write"], "package.json": ["prettier --cache --write"],
"*.vue": ["prettier --write", "eslint --cache --fix", "stylelint --fix"], "*.vue": [
"prettier --write",
"eslint --cache --fix",
"stylelint --fix --allow-empty-input"
],
"*.{css,scss,html}": [ "*.{css,scss,html}": [
"prettier --cache --ignore-unknown --write", "prettier --cache --ignore-unknown --write",
"stylelint --fix" "stylelint --fix --allow-empty-input"
], ],
"*.md": ["prettier --cache --ignore-unknown --write"] "*.md": ["prettier --cache --ignore-unknown --write"]
} }

View File

@ -7,7 +7,7 @@ import boxen, { type Options as BoxenOptions } from "boxen";
dayjs.extend(duration); dayjs.extend(duration);
const welcomeMessage = gradientString("cyan", "magenta").multiline( const welcomeMessage = gradientString("cyan", "magenta").multiline(
`Hello! 欢迎使用 pure-admin 开源项目\n我们为您精心准备了下面两个贴心的保姆级文档\nhttps://yiming_chang.gitee.io/pure-admin-doc\nhttps://pure-admin-utils.netlify.app` `您好! 欢迎使用 pure-admin 开源项目\n我们为您精心准备了下面两个贴心的保姆级文档\nhttps://yiming_chang.gitee.io/pure-admin-doc\nhttps://pure-admin-utils.netlify.app`
); );
const boxenOptions: BoxenOptions = { const boxenOptions: BoxenOptions = {

View File

@ -1,6 +1,6 @@
{ {
"name": "pure-admin-thin", "name": "pure-admin-thin",
"version": "5.1.0", "version": "5.2.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -48,16 +48,16 @@
"url": "https://github.com/xiaoxian521" "url": "https://github.com/xiaoxian521"
}, },
"dependencies": { "dependencies": {
"@pureadmin/descriptions": "^1.2.0", "@pureadmin/descriptions": "^1.2.1",
"@pureadmin/table": "^3.1.2", "@pureadmin/table": "^3.1.2",
"@pureadmin/utils": "^2.4.5", "@pureadmin/utils": "^2.4.7",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
"@vueuse/motion": "^2.1.0", "@vueuse/motion": "^2.1.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.6.7", "axios": "^1.6.8",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"element-plus": "^2.6.0", "element-plus": "^2.6.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
@ -65,7 +65,7 @@
"path": "^0.12.7", "path": "^0.12.7",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinyin-pro": "^3.19.6", "pinyin-pro": "^3.19.6",
"qs": "^6.11.2", "qs": "^6.12.0",
"responsive-storage": "^2.2.0", "responsive-storage": "^2.2.0",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
"vue": "^3.4.21", "vue": "^3.4.21",
@ -74,9 +74,9 @@
"vue-types": "^5.1.1" "vue-types": "^5.1.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^18.6.1", "@commitlint/cli": "^19.2.1",
"@commitlint/config-conventional": "^18.6.2", "@commitlint/config-conventional": "^19.1.0",
"@commitlint/types": "^18.6.1", "@commitlint/types": "^19.0.3",
"@eslint/js": "^8.57.0", "@eslint/js": "^8.57.0",
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@iconify-icons/ep": "^1.2.12", "@iconify-icons/ep": "^1.2.12",
@ -85,34 +85,34 @@
"@pureadmin/theme": "^3.2.0", "@pureadmin/theme": "^3.2.0",
"@types/gradient-string": "^1.1.5", "@types/gradient-string": "^1.1.5",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^20.11.24", "@types/node": "^20.11.30",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/qs": "^6.9.12", "@types/qs": "^6.9.14",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.1.1", "@typescript-eslint/parser": "^7.3.1",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0", "@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.18", "autoprefixer": "^10.4.19",
"boxen": "^7.1.1", "boxen": "^7.1.1",
"cloc": "^2.11.0", "cloc": "^2.11.0",
"cssnano": "^6.0.5", "cssnano": "^6.1.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^2.1.0", "eslint-define-config": "^2.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.22.0", "eslint-plugin-vue": "^9.23.0",
"gradient-string": "^2.0.2", "gradient-string": "^2.0.2",
"husky": "^9.0.11", "husky": "^9.0.11",
"lint-staged": "^15.2.2", "lint-staged": "^15.2.2",
"postcss": "^8.4.35", "postcss": "^8.4.38",
"postcss-html": "^1.6.0", "postcss-html": "^1.6.0",
"postcss-import": "^16.0.1", "postcss-import": "^16.1.0",
"postcss-scss": "^4.0.9", "postcss-scss": "^4.0.9",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.71.1", "sass": "^1.72.0",
"stylelint": "^16.2.1", "stylelint": "^16.2.1",
"stylelint-config-recess-order": "^5.0.0", "stylelint-config-recess-order": "^5.0.0",
"stylelint-config-recommended-vue": "^1.5.0", "stylelint-config-recommended-vue": "^1.5.0",
@ -120,8 +120,8 @@
"stylelint-prettier": "^5.0.0", "stylelint-prettier": "^5.0.0",
"svgo": "^3.2.0", "svgo": "^3.2.0",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.3.3", "typescript": "^5.4.3",
"vite": "^5.1.5", "vite": "^5.2.2",
"vite-plugin-cdn-import": "^0.3.5", "vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-fake-server": "^2.1.1", "vite-plugin-fake-server": "^2.1.1",

2141
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{ {
"Version": "5.1.0", "Version": "5.2.0",
"Title": "PureAdmin", "Title": "PureAdmin",
"FixedHeader": true, "FixedHeader": true,
"HiddenSideBar": false, "HiddenSideBar": false,
@ -13,6 +13,7 @@
"Weak": false, "Weak": false,
"HideTabs": false, "HideTabs": false,
"HideFooter": false, "HideFooter": false,
"Stretch": false,
"SidebarStatus": true, "SidebarStatus": true,
"EpThemeColor": "#409EFF", "EpThemeColor": "#409EFF",
"ShowLogo": true, "ShowLogo": true,

View File

@ -64,9 +64,10 @@ const fullscreenClass = computed(() => {
function eventsCallBack( function eventsCallBack(
event: EventType, event: EventType,
options: DialogOptions, options: DialogOptions,
index: number index: number,
isClickFullScreen = false
) { ) {
fullscreen.value = options?.fullscreen ?? false; if (!isClickFullScreen) fullscreen.value = options?.fullscreen ?? false;
if (options?.[event] && isFunction(options?.[event])) { if (options?.[event] && isFunction(options?.[event])) {
return options?.[event]({ options, index }); return options?.[event]({ options, index });
} }
@ -108,7 +109,17 @@ function handleClose(
<i <i
v-if="!options?.fullscreen" v-if="!options?.fullscreen"
:class="fullscreenClass" :class="fullscreenClass"
@click="fullscreen = !fullscreen" @click="
() => {
fullscreen = !fullscreen;
eventsCallBack(
'fullscreenCallBack',
{ ...options, fullscreen },
index,
true
);
}
"
> >
<IconifyIconOffline <IconifyIconOffline
class="pure-dialog-svg" class="pure-dialog-svg"

View File

@ -1,7 +1,12 @@
import type { CSSProperties, VNode, Component } from "vue"; import type { CSSProperties, VNode, Component } from "vue";
type DoneFn = (cancel?: boolean) => void; type DoneFn = (cancel?: boolean) => void;
type EventType = "open" | "close" | "openAutoFocus" | "closeAutoFocus"; type EventType =
| "open"
| "close"
| "openAutoFocus"
| "closeAutoFocus"
| "fullscreenCallBack";
type ArgsType = { type ArgsType = {
/** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */ /** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
command: "cancel" | "sure" | "close"; command: "cancel" | "sure" | "close";
@ -175,6 +180,14 @@ interface DialogOptions extends DialogProps {
index: number; index: number;
args: any; args: any;
}) => void; }) => void;
/** 点击全屏按钮时的回调 */
fullscreenCallBack?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** 输入焦点聚焦在 `Dialog` 内容时的回调 */ /** 输入焦点聚焦在 `Dialog` 内容时的回调 */
openAutoFocus?: ({ openAutoFocus?: ({
options, options,

View File

@ -8,6 +8,21 @@
border-radius: 2px; border-radius: 2px;
} }
.pure-segmented-block {
display: flex;
}
.pure-segmented-block .pure-segmented-item {
flex: 1;
min-width: 0;
}
.pure-segmented-block .pure-segmented-item > .pure-segmented-item-label > span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.pure-segmented-group { .pure-segmented-group {
position: relative; position: relative;
display: flex; display: flex;
@ -67,6 +82,7 @@
.pure-segmented-item-label { .pure-segmented-item-label {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
} }
.pure-segmented-item-icon svg { .pure-segmented-item-icon svg {

View File

@ -10,7 +10,12 @@ import {
} from "vue"; } from "vue";
import type { OptionsType } from "./type"; import type { OptionsType } from "./type";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { isFunction, isNumber, useDark } from "@pureadmin/utils"; import {
isFunction,
isNumber,
useDark,
useResizeObserver
} from "@pureadmin/utils";
const props = { const props = {
options: { options: {
@ -22,6 +27,11 @@ const props = {
type: undefined, type: undefined,
require: false, require: false,
default: "0" default: "0"
},
/** 将宽度调整为父元素宽度 */
block: {
type: Boolean,
default: false
} }
}; };
@ -77,6 +87,14 @@ export default defineComponent({
}); });
} }
if (props.block) {
useResizeObserver(".pure-segmented", () => {
nextTick(() => {
handleInit(curIndex.value);
});
});
}
watch( watch(
() => curIndex.value, () => curIndex.value,
index => { index => {
@ -148,7 +166,9 @@ export default defineComponent({
}; };
return () => ( return () => (
<div class="pure-segmented"> <div
class={["pure-segmented", props.block ? "pure-segmented-block" : ""]}
>
<div class="pure-segmented-group"> <div class="pure-segmented-group">
<div <div
class="pure-segmented-item-selected" class="pure-segmented-item-selected"

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import Footer from "./footer/index.vue"; import Footer from "./footer/index.vue";
import { useGlobal } from "@pureadmin/utils"; import { useGlobal, isNumber } from "@pureadmin/utils";
import KeepAliveFrame from "./keepAliveFrame/index.vue"; import KeepAliveFrame from "./keepAliveFrame/index.vue";
import backTop from "@/assets/svg/back_top.svg?component"; import backTop from "@/assets/svg/back_top.svg?component";
import { h, computed, Transition, defineComponent } from "vue"; import { h, computed, Transition, defineComponent } from "vue";
@ -30,16 +30,28 @@ const hideFooter = computed(() => {
return $storage?.configure.hideFooter; return $storage?.configure.hideFooter;
}); });
const stretch = computed(() => {
return $storage?.configure.stretch;
});
const layout = computed(() => { const layout = computed(() => {
return $storage?.layout.layout === "vertical"; return $storage?.layout.layout === "vertical";
}); });
const getMainWidth = computed(() => {
return isNumber(stretch.value)
? stretch.value + "px"
: stretch.value
? "1440px"
: "100%";
});
const getSectionStyle = computed(() => { const getSectionStyle = computed(() => {
return [ return [
hideTabs.value && layout ? "padding-top: 48px;" : "", hideTabs.value && layout ? "padding-top: 48px;" : "",
!hideTabs.value && layout ? "padding-top: 85px;" : "", !hideTabs.value && layout ? "padding-top: 81px;" : "",
hideTabs.value && !layout.value ? "padding-top: 48px;" : "", hideTabs.value && !layout.value ? "padding-top: 48px;" : "",
!hideTabs.value && !layout.value ? "padding-top: 85px;" : "", !hideTabs.value && !layout.value ? "padding-top: 81px;" : "",
props.fixedHeader props.fixedHeader
? "" ? ""
: `padding-top: 0;${ : `padding-top: 0;${
@ -96,12 +108,15 @@ const transitionMain = defineComponent({
v-if="props.fixedHeader" v-if="props.fixedHeader"
:wrap-style="{ :wrap-style="{
display: 'flex', display: 'flex',
'flex-wrap': 'wrap' 'flex-wrap': 'wrap',
'max-width': getMainWidth,
margin: '0 auto',
transition: 'all 300ms cubic-bezier(0.4, 0, 0.2, 1)'
}" }"
:view-style="{ :view-style="{
display: 'flex', display: 'flex',
flex: 'auto', flex: 'auto',
overflow: 'auto', overflow: 'hidden',
'flex-direction': 'column' 'flex-direction': 'column'
}" }"
> >

View File

@ -63,7 +63,6 @@ watch(
} }
); );
</script> </script>
<template> <template>
<template v-for="[fullPath, Comp] in compList" :key="fullPath"> <template v-for="[fullPath, Comp] in compList" :key="fullPath">
<div v-show="fullPath === props.currRoute.fullPath" class="w-full h-full"> <div v-show="fullPath === props.currRoute.fullPath" class="w-full h-full">

View File

@ -3,6 +3,7 @@ import Search from "./search/index.vue";
import Notice from "./notice/index.vue"; import Notice from "./notice/index.vue";
import mixNav from "./sidebar/mixNav.vue"; import mixNav from "./sidebar/mixNav.vue";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import FullScreen from "./sidebar/fullScreen.vue";
import Breadcrumb from "./sidebar/breadCrumb.vue"; import Breadcrumb from "./sidebar/breadCrumb.vue";
import topCollapse from "./sidebar/topCollapse.vue"; import topCollapse from "./sidebar/topCollapse.vue";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line"; import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
@ -40,7 +41,9 @@ const {
<div v-if="layout === 'vertical'" class="vertical-header-right"> <div v-if="layout === 'vertical'" class="vertical-header-right">
<!-- 菜单搜索 --> <!-- 菜单搜索 -->
<Search id="header-search" /> <Search id="header-search" />
<!-- 通知 --> <!-- 全屏 -->
<FullScreen id="full-screen" />
<!-- 消息通知 -->
<Notice id="header-notice" /> <Notice id="header-notice" />
<!-- 退出登录 --> <!-- 退出登录 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">

View File

@ -13,13 +13,15 @@ import panel from "../panel/index.vue";
import { emitter } from "@/utils/mitt"; import { emitter } from "@/utils/mitt";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import { useAppStoreHook } from "@/store/modules/app"; import { useAppStoreHook } from "@/store/modules/app";
import { useDark, useGlobal, debounce } from "@pureadmin/utils";
import { toggleTheme } from "@pureadmin/theme/dist/browser-utils"; import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import Segmented, { type OptionsType } from "@/components/ReSegmented"; import Segmented, { type OptionsType } from "@/components/ReSegmented";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange"; import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import { useDark, useGlobal, debounce, isNumber } from "@pureadmin/utils";
import Check from "@iconify-icons/ep/check"; import Check from "@iconify-icons/ep/check";
import LeftArrow from "@iconify-icons/ri/arrow-left-s-line";
import RightArrow from "@iconify-icons/ri/arrow-right-s-line";
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 systemIcon from "@/assets/svg/system.svg?component"; import systemIcon from "@/assets/svg/system.svg?component";
@ -64,7 +66,8 @@ const settings = reactive({
showLogo: $storage.configure.showLogo, showLogo: $storage.configure.showLogo,
showModel: $storage.configure.showModel, showModel: $storage.configure.showModel,
hideFooter: $storage.configure.hideFooter, hideFooter: $storage.configure.hideFooter,
multiTagsCache: $storage.configure.multiTagsCache multiTagsCache: $storage.configure.multiTagsCache,
stretch: $storage.configure.stretch
}); });
const getThemeColorStyle = computed(() => { const getThemeColorStyle = computed(() => {
@ -141,6 +144,30 @@ function setFalse(Doms): any {
}); });
} }
/** 页宽 */
const stretchTypeOptions: Array<OptionsType> = [
{
label: "固定",
tip: "紧凑页面,轻松找到所需信息",
value: "fixed"
},
{
label: "自定义",
tip: "最小1280、最大1600",
value: "custom"
}
];
const setStretch = value => {
settings.stretch = value;
storageConfigureChange("stretch", value);
};
const stretchTypeChange = ({ option }) => {
const { value } = option;
value === "custom" ? setStretch(1440) : setStretch(false);
};
/** 主题色 激活选择项 */ /** 主题色 激活选择项 */
const getThemeColor = computed(() => { const getThemeColor = computed(() => {
return current => { return current => {
@ -160,6 +187,10 @@ const getThemeColor = computed(() => {
}; };
}); });
const pClass = computed(() => {
return ["mb-[12px]", "font-medium", "text-sm", "dark:text-white"];
});
const themeOptions = computed<Array<OptionsType>>(() => { const themeOptions = computed<Array<OptionsType>>(() => {
return [ return [
{ {
@ -277,8 +308,8 @@ onUnmounted(() => removeMatchMedia);
<template> <template>
<panel> <panel>
<div class="p-6"> <div class="p-5">
<p class="mb-3 font-medium text-sm dark:text-white">整体风格</p> <p :class="pClass">整体风格</p>
<Segmented <Segmented
class="select-none" class="select-none"
:modelValue="overallStyle === 'system' ? 2 : dataTheme ? 1 : 0" :modelValue="overallStyle === 'system' ? 2 : dataTheme ? 1 : 0"
@ -295,7 +326,7 @@ onUnmounted(() => removeMatchMedia);
" "
/> />
<p class="mt-5 mb-3 font-medium text-sm dark:text-white">主题色</p> <p :class="['mt-5', pClass]">主题色</p>
<ul class="theme-color"> <ul class="theme-color">
<li <li
v-for="(item, index) in themeColors" v-for="(item, index) in themeColors"
@ -314,7 +345,7 @@ onUnmounted(() => removeMatchMedia);
</li> </li>
</ul> </ul>
<p class="mt-5 mb-3 font-medium text-sm dark:text-white">导航模式</p> <p :class="['mt-5', pClass]">导航模式</p>
<ul class="pure-theme"> <ul class="pure-theme">
<li <li
ref="verticalRef" ref="verticalRef"
@ -356,7 +387,50 @@ onUnmounted(() => removeMatchMedia);
</li> </li>
</ul> </ul>
<p class="mt-5 mb-3 font-medium text-base dark:text-white">页签风格</p> <span v-if="device !== 'mobile'">
<p :class="['mt-5', pClass]">页宽</p>
<Segmented
class="mb-2 select-none"
:modelValue="isNumber(settings.stretch) ? 1 : 0"
:options="stretchTypeOptions"
@change="stretchTypeChange"
/>
<el-input-number
v-if="isNumber(settings.stretch)"
v-model="settings.stretch as number"
:min="1280"
:max="1600"
controls-position="right"
@change="value => setStretch(value)"
/>
<button
v-else
v-ripple="{ class: 'text-gray-300' }"
class="bg-transparent flex-c w-full h-20 rounded-md border border-gray-100"
@click="setStretch(!settings.stretch)"
>
<div
class="flex-bc transition-all duration-300"
:class="[settings.stretch ? 'w-[24%]' : 'w-[50%]']"
style="color: var(--el-color-primary)"
>
<IconifyIconOffline
:icon="settings.stretch ? RightArrow : LeftArrow"
height="20"
/>
<div
class="flex-grow border-b border-dashed"
style="border-color: var(--el-color-primary)"
/>
<IconifyIconOffline
:icon="settings.stretch ? LeftArrow : RightArrow"
height="20"
/>
</div>
</button>
</span>
<p :class="['mt-4', pClass]">页签风格</p>
<Segmented <Segmented
class="select-none" class="select-none"
:modelValue="markValue === 'smart' ? 0 : 1" :modelValue="markValue === 'smart' ? 0 : 1"
@ -364,7 +438,7 @@ onUnmounted(() => removeMatchMedia);
@change="onChange" @change="onChange"
/> />
<p class="mt-5 mb-1 font-medium text-sm dark:text-white">界面显示</p> <p class="mt-5 font-medium text-sm dark:text-white">界面显示</p>
<ul class="setting"> <ul class="setting">
<li> <li>
<span class="dark:text-white">灰色模式</span> <span class="dark:text-white">灰色模式</span>
@ -543,7 +617,7 @@ onUnmounted(() => removeMatchMedia);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 4px 0; padding: 3px 0;
font-size: 14px; font-size: 14px;
} }
} }

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import { computed } from "vue";
import { useGlobal } from "@pureadmin/utils";
import { useNav } from "@/layout/hooks/useNav";
import ArrowLeft from "@iconify-icons/ri/arrow-left-double-fill";
interface Props {
isActive: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
});
const { tooltipEffect } = useNav();
const iconClass = computed(() => {
return ["w-[16px]", "h-[16px]"];
});
const { $storage } = useGlobal<GlobalPropertiesApi>();
const themeColor = computed(() => $storage.layout?.themeColor);
const emit = defineEmits<{
(e: "toggleClick"): void;
}>();
const toggleClick = () => {
emit("toggleClick");
};
</script>
<template>
<div
v-tippy="{
content: props.isActive ? '点击折叠' : '点击展开',
theme: tooltipEffect,
hideOnClick: 'toggle',
placement: 'right'
}"
class="center-collapse"
@click="toggleClick"
>
<IconifyIconOffline
:icon="ArrowLeft"
:class="[iconClass, themeColor === 'light' ? '' : 'text-primary']"
:style="{ transform: props.isActive ? 'none' : 'rotateY(180deg)' }"
/>
</div>
</template>
<style lang="scss" scoped>
.center-collapse {
position: absolute;
top: 50%;
right: 2px;
z-index: 1002;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 34px;
cursor: pointer;
background: var(--el-bg-color);
border: 1px solid var(--pure-border-color);
border-radius: 4px;
transform: translate(12px, -50%);
}
</style>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { useNav } from "@/layout/hooks/useNav";
const screenIcon = ref();
const { toggle, isFullscreen, Fullscreen, ExitFullscreen } = useNav();
isFullscreen.value = !!(
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement
);
watch(
isFullscreen,
full => {
screenIcon.value = full ? ExitFullscreen : Fullscreen;
},
{
immediate: true
}
);
</script>
<template>
<span class="fullscreen-icon navbar-bg-hover" @click="toggle">
<IconifyIconOffline :icon="screenIcon" />
</span>
</template>

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Search from "../search/index.vue"; import Search from "../search/index.vue";
import Notice from "../notice/index.vue"; import Notice from "../notice/index.vue";
import FullScreen from "./fullScreen.vue";
import SidebarItem from "./sidebarItem.vue"; import SidebarItem from "./sidebarItem.vue";
import { isAllEmpty } from "@pureadmin/utils"; import { isAllEmpty } from "@pureadmin/utils";
import { ref, nextTick, computed } from "vue"; import { ref, nextTick, computed } from "vue";
@ -59,7 +60,9 @@ nextTick(() => {
<div class="horizontal-header-right"> <div class="horizontal-header-right">
<!-- 菜单搜索 --> <!-- 菜单搜索 -->
<Search id="header-search" /> <Search id="header-search" />
<!-- 通知 --> <!-- 全屏 -->
<FullScreen id="full-screen" />
<!-- 消息通知 -->
<Notice id="header-notice" /> <Notice id="header-notice" />
<!-- 退出登录 --> <!-- 退出登录 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">

View File

@ -41,7 +41,7 @@ const toggleClick = () => {
</script> </script>
<template> <template>
<div class="collapse-container"> <div class="left-collapse">
<IconifyIconOffline <IconifyIconOffline
v-tippy="{ v-tippy="{
content: props.isActive ? '点击折叠' : '点击展开', content: props.isActive ? '点击折叠' : '点击展开',
@ -58,7 +58,7 @@ const toggleClick = () => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.collapse-container { .left-collapse {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
width: 100%; width: 100%;

View File

@ -2,6 +2,7 @@
import extraIcon from "./extraIcon.vue"; import extraIcon from "./extraIcon.vue";
import Search from "../search/index.vue"; import Search from "../search/index.vue";
import Notice from "../notice/index.vue"; import Notice from "../notice/index.vue";
import FullScreen from "./fullScreen.vue";
import { isAllEmpty } from "@pureadmin/utils"; import { isAllEmpty } from "@pureadmin/utils";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import { ref, toRaw, watch, onMounted, nextTick } from "vue"; import { ref, toRaw, watch, onMounted, nextTick } from "vue";
@ -91,7 +92,9 @@ watch(
<div class="horizontal-header-right"> <div class="horizontal-header-right">
<!-- 菜单搜索 --> <!-- 菜单搜索 -->
<Search id="header-search" /> <Search id="header-search" />
<!-- 通知 --> <!-- 全屏 -->
<FullScreen id="full-screen" />
<!-- 消息通知 -->
<Notice id="header-notice" /> <Notice id="header-notice" />
<!-- 退出登录 --> <!-- 退出登录 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">

View File

@ -3,8 +3,9 @@ import Logo from "./logo.vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { emitter } from "@/utils/mitt"; import { emitter } from "@/utils/mitt";
import SidebarItem from "./sidebarItem.vue"; import SidebarItem from "./sidebarItem.vue";
import leftCollapse from "./leftCollapse.vue"; import LeftCollapse from "./leftCollapse.vue";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import CenterCollapse from "./centerCollapse.vue";
import { responsiveStorageNameSpace } from "@/config"; import { responsiveStorageNameSpace } from "@/config";
import { storageLocal, isAllEmpty } from "@pureadmin/utils"; import { storageLocal, isAllEmpty } from "@pureadmin/utils";
import { findRouteByPath, getParentPaths } from "@/router/utils"; import { findRouteByPath, getParentPaths } from "@/router/utils";
@ -12,6 +13,7 @@ import { usePermissionStoreHook } from "@/store/modules/permission";
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue"; import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
const route = useRoute(); const route = useRoute();
const isShow = ref(false);
const showLogo = ref( const showLogo = ref(
storageLocal().getItem<StorageConfigs>( storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}configure` `${responsiveStorageNameSpace()}configure`
@ -88,6 +90,8 @@ onBeforeUnmount(() => {
<div <div
v-loading="loading" v-loading="loading"
:class="['sidebar-container', showLogo ? 'has-logo' : 'no-logo']" :class="['sidebar-container', showLogo ? 'has-logo' : 'no-logo']"
@mouseenter.prevent="isShow = true"
@mouseleave.prevent="isShow = false"
> >
<Logo v-if="showLogo" :collapse="isCollapse" /> <Logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar <el-scrollbar
@ -114,7 +118,12 @@ onBeforeUnmount(() => {
/> />
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
<leftCollapse <CenterCollapse
v-if="device !== 'mobile' && (isShow || isCollapse)"
:is-active="pureApp.sidebar.opened"
@toggleClick="toggleSideBar"
/>
<LeftCollapse
v-if="device !== 'mobile'" v-if="device !== 'mobile'"
:is-active="pureApp.sidebar.opened" :is-active="pureApp.sidebar.opened"
@toggleClick="toggleSideBar" @toggleClick="toggleSideBar"

View File

@ -3,7 +3,7 @@ import { emitter } from "@/utils/mitt";
import { RouteConfigs } from "../../types"; import { RouteConfigs } from "../../types";
import { useTags } from "../../hooks/useTag"; import { useTags } from "../../hooks/useTag";
import { routerArrays } from "@/layout/types"; import { routerArrays } from "@/layout/types";
import { useFullscreen, onClickOutside } from "@vueuse/core"; import { onClickOutside } from "@vueuse/core";
import { handleAliveRoute, getTopMenu } from "@/router/utils"; import { handleAliveRoute, getTopMenu } from "@/router/utils";
import { useSettingStoreHook } from "@/store/modules/settings"; import { useSettingStoreHook } from "@/store/modules/settings";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
@ -57,7 +57,6 @@ const contextmenuRef = ref();
const isShowArrow = ref(false); const isShowArrow = ref(false);
const topPath = getTopMenu()?.path; const topPath = getTopMenu()?.path;
const { VITE_HIDE_HOME } = import.meta.env; const { VITE_HIDE_HOME } = import.meta.env;
const { isFullscreen, toggle } = useFullscreen();
const dynamicTagView = async () => { const dynamicTagView = async () => {
await nextTick(); await nextTick();
@ -327,28 +326,15 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
handleAliveRoute(route as ToRouteType); handleAliveRoute(route as ToRouteType);
break; break;
case 6: case 6:
//
toggle();
setTimeout(() => {
if (isFullscreen.value) {
tagsViews[6].icon = ExitFullscreen;
tagsViews[6].text = "退出全屏";
} else {
tagsViews[6].icon = Fullscreen;
tagsViews[6].text = "全屏";
}
}, 100);
break;
case 7:
// //
onContentFullScreen(); onContentFullScreen();
setTimeout(() => { setTimeout(() => {
if (pureSetting.hiddenSideBar) { if (pureSetting.hiddenSideBar) {
tagsViews[7].icon = ExitFullscreen; tagsViews[6].icon = ExitFullscreen;
tagsViews[7].text = "内容区退出全屏"; tagsViews[6].text = "内容区退出全屏";
} else { } else {
tagsViews[7].icon = Fullscreen; tagsViews[6].icon = Fullscreen;
tagsViews[7].text = "内容区全屏"; tagsViews[6].text = "内容区全屏";
} }
}, 100); }, 100);
break; break;
@ -509,11 +495,6 @@ watch(route, () => {
dynamicTagView(); dynamicTagView();
}); });
watch(isFullscreen, () => {
tagsViews[6].icon = Fullscreen;
tagsViews[6].text = "全屏";
});
onMounted(() => { onMounted(() => {
if (!instance) return; if (!instance) return;

View File

@ -35,7 +35,8 @@ export function useLayout() {
hideFooter: $config.HideFooter ?? true, hideFooter: $config.HideFooter ?? true,
showLogo: $config?.ShowLogo ?? true, showLogo: $config?.ShowLogo ?? true,
showModel: $config?.ShowModel ?? "smart", showModel: $config?.ShowModel ?? "smart",
multiTagsCache: $config?.MultiTagsCache ?? false multiTagsCache: $config?.MultiTagsCache ?? false,
stretch: $config?.Stretch ?? false
}; };
} }
}; };

View File

@ -3,6 +3,7 @@ import { getConfig } from "@/config";
import { emitter } from "@/utils/mitt"; import { emitter } from "@/utils/mitt";
import userAvatar from "@/assets/user.jpg"; import userAvatar from "@/assets/user.jpg";
import { getTopMenu } from "@/router/utils"; import { getTopMenu } from "@/router/utils";
import { useFullscreen } from "@vueuse/core";
import { useGlobal } from "@pureadmin/utils"; import { useGlobal } from "@pureadmin/utils";
import type { routeMetaType } from "../types"; import type { routeMetaType } from "../types";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
@ -11,6 +12,8 @@ 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 { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
const errorInfo = "当前路由配置不正确,请检查配置"; const errorInfo = "当前路由配置不正确,请检查配置";
@ -18,6 +21,7 @@ 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 { 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";
@ -120,6 +124,10 @@ export function useNav() {
logout, logout,
routers, routers,
$storage, $storage,
isFullscreen,
Fullscreen,
ExitFullscreen,
toggle,
backTopMenu, backTopMenu,
onPanel, onPanel,
getDivStyle, getDivStyle,

View File

@ -103,17 +103,10 @@ export function useTags() {
disabled: multiTags.value.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
}, },
{
icon: Fullscreen,
text: "整体页面全屏",
divided: true,
disabled: false,
show: true
},
{ {
icon: Fullscreen, icon: Fullscreen,
text: "内容区全屏", text: "内容区全屏",
divided: false, divided: true,
disabled: false, disabled: false,
show: true show: true
} }

View File

@ -31,6 +31,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
text-align: center; text-align: center;
overflow: hidden;
} }
.login-form { .login-form {

View File

@ -194,7 +194,6 @@ button,
cursor: default; cursor: default;
} }
img,
svg, svg,
video, video,
canvas, canvas,

View File

@ -35,7 +35,8 @@
} }
} }
.set-icon { .set-icon,
.fullscreen-icon {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -91,7 +92,7 @@
z-index: 1001; z-index: 1001;
width: $sideBarWidth !important; width: $sideBarWidth !important;
height: 100%; height: 100%;
overflow: hidden; overflow: visible;
font-size: 0; font-size: 0;
background: $menuBg; background: $menuBg;
border-right: 1px solid var(--pure-border-color); border-right: 1px solid var(--pure-border-color);
@ -460,7 +461,9 @@
/* 搜索 */ /* 搜索 */
.search-container, .search-container,
/* 告警 */ /* 全屏 */
.fullscreen-icon,
/* 消息通知 */
.dropdown-badge, .dropdown-badge,
/* 用户名 */ /* 用户名 */
.el-dropdown-link, .el-dropdown-link,
@ -631,7 +634,9 @@ body[layout="vertical"] {
/* 搜索 */ /* 搜索 */
.search-container, .search-container,
/* 告警 */ /* 全屏 */
.fullscreen-icon,
/* 消息通知 */
.dropdown-badge, .dropdown-badge,
/* 用户名 */ /* 用户名 */
.el-dropdown-link, .el-dropdown-link,

View File

@ -26,7 +26,8 @@ export const injectResponsiveStorage = (app: App, config: PlatformConfigs) => {
hideFooter: config.HideFooter ?? true, hideFooter: config.HideFooter ?? true,
showLogo: config.ShowLogo ?? true, showLogo: config.ShowLogo ?? true,
showModel: config.ShowModel ?? "smart", showModel: config.ShowModel ?? "smart",
multiTagsCache: config.MultiTagsCache ?? false multiTagsCache: config.MultiTagsCache ?? false,
stretch: config.Stretch ?? false
} }
}, },
config.MultiTagsCache config.MultiTagsCache

View File

@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */ import type { Config } from "tailwindcss";
module.exports = {
export default {
darkMode: "class", darkMode: "class",
corePlugins: { corePlugins: {
preflight: false preflight: false
@ -15,4 +16,4 @@ module.exports = {
} }
} }
} }
}; } satisfies Config;

11
types/global.d.ts vendored
View File

@ -38,6 +38,15 @@ declare global {
msRequestAnimationFrame: (callback: FrameRequestCallback) => number; msRequestAnimationFrame: (callback: FrameRequestCallback) => number;
} }
/**
* Document
*/
interface Document {
webkitFullscreenElement?: Element;
mozFullScreenElement?: Element;
msFullscreenElement?: Element;
}
/** /**
* *
*/ */
@ -88,6 +97,7 @@ declare global {
Weak?: boolean; Weak?: boolean;
HideTabs?: boolean; HideTabs?: boolean;
HideFooter?: boolean; HideFooter?: boolean;
Stretch?: boolean | number;
SidebarStatus?: boolean; SidebarStatus?: boolean;
EpThemeColor?: string; EpThemeColor?: string;
ShowLogo?: boolean; ShowLogo?: boolean;
@ -152,6 +162,7 @@ declare global {
showLogo?: boolean; showLogo?: boolean;
showModel?: string; showModel?: string;
multiTagsCache?: boolean; multiTagsCache?: boolean;
stretch?: boolean | number;
}; };
tags?: Array<any>; tags?: Array<any>;
} }