新增前端vue

This commit is contained in:
2025-11-26 13:55:01 +08:00
parent ae391f1b94
commit ffd5a6ad66
781 changed files with 83348 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
import { withInstall } from '@jeesite/core/utils';
import pageFooter from './src/PageFooter.vue';
import pageWrapper from './src/PageWrapper.vue';
export const PageFooter = withInstall(pageFooter);
export const PageWrapper = withInstall(pageWrapper);
export const PageWrapperFixedHeightKey = 'PageWrapperFixedHeight';

View File

@@ -0,0 +1,52 @@
<template>
<div :class="prefixCls" :style="{ width: getCalcContentWidth }">
<div :class="`${prefixCls}__left`">
<slot name="left"></slot>
</div>
<slot></slot>
<div :class="`${prefixCls}__right`">
<slot name="right"></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMenuSetting } from '@jeesite/core/hooks/setting/useMenuSetting';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
export default defineComponent({
name: 'PageFooter',
inheritAttrs: false,
setup() {
const { prefixCls } = useDesign('page-footer');
const { getCalcContentWidth } = useMenuSetting();
return { prefixCls, getCalcContentWidth };
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-page-footer';
.@{prefix-cls} {
position: fixed;
right: 0;
bottom: 0;
z-index: @page-footer-z-index;
display: flex;
width: 100%;
align-items: center;
padding: 0 24px;
line-height: 35.5px;
background-color: @component-background;
border-top: 1px solid @header-light-bottom-border-color;
//box-shadow:
// 0 -6px 16px -8px rgb(0 0 0 / 8%),
// 0 -9px 28px 0 rgb(0 0 0 / 5%),
// 0 -12px 48px 16px rgb(0 0 0 / 3%);
transition: width 0.2s;
&__left {
flex: 1 1;
}
}
</style>

View File

@@ -0,0 +1,477 @@
<!--
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author VbenThinkGem
-->
<template>
<div :class="getClass" ref="wrapperRef">
<PageHeader ref="headerRef" v-if="getShowHeader" v-bind="omit($attrs, 'class')" :ghost="ghost" :title="title">
<template #title v-if="$slots.headerTitle">
<slot name="headerTitle"></slot>
</template>
<template #subTitle v-if="$slots.headerSubTitle">
<slot name="headerSubTitle"></slot>
</template>
<template #default>
<template v-if="content">
{{ content }}
</template>
<slot name="headerContent" v-else></slot>
</template>
<template #[item]="data" v-for="item in getHeaderSlots">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</PageHeader>
<div :class="getContentClass" :style="getContentStyle" ref="contentRef">
<Layout v-if="sidebar || sidebarRight" :style="getSidebarContentStyle">
<Layout.Sider
v-if="sidebar"
class="sidebar"
v-model:collapsed="collapsed"
:collapsedWidth="0"
:collapsible="true"
:trigger="null"
:width="getSidebarWidth"
breakpoint="md"
@breakpoint="onBreakpoint"
>
<div class="sidebar-content" :style="getSidebarContentStyle">
<slot name="sidebar"></slot>
</div>
<template v-if="!sidebarResizer">
<div class="sidebar-close" v-if="!collapsed" @click="collapsed = !collapsed">
<Icon icon="i-fa:angle-left" />
</div>
<div class="sidebar-open" v-else @click="collapsed = !collapsed">
<Icon icon="i-fa:angle-left" style="transform: rotate(180deg)" />
</div>
</template>
</Layout.Sider>
<template v-if="sidebar && sidebarResizer">
<Resizer position="left" v-model:collapsed="collapsed" @move="onSidebarMove" />
</template>
<template v-if="sidebar && !sidebarResizer">
<div v-if="!collapsed" style="margin-right: 12px"></div>
</template>
<Layout.Content>
<slot></slot>
</Layout.Content>
<template v-if="sidebarRight && sidebarResizerRight">
<Resizer position="right" v-model:collapsed="collapsedRight" @move="onSidebarMoveRight" />
</template>
<template v-if="sidebarRight && !sidebarResizerRight">
<div v-if="!collapsedRight" style="margin-left: 12px"></div>
</template>
<Layout.Sider
v-if="sidebarRight"
:reverseArrow="true"
class="sidebar right"
v-model:collapsed="collapsedRight"
:collapsedWidth="0"
:collapsible="true"
:trigger="null"
:width="getSidebarWidthRight"
breakpoint="md"
@breakpoint="onBreakpointRight"
>
<template v-if="!sidebarResizerRight">
<div class="sidebar-close right" v-if="!collapsedRight" @click="collapsedRight = !collapsedRight">
<Icon icon="i-fa:angle-right" />
</div>
<div class="sidebar-open right" v-else @click="collapsedRight = !collapsedRight">
<Icon icon="i-fa:angle-right" style="transform: rotate(180deg)" />
</div>
</template>
<div class="sidebar-content" :style="getSidebarContentStyle">
<slot name="sidebarRight"></slot>
</div>
</Layout.Sider>
</Layout>
<slot v-else></slot>
</div>
<PageFooter v-if="getShowFooter" ref="footerRef">
<template #left>
<slot name="leftFooter"></slot>
</template>
<template #right>
<slot name="rightFooter"></slot>
</template>
</PageFooter>
</div>
</template>
<script lang="ts" setup name="PageWrapper">
import {
computed,
CSSProperties,
PropType,
provide,
onBeforeMount,
onUpdated,
useSlots,
useAttrs,
watch,
ref,
unref,
} from 'vue';
import { useDebounceFn } from '@vueuse/core';
import PageFooter from './PageFooter.vue';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useEmitter } from '@jeesite/core/store/modules/user';
import { propTypes } from '@jeesite/core/utils/propTypes';
import { omit } from 'lodash-es';
import { Layout, PageHeader } from 'ant-design-vue';
import { useContentHeight } from '@jeesite/core/hooks/web/useContentHeight';
import { PageWrapperFixedHeightKey } from '..';
import { Icon } from '@jeesite/core/components/Icon';
import { Resizer } from '@jeesite/core/components/Resizer';
defineOptions({
inheritAttrs: false,
});
const props = defineProps({
title: propTypes.string,
dense: propTypes.bool,
ghost: propTypes.bool,
content: propTypes.string,
contentStyle: {
type: Object as PropType<CSSProperties>,
},
contentBackground: propTypes.bool.def(true),
contentFullHeight: propTypes.bool,
contentClass: propTypes.string,
fixedHeight: propTypes.bool.def(true),
upwardSpace: propTypes.oneOfType([propTypes.number, propTypes.string]).def(0),
sidebarWidth: propTypes.number.def(230),
sidebarResizer: propTypes.bool.def(true),
sidebarMinWidth: propTypes.number.def(0),
sidebarWidthRight: propTypes.number.def(230),
sidebarResizerRight: propTypes.bool.def(true),
sidebarMinWidthRight: propTypes.number.def(0),
});
const slots = useSlots();
const attrs = useAttrs();
const emitter = useEmitter();
const wrapperRef = ref(null);
const headerRef = ref(null);
const contentRef = ref(null);
const footerRef = ref(null);
const { prefixCls } = useDesign('page-wrapper');
const collapsed = ref<boolean>(false);
const sidebar = !!slots.sidebar;
const offsetXMoved = ref(0);
const collapsedRight = ref<boolean>(false);
const sidebarRight = !!slots.sidebarRight;
const offsetXMovedRight = ref(0);
provide(
PageWrapperFixedHeightKey,
computed(() => props.fixedHeight),
);
const getIsContentFullHeight = computed(() => {
return props.contentFullHeight || sidebar;
});
const getUpwardSpace = computed(() => props.upwardSpace);
const { redoHeight, setCompensation, contentHeight } = useContentHeight(
getIsContentFullHeight,
wrapperRef,
[headerRef, footerRef],
[contentRef],
getUpwardSpace,
);
setCompensation({ useLayoutFooter: true, elements: [footerRef] });
const getClass = computed(() => {
return [
prefixCls,
{
[`${prefixCls}--dense`]: props.dense || sidebar,
},
attrs.class ?? {},
];
});
const getHeaderSlots = computed(() => {
return Object.keys(omit(slots, 'default', 'leftFooter', 'rightFooter', 'headerContent'));
});
const getShowHeader = computed(
() =>
props.title !== 'false' && (props.content || slots.headerContent || props.title || getHeaderSlots.value.length),
);
const getShowFooter = computed(() => slots?.leftFooter || slots?.rightFooter);
const getContentStyle = computed((): CSSProperties => {
const { contentFullHeight, contentStyle, fixedHeight } = props;
const height = `${(unref(contentHeight) || 800) - (!sidebar ? -12 : 0)}px`;
if (sidebar) {
return {
...contentStyle,
minHeight: height,
};
} else if (contentFullHeight) {
return {
...contentStyle,
minHeight: height,
...(fixedHeight || sidebar ? { height } : {}),
};
}
return { ...contentStyle };
});
const getSidebarContentHeight = ref(0);
const getSidebarContentStyle = computed((): CSSProperties => {
if (getSidebarContentHeight.value <= 0) return {};
return {
height: `${getSidebarContentHeight.value}px`,
minHeight: `${getSidebarContentHeight.value}px`,
};
});
// 自适应侧边栏高度 by think gem
function calcSidebarContentHeight() {
if (props.contentFullHeight && contentHeight.value) {
const height = contentHeight.value - 14;
getSidebarContentHeight.value = height < 300 ? 300 : height;
return;
}
let height = 0;
const el = unref(contentRef) as any;
if (!el || el.clientHeight <= 0) return;
const table = el.querySelector('.jeesite-basic-table');
if (table) {
height = table.clientHeight;
}
if (height <= 0) {
const content = el.querySelector('.ant-layout-content');
if (content && content.children && content.children.length > 0) {
height = content.children[0].clientHeight;
}
}
const mainContentHeight = contentHeight.value || 0;
if (height < mainContentHeight) {
height = mainContentHeight - 12;
}
// console.log('calcSidebarContentHeight', height);
getSidebarContentHeight.value = height;
}
if (sidebar) {
onBeforeMount(() => {
emitter.on('on-page-wrapper-resize', () => {
setTimeout(calcSidebarContentHeight, 500);
});
});
onUpdated(useDebounceFn(calcSidebarContentHeight, 300));
}
const getContentClass = computed(() => {
const { contentBackground, contentClass } = props;
return [
`${prefixCls}-content`,
contentClass,
{
[`${prefixCls}-content-bg`]: contentBackground && !sidebar,
},
];
});
watch(
() => [getShowFooter.value],
() => {
redoHeight();
},
{
flush: 'post',
immediate: true,
},
);
const getSidebarWidth = computed(() => {
const width = props.sidebarWidth + offsetXMoved.value - 12;
return width < props.sidebarMinWidth ? props.sidebarMinWidth : width;
});
function onBreakpoint(broken: boolean) {
if (broken) collapsed.value = true;
}
function onSidebarMove(_event, offsetX: number) {
offsetXMoved.value = offsetXMoved.value - offsetX;
}
const getSidebarWidthRight = computed(() => {
const width = props.sidebarWidthRight + offsetXMovedRight.value - 12;
return width < props.sidebarMinWidthRight ? props.sidebarMinWidthRight : width;
});
function onBreakpointRight(broken: boolean) {
if (broken) collapsedRight.value = true;
}
function onSidebarMoveRight(_event, offsetX: number) {
offsetXMovedRight.value = offsetXMovedRight.value + offsetX;
}
</script>
<style lang="less">
@prefix-cls: ~'jeesite-page-wrapper';
.@{prefix-cls} {
position: relative;
.@{prefix-cls}-content {
// margin: 16px;
padding: 12px;
margin-bottom: 13px;
border-radius: 5px;
color: @text-color-base;
overflow-y: auto;
}
.ant-page-header {
// margin: 16px;
margin-bottom: 12px;
padding: 4px 16px;
border-radius: 5px;
.ant-page-header-heading-title {
font-size: 16px;
font-weight: normal;
}
.ant-page-header-content {
font-size: 14px;
color: #666;
padding: 0 0 8px;
}
.anticon {
color: @primary-color;
}
&:empty {
padding: 0;
}
}
&-content-bg {
background-color: @component-background;
}
&--dense {
.@{prefix-cls}-content {
margin: 0;
padding: 0;
border-radius: 0;
}
.ant-page-header {
margin: 0;
padding: 0;
border-radius: 0;
}
}
.ant-layout {
background-color: @content-bg;
.sidebar {
background-color: @content-bg;
transition: none;
//min-height: 400px;
&-content {
// margin: 15px 0 0 15px;
// margin-right: 15px;
//height: calc(100% - 14px);
height: 100%;
overflow: hidden;
border-radius: 4px;
.jeesite-basic-tree-header {
padding: 10px 6px;
min-height: 44px;
}
}
&-open,
&-close {
cursor: pointer;
background: #fff;
position: absolute;
z-index: 100;
top: 54px;
width: 24px;
height: 24px;
text-align: center;
transition: transform 0.3s;
color: rgb(0 0 0 / 50%);
border-radius: 40px;
box-shadow:
0 2px 8px -2px rgb(0 0 0 / 5%),
0 1px 4px -1px rgb(25 15 15 / 7%),
0 0 1px 0 rgb(0 0 0 / 8%);
&:hover {
color: rgb(0 0 0 / 80%);
}
svg {
font-size: 12px;
}
}
&-open {
left: -12px;
&.right {
right: -12px;
}
}
&-close {
right: -12px;
&.right {
left: -12px;
}
}
}
}
}
html[data-theme='dark'] {
.@{prefix-cls} {
.ant-layout {
background: transparent;
}
.sidebar {
&-open,
&-close {
border: 1px solid #555;
background: transparent;
}
&-open {
border-left-width: 0;
}
&-close {
border-right-width: 0;
}
}
}
}
</style>