mirror of
https://gitee.com/hhyykk/ipms-sjy.git
synced 2025-11-11 15:48:43 +08:00
feat: add vue3(element-plus)
This commit is contained in:
3
yudao-ui-admin-vue3/src/components/Backtop/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Backtop/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Backtop from './src/Backtop.vue'
|
||||
|
||||
export { Backtop }
|
||||
15
yudao-ui-admin-vue3/src/components/Backtop/src/Backtop.vue
Normal file
15
yudao-ui-admin-vue3/src/components/Backtop/src/Backtop.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ElBacktop } from 'element-plus'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls, variables } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('backtop')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElBacktop
|
||||
:class="`${prefixCls}-backtop`"
|
||||
:target="`.${variables.namespace}-layout-content-scrollbar .${variables.elNamespace}-scrollbar__wrap`"
|
||||
/>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/Breadcrumb/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Breadcrumb/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Breadcrumb from './src/Breadcrumb.vue'
|
||||
|
||||
export { Breadcrumb }
|
||||
127
yudao-ui-admin-vue3/src/components/Breadcrumb/src/Breadcrumb.vue
Normal file
127
yudao-ui-admin-vue3/src/components/Breadcrumb/src/Breadcrumb.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<script lang="tsx">
|
||||
import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
|
||||
import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { filterBreadcrumb } from './helper'
|
||||
import { filter, treeToList } from '@/utils/tree'
|
||||
import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('breadcrumb')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 面包屑图标
|
||||
const breadcrumbIcon = computed(() => appStore.getBreadcrumbIcon)
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Breadcrumb',
|
||||
setup() {
|
||||
const { currentRoute } = useRouter()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const levelList = ref<AppRouteRecordRaw[]>([])
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const menuRouters = computed(() => {
|
||||
const routers = permissionStore.getRouters
|
||||
return filterBreadcrumb(routers)
|
||||
})
|
||||
|
||||
const getBreadcrumb = () => {
|
||||
const currentPath = currentRoute.value.path
|
||||
|
||||
levelList.value = filter<AppRouteRecordRaw>(unref(menuRouters), (node: AppRouteRecordRaw) => {
|
||||
return node.path === currentPath
|
||||
})
|
||||
}
|
||||
|
||||
const renderBreadcrumb = () => {
|
||||
const breadcrumbList = treeToList<AppRouteRecordRaw[]>(unref(levelList))
|
||||
return breadcrumbList.map((v) => {
|
||||
const disabled = v.redirect === 'noredirect'
|
||||
const meta = v.meta as RouteMeta
|
||||
return (
|
||||
<ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
|
||||
{meta?.icon && breadcrumbIcon.value ? (
|
||||
<>
|
||||
<Icon icon={meta.icon} class="mr-[5px]"></Icon> {t(v?.meta?.title)}
|
||||
</>
|
||||
) : (
|
||||
t(v?.meta?.title)
|
||||
)}
|
||||
</ElBreadcrumbItem>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
(route: RouteLocationNormalizedLoaded) => {
|
||||
if (route.path.startsWith('/redirect/')) {
|
||||
return
|
||||
}
|
||||
getBreadcrumb()
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
return () => (
|
||||
<ElBreadcrumb separator="/" class={`${prefixCls} flex items-center h-full ml-[10px]`}>
|
||||
<TransitionGroup appear enter-active-class="animate__animated animate__fadeInRight">
|
||||
{renderBreadcrumb()}
|
||||
</TransitionGroup>
|
||||
</ElBreadcrumb>
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{elNamespace}-breadcrumb';
|
||||
|
||||
.@{prefix-cls} {
|
||||
:deep(&__item) {
|
||||
display: flex;
|
||||
.@{prefix-cls}__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--top-header-text-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(&__item):not(:last-child) {
|
||||
.@{prefix-cls}__inner {
|
||||
color: var(--top-header-text-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(&__item):last-child {
|
||||
.@{prefix-cls}__inner {
|
||||
color: var(--el-text-color-placeholder);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
31
yudao-ui-admin-vue3/src/components/Breadcrumb/src/helper.ts
Normal file
31
yudao-ui-admin-vue3/src/components/Breadcrumb/src/helper.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { pathResolve } from '@/utils/routerHelper'
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
|
||||
export const filterBreadcrumb = (
|
||||
routes: AppRouteRecordRaw[],
|
||||
parentPath = ''
|
||||
): AppRouteRecordRaw[] => {
|
||||
const res: AppRouteRecordRaw[] = []
|
||||
|
||||
for (const route of routes) {
|
||||
const meta = route?.meta as RouteMeta
|
||||
if (meta.hidden && !meta.canTo) {
|
||||
continue
|
||||
}
|
||||
|
||||
const data: AppRouteRecordRaw =
|
||||
!meta.alwaysShow && route.children?.length === 1
|
||||
? { ...route.children[0], path: pathResolve(route.path, route.children[0].path) }
|
||||
: { ...route }
|
||||
|
||||
data.path = pathResolve(parentPath, data.path)
|
||||
|
||||
if (data.children) {
|
||||
data.children = filterBreadcrumb(data.children, data.path)
|
||||
}
|
||||
if (data) {
|
||||
res.push(data)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
3
yudao-ui-admin-vue3/src/components/Collapse/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Collapse/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Collapse from './src/Collapse.vue'
|
||||
|
||||
export { Collapse }
|
||||
35
yudao-ui-admin-vue3/src/components/Collapse/src/Collapse.vue
Normal file
35
yudao-ui-admin-vue3/src/components/Collapse/src/Collapse.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, unref } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('collapse')
|
||||
|
||||
defineProps({
|
||||
color: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const collapse = computed(() => appStore.getCollapse)
|
||||
|
||||
const toggleCollapse = () => {
|
||||
const collapsed = unref(collapse)
|
||||
appStore.setCollapse(!collapsed)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<Icon
|
||||
:size="18"
|
||||
:icon="collapse ? 'ep:expand' : 'ep:fold'"
|
||||
:color="color"
|
||||
class="cursor-pointer"
|
||||
@click="toggleCollapse"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/ConfigGlobal/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/ConfigGlobal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ConfigGlobal from './src/ConfigGlobal.vue'
|
||||
|
||||
export { ConfigGlobal }
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { provide, computed, watch, onMounted } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import { useLocaleStore } from '@/store/modules/locale'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { setCssVar } from '@/utils'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { variables } = useDesign()
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const props = defineProps({
|
||||
size: propTypes.oneOf<ElememtPlusSize[]>(['default', 'small', 'large']).def('default')
|
||||
})
|
||||
|
||||
provide('configGlobal', props)
|
||||
|
||||
// 初始化所有主题色
|
||||
onMounted(() => {
|
||||
appStore.setCssVarTheme()
|
||||
})
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
// 监听窗口变化
|
||||
watch(
|
||||
() => width.value,
|
||||
(width: number) => {
|
||||
if (width < 768) {
|
||||
!appStore.getMobile ? appStore.setMobile(true) : undefined
|
||||
setCssVar('--left-menu-min-width', '0')
|
||||
appStore.setCollapse(true)
|
||||
appStore.getLayout !== 'classic' ? appStore.setLayout('classic') : undefined
|
||||
} else {
|
||||
appStore.getMobile ? appStore.setMobile(false) : undefined
|
||||
setCssVar('--left-menu-min-width', '64px')
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
// 多语言相关
|
||||
const localeStore = useLocaleStore()
|
||||
|
||||
const currentLocale = computed(() => localeStore.currentLocale)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElConfigProvider
|
||||
:namespace="variables.elNamespace"
|
||||
:locale="currentLocale.elLocale"
|
||||
:message="{ max: 1 }"
|
||||
:size="size"
|
||||
>
|
||||
<slot></slot>
|
||||
</ElConfigProvider>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
import ContentDetailWrap from './src/ContentDetailWrap.vue'
|
||||
|
||||
export { ContentDetailWrap }
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { ElCard } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { ref, onMounted, defineEmits } from 'vue'
|
||||
import { Sticky } from '@/components/Sticky'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
const { t } = useI18n()
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('content-detail-wrap')
|
||||
|
||||
defineProps({
|
||||
title: propTypes.string.def(''),
|
||||
message: propTypes.string.def('')
|
||||
})
|
||||
const emit = defineEmits(['back'])
|
||||
const offset = ref(85)
|
||||
const contentDetailWrap = ref()
|
||||
onMounted(() => {
|
||||
offset.value = contentDetailWrap.value.getBoundingClientRect().top
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[`${prefixCls}-container`, 'relative bg-[#fff]']" ref="contentDetailWrap">
|
||||
<Sticky :offset="offset">
|
||||
<div
|
||||
:class="[
|
||||
`${prefixCls}-header`,
|
||||
'flex border-bottom-1 h-50px items-center text-center bg-white pr-10px'
|
||||
]"
|
||||
>
|
||||
<div :class="[`${prefixCls}-header__back`, 'flex pl-10px pr-10px ']">
|
||||
<ElButton @click="emit('back')">
|
||||
<Icon icon="ep:arrow-left" class="mr-5px" />
|
||||
{{ t('common.back') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div :class="[`${prefixCls}-header__title`, 'flex flex-1 justify-center']">
|
||||
<slot name="title">
|
||||
<label class="text-16px font-700">{{ title }}</label>
|
||||
</slot>
|
||||
</div>
|
||||
<div :class="[`${prefixCls}-header__right`, 'flex pl-10px pr-10px']">
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</Sticky>
|
||||
<div style="padding: var(--app-content-padding)">
|
||||
<ElCard :class="[`${prefixCls}-body`, 'mb-20px']" shadow="never">
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/ContentWrap/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/ContentWrap/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ContentWrap from './src/ContentWrap.vue'
|
||||
|
||||
export { ContentWrap }
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { ElCard, ElTooltip } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('content-wrap')
|
||||
|
||||
defineProps({
|
||||
title: propTypes.string.def(''),
|
||||
message: propTypes.string.def('')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCard :class="[prefixCls, 'mb-20px']" shadow="never">
|
||||
<template v-if="title" #header>
|
||||
<div class="flex items-center">
|
||||
<span class="text-16px font-700">{{ title }}</span>
|
||||
<ElTooltip v-if="message" effect="dark" placement="right">
|
||||
<template #content>
|
||||
<div class="max-w-200px">{{ message }}</div>
|
||||
</template>
|
||||
<Icon class="ml-5px" icon="ep:question-filled" :size="14" />
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
10
yudao-ui-admin-vue3/src/components/ContextMenu/index.ts
Normal file
10
yudao-ui-admin-vue3/src/components/ContextMenu/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import ContextMenu from './src/ContextMenu.vue'
|
||||
import { ElDropdown } from 'element-plus'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
export interface ContextMenuExpose {
|
||||
elDropdownMenuRef: ComponentRef<typeof ElDropdown>
|
||||
tagItem: RouteLocationNormalizedLoaded
|
||||
}
|
||||
|
||||
export { ContextMenu }
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||
import { PropType, ref } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('context-menu')
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits(['visibleChange'])
|
||||
|
||||
const props = defineProps({
|
||||
schema: {
|
||||
type: Array as PropType<contextMenuSchema[]>,
|
||||
default: () => []
|
||||
},
|
||||
trigger: {
|
||||
type: String as PropType<'click' | 'hover' | 'focus' | 'contextmenu'>,
|
||||
default: 'contextmenu'
|
||||
},
|
||||
tagItem: {
|
||||
type: Object as PropType<RouteLocationNormalizedLoaded>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const command = (item: contextMenuSchema) => {
|
||||
item.command && item.command(item)
|
||||
}
|
||||
|
||||
const visibleChange = (visible: boolean) => {
|
||||
emit('visibleChange', visible, props.tagItem)
|
||||
}
|
||||
|
||||
const elDropdownMenuRef = ref<ComponentRef<typeof ElDropdown>>()
|
||||
|
||||
defineExpose({
|
||||
elDropdownMenuRef,
|
||||
tagItem: props.tagItem
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDropdown
|
||||
ref="elDropdownMenuRef"
|
||||
:class="prefixCls"
|
||||
:trigger="trigger"
|
||||
placement="bottom-start"
|
||||
@command="command"
|
||||
@visible-change="visibleChange"
|
||||
popper-class="v-context-menu-popper"
|
||||
>
|
||||
<slot></slot>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-for="(item, index) in schema"
|
||||
:key="`dropdown${index}`"
|
||||
:divided="item.divided"
|
||||
:disabled="item.disabled"
|
||||
:command="item"
|
||||
>
|
||||
<Icon :icon="item.icon" /> {{ t(item.label) }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/CountTo/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/CountTo/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import CountTo from './src/CountTo.vue'
|
||||
|
||||
export { CountTo }
|
||||
180
yudao-ui-admin-vue3/src/components/CountTo/src/CountTo.vue
Normal file
180
yudao-ui-admin-vue3/src/components/CountTo/src/CountTo.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed, watch, onMounted, unref, toRef, PropType } from 'vue'
|
||||
import { isNumber } from '@/utils/is'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('count-to')
|
||||
|
||||
const props = defineProps({
|
||||
startVal: propTypes.number.def(0),
|
||||
endVal: propTypes.number.def(2021),
|
||||
duration: propTypes.number.def(3000),
|
||||
autoplay: propTypes.bool.def(true),
|
||||
decimals: propTypes.number.validate((value: number) => value >= 0).def(0),
|
||||
decimal: propTypes.string.def('.'),
|
||||
separator: propTypes.string.def(','),
|
||||
prefix: propTypes.string.def(''),
|
||||
suffix: propTypes.string.def(''),
|
||||
useEasing: propTypes.bool.def(true),
|
||||
easingFn: {
|
||||
type: Function as PropType<(t: number, b: number, c: number, d: number) => number>,
|
||||
default(t: number, b: number, c: number, d: number) {
|
||||
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['mounted', 'callback'])
|
||||
|
||||
const formatNumber = (num: number | string) => {
|
||||
const { decimals, decimal, separator, suffix, prefix } = props
|
||||
num = Number(num).toFixed(decimals)
|
||||
num += ''
|
||||
const x = num.split('.')
|
||||
let x1 = x[0]
|
||||
const x2 = x.length > 1 ? decimal + x[1] : ''
|
||||
const rgx = /(\d+)(\d{3})/
|
||||
if (separator && !isNumber(separator)) {
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, '$1' + separator + '$2')
|
||||
}
|
||||
}
|
||||
return prefix + x1 + x2 + suffix
|
||||
}
|
||||
|
||||
const state = reactive<{
|
||||
localStartVal: number
|
||||
printVal: number | null
|
||||
displayValue: string
|
||||
paused: boolean
|
||||
localDuration: number | null
|
||||
startTime: number | null
|
||||
timestamp: number | null
|
||||
rAF: any
|
||||
remaining: number | null
|
||||
}>({
|
||||
localStartVal: props.startVal,
|
||||
displayValue: formatNumber(props.startVal),
|
||||
printVal: null,
|
||||
paused: false,
|
||||
localDuration: props.duration,
|
||||
startTime: null,
|
||||
timestamp: null,
|
||||
remaining: null,
|
||||
rAF: null
|
||||
})
|
||||
|
||||
const displayValue = toRef(state, 'displayValue')
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoplay) {
|
||||
start()
|
||||
}
|
||||
emit('mounted')
|
||||
})
|
||||
|
||||
const getCountDown = computed(() => {
|
||||
return props.startVal > props.endVal
|
||||
})
|
||||
|
||||
watch([() => props.startVal, () => props.endVal], () => {
|
||||
if (props.autoplay) {
|
||||
start()
|
||||
}
|
||||
})
|
||||
|
||||
const start = () => {
|
||||
const { startVal, duration } = props
|
||||
state.localStartVal = startVal
|
||||
state.startTime = null
|
||||
state.localDuration = duration
|
||||
state.paused = false
|
||||
state.rAF = requestAnimationFrame(count)
|
||||
}
|
||||
|
||||
const pauseResume = () => {
|
||||
if (state.paused) {
|
||||
resume()
|
||||
state.paused = false
|
||||
} else {
|
||||
pause()
|
||||
state.paused = true
|
||||
}
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
cancelAnimationFrame(state.rAF)
|
||||
}
|
||||
|
||||
const resume = () => {
|
||||
state.startTime = null
|
||||
state.localDuration = +(state.remaining as number)
|
||||
state.localStartVal = +(state.printVal as number)
|
||||
requestAnimationFrame(count)
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
state.startTime = null
|
||||
cancelAnimationFrame(state.rAF)
|
||||
state.displayValue = formatNumber(props.startVal)
|
||||
}
|
||||
|
||||
const count = (timestamp: number) => {
|
||||
const { useEasing, easingFn, endVal } = props
|
||||
if (!state.startTime) state.startTime = timestamp
|
||||
state.timestamp = timestamp
|
||||
const progress = timestamp - state.startTime
|
||||
state.remaining = (state.localDuration as number) - progress
|
||||
if (useEasing) {
|
||||
if (unref(getCountDown)) {
|
||||
state.printVal =
|
||||
state.localStartVal -
|
||||
easingFn(progress, 0, state.localStartVal - endVal, state.localDuration as number)
|
||||
} else {
|
||||
state.printVal = easingFn(
|
||||
progress,
|
||||
state.localStartVal,
|
||||
endVal - state.localStartVal,
|
||||
state.localDuration as number
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (unref(getCountDown)) {
|
||||
state.printVal =
|
||||
state.localStartVal -
|
||||
(state.localStartVal - endVal) * (progress / (state.localDuration as number))
|
||||
} else {
|
||||
state.printVal =
|
||||
state.localStartVal +
|
||||
(endVal - state.localStartVal) * (progress / (state.localDuration as number))
|
||||
}
|
||||
}
|
||||
if (unref(getCountDown)) {
|
||||
state.printVal = state.printVal < endVal ? endVal : state.printVal
|
||||
} else {
|
||||
state.printVal = state.printVal > endVal ? endVal : state.printVal
|
||||
}
|
||||
state.displayValue = formatNumber(state.printVal)
|
||||
if (progress < (state.localDuration as number)) {
|
||||
state.rAF = requestAnimationFrame(count)
|
||||
} else {
|
||||
emit('callback')
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
pauseResume,
|
||||
reset,
|
||||
start,
|
||||
pause
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="prefixCls">
|
||||
{{ displayValue }}
|
||||
</span>
|
||||
</template>
|
||||
174
yudao-ui-admin-vue3/src/components/Crontab/day.vue
Normal file
174
yudao-ui-admin-vue3/src/components/Crontab/day.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="1"> 日,允许的通配符[, - * ? / L W] </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2"> 不指定 </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
周期从
|
||||
<el-input-number v-model="cycle01" :min="1" :max="30" /> -
|
||||
<el-input-number v-model="cycle02" :min="cycle01 ? cycle01 + 1 : 2" :max="31" /> 日
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
从
|
||||
<el-input-number v-model="average01" :min="1" :max="30" /> 号开始,每
|
||||
<el-input-number v-model="average02" :min="1" :max="31 - average01 || 1" /> 日执行一次
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="5">
|
||||
每月
|
||||
<el-input-number v-model="workday" :min="1" :max="31" /> 号最近的那个工作日
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="6"> 本月最后一天 </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="7">
|
||||
指定
|
||||
<el-select
|
||||
clearable
|
||||
v-model="checkboxList"
|
||||
placeholder="可多选"
|
||||
multiple
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in 31" :key="item" :value="item">{{ item }}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
check: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
cron: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update'])
|
||||
|
||||
const radioValue = ref(1)
|
||||
const workday = ref(1)
|
||||
const cycle01 = ref(1)
|
||||
const cycle02 = ref(2)
|
||||
const average01 = ref(1)
|
||||
const average02 = ref(1)
|
||||
|
||||
const checkboxList = ref([])
|
||||
|
||||
defineExpose({
|
||||
radioValue,
|
||||
workday,
|
||||
cycle01,
|
||||
cycle02,
|
||||
average01,
|
||||
average02,
|
||||
checkboxList
|
||||
})
|
||||
|
||||
/**
|
||||
* 计算两个周期值
|
||||
* @type {ComputedRef<string>}
|
||||
*/
|
||||
const cycleTotal = computed(() => {
|
||||
const cycle1 = props.check(cycle01.value, 1, 30)
|
||||
const cycle2 = props.check(cycle02.value, cycle1 ? cycle1 + 1 : 2, 31, 31)
|
||||
return cycle1 + '-' + cycle2
|
||||
})
|
||||
|
||||
/**
|
||||
* 计算平均用到的值
|
||||
*/
|
||||
const averageTotal = computed(() => {
|
||||
const average1 = props.check(average01.value, 1, 30)
|
||||
const average2 = props.check(average02.value, 1, 31 - average1 || 0)
|
||||
return average1 + '/' + average2
|
||||
})
|
||||
|
||||
/**
|
||||
* 计算工作日格式
|
||||
*/
|
||||
const workdayCheck = computed(() => props.check(workday, 1, 31))
|
||||
|
||||
/**
|
||||
* 计算勾选的checkbox值合集
|
||||
*/
|
||||
const checkboxString = computed(() => {
|
||||
const str = checkboxList.value.join()
|
||||
return str.length === 0 ? '*' : str
|
||||
})
|
||||
|
||||
watch(radioValue, () => {
|
||||
if (parseInt(radioValue.value.toString()) !== 2 && props.cron?.week !== '?') {
|
||||
emits('update', 'week', '?', 'day')
|
||||
}
|
||||
|
||||
switch (parseInt(radioValue.value.toString())) {
|
||||
case 1:
|
||||
emits('update', 'day', '*')
|
||||
break
|
||||
case 2:
|
||||
emits('update', 'day', '?')
|
||||
break
|
||||
case 3:
|
||||
emits('update', 'day', cycleTotal)
|
||||
break
|
||||
case 4:
|
||||
emits('update', 'day', averageTotal)
|
||||
break
|
||||
case 5:
|
||||
emits('update', 'day', workday.value + 'W')
|
||||
break
|
||||
case 6:
|
||||
emits('update', 'day', 'L')
|
||||
break
|
||||
case 7:
|
||||
emits('update', 'day', checkboxString)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 3) {
|
||||
emits('update', 'day', cycleTotal)
|
||||
}
|
||||
})
|
||||
|
||||
watch(averageTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 4) {
|
||||
emits('update', 'day', averageTotal)
|
||||
}
|
||||
})
|
||||
|
||||
watch(workdayCheck, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 5) {
|
||||
emits('update', 'day', workdayCheck.value + 'W')
|
||||
}
|
||||
})
|
||||
|
||||
watch(checkboxString, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 7) {
|
||||
emits('update', 'day', checkboxString)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
121
yudao-ui-admin-vue3/src/components/Crontab/hour.vue
Normal file
121
yudao-ui-admin-vue3/src/components/Crontab/hour.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="1"> 小时,允许的通配符[, - * /] </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
周期从
|
||||
<el-input-number v-model="cycle01" :min="0" :max="22" /> -
|
||||
<el-input-number v-model="cycle02" :min="cycle01 ? cycle01 + 1 : 1" :max="23" /> 小时
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
从
|
||||
<el-input-number v-model="average01" :min="0" :max="22" /> 小时开始,每
|
||||
<el-input-number v-model="average02" :min="1" :max="23 - average01 || 0" /> 小时执行一次
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
指定
|
||||
<el-select
|
||||
clearable
|
||||
v-model="checkboxList"
|
||||
placeholder="可多选"
|
||||
multiple
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in 24" :key="item" :value="item - 1">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
check: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
cron: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['update'])
|
||||
|
||||
const radioValue = ref(1)
|
||||
const cycle01 = ref(0)
|
||||
const cycle02 = ref(1)
|
||||
const average01 = ref(0)
|
||||
const average02 = ref(1)
|
||||
const checkboxList = ref([])
|
||||
|
||||
defineExpose({
|
||||
radioValue,
|
||||
cycle01,
|
||||
cycle02,
|
||||
average01,
|
||||
average02,
|
||||
checkboxList
|
||||
})
|
||||
|
||||
const cycleTotal = computed(() => {
|
||||
const cycle1 = props.check(cycle01.value, 0, 22)
|
||||
const cycle2 = props.check(cycle02.value, cycle1 ? cycle1 + 1 : 1, 23)
|
||||
return cycle1 + '-' + cycle2
|
||||
})
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 2) {
|
||||
emits('update', 'hour', cycleTotal)
|
||||
}
|
||||
})
|
||||
|
||||
const averageTotal = computed(() => {
|
||||
const average1 = props.check(average01.value, 0, 22)
|
||||
const average2 = props.check(average02.value, 1, 23 - average1 || 0)
|
||||
return average1 + '/' + average2
|
||||
})
|
||||
|
||||
watch(averageTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 3) {
|
||||
emits('update', 'hour', averageTotal)
|
||||
}
|
||||
})
|
||||
|
||||
const checkboxString = computed(() => {
|
||||
let str = checkboxList.value.join()
|
||||
return str.length === 0 ? '*' : str
|
||||
})
|
||||
|
||||
watch(checkboxString, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 4) {
|
||||
emits('update', 'hour', checkboxString)
|
||||
}
|
||||
})
|
||||
|
||||
watch(radioValue, () => {
|
||||
switch (parseInt(radioValue.value.toString())) {
|
||||
case 1:
|
||||
emits('update', 'hour', '*')
|
||||
break
|
||||
case 2:
|
||||
emits('update', 'hour', cycleTotal)
|
||||
break
|
||||
case 3:
|
||||
emits('update', 'hour', averageTotal)
|
||||
break
|
||||
case 4:
|
||||
emits('update', 'hour', checkboxString)
|
||||
break
|
||||
}
|
||||
})
|
||||
</script>
|
||||
436
yudao-ui-admin-vue3/src/components/Crontab/index.vue
Normal file
436
yudao-ui-admin-vue3/src/components/Crontab/index.vue
Normal file
@@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane label="秒" v-if="shouldHide('second')">
|
||||
<CrontabSecond
|
||||
@update="updateCrontabValue"
|
||||
:check="checkNumber"
|
||||
:cron="crontabValueObj"
|
||||
ref="cronsecond"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="分钟" v-if="shouldHide('min')">
|
||||
<CrontabMin
|
||||
@update="updateCrontabValue"
|
||||
:check="checkNumber"
|
||||
:cron="crontabValueObj"
|
||||
ref="cronmin"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="小时" v-if="shouldHide('hour')">
|
||||
<CrontabHour
|
||||
@update="updateCrontabValue"
|
||||
:check="checkNumber"
|
||||
:cron="crontabValueObj"
|
||||
ref="cronhour"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="日" v-if="shouldHide('day')">
|
||||
<CrontabDay
|
||||
@update="updateCrontabValue"
|
||||
:check="checkNumber"
|
||||
:cron="crontabValueObj"
|
||||
ref="cronday"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="月" v-if="shouldHide('month')">
|
||||
<CrontabMonth
|
||||
@update="updateCrontabValue"
|
||||
:check="checkNumber"
|
||||
:cron="crontabValueObj"
|
||||
ref="cronmonth"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="周" v-if="shouldHide('week')">
|
||||
<CrontabWeek
|
||||
@update="updateCrontabValue"
|
||||
:check="checkNumber"
|
||||
:cron="crontabValueObj"
|
||||
ref="cronweek"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="年" v-if="shouldHide('year')">
|
||||
<CrontabYear
|
||||
@update="updateCrontabValue"
|
||||
:check="checkNumber"
|
||||
:cron="crontabValueObj"
|
||||
ref="cronyear"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="popup-main">
|
||||
<div class="popup-result">
|
||||
<p class="title">时间表达式</p>
|
||||
<table>
|
||||
<thead>
|
||||
<th v-for="item of tabTitles" width="40" :key="item">{{ item }}</th>
|
||||
<th>Cron 表达式</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.second }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.min }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.hour }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.day }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.month }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.week }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueObj.year }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ crontabValueString }}</span>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<CrontabResult :ex="crontabValueString" />
|
||||
|
||||
<div class="pop_btn">
|
||||
<el-button size="small" type="primary" @click="submitFill">确定</el-button>
|
||||
<el-button size="small" type="warning" @click="clearCron">重置</el-button>
|
||||
<el-button size="small" @click="hidePopup">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CrontabSecond from './second.vue'
|
||||
import CrontabMin from './min.vue'
|
||||
import CrontabHour from './hour.vue'
|
||||
import CrontabDay from './day.vue'
|
||||
import CrontabMonth from './month.vue'
|
||||
import CrontabWeek from './week.vue'
|
||||
import CrontabYear from './year.vue'
|
||||
import CrontabResult from './result.vue'
|
||||
import { computed, defineEmits, defineProps, onMounted, Ref, ref, watch } from 'vue'
|
||||
|
||||
const cronsecond = ref(null)
|
||||
const cronmin = ref(null)
|
||||
const cronhour = ref(null)
|
||||
const cronday = ref(null)
|
||||
const cronmonth = ref(null)
|
||||
const cronweek = ref(null)
|
||||
const cronyear = ref(null)
|
||||
|
||||
const refs = ref<Record<string, Ref>>({})
|
||||
onMounted(() => {
|
||||
refs.value.cronsecond = cronsecond
|
||||
refs.value.cronmin = cronmin
|
||||
refs.value.cronhour = cronhour
|
||||
refs.value.cronday = cronday
|
||||
refs.value.cronmonth = cronmonth
|
||||
refs.value.cronweek = cronweek
|
||||
refs.value.cronyear = cronyear
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
expression: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
hideComponent: {
|
||||
type: Array
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['hide', 'fill'])
|
||||
|
||||
const tabTitles = ['秒', '分钟', '小时', '日', '月', '周', '年']
|
||||
|
||||
const crontabValueObj = ref({
|
||||
second: '*',
|
||||
min: '*',
|
||||
hour: '*',
|
||||
day: '*',
|
||||
month: '*',
|
||||
week: '?',
|
||||
year: ''
|
||||
})
|
||||
|
||||
function shouldHide(key) {
|
||||
return !(props.hideComponent && props.hideComponent.includes(key))
|
||||
}
|
||||
|
||||
function resolveExp() {
|
||||
// 反解析 表达式
|
||||
if (props.expression) {
|
||||
let arr = props.expression.split(' ')
|
||||
if (arr.length >= 6) {
|
||||
//6 位以上是合法表达式
|
||||
let obj = {
|
||||
second: arr[0],
|
||||
min: arr[1],
|
||||
hour: arr[2],
|
||||
day: arr[3],
|
||||
month: arr[4],
|
||||
week: arr[5],
|
||||
year: arr[6] ? arr[6] : ''
|
||||
}
|
||||
crontabValueObj.value = {
|
||||
...obj
|
||||
}
|
||||
for (let i in obj) {
|
||||
if (obj[i]) {
|
||||
changeRadio(i, obj[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 没有传入的表达式 则还原
|
||||
clearCron()
|
||||
}
|
||||
}
|
||||
|
||||
// 由子组件触发,更改表达式组成的字段值
|
||||
function updateCrontabValue(name, value, from) {
|
||||
crontabValueObj.value[name] = value
|
||||
if (from && from !== name) {
|
||||
console.log(`来自组件 ${from} 改变了 ${name} ${value}`)
|
||||
changeRadio(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
function changeRadio(name, value) {
|
||||
let arr = ['second', 'min', 'hour', 'month'],
|
||||
refName = 'cron' + name,
|
||||
insValue
|
||||
|
||||
if (!refs.value[refName]) return
|
||||
|
||||
if (arr.includes(name)) {
|
||||
if (value === '*') {
|
||||
insValue = 1
|
||||
} else if (value.indexOf('-') > -1) {
|
||||
let indexArr = value.split('-')
|
||||
isNaN(indexArr[0])
|
||||
? (refs.value[refName].cycle01 = 0)
|
||||
: (refs.value[refName].cycle01 = indexArr[0])
|
||||
refs.value[refName].cycle02 = indexArr[1]
|
||||
insValue = 2
|
||||
} else if (value.indexOf('/') > -1) {
|
||||
let indexArr = value.split('/')
|
||||
isNaN(indexArr[0])
|
||||
? (refs.value[refName].average01 = 0)
|
||||
: (refs.value[refName].average01 = indexArr[0])
|
||||
refs.value[refName].average02 = indexArr[1]
|
||||
insValue = 3
|
||||
} else {
|
||||
insValue = 4
|
||||
refs.value[refName].checkboxList = value.split(',')
|
||||
}
|
||||
} else if (name == 'day') {
|
||||
if (value === '*') {
|
||||
insValue = 1
|
||||
} else if (value == '?') {
|
||||
insValue = 2
|
||||
} else if (value.indexOf('-') > -1) {
|
||||
let indexArr = value.split('-')
|
||||
isNaN(indexArr[0])
|
||||
? (refs.value[refName].cycle01 = 0)
|
||||
: (refs.value[refName].cycle01 = indexArr[0])
|
||||
refs.value[refName].cycle02 = indexArr[1]
|
||||
insValue = 3
|
||||
} else if (value.indexOf('/') > -1) {
|
||||
let indexArr = value.split('/')
|
||||
isNaN(indexArr[0])
|
||||
? (refs.value[refName].average01 = 0)
|
||||
: (refs.value[refName].average01 = indexArr[0])
|
||||
refs.value[refName].average02 = indexArr[1]
|
||||
insValue = 4
|
||||
} else if (value.indexOf('W') > -1) {
|
||||
let indexArr = value.split('W')
|
||||
isNaN(indexArr[0])
|
||||
? (refs.value[refName].workday = 0)
|
||||
: (refs.value[refName].workday = indexArr[0])
|
||||
insValue = 5
|
||||
} else if (value === 'L') {
|
||||
insValue = 6
|
||||
} else {
|
||||
refs.value[refName].checkboxList = value.split(',')
|
||||
insValue = 7
|
||||
}
|
||||
} else if (name == 'week') {
|
||||
if (value === '*') {
|
||||
insValue = 1
|
||||
} else if (value == '?') {
|
||||
insValue = 2
|
||||
} else if (value.indexOf('-') > -1) {
|
||||
let indexArr = value.split('-')
|
||||
isNaN(indexArr[0])
|
||||
? (refs.value[refName].cycle01 = 0)
|
||||
: (refs.value[refName].cycle01 = indexArr[0])
|
||||
refs.value[refName].cycle02 = indexArr[1]
|
||||
insValue = 3
|
||||
} else if (value.indexOf('#') > -1) {
|
||||
let indexArr = value.split('#')
|
||||
isNaN(indexArr[0])
|
||||
? (refs.value[refName].average01 = 1)
|
||||
: (refs.value[refName].average01 = indexArr[0])
|
||||
refs.value[refName].average02 = indexArr[1]
|
||||
insValue = 4
|
||||
} else if (value.indexOf('L') > -1) {
|
||||
let indexArr = value.split('L')
|
||||
isNaN(indexArr[0])
|
||||
? (refs.value[refName].weekday = 1)
|
||||
: (refs.value[refName].weekday = indexArr[0])
|
||||
insValue = 5
|
||||
} else {
|
||||
refs.value[refName].checkboxList = value.split(',')
|
||||
insValue = 6
|
||||
}
|
||||
} else if (name == 'year') {
|
||||
if (value == '') {
|
||||
insValue = 1
|
||||
} else if (value == '*') {
|
||||
insValue = 2
|
||||
} else if (value.indexOf('-') > -1) {
|
||||
insValue = 3
|
||||
} else if (value.indexOf('/') > -1) {
|
||||
insValue = 4
|
||||
} else {
|
||||
refs.value[refName].checkboxList = value.split(',')
|
||||
insValue = 5
|
||||
}
|
||||
}
|
||||
refs.value[refName].radioValue = insValue
|
||||
}
|
||||
|
||||
// 表单选项的子组件校验数字格式(通过-props传递)
|
||||
function checkNumber(value, minLimit, maxLimit) {
|
||||
// 检查必须为整数
|
||||
value = Math.floor(value)
|
||||
if (value < minLimit) {
|
||||
value = minLimit
|
||||
} else if (value > maxLimit) {
|
||||
value = maxLimit
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// 隐藏弹窗
|
||||
function hidePopup() {
|
||||
emit('hide')
|
||||
}
|
||||
|
||||
// 填充表达式
|
||||
function submitFill() {
|
||||
emit('fill', crontabValueString)
|
||||
hidePopup()
|
||||
}
|
||||
|
||||
const crontabValueString = computed(() => {
|
||||
let obj = crontabValueObj.value
|
||||
return (
|
||||
obj.second +
|
||||
' ' +
|
||||
obj.min +
|
||||
' ' +
|
||||
obj.hour +
|
||||
' ' +
|
||||
obj.day +
|
||||
' ' +
|
||||
obj.month +
|
||||
' ' +
|
||||
obj.week +
|
||||
(obj.year === '' ? '' : ' ' + obj.year)
|
||||
)
|
||||
})
|
||||
|
||||
function clearCron() {
|
||||
// 还原选择项
|
||||
crontabValueObj.value = {
|
||||
second: '*',
|
||||
min: '*',
|
||||
hour: '*',
|
||||
day: '*',
|
||||
month: '*',
|
||||
week: '?',
|
||||
year: ''
|
||||
}
|
||||
for (let j in crontabValueObj.value) {
|
||||
changeRadio(j, crontabValueObj.value[j])
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.expression, resolveExp)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pop_btn {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.popup-main {
|
||||
position: relative;
|
||||
margin: 10px auto;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.popup-title {
|
||||
overflow: hidden;
|
||||
line-height: 34px;
|
||||
padding-top: 6px;
|
||||
background: #f2f2f2;
|
||||
}
|
||||
.popup-result {
|
||||
box-sizing: border-box;
|
||||
line-height: 24px;
|
||||
margin: 25px auto;
|
||||
padding: 15px 10px 10px;
|
||||
border: 1px solid #ccc;
|
||||
position: relative;
|
||||
}
|
||||
.popup-result .title {
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
left: 50%;
|
||||
width: 140px;
|
||||
font-size: 14px;
|
||||
margin-left: -70px;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
background: #fff;
|
||||
}
|
||||
.popup-result table {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.popup-result table span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-family: arial;
|
||||
line-height: 30px;
|
||||
height: 30px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
.popup-result-scroll {
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
height: 10em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
121
yudao-ui-admin-vue3/src/components/Crontab/min.vue
Normal file
121
yudao-ui-admin-vue3/src/components/Crontab/min.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="1"> 分钟,允许的通配符[, - * /] </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
周期从
|
||||
<el-input-number v-model="cycle01" :min="0" :max="58" /> -
|
||||
<el-input-number v-model="cycle02" :min="cycle01 ? cycle01 + 1 : 1" :max="59" /> 分钟
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
从
|
||||
<el-input-number v-model="average01" :min="0" :max="58" /> 分钟开始,每
|
||||
<el-input-number v-model="average02" :min="1" :max="59 - average01 || 0" /> 分钟执行一次
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
指定
|
||||
<el-select
|
||||
clearable
|
||||
v-model="checkboxList"
|
||||
placeholder="可多选"
|
||||
multiple
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in 60" :key="item" :value="item - 1">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
check: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
cron: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['update'])
|
||||
|
||||
const radioValue = ref(1)
|
||||
const cycle01 = ref(0)
|
||||
const cycle02 = ref(1)
|
||||
const average01 = ref(0)
|
||||
const average02 = ref(1)
|
||||
const checkboxList = ref([])
|
||||
|
||||
defineExpose({
|
||||
radioValue,
|
||||
cycle01,
|
||||
cycle02,
|
||||
average01,
|
||||
average02,
|
||||
checkboxList
|
||||
})
|
||||
|
||||
const cycleTotal = computed(() => {
|
||||
const cycle1 = props.check(cycle01.value, 0, 58)
|
||||
const cycle2 = props.check(cycle02.value, cycle1 ? cycle1 + 1 : 1, 59)
|
||||
return cycle1 + '-' + cycle2
|
||||
})
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 2) {
|
||||
emits('update', 'min', cycleTotal, 'min')
|
||||
}
|
||||
})
|
||||
|
||||
const averageTotal = computed(() => {
|
||||
const average1 = props.check(average01.value, 0, 58)
|
||||
const average2 = props.check(average02.value, 1, 59 - average1 || 0)
|
||||
return average1 + '/' + average2
|
||||
})
|
||||
|
||||
watch(averageTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 3) {
|
||||
emits('update', 'min', averageTotal, 'min')
|
||||
}
|
||||
})
|
||||
|
||||
const checkboxString = computed(() => {
|
||||
let str = checkboxList.value.join()
|
||||
return str.length === 0 ? '*' : str
|
||||
})
|
||||
|
||||
watch(checkboxString, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 4) {
|
||||
emits('update', 'min', checkboxString, 'min')
|
||||
}
|
||||
})
|
||||
|
||||
watch(radioValue, () => {
|
||||
switch (parseInt(radioValue.value.toString())) {
|
||||
case 1:
|
||||
emits('update', 'min', '*', 'min')
|
||||
break
|
||||
case 2:
|
||||
emits('update', 'min', cycleTotal, 'min')
|
||||
break
|
||||
case 3:
|
||||
emits('update', 'min', averageTotal, 'min')
|
||||
break
|
||||
case 4:
|
||||
emits('update', 'min', checkboxString, 'min')
|
||||
break
|
||||
}
|
||||
})
|
||||
</script>
|
||||
121
yudao-ui-admin-vue3/src/components/Crontab/month.vue
Normal file
121
yudao-ui-admin-vue3/src/components/Crontab/month.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="1"> 月,允许的通配符[, - * /] </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
周期从
|
||||
<el-input-number v-model="cycle01" :min="1" :max="11" /> -
|
||||
<el-input-number v-model="cycle02" :min="cycle01 ? cycle01 + 1 : 2" :max="12" /> 月
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
从
|
||||
<el-input-number v-model="average01" :min="1" :max="11" /> 月开始,每
|
||||
<el-input-number v-model="average02" :min="1" :max="12 - average01 || 0" /> 月月执行一次
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
指定
|
||||
<el-select
|
||||
clearable
|
||||
v-model="checkboxList"
|
||||
placeholder="可多选"
|
||||
multiple
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in 12" :key="item" :value="item">{{ item }}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
check: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
cron: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['update'])
|
||||
|
||||
const radioValue = ref(1)
|
||||
const cycle01 = ref(1)
|
||||
const cycle02 = ref(2)
|
||||
const average01 = ref(1)
|
||||
const average02 = ref(1)
|
||||
const checkboxList = ref([])
|
||||
|
||||
defineExpose({
|
||||
radioValue,
|
||||
cycle01,
|
||||
cycle02,
|
||||
average01,
|
||||
average02,
|
||||
checkboxList
|
||||
})
|
||||
|
||||
const cycleTotal = computed(() => {
|
||||
const cycle1 = props.check(cycle01.value, 1, 11)
|
||||
const cycle2 = props.check(cycle02.value, cycle1 ? cycle1 + 1 : 2, 12)
|
||||
return cycle1 + '-' + cycle2
|
||||
})
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 2) {
|
||||
emits('update', 'month', cycleTotal)
|
||||
}
|
||||
})
|
||||
|
||||
const averageTotal = computed(() => {
|
||||
const average1 = props.check(average01.value, 1, 11)
|
||||
const average2 = props.check(average02.value, 1, 12 - average1 || 0)
|
||||
return average1 + '/' + average2
|
||||
})
|
||||
|
||||
watch(averageTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 3) {
|
||||
emits('update', 'month', averageTotal)
|
||||
}
|
||||
})
|
||||
|
||||
const checkboxString = computed(() => {
|
||||
let str = checkboxList.value.join()
|
||||
return str.length === 0 ? '*' : str
|
||||
})
|
||||
|
||||
watch(checkboxString, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 4) {
|
||||
emits('update', 'month', checkboxString)
|
||||
}
|
||||
})
|
||||
|
||||
watch(radioValue, () => {
|
||||
switch (parseInt(radioValue.value.toString())) {
|
||||
case 1:
|
||||
emits('update', 'month', '*')
|
||||
break
|
||||
case 2:
|
||||
emits('update', 'month', cycleTotal)
|
||||
break
|
||||
case 3:
|
||||
emits('update', 'month', averageTotal)
|
||||
break
|
||||
case 4:
|
||||
emits('update', 'month', checkboxString)
|
||||
break
|
||||
}
|
||||
})
|
||||
</script>
|
||||
574
yudao-ui-admin-vue3/src/components/Crontab/result.vue
Normal file
574
yudao-ui-admin-vue3/src/components/Crontab/result.vue
Normal file
@@ -0,0 +1,574 @@
|
||||
<template>
|
||||
<div class="popup-result">
|
||||
<p class="title">最近5次运行时间</p>
|
||||
<ul class="popup-result-scroll">
|
||||
<template v-if="isShow">
|
||||
<li v-for="item in resultList" :key="item">{{ item }}</li>
|
||||
</template>
|
||||
<li v-else>计算结果中...</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
ex: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.ex, expressionChange)
|
||||
|
||||
onMounted(() => {
|
||||
expressionChange()
|
||||
})
|
||||
|
||||
const dayRule = ref('')
|
||||
const dayRuleSup = ref<string | number | any[]>([])
|
||||
const dateArr = ref<any[][]>([])
|
||||
const resultList = ref<string[]>([])
|
||||
const isShow = ref(false)
|
||||
|
||||
function expressionChange() {
|
||||
// 计算开始-隐藏结果
|
||||
isShow.value = false
|
||||
// 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
|
||||
let ruleArr = props.ex.split(' ')
|
||||
// 用于记录进入循环的次数
|
||||
let nums = 0
|
||||
// 用于暂时存符号时间规则结果的数组
|
||||
let resultArr: string[] = []
|
||||
// 获取当前时间精确至[年、月、日、时、分、秒]
|
||||
let nTime = new Date()
|
||||
let nYear = nTime.getFullYear()
|
||||
let nMonth = nTime.getMonth() + 1
|
||||
let nDay = nTime.getDate()
|
||||
let nHour = nTime.getHours()
|
||||
let nMin = nTime.getMinutes()
|
||||
let nSecond = nTime.getSeconds()
|
||||
// 根据规则获取到近100年可能年数组、月数组等等
|
||||
getSecondArr(ruleArr[0])
|
||||
getMinArr(ruleArr[1])
|
||||
getHourArr(ruleArr[2])
|
||||
getDayArr(ruleArr[3])
|
||||
getMonthArr(ruleArr[4])
|
||||
getWeekArr(ruleArr[5])
|
||||
getYearArr(ruleArr[6], nYear)
|
||||
// 将获取到的数组赋值-方便使用
|
||||
let sDate = dateArr.value[0]
|
||||
let mDate = dateArr.value[1]
|
||||
let hDate = dateArr.value[2]
|
||||
let DDate = dateArr.value[3]
|
||||
let MDate = dateArr.value[4]
|
||||
let YDate = dateArr.value[5]
|
||||
// 获取当前时间在数组中的索引
|
||||
let sIdx = getIndex(sDate, nSecond)
|
||||
let mIdx = getIndex(mDate, nMin)
|
||||
let hIdx = getIndex(hDate, nHour)
|
||||
let DIdx = getIndex(DDate, nDay)
|
||||
let MIdx = getIndex(MDate, nMonth)
|
||||
let YIdx = getIndex(YDate, nYear)
|
||||
// 重置月日时分秒的函数(后面用的比较多)
|
||||
const resetSecond = function () {
|
||||
sIdx = 0
|
||||
nSecond = sDate[sIdx]
|
||||
}
|
||||
const resetMin = function () {
|
||||
mIdx = 0
|
||||
nMin = mDate[mIdx]
|
||||
resetSecond()
|
||||
}
|
||||
const resetHour = function () {
|
||||
hIdx = 0
|
||||
nHour = hDate[hIdx]
|
||||
resetMin()
|
||||
}
|
||||
const resetDay = function () {
|
||||
DIdx = 0
|
||||
nDay = DDate[DIdx]
|
||||
resetHour()
|
||||
}
|
||||
const resetMonth = function () {
|
||||
MIdx = 0
|
||||
nMonth = MDate[MIdx]
|
||||
resetDay()
|
||||
}
|
||||
// 如果当前年份不为数组中当前值
|
||||
if (nYear !== YDate[YIdx]) {
|
||||
resetMonth()
|
||||
}
|
||||
// 如果当前月份不为数组中当前值
|
||||
if (nMonth !== MDate[MIdx]) {
|
||||
resetDay()
|
||||
}
|
||||
// 如果当前“日”不为数组中当前值
|
||||
if (nDay !== DDate[DIdx]) {
|
||||
resetHour()
|
||||
}
|
||||
// 如果当前“时”不为数组中当前值
|
||||
if (nHour !== hDate[hIdx]) {
|
||||
resetMin()
|
||||
}
|
||||
// 如果当前“分”不为数组中当前值
|
||||
if (nMin !== mDate[mIdx]) {
|
||||
resetSecond()
|
||||
}
|
||||
|
||||
// 循环年份数组
|
||||
goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
|
||||
let YY = YDate[Yi]
|
||||
// 如果到达最大值时
|
||||
if (nMonth > MDate[MDate.length - 1]) {
|
||||
resetMonth()
|
||||
continue
|
||||
}
|
||||
// 循环月份数组
|
||||
goMonth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
|
||||
// 赋值、方便后面运算
|
||||
let MM = MDate[Mi]
|
||||
MM = MM < 10 ? '0' + MM : MM
|
||||
// 如果到达最大值时
|
||||
if (nDay > DDate[DDate.length - 1]) {
|
||||
resetDay()
|
||||
if (Mi == MDate.length - 1) {
|
||||
resetMonth()
|
||||
continue goYear
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 循环日期数组
|
||||
goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
|
||||
// 赋值、方便后面运算
|
||||
let DD = DDate[Di]
|
||||
let thisDD = DD < 10 ? '0' + DD : DD
|
||||
|
||||
// 如果到达最大值时
|
||||
if (nHour > hDate[hDate.length - 1]) {
|
||||
resetHour()
|
||||
if (Di == DDate.length - 1) {
|
||||
resetDay()
|
||||
if (Mi == MDate.length - 1) {
|
||||
resetMonth()
|
||||
continue goYear
|
||||
}
|
||||
continue goMonth
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 判断日期的合法性,不合法的话也是跳出当前循环
|
||||
if (
|
||||
checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true &&
|
||||
dayRule.value !== 'workDay' &&
|
||||
dayRule.value !== 'lastWeek' &&
|
||||
dayRule.value !== 'lastDay'
|
||||
) {
|
||||
resetDay()
|
||||
continue goMonth
|
||||
}
|
||||
// 如果日期规则中有值时
|
||||
if (dayRule.value == 'lastDay') {
|
||||
// 如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
|
||||
|
||||
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
DD--
|
||||
|
||||
thisDD = DD < 10 ? '0' + DD : DD
|
||||
}
|
||||
}
|
||||
} else if (dayRule.value == 'workDay') {
|
||||
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
|
||||
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
DD--
|
||||
thisDD = DD < 10 ? '0' + DD : DD
|
||||
}
|
||||
}
|
||||
// 获取达到条件的日期是星期X
|
||||
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
|
||||
// 当星期日时
|
||||
if (thisWeek == 1) {
|
||||
// 先找下一个日,并判断是否为月底
|
||||
DD++
|
||||
thisDD = DD < 10 ? '0' + DD : DD
|
||||
// 判断下一日已经不是合法日期
|
||||
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
DD -= 3
|
||||
}
|
||||
} else if (thisWeek == 7) {
|
||||
// 当星期6时只需判断不是1号就可进行操作
|
||||
if (dayRuleSup.value !== 1) {
|
||||
DD--
|
||||
} else {
|
||||
DD += 2
|
||||
}
|
||||
}
|
||||
} else if (dayRule.value == 'weekDay') {
|
||||
// 如果指定了是星期几
|
||||
// 获取当前日期是属于星期几
|
||||
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
|
||||
// 校验当前星期是否在星期池(dayRuleSup)中
|
||||
if ((dayRuleSup.value as any[]).indexOf(thisWeek) < 0) {
|
||||
// 如果到达最大值时
|
||||
if (Di == DDate.length - 1) {
|
||||
resetDay()
|
||||
if (Mi == MDate.length - 1) {
|
||||
resetMonth()
|
||||
continue goYear
|
||||
}
|
||||
continue goMonth
|
||||
}
|
||||
continue
|
||||
}
|
||||
} else if (dayRule.value == 'assWeek') {
|
||||
// 如果指定了是第几周的星期几
|
||||
// 获取每月1号是属于星期几
|
||||
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
|
||||
if (dayRuleSup.value[1] >= thisWeek) {
|
||||
DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1
|
||||
} else {
|
||||
DD = dayRuleSup.value[0] * 7 + dayRuleSup.value[1] - thisWeek + 1
|
||||
}
|
||||
} else if (dayRule.value == 'lastWeek') {
|
||||
// 如果指定了每月最后一个星期几
|
||||
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
|
||||
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
|
||||
DD--
|
||||
thisDD = DD < 10 ? '0' + DD : DD
|
||||
}
|
||||
}
|
||||
// 获取月末最后一天是星期几
|
||||
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
|
||||
// 找到要求中最近的那个星期几
|
||||
if (dayRuleSup.value < thisWeek) {
|
||||
DD -= thisWeek - (dayRuleSup.value as number)
|
||||
} else if (dayRuleSup.value > thisWeek) {
|
||||
DD -= 7 - ((dayRuleSup.value as number) - thisWeek)
|
||||
}
|
||||
}
|
||||
// 判断时间值是否小于10置换成“05”这种格式
|
||||
DD = DD < 10 ? '0' + DD : DD
|
||||
|
||||
// 循环“时”数组
|
||||
goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
|
||||
let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
|
||||
|
||||
// 如果到达最大值时
|
||||
if (nMin > mDate[mDate.length - 1]) {
|
||||
resetMin()
|
||||
if (hi == hDate.length - 1) {
|
||||
resetHour()
|
||||
if (Di == DDate.length - 1) {
|
||||
resetDay()
|
||||
if (Mi == MDate.length - 1) {
|
||||
resetMonth()
|
||||
continue goYear
|
||||
}
|
||||
continue goMonth
|
||||
}
|
||||
continue goDay
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 循环"分"数组
|
||||
goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
|
||||
let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi]
|
||||
|
||||
// 如果到达最大值时
|
||||
if (nSecond > sDate[sDate.length - 1]) {
|
||||
resetSecond()
|
||||
if (mi == mDate.length - 1) {
|
||||
resetMin()
|
||||
if (hi == hDate.length - 1) {
|
||||
resetHour()
|
||||
if (Di == DDate.length - 1) {
|
||||
resetDay()
|
||||
if (Mi == MDate.length - 1) {
|
||||
resetMonth()
|
||||
continue goYear
|
||||
}
|
||||
continue goMonth
|
||||
}
|
||||
continue goDay
|
||||
}
|
||||
continue goHour
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 循环"秒"数组
|
||||
goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
|
||||
let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si]
|
||||
// 添加当前时间(时间合法性在日期循环时已经判断)
|
||||
if (MM !== '00' && DD !== '00') {
|
||||
resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss)
|
||||
nums++
|
||||
}
|
||||
// 如果条数满了就退出循环
|
||||
if (nums == 5) break goYear
|
||||
// 如果到达最大值时
|
||||
if (si == sDate.length - 1) {
|
||||
resetSecond()
|
||||
if (mi == mDate.length - 1) {
|
||||
resetMin()
|
||||
if (hi == hDate.length - 1) {
|
||||
resetHour()
|
||||
if (Di == DDate.length - 1) {
|
||||
resetDay()
|
||||
if (Mi == MDate.length - 1) {
|
||||
resetMonth()
|
||||
continue goYear
|
||||
}
|
||||
continue goMonth
|
||||
}
|
||||
continue goDay
|
||||
}
|
||||
continue goHour
|
||||
}
|
||||
continue goMin
|
||||
}
|
||||
} //goSecond
|
||||
} //goMin
|
||||
} //goHour
|
||||
} //goDay
|
||||
} //goMonth
|
||||
}
|
||||
// 判断100年内的结果条数
|
||||
if (resultArr.length == 0) {
|
||||
resultList.value = ['没有达到条件的结果!']
|
||||
} else {
|
||||
resultList.value = resultArr
|
||||
if (resultArr.length !== 5) {
|
||||
resultList.value.push('最近100年内只有上面' + resultArr.length + '条结果!')
|
||||
}
|
||||
}
|
||||
// 计算完成-显示结果
|
||||
isShow.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于计算某位数字在数组中的索引
|
||||
*/
|
||||
function getIndex(arr, value): number {
|
||||
if (value <= arr[0] || value > arr[arr.length - 1]) {
|
||||
return 0
|
||||
} else {
|
||||
for (let i = 0; i < arr.length - 1; i++) {
|
||||
if (value > arr[i] && value <= arr[i + 1]) {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function getYearArr(rule, year) {
|
||||
dateArr.value[5] = getOrderArr(year, year + 100)
|
||||
if (rule !== undefined) {
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dateArr.value[5] = getCycleArr(rule, year + 100, false)
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
dateArr.value[5] = getAverageArr(rule, year + 100)
|
||||
} else if (rule !== '*') {
|
||||
dateArr.value[5] = getAssignArr(rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMonthArr(rule) {
|
||||
dateArr.value[4] = getOrderArr(1, 12)
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dateArr.value[4] = getCycleArr(rule, 12, false)
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
dateArr.value[4] = getAverageArr(rule, 12)
|
||||
} else if (rule !== '*') {
|
||||
dateArr.value[4] = getAssignArr(rule)
|
||||
}
|
||||
}
|
||||
|
||||
function getWeekArr(rule) {
|
||||
// 只有当日期规则的两个值均为“”时则表达日期是有选项的
|
||||
if (dayRule.value === '' && dayRuleSup.value === '') {
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dayRule.value = 'weekDay'
|
||||
dayRuleSup.value = getCycleArr(rule, 7, false)
|
||||
} else if (rule.indexOf('#') >= 0) {
|
||||
dayRule.value = 'assWeek'
|
||||
let matchRule = rule.match(/[0-9]{1}/g)
|
||||
dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])]
|
||||
dateArr.value[3] = [1]
|
||||
if (dayRuleSup.value[1] == 7) {
|
||||
dayRuleSup.value[1] = 0
|
||||
}
|
||||
} else if (rule.indexOf('L') >= 0) {
|
||||
dayRule.value = 'lastWeek'
|
||||
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
|
||||
dateArr.value[3] = [31]
|
||||
if (dayRuleSup.value == 7) {
|
||||
dayRuleSup.value = 0
|
||||
}
|
||||
} else if (rule !== '*' && rule !== '?') {
|
||||
dayRule.value = 'weekDay'
|
||||
dayRuleSup.value = getAssignArr(rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDayArr(rule) {
|
||||
dateArr.value[3] = getOrderArr(1, 31)
|
||||
dayRule.value = ''
|
||||
dayRuleSup.value = ''
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dateArr.value[3] = getCycleArr(rule, 31, false)
|
||||
dayRuleSup.value = 'null'
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
dateArr.value[3] = getAverageArr(rule, 31)
|
||||
dayRuleSup.value = 'null'
|
||||
} else if (rule.indexOf('W') >= 0) {
|
||||
dayRule.value = 'workDay'
|
||||
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
|
||||
dateArr.value[3] = [dayRuleSup.value]
|
||||
} else if (rule.indexOf('L') >= 0) {
|
||||
dayRule.value = 'lastDay'
|
||||
dayRuleSup.value = 'null'
|
||||
dateArr.value[3] = [31]
|
||||
} else if (rule !== '*' && rule !== '?') {
|
||||
dateArr.value[3] = getAssignArr(rule)
|
||||
dayRuleSup.value = 'null'
|
||||
} else if (rule == '*') {
|
||||
dayRuleSup.value = 'null'
|
||||
}
|
||||
}
|
||||
|
||||
function getHourArr(rule) {
|
||||
dateArr.value[2] = getOrderArr(0, 23)
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dateArr.value[2] = getCycleArr(rule, 24, true)
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
dateArr.value[2] = getAverageArr(rule, 23)
|
||||
} else if (rule !== '*') {
|
||||
dateArr.value[2] = getAssignArr(rule)
|
||||
}
|
||||
}
|
||||
|
||||
function getMinArr(rule) {
|
||||
dateArr.value[1] = getOrderArr(0, 59)
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dateArr.value[1] = getCycleArr(rule, 60, true)
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
dateArr.value[1] = getAverageArr(rule, 59)
|
||||
} else if (rule !== '*') {
|
||||
dateArr.value[1] = getAssignArr(rule)
|
||||
}
|
||||
}
|
||||
|
||||
function getSecondArr(rule) {
|
||||
dateArr.value[0] = getOrderArr(0, 59)
|
||||
if (rule.indexOf('-') >= 0) {
|
||||
dateArr.value[0] = getCycleArr(rule, 60, true)
|
||||
} else if (rule.indexOf('/') >= 0) {
|
||||
dateArr.value[0] = getAverageArr(rule, 59)
|
||||
} else if (rule !== '*') {
|
||||
dateArr.value[0] = getAssignArr(rule)
|
||||
}
|
||||
}
|
||||
|
||||
function getOrderArr(min: number, max: number): number[] {
|
||||
let arr: number[] = []
|
||||
for (let i = min; i <= max; i++) {
|
||||
arr.push(i)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
function getAssignArr(rule) {
|
||||
let arr: number[] = []
|
||||
let assiginArr = rule.split(',')
|
||||
for (let i = 0; i < assiginArr.length; i++) {
|
||||
arr[i] = Number(assiginArr[i])
|
||||
}
|
||||
arr.sort(compare)
|
||||
return arr
|
||||
}
|
||||
|
||||
function compare(value1, value2) {
|
||||
if (value2 - value1 > 0) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
function getAverageArr(rule, limit): number[] {
|
||||
let arr: number[] = []
|
||||
let agArr = rule.split('/')
|
||||
let min = Number(agArr[0])
|
||||
let step = Number(agArr[1])
|
||||
while (min <= limit) {
|
||||
arr.push(min)
|
||||
min += step
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
function getCycleArr(rule, limit: number, status): number[] {
|
||||
// status--表示是否从0开始(则从1开始)
|
||||
let arr: number[] = []
|
||||
let cycleArr = rule.split('-')
|
||||
let min = Number(cycleArr[0])
|
||||
let max = Number(cycleArr[1])
|
||||
if (min > max) {
|
||||
max += limit
|
||||
}
|
||||
for (let i = min; i <= max; i++) {
|
||||
let add = 0
|
||||
if (status == false && i % limit == 0) {
|
||||
add = limit
|
||||
}
|
||||
arr.push(Math.round((i % limit) + add))
|
||||
}
|
||||
arr.sort(compare)
|
||||
return arr
|
||||
}
|
||||
|
||||
function formatDate(value, type?) {
|
||||
// 计算日期相关值
|
||||
let time = typeof value == 'number' ? new Date(value) : value
|
||||
let Y = time.getFullYear()
|
||||
let M = time.getMonth() + 1
|
||||
let D = time.getDate()
|
||||
let h = time.getHours()
|
||||
let m = time.getMinutes()
|
||||
let s = time.getSeconds()
|
||||
let week = time.getDay()
|
||||
// 如果传递了type的话
|
||||
if (type == undefined) {
|
||||
return (
|
||||
Y +
|
||||
'-' +
|
||||
(M < 10 ? '0' + M : M) +
|
||||
'-' +
|
||||
(D < 10 ? '0' + D : D) +
|
||||
' ' +
|
||||
(h < 10 ? '0' + h : h) +
|
||||
':' +
|
||||
(m < 10 ? '0' + m : m) +
|
||||
':' +
|
||||
(s < 10 ? '0' + s : s)
|
||||
)
|
||||
} else if (type == 'week') {
|
||||
// 在quartz中 1为星期日
|
||||
return week + 1
|
||||
}
|
||||
}
|
||||
|
||||
function checkDate(value) {
|
||||
let time = new Date(value)
|
||||
let format = formatDate(time)
|
||||
return value === format
|
||||
}
|
||||
</script>
|
||||
121
yudao-ui-admin-vue3/src/components/Crontab/second.vue
Normal file
121
yudao-ui-admin-vue3/src/components/Crontab/second.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="1"> 秒,允许的通配符[, - * /] </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
周期从
|
||||
<el-input-number v-model="cycle01" :min="0" :max="58" /> -
|
||||
<el-input-number v-model="cycle02" :min="cycle01 ? cycle01 + 1 : 1" :max="59" /> 秒
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
从
|
||||
<el-input-number v-model="average01" :min="0" :max="58" /> 秒开始,每
|
||||
<el-input-number v-model="average02" :min="1" :max="59 - average01 || 0" /> 秒执行一次
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
指定
|
||||
<el-select
|
||||
clearable
|
||||
v-model="checkboxList"
|
||||
placeholder="可多选"
|
||||
multiple
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in 60" :key="item" :value="item - 1">{{ item - 1 }}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
check: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
cron: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['update'])
|
||||
|
||||
const radioValue = ref(1)
|
||||
const cycle01 = ref(1)
|
||||
const cycle02 = ref(2)
|
||||
const average01 = ref(0)
|
||||
const average02 = ref(1)
|
||||
const checkboxList = ref([])
|
||||
|
||||
defineExpose({
|
||||
radioValue,
|
||||
cycle01,
|
||||
cycle02,
|
||||
average01,
|
||||
average02,
|
||||
checkboxList
|
||||
})
|
||||
|
||||
const cycleTotal = computed(() => {
|
||||
const cycle1 = props.check(cycle01.value, 0, 58)
|
||||
const cycle2 = props.check(cycle02.value, cycle1 ? cycle1 + 1 : 1, 59)
|
||||
return cycle1 + '-' + cycle2
|
||||
})
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 2) {
|
||||
emits('update', 'second', cycleTotal)
|
||||
}
|
||||
})
|
||||
|
||||
const averageTotal = computed(() => {
|
||||
const average1 = props.check(average01.value, 0, 58)
|
||||
const average2 = props.check(average02.value, 1, 59 - average1 || 0)
|
||||
return average1 + '/' + average2
|
||||
})
|
||||
|
||||
watch(averageTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 3) {
|
||||
emits('update', 'second', averageTotal)
|
||||
}
|
||||
})
|
||||
|
||||
const checkboxString = computed(() => {
|
||||
let str = checkboxList.value.join()
|
||||
return str.length === 0 ? '*' : str
|
||||
})
|
||||
|
||||
watch(checkboxString, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 4) {
|
||||
emits('update', 'second', checkboxString)
|
||||
}
|
||||
})
|
||||
|
||||
watch(radioValue, () => {
|
||||
switch (parseInt(radioValue.value.toString())) {
|
||||
case 1:
|
||||
emits('update', 'second', '*', 'second')
|
||||
break
|
||||
case 2:
|
||||
emits('update', 'second', cycleTotal)
|
||||
break
|
||||
case 3:
|
||||
emits('update', 'second', averageTotal)
|
||||
break
|
||||
case 4:
|
||||
emits('update', 'second', checkboxString)
|
||||
break
|
||||
}
|
||||
})
|
||||
</script>
|
||||
224
yudao-ui-admin-vue3/src/components/Crontab/week.vue
Normal file
224
yudao-ui-admin-vue3/src/components/Crontab/week.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="1"> 周,允许的通配符[, - * ? / L #] </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2"> 不指定 </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
周期从星期
|
||||
<el-select clearable v-model="cycle01">
|
||||
<el-option
|
||||
v-for="(item, index) of weekList"
|
||||
:key="index"
|
||||
:label="item.value"
|
||||
:value="item.key"
|
||||
:disabled="item.key === 1"
|
||||
>{{ item.value }}</el-option
|
||||
>
|
||||
</el-select>
|
||||
-
|
||||
<el-select clearable v-model="cycle02">
|
||||
<el-option
|
||||
v-for="(item, index) of weekList"
|
||||
:key="index"
|
||||
:label="item.value"
|
||||
:value="item.key"
|
||||
:disabled="item.key < cycle01 && item.key !== 1"
|
||||
>{{ item.value }}</el-option
|
||||
>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
第
|
||||
<el-input-number v-model="average01" :min="1" :max="4" /> 周的星期
|
||||
<el-select clearable v-model="average02">
|
||||
<el-option
|
||||
v-for="(item, index) of weekList"
|
||||
:key="index"
|
||||
:label="item.value"
|
||||
:value="item.key"
|
||||
>{{ item.value }}</el-option
|
||||
>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="5">
|
||||
本月最后一个星期
|
||||
<el-select clearable v-model="weekday">
|
||||
<el-option
|
||||
v-for="(item, index) of weekList"
|
||||
:key="index"
|
||||
:label="item.value"
|
||||
:value="item.key"
|
||||
>{{ item.value }}</el-option
|
||||
>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="6">
|
||||
指定
|
||||
<el-select
|
||||
clearable
|
||||
v-model="checkboxList"
|
||||
placeholder="可多选"
|
||||
multiple
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) of weekList"
|
||||
:key="index"
|
||||
:label="item.value"
|
||||
:value="String(item.key)"
|
||||
>{{ item.value }}</el-option
|
||||
>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
check: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
cron: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['update'])
|
||||
|
||||
const radioValue = ref<number>(2)
|
||||
const weekday = ref<number>(2)
|
||||
const cycle01 = ref<number>(2)
|
||||
const cycle02 = ref<number>(3)
|
||||
const average01 = ref<number>(1)
|
||||
const average02 = ref<number>(2)
|
||||
const checkboxList = ref([])
|
||||
const weekList = ref([
|
||||
{
|
||||
key: 2,
|
||||
value: '星期一'
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
value: '星期二'
|
||||
},
|
||||
{
|
||||
key: 4,
|
||||
value: '星期三'
|
||||
},
|
||||
{
|
||||
key: 5,
|
||||
value: '星期四'
|
||||
},
|
||||
{
|
||||
key: 6,
|
||||
value: '星期五'
|
||||
},
|
||||
{
|
||||
key: 7,
|
||||
value: '星期六'
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
value: '星期日'
|
||||
}
|
||||
])
|
||||
|
||||
defineExpose({
|
||||
radioValue,
|
||||
weekday,
|
||||
cycle01,
|
||||
cycle02,
|
||||
average01,
|
||||
average02,
|
||||
checkboxList
|
||||
})
|
||||
|
||||
const cycleTotal = computed(() => {
|
||||
const cycle1 = props.check(cycle01.value, 1, 7)
|
||||
const cycle2 = props.check(cycle02.value, 1, 7)
|
||||
return cycle1 + '-' + cycle2
|
||||
})
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 3) {
|
||||
emits('update', 'month', cycleTotal)
|
||||
}
|
||||
})
|
||||
|
||||
const averageTotal = computed(() => {
|
||||
const average1 = props.check(average01.value, 1, 4)
|
||||
const average2 = props.check(average02.value, 1, 7)
|
||||
return average1 + '#' + average2
|
||||
})
|
||||
|
||||
watch(averageTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 4) {
|
||||
emits('update', 'month', averageTotal)
|
||||
}
|
||||
})
|
||||
|
||||
const weekdayCheck = computed(() => {
|
||||
return props.check(weekday.value, 1, 7)
|
||||
})
|
||||
|
||||
watch(weekdayCheck, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 5) {
|
||||
emits('update', 'week', weekday.value + 'L')
|
||||
}
|
||||
})
|
||||
|
||||
const checkboxString = computed(() => {
|
||||
let str = checkboxList.value.join()
|
||||
return str.length === 0 ? '*' : str
|
||||
})
|
||||
|
||||
watch(checkboxString, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 6) {
|
||||
emits('update', 'month', checkboxString)
|
||||
}
|
||||
})
|
||||
|
||||
watch(radioValue, () => {
|
||||
if (parseInt(radioValue.value.toString()) !== 2 && radioValue.value.toString() !== '?') {
|
||||
emits('update', 'day', '?', 'week')
|
||||
}
|
||||
switch (parseInt(radioValue.value.toString())) {
|
||||
case 1:
|
||||
emits('update', 'week', '*')
|
||||
break
|
||||
case 2:
|
||||
emits('update', 'week', '?')
|
||||
break
|
||||
case 3:
|
||||
emits('update', 'week', cycleTotal)
|
||||
break
|
||||
case 4:
|
||||
emits('update', 'week', averageTotal)
|
||||
break
|
||||
case 5:
|
||||
emits('update', 'week', weekdayCheck.value + 'L')
|
||||
break
|
||||
case 6:
|
||||
emits('update', 'week', checkboxString)
|
||||
break
|
||||
}
|
||||
})
|
||||
</script>
|
||||
140
yudao-ui-admin-vue3/src/components/Crontab/year.vue
Normal file
140
yudao-ui-admin-vue3/src/components/Crontab/year.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<el-form size="small">
|
||||
<el-form-item>
|
||||
<el-radio label="1" v-model="radioValue"> 不填,允许的通配符[, - * /] </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio label="2" v-model="radioValue"> 每年 </el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio label="3" v-model="radioValue">
|
||||
周期从
|
||||
<el-input-number v-model="cycle01" :min="fullYear" :max="2098" /> -
|
||||
<el-input-number
|
||||
v-model="cycle02"
|
||||
:min="cycle01 ? cycle01 + 1 : fullYear + 1"
|
||||
:max="2099"
|
||||
/>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio label="4" v-model="radioValue">
|
||||
从
|
||||
<el-input-number v-model="average01" :min="fullYear" :max="2098" /> 年开始,每
|
||||
<el-input-number v-model="average02" :min="1" :max="2099 - average01 || fullYear" />
|
||||
年执行一次
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio label="5" v-model="radioValue">
|
||||
指定
|
||||
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple>
|
||||
<el-option
|
||||
v-for="item in 9"
|
||||
:key="item"
|
||||
:value="item - 1 + fullYear"
|
||||
:label="item - 1 + fullYear"
|
||||
/>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
check: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
cron: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['update'])
|
||||
|
||||
const fullYear = ref<number>(0)
|
||||
const radioValue = ref<number>(1)
|
||||
const cycle01 = ref<number>(0)
|
||||
const cycle02 = ref<number>(0)
|
||||
const average01 = ref<number>(0)
|
||||
const average02 = ref<number>(1)
|
||||
const checkboxList = ref([])
|
||||
|
||||
defineExpose({
|
||||
fullYear,
|
||||
radioValue,
|
||||
cycle01,
|
||||
cycle02,
|
||||
average01,
|
||||
average02,
|
||||
checkboxList
|
||||
})
|
||||
|
||||
const cycleTotal = computed(() => {
|
||||
const cycle1 = props.check(cycle01.value, fullYear.value, 2098)
|
||||
const cycle2 = props.check(cycle02.value, cycle1 ? cycle1 + 1 : fullYear.value, 2099)
|
||||
return cycle1 + '-' + cycle2
|
||||
})
|
||||
|
||||
watch(cycleTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 3) {
|
||||
emits('update', 'year', cycleTotal)
|
||||
}
|
||||
})
|
||||
|
||||
const averageTotal = computed(() => {
|
||||
const average1 = props.check(average01.value, fullYear.value, 2098)
|
||||
const average2 = props.check(average02.value, 1, 2099 - average1 || fullYear.value)
|
||||
return average1 + '/' + average2
|
||||
})
|
||||
|
||||
watch(averageTotal, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 4) {
|
||||
emits('update', 'year', averageTotal)
|
||||
}
|
||||
})
|
||||
|
||||
const checkboxString = computed(() => {
|
||||
let str = checkboxList.value.join()
|
||||
return str.length === 0 ? '*' : str
|
||||
})
|
||||
|
||||
watch(checkboxString, () => {
|
||||
if (parseInt(radioValue.value.toString()) === 5) {
|
||||
emits('update', 'year', checkboxString)
|
||||
}
|
||||
})
|
||||
|
||||
watch(radioValue, () => {
|
||||
switch (parseInt(radioValue.value.toString())) {
|
||||
case 1:
|
||||
emits('update', 'year', '')
|
||||
break
|
||||
case 2:
|
||||
emits('update', 'year', '*')
|
||||
break
|
||||
case 3:
|
||||
emits('update', 'year', cycleTotal, 'min')
|
||||
break
|
||||
case 4:
|
||||
emits('update', 'year', averageTotal, 'min')
|
||||
break
|
||||
case 5:
|
||||
emits('update', 'year', checkboxString, 'min')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fullYear.value = Number(new Date().getFullYear())
|
||||
cycle01.value = fullYear.value
|
||||
average01.value = fullYear.value
|
||||
})
|
||||
</script>
|
||||
3
yudao-ui-admin-vue3/src/components/Descriptions/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Descriptions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Descriptions from './src/Descriptions.vue'
|
||||
|
||||
export { Descriptions }
|
||||
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { ElCollapseTransition, ElDescriptions, ElDescriptionsItem, ElTooltip } from 'element-plus'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { ref, unref, PropType, computed, useAttrs } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const mobile = computed(() => appStore.getMobile)
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
const props = defineProps({
|
||||
title: propTypes.string.def(''),
|
||||
message: propTypes.string.def(''),
|
||||
collapse: propTypes.bool.def(true),
|
||||
schema: {
|
||||
type: Array as PropType<DescriptionsSchema[]>,
|
||||
default: () => []
|
||||
},
|
||||
data: {
|
||||
type: Object as PropType<Recordable>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('descriptions')
|
||||
|
||||
const getBindValue = computed(() => {
|
||||
const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class']
|
||||
const obj = { ...attrs, ...props }
|
||||
for (const key in obj) {
|
||||
if (delArr.indexOf(key) !== -1) {
|
||||
delete obj[key]
|
||||
}
|
||||
}
|
||||
return obj
|
||||
})
|
||||
|
||||
const getBindItemValue = (item: DescriptionsSchema) => {
|
||||
const delArr: string[] = ['field']
|
||||
const obj = { ...item }
|
||||
for (const key in obj) {
|
||||
if (delArr.indexOf(key) !== -1) {
|
||||
delete obj[key]
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// 折叠
|
||||
const show = ref(true)
|
||||
|
||||
const toggleClick = () => {
|
||||
if (props.collapse) {
|
||||
show.value = !unref(show)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
prefixCls,
|
||||
'bg-[var(--el-color-white)] dark:(bg-[var(--el-bg-color)] border-[var(--el-border-color)] border-1px)'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="title"
|
||||
:class="[
|
||||
`${prefixCls}-header`,
|
||||
'h-50px flex justify-between items-center mb-10px border-bottom-1 border-solid border-[var(--tags-view-border-color)] px-10px cursor-pointer dark:border-[var(--el-border-color)]'
|
||||
]"
|
||||
@click="toggleClick"
|
||||
>
|
||||
<div :class="[`${prefixCls}-header__title`, 'relative font-18px font-bold ml-10px']">
|
||||
<div class="flex items-center">
|
||||
{{ title }}
|
||||
<ElTooltip v-if="message" :content="message" placement="right">
|
||||
<Icon icon="ep:warning" class="ml-5px" />
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Icon v-if="collapse" :icon="show ? 'ep:arrow-down' : 'ep:arrow-up'" />
|
||||
</div>
|
||||
|
||||
<ElCollapseTransition>
|
||||
<div v-show="show" :class="[`${prefixCls}-content`, 'p-10px']">
|
||||
<ElDescriptions
|
||||
:column="2"
|
||||
border
|
||||
:direction="mobile ? 'vertical' : 'horizontal'"
|
||||
v-bind="getBindValue"
|
||||
>
|
||||
<ElDescriptionsItem
|
||||
v-for="item in schema"
|
||||
:key="item.field"
|
||||
v-bind="getBindItemValue(item)"
|
||||
>
|
||||
<template #label>
|
||||
<slot :name="`${item.field}-label`" :label="item.label">{{ item.label }}</slot>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<slot :name="item.field" :row="data">{{ data[item.field] }}</slot>
|
||||
</template>
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</div>
|
||||
</ElCollapseTransition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-descriptions';
|
||||
|
||||
.@{prefix-cls}-header {
|
||||
&__title {
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: -10px;
|
||||
width: 4px;
|
||||
height: 70%;
|
||||
background: var(--el-color-primary);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls}-content {
|
||||
:deep(.@{elNamespace}-descriptions__cell) {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
yudao-ui-admin-vue3/src/components/Dialog/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Dialog/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Dialog from './src/Dialog.vue'
|
||||
|
||||
export { Dialog }
|
||||
123
yudao-ui-admin-vue3/src/components/Dialog/src/Dialog.vue
Normal file
123
yudao-ui-admin-vue3/src/components/Dialog/src/Dialog.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { ElDialog, ElScrollbar } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { computed, useAttrs, ref, unref, useSlots, watch, nextTick } from 'vue'
|
||||
import { isNumber } from '@/utils/is'
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: propTypes.bool.def(false),
|
||||
title: propTypes.string.def('Dialog'),
|
||||
fullscreen: propTypes.bool.def(true),
|
||||
maxHeight: propTypes.oneOfType([String, Number]).def('300px'),
|
||||
width: propTypes.oneOfType([String, Number]).def('35%')
|
||||
})
|
||||
|
||||
const getBindValue = computed(() => {
|
||||
const delArr: string[] = ['fullscreen', 'title', 'maxHeight']
|
||||
const attrs = useAttrs()
|
||||
const obj = { ...attrs, ...props }
|
||||
for (const key in obj) {
|
||||
if (delArr.indexOf(key) !== -1) {
|
||||
delete obj[key]
|
||||
}
|
||||
}
|
||||
return obj
|
||||
})
|
||||
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
const toggleFull = () => {
|
||||
isFullscreen.value = !unref(isFullscreen)
|
||||
}
|
||||
|
||||
const dialogHeight = ref(isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight)
|
||||
|
||||
watch(
|
||||
() => isFullscreen.value,
|
||||
async (val: boolean) => {
|
||||
await nextTick()
|
||||
if (val) {
|
||||
const windowHeight = document.documentElement.offsetHeight
|
||||
dialogHeight.value = `${windowHeight - 55 - 60 - (slots.footer ? 63 : 0)}px`
|
||||
} else {
|
||||
dialogHeight.value = isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
const dialogStyle = computed(() => {
|
||||
return {
|
||||
height: unref(dialogHeight)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDialog
|
||||
v-bind="getBindValue"
|
||||
:fullscreen="isFullscreen"
|
||||
destroy-on-close
|
||||
lock-scroll
|
||||
draggable
|
||||
:width="width"
|
||||
:close-on-click-modal="true"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
<Icon
|
||||
v-if="fullscreen"
|
||||
class="mr-22px cursor-pointer is-hover mt-2px"
|
||||
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
|
||||
color="var(--el-color-info)"
|
||||
@click="toggleFull"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElScrollbar :style="dialogStyle">
|
||||
<slot></slot>
|
||||
</ElScrollbar>
|
||||
|
||||
<template v-if="slots.footer" #footer>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.@{elNamespace}-dialog__header {
|
||||
border-bottom: 1px solid var(--tags-view-border-color);
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.@{elNamespace}-dialog__footer {
|
||||
border-top: 0px solid var(--tags-view-border-color);
|
||||
}
|
||||
.dialog-footer button:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.is-hover {
|
||||
&:hover {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.@{elNamespace}-dialog__header {
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.@{elNamespace}-dialog__footer {
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
yudao-ui-admin-vue3/src/components/DictTag/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/DictTag/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import DictTag from './src/DictTag.vue'
|
||||
|
||||
export { DictTag }
|
||||
69
yudao-ui-admin-vue3/src/components/DictTag/src/DictTag.vue
Normal file
69
yudao-ui-admin-vue3/src/components/DictTag/src/DictTag.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUpdated, PropType, ref } from 'vue'
|
||||
import { getDictOptions, DictDataType } from '@/utils/dict'
|
||||
import { ElTag } from 'element-plus'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DictTag',
|
||||
components: {
|
||||
ElTag
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String as PropType<string>,
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const dictData = ref<DictDataType>()
|
||||
function getDictObj(dictType: string, value: string) {
|
||||
const dictOptions = getDictOptions(dictType)
|
||||
dictOptions.forEach((dict: DictDataType) => {
|
||||
if (dict.value === value) {
|
||||
dictData.value = dict
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
return getDictObj(props.type, props.value?.toString())
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
getDictObj(props.type, props.value?.toString())
|
||||
})
|
||||
return {
|
||||
props,
|
||||
dictData
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<!-- 默认样式 -->
|
||||
<span
|
||||
v-if="
|
||||
dictData?.colorType === 'default' ||
|
||||
dictData?.colorType === '' ||
|
||||
dictData?.colorType === undefined
|
||||
"
|
||||
:key="dictData?.value"
|
||||
:class="dictData?.cssClass"
|
||||
>
|
||||
{{ dictData?.label }}
|
||||
</span>
|
||||
<!-- Tag 样式 -->
|
||||
<ElTag
|
||||
v-else
|
||||
:disable-transitions="true"
|
||||
:key="dictData?.value + ''"
|
||||
:type="dictData?.colorType === 'primary' ? 'success' : dictData?.colorType"
|
||||
:class="dictData?.cssClass"
|
||||
>
|
||||
{{ dictData?.label }}
|
||||
</ElTag>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/Echart/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Echart/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Echart from './src/Echart.vue'
|
||||
|
||||
export { Echart }
|
||||
113
yudao-ui-admin-vue3/src/components/Echart/src/Echart.vue
Normal file
113
yudao-ui-admin-vue3/src/components/Echart/src/Echart.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import echarts from '@/plugins/echarts'
|
||||
import { debounce } from 'lodash-es'
|
||||
import 'echarts-wordcloud'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { computed, PropType, ref, unref, watch, onMounted, onBeforeUnmount, onActivated } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { isString } from '@/utils/is'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls, variables } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('echart')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object as PropType<EChartsOption>,
|
||||
required: true
|
||||
},
|
||||
width: propTypes.oneOfType([Number, String]).def(''),
|
||||
height: propTypes.oneOfType([Number, String]).def('500px')
|
||||
})
|
||||
|
||||
const isDark = computed(() => appStore.getIsDark)
|
||||
|
||||
const theme = computed(() => {
|
||||
const echartTheme: boolean | string = unref(isDark) ? true : 'auto'
|
||||
|
||||
return echartTheme
|
||||
})
|
||||
|
||||
const options = computed(() => {
|
||||
return Object.assign(props.options, {
|
||||
darkMode: unref(theme)
|
||||
})
|
||||
})
|
||||
|
||||
const elRef = ref<ElRef>()
|
||||
|
||||
let echartRef: Nullable<echarts.ECharts> = null
|
||||
|
||||
const contentEl = ref<Element>()
|
||||
|
||||
const styles = computed(() => {
|
||||
const width = isString(props.width) ? props.width : `${props.width}px`
|
||||
const height = isString(props.height) ? props.height : `${props.height}px`
|
||||
|
||||
return {
|
||||
width,
|
||||
height
|
||||
}
|
||||
})
|
||||
|
||||
const initChart = () => {
|
||||
if (unref(elRef) && props.options) {
|
||||
echartRef = echarts.init(unref(elRef) as HTMLElement)
|
||||
echartRef?.setOption(unref(options))
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => options.value,
|
||||
(options) => {
|
||||
if (echartRef) {
|
||||
echartRef?.setOption(options)
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
const resizeHandler = debounce(() => {
|
||||
if (echartRef) {
|
||||
echartRef.resize()
|
||||
}
|
||||
}, 100)
|
||||
|
||||
const contentResizeHandler = async (e: TransitionEvent) => {
|
||||
if (e.propertyName === 'width') {
|
||||
resizeHandler()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
|
||||
contentEl.value = document.getElementsByClassName(`${variables.namespace}-layout-content`)[0]
|
||||
unref(contentEl) &&
|
||||
(unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resizeHandler)
|
||||
unref(contentEl) &&
|
||||
(unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler)
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
if (echartRef) {
|
||||
echartRef.resize()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="elRef" :class="[$attrs.class, prefixCls]" :style="styles"></div>
|
||||
</template>
|
||||
8
yudao-ui-admin-vue3/src/components/Editor/index.ts
Normal file
8
yudao-ui-admin-vue3/src/components/Editor/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import Editor from './src/Editor.vue'
|
||||
import { IDomEditor } from '@wangeditor/editor'
|
||||
|
||||
export interface EditorExpose {
|
||||
getEditorRef: () => Promise<IDomEditor>
|
||||
}
|
||||
|
||||
export { Editor }
|
||||
139
yudao-ui-admin-vue3/src/components/Editor/src/Editor.vue
Normal file
139
yudao-ui-admin-vue3/src/components/Editor/src/Editor.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, computed, PropType, unref, nextTick, ref, watch, shallowRef } from 'vue'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import { IDomEditor, IEditorConfig, i18nChangeLanguage } from '@wangeditor/editor'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { isNumber } from '@/utils/is'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useLocaleStore } from '@/store/modules/locale'
|
||||
|
||||
const localeStore = useLocaleStore()
|
||||
|
||||
const currentLocale = computed(() => localeStore.getCurrentLocale)
|
||||
|
||||
i18nChangeLanguage(unref(currentLocale).lang)
|
||||
|
||||
const props = defineProps({
|
||||
editorId: propTypes.string.def('wangeEditor-1'),
|
||||
height: propTypes.oneOfType([Number, String]).def('500px'),
|
||||
editorConfig: {
|
||||
type: Object as PropType<IEditorConfig>,
|
||||
default: () => undefined
|
||||
},
|
||||
modelValue: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change', 'update:modelValue'])
|
||||
|
||||
// 编辑器实例,必须用 shallowRef
|
||||
const editorRef = shallowRef<IDomEditor>()
|
||||
|
||||
const valueHtml = ref('')
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val: string) => {
|
||||
if (val === unref(valueHtml)) return
|
||||
valueHtml.value = val
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
// 监听
|
||||
watch(
|
||||
() => valueHtml.value,
|
||||
(val: string) => {
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
)
|
||||
|
||||
const handleCreated = (editor: IDomEditor) => {
|
||||
editorRef.value = editor
|
||||
}
|
||||
|
||||
// 编辑器配置
|
||||
const editorConfig = computed((): IEditorConfig => {
|
||||
return Object.assign(
|
||||
{
|
||||
placeholder: '请输入内容...',
|
||||
readOnly: false,
|
||||
customAlert: (s: string, t: string) => {
|
||||
switch (t) {
|
||||
case 'success':
|
||||
ElMessage.success(s)
|
||||
break
|
||||
case 'info':
|
||||
ElMessage.info(s)
|
||||
break
|
||||
case 'warning':
|
||||
ElMessage.warning(s)
|
||||
break
|
||||
case 'error':
|
||||
ElMessage.error(s)
|
||||
break
|
||||
default:
|
||||
ElMessage.info(s)
|
||||
break
|
||||
}
|
||||
},
|
||||
autoFocus: false,
|
||||
scroll: true,
|
||||
uploadImgShowBase64: true
|
||||
},
|
||||
props.editorConfig || {}
|
||||
)
|
||||
})
|
||||
|
||||
const editorStyle = computed(() => {
|
||||
return {
|
||||
height: isNumber(props.height) ? `${props.height}px` : props.height
|
||||
}
|
||||
})
|
||||
|
||||
// 回调函数
|
||||
const handleChange = (editor: IDomEditor) => {
|
||||
emit('change', editor)
|
||||
}
|
||||
|
||||
// 组件销毁时,及时销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = unref(editorRef.value)
|
||||
if (editor === null) return
|
||||
|
||||
// 销毁,并移除 editor
|
||||
editor?.destroy()
|
||||
})
|
||||
|
||||
const getEditorRef = async (): Promise<IDomEditor> => {
|
||||
await nextTick()
|
||||
return unref(editorRef.value) as IDomEditor
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getEditorRef
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-1 border-solid border-[var(--tags-view-border-color)]">
|
||||
<!-- 工具栏 -->
|
||||
<Toolbar
|
||||
:editor="editorRef"
|
||||
:editorId="editorId"
|
||||
class="border-bottom-1 border-solid border-[var(--tags-view-border-color)]"
|
||||
/>
|
||||
<!-- 编辑器 -->
|
||||
<Editor
|
||||
v-model="valueHtml"
|
||||
:editorId="editorId"
|
||||
:defaultConfig="editorConfig"
|
||||
:style="editorStyle"
|
||||
@on-change="handleChange"
|
||||
@on-created="handleCreated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style src="@wangeditor/editor/dist/css/style.css"></style>
|
||||
3
yudao-ui-admin-vue3/src/components/Error/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Error/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Error from './src/Error.vue'
|
||||
|
||||
export { Error }
|
||||
57
yudao-ui-admin-vue3/src/components/Error/src/Error.vue
Normal file
57
yudao-ui-admin-vue3/src/components/Error/src/Error.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import pageError from '@/assets/svgs/404.svg'
|
||||
import networkError from '@/assets/svgs/500.svg'
|
||||
import noPermission from '@/assets/svgs/403.svg'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
|
||||
interface ErrorMap {
|
||||
url: string
|
||||
message: string
|
||||
buttonText: string
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const errorMap: {
|
||||
[key: string]: ErrorMap
|
||||
} = {
|
||||
'404': {
|
||||
url: pageError,
|
||||
message: t('error.pageError'),
|
||||
buttonText: t('error.returnToHome')
|
||||
},
|
||||
'500': {
|
||||
url: networkError,
|
||||
message: t('error.networkError'),
|
||||
buttonText: t('error.returnToHome')
|
||||
},
|
||||
'403': {
|
||||
url: noPermission,
|
||||
message: t('error.noPermission'),
|
||||
buttonText: t('error.returnToHome')
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
type: propTypes.string.validate((v: string) => ['404', '500', '403'].includes(v)).def('404')
|
||||
})
|
||||
|
||||
const emit = defineEmits(['errorClick'])
|
||||
|
||||
const btnClick = () => {
|
||||
emit('errorClick', props.type)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center">
|
||||
<div class="text-center">
|
||||
<img width="350" :src="errorMap[type].url" alt="" />
|
||||
<div class="text-14px text-[var(--el-color-info)]">{{ errorMap[type].message }}</div>
|
||||
<div class="mt-20px">
|
||||
<ElButton type="primary" @click="btnClick">{{ errorMap[type].buttonText }}</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/Footer/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Footer/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Footer from './src/Footer.vue'
|
||||
|
||||
export { Footer }
|
||||
22
yudao-ui-admin-vue3/src/components/Footer/src/Footer.vue
Normal file
22
yudao-ui-admin-vue3/src/components/Footer/src/Footer.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { computed } from 'vue'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('footer')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const title = computed(() => appStore.getTitle)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="prefixCls"
|
||||
class="text-center text-[var(--el-text-color-placeholder)] bg-[var(--app-contnet-bg-color)] h-[var(--app-footer-height)] leading-[var(--app-footer-height)] dark:bg-[var(--el-bg-color)]"
|
||||
>
|
||||
Copyright ©2022-{{ title }}
|
||||
</div>
|
||||
</template>
|
||||
14
yudao-ui-admin-vue3/src/components/Form/index.ts
Normal file
14
yudao-ui-admin-vue3/src/components/Form/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import Form from './src/Form.vue'
|
||||
import { ElForm } from 'element-plus'
|
||||
|
||||
export interface FormExpose {
|
||||
setValues: (data: Recordable) => void
|
||||
setProps: (props: Recordable) => void
|
||||
delSchema: (field: string) => void
|
||||
addSchema: (formSchema: FormSchema, index?: number) => void
|
||||
setSchema: (schemaProps: FormSetPropsType[]) => void
|
||||
formModel: Recordable
|
||||
getElFormRef: () => ComponentRef<typeof ElForm>
|
||||
}
|
||||
|
||||
export { Form }
|
||||
299
yudao-ui-admin-vue3/src/components/Form/src/Form.vue
Normal file
299
yudao-ui-admin-vue3/src/components/Form/src/Form.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<script lang="tsx">
|
||||
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
|
||||
import { ElForm, ElFormItem, ElRow, ElCol, ElTooltip } from 'element-plus'
|
||||
import { componentMap } from './componentMap'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { getSlot } from '@/utils/tsxHelper'
|
||||
import {
|
||||
setTextPlaceholder,
|
||||
setGridProp,
|
||||
setComponentProps,
|
||||
setItemComponentSlots,
|
||||
initModel,
|
||||
setFormItemSlots
|
||||
} from './helper'
|
||||
import { useRenderSelect } from './components/useRenderSelect'
|
||||
import { useRenderRadio } from './components/useRenderRadio'
|
||||
import { useRenderCheckbox } from './components/useRenderCheckbox'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { findIndex } from '@/utils'
|
||||
import { set } from 'lodash-es'
|
||||
import { FormProps } from './types'
|
||||
import { Icon } from '@/components/Icon'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('form')
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Form',
|
||||
props: {
|
||||
// 生成Form的布局结构数组
|
||||
schema: {
|
||||
type: Array as PropType<FormSchema[]>,
|
||||
default: () => []
|
||||
},
|
||||
// 是否需要栅格布局
|
||||
isCol: propTypes.bool.def(true),
|
||||
// 表单数据对象
|
||||
model: {
|
||||
type: Object as PropType<Recordable>,
|
||||
default: () => ({})
|
||||
},
|
||||
// 是否自动设置placeholder
|
||||
autoSetPlaceholder: propTypes.bool.def(true),
|
||||
// 是否自定义内容
|
||||
isCustom: propTypes.bool.def(false),
|
||||
// 表单label宽度
|
||||
labelWidth: propTypes.oneOfType([String, Number]).def('auto')
|
||||
},
|
||||
emits: ['register'],
|
||||
setup(props, { slots, expose, emit }) {
|
||||
// element form 实例
|
||||
const elFormRef = ref<ComponentRef<typeof ElForm>>()
|
||||
|
||||
// useForm传入的props
|
||||
const outsideProps = ref<FormProps>({})
|
||||
|
||||
const mergeProps = ref<FormProps>({})
|
||||
|
||||
const getProps = computed(() => {
|
||||
const propsObj = { ...props }
|
||||
Object.assign(propsObj, unref(mergeProps))
|
||||
return propsObj
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formModel = ref<Recordable>({})
|
||||
|
||||
onMounted(() => {
|
||||
emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
|
||||
})
|
||||
|
||||
// 对表单赋值
|
||||
const setValues = (data: Recordable = {}) => {
|
||||
formModel.value = Object.assign(unref(formModel), data)
|
||||
}
|
||||
|
||||
const setProps = (props: FormProps = {}) => {
|
||||
mergeProps.value = Object.assign(unref(mergeProps), props)
|
||||
outsideProps.value = props
|
||||
}
|
||||
|
||||
const delSchema = (field: string) => {
|
||||
const { schema } = unref(getProps)
|
||||
|
||||
const index = findIndex(schema, (v: FormSchema) => v.field === field)
|
||||
if (index > -1) {
|
||||
schema.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const addSchema = (formSchema: FormSchema, index?: number) => {
|
||||
const { schema } = unref(getProps)
|
||||
if (index !== void 0) {
|
||||
schema.splice(index, 0, formSchema)
|
||||
return
|
||||
}
|
||||
schema.push(formSchema)
|
||||
}
|
||||
|
||||
const setSchema = (schemaProps: FormSetPropsType[]) => {
|
||||
const { schema } = unref(getProps)
|
||||
for (const v of schema) {
|
||||
for (const item of schemaProps) {
|
||||
if (v.field === item.field) {
|
||||
set(v, item.path, item.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getElFormRef = (): ComponentRef<typeof ElForm> => {
|
||||
return unref(elFormRef) as ComponentRef<typeof ElForm>
|
||||
}
|
||||
|
||||
expose({
|
||||
setValues,
|
||||
formModel,
|
||||
setProps,
|
||||
delSchema,
|
||||
addSchema,
|
||||
setSchema,
|
||||
getElFormRef
|
||||
})
|
||||
|
||||
// 监听表单结构化数组,重新生成formModel
|
||||
watch(
|
||||
() => unref(getProps).schema,
|
||||
(schema = []) => {
|
||||
formModel.value = initModel(schema, unref(formModel))
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
// 渲染包裹标签,是否使用栅格布局
|
||||
const renderWrap = () => {
|
||||
const { isCol } = unref(getProps)
|
||||
const content = isCol ? (
|
||||
<ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
|
||||
) : (
|
||||
renderFormItemWrap()
|
||||
)
|
||||
return content
|
||||
}
|
||||
|
||||
// 是否要渲染el-col
|
||||
const renderFormItemWrap = () => {
|
||||
// hidden属性表示隐藏,不做渲染
|
||||
const { schema = [], isCol } = unref(getProps)
|
||||
|
||||
return schema
|
||||
.filter((v) => !v.hidden)
|
||||
.map((item) => {
|
||||
// 如果是 Divider 组件,需要自己占用一行
|
||||
const isDivider = item.component === 'Divider'
|
||||
const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
|
||||
return isDivider ? (
|
||||
<Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
|
||||
) : isCol ? (
|
||||
// 如果需要栅格,需要包裹 ElCol
|
||||
<ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
|
||||
) : (
|
||||
renderFormItem(item)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染formItem
|
||||
const renderFormItem = (item: FormSchema) => {
|
||||
// 单独给只有options属性的组件做判断
|
||||
const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
|
||||
const slotsMap: Recordable = {
|
||||
...setItemComponentSlots(slots, item?.componentProps?.slots, item.field)
|
||||
}
|
||||
if (
|
||||
item?.component !== 'SelectV2' &&
|
||||
item?.component !== 'Cascader' &&
|
||||
item?.componentProps?.options
|
||||
) {
|
||||
slotsMap.default = () => renderOptions(item)
|
||||
}
|
||||
|
||||
const formItemSlots: Recordable = setFormItemSlots(slots, item.field)
|
||||
// 如果有 labelMessage,自动使用插槽渲染
|
||||
if (item?.labelMessage) {
|
||||
formItemSlots.label = () => {
|
||||
return (
|
||||
<>
|
||||
<span>{item.label}</span>
|
||||
<ElTooltip placement="right" raw-content>
|
||||
{{
|
||||
content: () => <span v-html={item.labelMessage}></span>,
|
||||
default: () => (
|
||||
<Icon
|
||||
icon="ep:warning"
|
||||
size={16}
|
||||
color="var(--el-color-primary)"
|
||||
class="ml-2px relative top-1px"
|
||||
></Icon>
|
||||
)
|
||||
}}
|
||||
</ElTooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ElFormItem {...(item.formItemProps || {})} prop={item.field} label={item.label || ''}>
|
||||
{{
|
||||
...formItemSlots,
|
||||
default: () => {
|
||||
const Com = componentMap[item.component as string] as ReturnType<
|
||||
typeof defineComponent
|
||||
>
|
||||
|
||||
const { autoSetPlaceholder } = unref(getProps)
|
||||
|
||||
return slots[item.field] ? (
|
||||
getSlot(slots, item.field, formModel.value)
|
||||
) : (
|
||||
<Com
|
||||
vModel={formModel.value[item.field]}
|
||||
{...(autoSetPlaceholder && setTextPlaceholder(item))}
|
||||
{...setComponentProps(item)}
|
||||
{...(notRenderOptions.includes(item?.component as string) &&
|
||||
item?.componentProps?.options
|
||||
? { options: item?.componentProps?.options || [] }
|
||||
: {})}
|
||||
>
|
||||
{{ ...slotsMap }}
|
||||
</Com>
|
||||
)
|
||||
}
|
||||
}}
|
||||
</ElFormItem>
|
||||
)
|
||||
}
|
||||
|
||||
// 渲染options
|
||||
const renderOptions = (item: FormSchema) => {
|
||||
switch (item.component) {
|
||||
case 'Select':
|
||||
const { renderSelectOptions } = useRenderSelect(slots)
|
||||
return renderSelectOptions(item)
|
||||
case 'Radio':
|
||||
case 'RadioButton':
|
||||
const { renderRadioOptions } = useRenderRadio()
|
||||
return renderRadioOptions(item)
|
||||
case 'Checkbox':
|
||||
case 'CheckboxButton':
|
||||
const { renderChcekboxOptions } = useRenderCheckbox()
|
||||
return renderChcekboxOptions(item)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤传入Form组件的属性
|
||||
const getFormBindValue = () => {
|
||||
// 避免在标签上出现多余的属性
|
||||
const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model']
|
||||
const props = { ...unref(getProps) }
|
||||
for (const key in props) {
|
||||
if (delKeys.indexOf(key) !== -1) {
|
||||
delete props[key]
|
||||
}
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
return () => (
|
||||
<ElForm
|
||||
ref={elFormRef}
|
||||
{...getFormBindValue()}
|
||||
model={props.isCustom ? props.model : formModel}
|
||||
class={prefixCls}
|
||||
>
|
||||
{{
|
||||
// 如果需要自定义,就什么都不渲染,而是提供默认插槽
|
||||
default: () => {
|
||||
const { isCustom } = unref(getProps)
|
||||
return isCustom ? getSlot(slots, 'default') : renderWrap()
|
||||
}
|
||||
}}
|
||||
</ElForm>
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.@{elNamespace}-form.@{namespace}-form .@{elNamespace}-row {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
</style>
|
||||
48
yudao-ui-admin-vue3/src/components/Form/src/componentMap.ts
Normal file
48
yudao-ui-admin-vue3/src/components/Form/src/componentMap.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Component } from 'vue'
|
||||
import {
|
||||
ElCascader,
|
||||
ElCheckboxGroup,
|
||||
ElColorPicker,
|
||||
ElDatePicker,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElRadioGroup,
|
||||
ElRate,
|
||||
ElSelect,
|
||||
ElSelectV2,
|
||||
ElSlider,
|
||||
ElSwitch,
|
||||
ElTimePicker,
|
||||
ElTimeSelect,
|
||||
ElTransfer,
|
||||
ElAutocomplete,
|
||||
ElDivider
|
||||
} from 'element-plus'
|
||||
import { InputPassword } from '@/components/InputPassword'
|
||||
import { Editor } from '@/components/Editor'
|
||||
|
||||
const componentMap: Recordable<Component, ComponentName> = {
|
||||
Radio: ElRadioGroup,
|
||||
Checkbox: ElCheckboxGroup,
|
||||
CheckboxButton: ElCheckboxGroup,
|
||||
Input: ElInput,
|
||||
Autocomplete: ElAutocomplete,
|
||||
InputNumber: ElInputNumber,
|
||||
Select: ElSelect,
|
||||
Cascader: ElCascader,
|
||||
Switch: ElSwitch,
|
||||
Slider: ElSlider,
|
||||
TimePicker: ElTimePicker,
|
||||
DatePicker: ElDatePicker,
|
||||
Rate: ElRate,
|
||||
ColorPicker: ElColorPicker,
|
||||
Transfer: ElTransfer,
|
||||
Divider: ElDivider,
|
||||
TimeSelect: ElTimeSelect,
|
||||
SelectV2: ElSelectV2,
|
||||
RadioButton: ElRadioGroup,
|
||||
InputPassword: InputPassword,
|
||||
Editor: Editor
|
||||
}
|
||||
|
||||
export { componentMap }
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ElCheckbox, ElCheckboxButton } from 'element-plus'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export const useRenderCheckbox = () => {
|
||||
const renderChcekboxOptions = (item: FormSchema) => {
|
||||
// 如果有别名,就取别名
|
||||
const labelAlias = item?.componentProps?.optionsAlias?.labelField
|
||||
const valueAlias = item?.componentProps?.optionsAlias?.valueField
|
||||
const Com = (item.component === 'Checkbox' ? ElCheckbox : ElCheckboxButton) as ReturnType<
|
||||
typeof defineComponent
|
||||
>
|
||||
return item?.componentProps?.options?.map((option) => {
|
||||
return <Com label={option[labelAlias || 'value']}>{option[valueAlias || 'label']}</Com>
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
renderChcekboxOptions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ElRadio, ElRadioButton } from 'element-plus'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export const useRenderRadio = () => {
|
||||
const renderRadioOptions = (item: FormSchema) => {
|
||||
// 如果有别名,就取别名
|
||||
const labelAlias = item?.componentProps?.optionsAlias?.labelField
|
||||
const valueAlias = item?.componentProps?.optionsAlias?.valueField
|
||||
const Com = (item.component === 'Radio' ? ElRadio : ElRadioButton) as ReturnType<
|
||||
typeof defineComponent
|
||||
>
|
||||
return item?.componentProps?.options?.map((option) => {
|
||||
return <Com label={option[labelAlias || 'value']}>{option[valueAlias || 'label']}</Com>
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
renderRadioOptions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ElOption, ElOptionGroup } from 'element-plus'
|
||||
import { getSlot } from '@/utils/tsxHelper'
|
||||
import { Slots } from 'vue'
|
||||
|
||||
export const useRenderSelect = (slots: Slots) => {
|
||||
// 渲染 select options
|
||||
const renderSelectOptions = (item: FormSchema) => {
|
||||
// 如果有别名,就取别名
|
||||
const labelAlias = item?.componentProps?.optionsAlias?.labelField
|
||||
return item?.componentProps?.options?.map((option) => {
|
||||
if (option?.options?.length) {
|
||||
return (
|
||||
<ElOptionGroup label={option[labelAlias || 'label']}>
|
||||
{() => {
|
||||
return option?.options?.map((v) => {
|
||||
return renderSelectOptionItem(item, v)
|
||||
})
|
||||
}}
|
||||
</ElOptionGroup>
|
||||
)
|
||||
} else {
|
||||
return renderSelectOptionItem(item, option)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 渲染 select option item
|
||||
const renderSelectOptionItem = (item: FormSchema, option: ComponentOptions) => {
|
||||
// 如果有别名,就取别名
|
||||
const labelAlias = item?.componentProps?.optionsAlias?.labelField
|
||||
const valueAlias = item?.componentProps?.optionsAlias?.valueField
|
||||
return (
|
||||
<ElOption label={option[labelAlias || 'label']} value={option[valueAlias || 'value']}>
|
||||
{{
|
||||
default: () =>
|
||||
// option 插槽名规则,{field}-option
|
||||
item?.componentProps?.optionsSlot
|
||||
? getSlot(slots, `${item.field}-option`, { item: option })
|
||||
: undefined
|
||||
}}
|
||||
</ElOption>
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
renderSelectOptions
|
||||
}
|
||||
}
|
||||
152
yudao-ui-admin-vue3/src/components/Form/src/helper.ts
Normal file
152
yudao-ui-admin-vue3/src/components/Form/src/helper.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
// import { useI18n } from '@/hooks/web/useI18n'
|
||||
import type { Slots } from 'vue'
|
||||
import { getSlot } from '@/utils/tsxHelper'
|
||||
import { PlaceholderMoel } from './types'
|
||||
|
||||
// const { t } = useI18n()
|
||||
|
||||
/**
|
||||
*
|
||||
* @param schema 对应组件数据
|
||||
* @returns 返回提示信息对象
|
||||
* @description 用于自动设置placeholder
|
||||
*/
|
||||
export const setTextPlaceholder = (schema: FormSchema): PlaceholderMoel => {
|
||||
const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword']
|
||||
const selectMap = ['Select', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect']
|
||||
if (textMap.includes(schema?.component as string)) {
|
||||
return {
|
||||
// placeholder: t('common.inputText')
|
||||
placeholder: '请输入'
|
||||
}
|
||||
}
|
||||
if (selectMap.includes(schema?.component as string)) {
|
||||
// 一些范围选择器
|
||||
const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange']
|
||||
if (
|
||||
twoTextMap.includes(
|
||||
(schema?.componentProps?.type || schema?.componentProps?.isRange) as string
|
||||
)
|
||||
) {
|
||||
return {
|
||||
// startPlaceholder: t('common.startTimeText'),
|
||||
// endPlaceholder: t('common.endTimeText'),
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
rangeSeparator: '-'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
// placeholder: t('common.selectText')
|
||||
placeholder: '请选择'
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param col 内置栅格
|
||||
* @returns 返回栅格属性
|
||||
* @description 合并传入进来的栅格属性
|
||||
*/
|
||||
export const setGridProp = (col: ColProps = {}): ColProps => {
|
||||
const colProps: ColProps = {
|
||||
// 如果有span,代表用户优先级更高,所以不需要默认栅格
|
||||
...(col.span
|
||||
? {}
|
||||
: {
|
||||
xs: 24,
|
||||
sm: 12,
|
||||
md: 12,
|
||||
lg: 12,
|
||||
xl: 12
|
||||
}),
|
||||
...col
|
||||
}
|
||||
return colProps
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param item 传入的组件属性
|
||||
* @returns 默认添加 clearable 属性
|
||||
*/
|
||||
export const setComponentProps = (item: FormSchema): Recordable => {
|
||||
const notNeedClearable = ['ColorPicker']
|
||||
const componentProps: Recordable = notNeedClearable.includes(item.component as string)
|
||||
? { ...item.componentProps }
|
||||
: {
|
||||
clearable: true,
|
||||
...item.componentProps
|
||||
}
|
||||
// 需要删除额外的属性
|
||||
delete componentProps?.slots
|
||||
return componentProps
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param slots 插槽
|
||||
* @param slotsProps 插槽属性
|
||||
* @param field 字段名
|
||||
*/
|
||||
export const setItemComponentSlots = (
|
||||
slots: Slots,
|
||||
slotsProps: Recordable = {},
|
||||
field: string
|
||||
): Recordable => {
|
||||
const slotObj: Recordable = {}
|
||||
for (const key in slotsProps) {
|
||||
if (slotsProps[key]) {
|
||||
// 由于组件有可能重复,需要有一个唯一的前缀
|
||||
slotObj[key] = (data: Recordable) => {
|
||||
return getSlot(slots, `${field}-${key}`, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
return slotObj
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param schema Form表单结构化数组
|
||||
* @param formModel FormMoel
|
||||
* @returns FormMoel
|
||||
* @description 生成对应的formModel
|
||||
*/
|
||||
export const initModel = (schema: FormSchema[], formModel: Recordable) => {
|
||||
const model: Recordable = { ...formModel }
|
||||
schema.map((v) => {
|
||||
// 如果是hidden,就删除对应的值
|
||||
if (v.hidden) {
|
||||
delete model[v.field]
|
||||
} else if (v.component && v.component !== 'Divider') {
|
||||
const hasField = Reflect.has(model, v.field)
|
||||
// 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值
|
||||
model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : ''
|
||||
}
|
||||
})
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* @param slots 插槽
|
||||
* @param field 字段名
|
||||
* @returns 返回FormIiem插槽
|
||||
*/
|
||||
export const setFormItemSlots = (slots: Slots, field: string): Recordable => {
|
||||
const slotObj: Recordable = {}
|
||||
if (slots[`${field}-error`]) {
|
||||
slotObj['error'] = (data: Recordable) => {
|
||||
return getSlot(slots, `${field}-error`, data)
|
||||
}
|
||||
}
|
||||
if (slots[`${field}-label`]) {
|
||||
slotObj['label'] = (data: Recordable) => {
|
||||
return getSlot(slots, `${field}-label`, data)
|
||||
}
|
||||
}
|
||||
return slotObj
|
||||
}
|
||||
15
yudao-ui-admin-vue3/src/components/Form/src/types.ts
Normal file
15
yudao-ui-admin-vue3/src/components/Form/src/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface PlaceholderMoel {
|
||||
placeholder?: string
|
||||
startPlaceholder?: string
|
||||
endPlaceholder?: string
|
||||
rangeSeparator?: string
|
||||
}
|
||||
|
||||
export type FormProps = {
|
||||
schema?: FormSchema[]
|
||||
isCol?: boolean
|
||||
model?: Recordable
|
||||
autoSetPlaceholder?: boolean
|
||||
isCustom?: boolean
|
||||
labelWidth?: string | number
|
||||
} & Recordable
|
||||
3
yudao-ui-admin-vue3/src/components/Highlight/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Highlight/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Highlight from './src/Highlight.vue'
|
||||
|
||||
export { Highlight }
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="tsx">
|
||||
import { defineComponent, PropType, computed, h, unref } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Highlight',
|
||||
props: {
|
||||
tag: propTypes.string.def('span'),
|
||||
keys: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => []
|
||||
},
|
||||
color: propTypes.string.def('var(--el-color-primary)')
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit, slots }) {
|
||||
const keyNodes = computed(() => {
|
||||
return props.keys.map((key) => {
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
onClick: () => {
|
||||
emit('click', key)
|
||||
},
|
||||
style: {
|
||||
color: props.color,
|
||||
cursor: 'pointer'
|
||||
}
|
||||
},
|
||||
key
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const parseText = (text: string) => {
|
||||
props.keys.forEach((key, index) => {
|
||||
const regexp = new RegExp(key, 'g')
|
||||
text = text.replace(regexp, `{{${index}}}`)
|
||||
})
|
||||
return text.split(/{{|}}/)
|
||||
}
|
||||
|
||||
const renderText = () => {
|
||||
if (!slots?.default) return null
|
||||
const node = slots?.default()[0].children
|
||||
|
||||
if (!node) {
|
||||
return slots?.default()[0]
|
||||
}
|
||||
|
||||
const textArray = parseText(node as string)
|
||||
const regexp = /^[0-9]*$/
|
||||
const nodes = textArray.map((t) => {
|
||||
if (regexp.test(t)) {
|
||||
return unref(keyNodes)[t] || t
|
||||
}
|
||||
return t
|
||||
})
|
||||
return h(props.tag, nodes)
|
||||
}
|
||||
|
||||
return () => renderText()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
3
yudao-ui-admin-vue3/src/components/IFrame/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/IFrame/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import IFrame from './src/IFrame.vue'
|
||||
|
||||
export { IFrame }
|
||||
33
yudao-ui-admin-vue3/src/components/IFrame/src/IFrame.vue
Normal file
33
yudao-ui-admin-vue3/src/components/IFrame/src/IFrame.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { ref, onMounted } from 'vue'
|
||||
const props = defineProps({
|
||||
src: propTypes.string.def('')
|
||||
})
|
||||
const loading = ref(true)
|
||||
const frameSrc = ref<string>('')
|
||||
const height = ref('')
|
||||
const frameRef = ref<HTMLElement | null>(null)
|
||||
const init = () => {
|
||||
frameSrc.value = props.src
|
||||
height.value = document.documentElement.clientHeight - 94.5 + 'px'
|
||||
loading.value = false
|
||||
}
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
init()
|
||||
}, 300)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div v-loading="loading" :style="'height:' + height">
|
||||
<iframe
|
||||
:src="frameSrc"
|
||||
style="width: 100%; height: 100%"
|
||||
frameborder="no"
|
||||
scrolling="auto"
|
||||
ref="frameRef"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="less" scoped></script>
|
||||
4
yudao-ui-admin-vue3/src/components/Icon/index.ts
Normal file
4
yudao-ui-admin-vue3/src/components/Icon/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import Icon from './src/Icon.vue'
|
||||
import IconSelect from './src/IconSelect.vue'
|
||||
|
||||
export { Icon, IconSelect }
|
||||
78
yudao-ui-admin-vue3/src/components/Icon/src/Icon.vue
Normal file
78
yudao-ui-admin-vue3/src/components/Icon/src/Icon.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, unref, ref, watch, nextTick } from 'vue'
|
||||
import { ElIcon } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import Iconify from '@purge-icons/generated'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('icon')
|
||||
|
||||
const props = defineProps({
|
||||
// icon name
|
||||
icon: propTypes.string,
|
||||
// icon color
|
||||
color: propTypes.string,
|
||||
// icon size
|
||||
size: propTypes.number.def(16)
|
||||
})
|
||||
|
||||
const elRef = ref<ElRef>(null)
|
||||
|
||||
const isLocal = computed(() => props.icon.startsWith('svg-icon:'))
|
||||
|
||||
const symbolId = computed(() => {
|
||||
return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon
|
||||
})
|
||||
|
||||
const getIconifyStyle = computed(() => {
|
||||
const { color, size } = props
|
||||
return {
|
||||
fontSize: `${size}px`,
|
||||
color
|
||||
}
|
||||
})
|
||||
|
||||
const updateIcon = async (icon: string) => {
|
||||
if (unref(isLocal)) return
|
||||
|
||||
const el = unref(elRef)
|
||||
if (!el) return
|
||||
|
||||
await nextTick()
|
||||
|
||||
if (!icon) return
|
||||
|
||||
const svg = Iconify.renderSVG(icon, {})
|
||||
if (svg) {
|
||||
el.textContent = ''
|
||||
el.appendChild(svg)
|
||||
} else {
|
||||
const span = document.createElement('span')
|
||||
span.className = 'iconify'
|
||||
span.dataset.icon = icon
|
||||
el.textContent = ''
|
||||
el.appendChild(span)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.icon,
|
||||
(icon: string) => {
|
||||
updateIcon(icon)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElIcon :class="prefixCls" :size="size" :color="color">
|
||||
<svg v-if="isLocal" aria-hidden="true">
|
||||
<use :xlink:href="symbolId" />
|
||||
</svg>
|
||||
|
||||
<span v-else ref="elRef" :class="$attrs.class" :style="getIconifyStyle">
|
||||
<span class="iconify" :data-icon="symbolId"></span>
|
||||
</span>
|
||||
</ElIcon>
|
||||
</template>
|
||||
231
yudao-ui-admin-vue3/src/components/Icon/src/IconSelect.vue
Normal file
231
yudao-ui-admin-vue3/src/components/Icon/src/IconSelect.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<script setup lang="ts">
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { ref, computed, CSSProperties, toRef, watch } from 'vue'
|
||||
import {
|
||||
ElInput,
|
||||
ElPopover,
|
||||
ElDivider,
|
||||
ElScrollbar,
|
||||
ElTabs,
|
||||
ElTabPane,
|
||||
ElPagination
|
||||
} from 'element-plus'
|
||||
import { IconJson } from '@/components/Icon/src/data'
|
||||
|
||||
type ParameterCSSProperties = (item?: string) => CSSProperties | undefined
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
require: false,
|
||||
type: String
|
||||
}
|
||||
})
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', v: string) }>()
|
||||
|
||||
const visible = ref(false)
|
||||
const inputValue = toRef(props, 'modelValue')
|
||||
const iconList = ref(IconJson)
|
||||
const icon = ref('add-location')
|
||||
const currentActiveType = ref('ep:')
|
||||
// 深拷贝图标数据,前端做搜索
|
||||
const copyIconList = cloneDeep(iconList.value)
|
||||
|
||||
const pageSize = ref(96)
|
||||
const currentPage = ref(1)
|
||||
|
||||
// 搜索条件
|
||||
const filterValue = ref('')
|
||||
|
||||
const tabsList = [
|
||||
{
|
||||
label: 'Element Plus',
|
||||
name: 'ep:'
|
||||
},
|
||||
{
|
||||
label: 'Font Awesome 4',
|
||||
name: 'fa:'
|
||||
},
|
||||
{
|
||||
label: 'Font Awesome 5 Solid',
|
||||
name: 'fa-solid:'
|
||||
}
|
||||
]
|
||||
|
||||
const pageList = computed(() => {
|
||||
if (currentPage.value === 1) {
|
||||
return copyIconList[currentActiveType.value]
|
||||
.filter((v) => v.includes(filterValue.value))
|
||||
.slice(currentPage.value - 1, pageSize.value)
|
||||
} else {
|
||||
return copyIconList[currentActiveType.value]
|
||||
.filter((v) => v.includes(filterValue.value))
|
||||
.slice(
|
||||
pageSize.value * (currentPage.value - 1),
|
||||
pageSize.value * (currentPage.value - 1) + pageSize.value
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const iconItemStyle = computed((): ParameterCSSProperties => {
|
||||
return (item) => {
|
||||
if (inputValue.value === currentActiveType.value + item) {
|
||||
return {
|
||||
borderColor: 'var(--el-color-primary)',
|
||||
color: 'var(--el-color-primary)'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function handleClick({ props }) {
|
||||
currentPage.value = 1
|
||||
currentActiveType.value = props.name
|
||||
emit('update:modelValue', currentActiveType.value + iconList.value[currentActiveType.value][0])
|
||||
icon.value = iconList.value[currentActiveType.value][0]
|
||||
}
|
||||
|
||||
function onChangeIcon(item) {
|
||||
icon.value = item
|
||||
emit('update:modelValue', currentActiveType.value + item)
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
function onCurrentChange(page) {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
watch(
|
||||
() => {
|
||||
return props.modelValue
|
||||
},
|
||||
() => {
|
||||
if (props.modelValue) {
|
||||
currentActiveType.value = props.modelValue.substring(0, props.modelValue.indexOf(':') + 1)
|
||||
icon.value = props.modelValue.substring(props.modelValue.indexOf(':') + 1)
|
||||
}
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => {
|
||||
return filterValue.value
|
||||
},
|
||||
() => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="selector">
|
||||
<ElInput v-model="inputValue" @click="visible = !visible">
|
||||
<template #append>
|
||||
<ElPopover
|
||||
:width="350"
|
||||
trigger="click"
|
||||
popper-class="pure-popper"
|
||||
:popper-options="{
|
||||
placement: 'auto'
|
||||
}"
|
||||
:visible="visible"
|
||||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="w-40px h-32px cursor-pointer flex justify-center items-center"
|
||||
@click="visible = !visible"
|
||||
>
|
||||
<Icon :icon="currentActiveType + icon" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElInput class="p-2" v-model="filterValue" placeholder="搜索图标" clearable />
|
||||
<ElDivider border-style="dashed" />
|
||||
|
||||
<ElTabs v-model="currentActiveType" @tab-click="handleClick">
|
||||
<ElTabPane
|
||||
v-for="(pane, index) in tabsList"
|
||||
:key="index"
|
||||
:label="pane.label"
|
||||
:name="pane.name"
|
||||
>
|
||||
<ElDivider class="tab-divider" border-style="dashed" />
|
||||
<ElScrollbar height="220px">
|
||||
<ul class="flex flex-wrap px-2 ml-2">
|
||||
<li
|
||||
v-for="(item, key) in pageList"
|
||||
:key="key"
|
||||
:title="item"
|
||||
class="icon-item p-2 w-1/10 cursor-pointer mr-2 mt-1 flex justify-center items-center border border-solid"
|
||||
:style="iconItemStyle(item)"
|
||||
@click="onChangeIcon(item)"
|
||||
>
|
||||
<Icon :icon="currentActiveType + item" />
|
||||
</li>
|
||||
</ul>
|
||||
</ElScrollbar>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
<ElDivider border-style="dashed" />
|
||||
|
||||
<ElPagination
|
||||
small
|
||||
:total="copyIconList[currentActiveType].length"
|
||||
:page-size="pageSize"
|
||||
:current-page="currentPage"
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
class="flex items-center justify-center h-10"
|
||||
@current-change="onCurrentChange"
|
||||
/>
|
||||
</ElPopover>
|
||||
</template>
|
||||
</ElInput>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.el-divider--horizontal {
|
||||
margin: 1px auto !important;
|
||||
}
|
||||
|
||||
.tab-divider.el-divider--horizontal {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.icon-item {
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
color: var(--el-color-primary);
|
||||
transition: all 0.4s;
|
||||
transform: scaleX(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-next) {
|
||||
font-size: 15px;
|
||||
line-height: 32px;
|
||||
box-shadow: -5px 0 5px -6px #ccc;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-prev) {
|
||||
font-size: 15px;
|
||||
line-height: 32px;
|
||||
box-shadow: 5px 0 5px -6px #ccc;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__header),
|
||||
:deep(.el-tabs__nav-wrap) {
|
||||
margin: 0;
|
||||
position: static;
|
||||
}
|
||||
</style>
|
||||
1961
yudao-ui-admin-vue3/src/components/Icon/src/data.ts
Normal file
1961
yudao-ui-admin-vue3/src/components/Icon/src/data.ts
Normal file
File diff suppressed because it is too large
Load Diff
33
yudao-ui-admin-vue3/src/components/ImageViewer/index.ts
Normal file
33
yudao-ui-admin-vue3/src/components/ImageViewer/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import ImageViewer from './src/ImageViewer.vue'
|
||||
import { isClient } from '@/utils/is'
|
||||
import { createVNode, render, VNode } from 'vue'
|
||||
import { ImageViewerProps } from './src/types'
|
||||
|
||||
let instance: Nullable<VNode> = null
|
||||
|
||||
export function createImageViewer(options: ImageViewerProps) {
|
||||
if (!isClient) return
|
||||
const {
|
||||
urlList,
|
||||
initialIndex = 0,
|
||||
infinite = true,
|
||||
hideOnClickModal = false,
|
||||
appendToBody = false,
|
||||
zIndex = 2000,
|
||||
show = true
|
||||
} = options
|
||||
|
||||
const propsData: Partial<ImageViewerProps> = {}
|
||||
const container = document.createElement('div')
|
||||
propsData.urlList = urlList
|
||||
propsData.initialIndex = initialIndex
|
||||
propsData.infinite = infinite
|
||||
propsData.hideOnClickModal = hideOnClickModal
|
||||
propsData.appendToBody = appendToBody
|
||||
propsData.zIndex = zIndex
|
||||
propsData.show = show
|
||||
|
||||
document.body.appendChild(container)
|
||||
instance = createVNode(ImageViewer, propsData)
|
||||
render(instance, container)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { ElImageViewer } from 'element-plus'
|
||||
import { computed, ref, PropType } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
const props = defineProps({
|
||||
urlList: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: (): string[] => []
|
||||
},
|
||||
zIndex: propTypes.number.def(200),
|
||||
initialIndex: propTypes.number.def(0),
|
||||
infinite: propTypes.bool.def(true),
|
||||
hideOnClickModal: propTypes.bool.def(false),
|
||||
appendToBody: propTypes.bool.def(false),
|
||||
show: propTypes.bool.def(false)
|
||||
})
|
||||
|
||||
const getBindValue = computed(() => {
|
||||
const propsData: Recordable = { ...props }
|
||||
delete propsData.show
|
||||
return propsData
|
||||
})
|
||||
|
||||
const show = ref(props.show)
|
||||
|
||||
const close = () => {
|
||||
show.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElImageViewer v-if="show" v-bind="getBindValue" @close="close" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface ImageViewerProps {
|
||||
urlList?: string[]
|
||||
zIndex?: number
|
||||
initialIndex?: number
|
||||
infinite?: boolean
|
||||
hideOnClickModal?: boolean
|
||||
appendToBody?: boolean
|
||||
show?: boolean
|
||||
}
|
||||
3
yudao-ui-admin-vue3/src/components/Infotip/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Infotip/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Infotip from './src/Infotip.vue'
|
||||
|
||||
export { Infotip }
|
||||
52
yudao-ui-admin-vue3/src/components/Infotip/src/Infotip.vue
Normal file
52
yudao-ui-admin-vue3/src/components/Infotip/src/Infotip.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
import { Highlight } from '@//components/Highlight'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('infotip')
|
||||
|
||||
defineProps({
|
||||
title: propTypes.string.def(''),
|
||||
schema: {
|
||||
type: Array as PropType<Array<string | TipSchema>>,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
showIndex: propTypes.bool.def(true),
|
||||
highlightColor: propTypes.string.def('var(--el-color-primary)')
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const keyClick = (key: string) => {
|
||||
emit('click', key)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
prefixCls,
|
||||
'p-20px mb-20px border-1px border-solid border-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)]'
|
||||
]"
|
||||
>
|
||||
<div v-if="title" :class="[`${prefixCls}__header`, 'flex items-center']">
|
||||
<Icon icon="ep:warning-filled" :size="22" color="var(--el-color-primary)" />
|
||||
<span :class="[`${prefixCls}__title`, 'pl-5px text-16px font-bold']">{{ title }}</span>
|
||||
</div>
|
||||
<div :class="`${prefixCls}__content`">
|
||||
<p v-for="(item, $index) in schema" :key="$index" class="text-14px mt-15px">
|
||||
<Highlight
|
||||
:keys="typeof item === 'string' ? [] : item.keys"
|
||||
:color="highlightColor"
|
||||
@click="keyClick"
|
||||
>
|
||||
{{ showIndex ? `${$index + 1}、` : '' }}{{ typeof item === 'string' ? item : item.label }}
|
||||
</Highlight>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
import InputPassword from './src/InputPassword.vue'
|
||||
|
||||
export { InputPassword }
|
||||
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, unref, computed, watch } from 'vue'
|
||||
import { ElInput } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useConfigGlobal } from '@/hooks/web/useConfigGlobal'
|
||||
import { zxcvbn } from '@zxcvbn-ts/core'
|
||||
import type { ZxcvbnResult } from '@zxcvbn-ts/core'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('input-password')
|
||||
|
||||
const props = defineProps({
|
||||
// 是否显示密码强度
|
||||
strength: propTypes.bool.def(false),
|
||||
modelValue: propTypes.string.def('')
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val: string) => {
|
||||
if (val === unref(valueRef)) return
|
||||
valueRef.value = val
|
||||
}
|
||||
)
|
||||
|
||||
const { configGlobal } = useConfigGlobal()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// 设置input的type属性
|
||||
const textType = ref<'password' | 'text'>('password')
|
||||
|
||||
const changeTextType = () => {
|
||||
textType.value = unref(textType) === 'text' ? 'password' : 'text'
|
||||
}
|
||||
|
||||
// 输入框的值
|
||||
const valueRef = ref(props.modelValue)
|
||||
|
||||
// 监听
|
||||
watch(
|
||||
() => valueRef.value,
|
||||
(val: string) => {
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
)
|
||||
|
||||
// 获取密码强度
|
||||
const getPasswordStrength = computed(() => {
|
||||
const value = unref(valueRef)
|
||||
const zxcvbnRef = zxcvbn(unref(valueRef)) as ZxcvbnResult
|
||||
return value ? zxcvbnRef.score : -1
|
||||
})
|
||||
|
||||
const getIconName = computed(() => (unref(textType) === 'password' ? 'ep:hide' : 'ep:view'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[prefixCls, `${prefixCls}--${configGlobal?.size}`]">
|
||||
<ElInput v-bind="$attrs" v-model="valueRef" :type="textType">
|
||||
<template #suffix>
|
||||
<Icon class="el-input__icon cursor-pointer" :icon="getIconName" @click="changeTextType" />
|
||||
</template>
|
||||
</ElInput>
|
||||
<div
|
||||
v-if="strength"
|
||||
:class="`${prefixCls}__bar`"
|
||||
class="relative h-6px mt-10px mb-6px mr-auto ml-auto"
|
||||
>
|
||||
<div :class="`${prefixCls}__bar--fill`" :data-score="getPasswordStrength"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-input-password';
|
||||
|
||||
.@{prefix-cls} {
|
||||
:deep(.@{elNamespace}-input__clear) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
background-color: var(--el-text-color-disabled);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: block;
|
||||
width: 20%;
|
||||
height: inherit;
|
||||
background-color: transparent;
|
||||
border-color: var(--el-color-white);
|
||||
border-style: solid;
|
||||
border-width: 0 5px 0 5px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 20%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: 20%;
|
||||
}
|
||||
|
||||
&--fill {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: inherit;
|
||||
background-color: transparent;
|
||||
border-radius: inherit;
|
||||
transition: width 0.5s ease-in-out, background 0.25s;
|
||||
|
||||
&[data-score='0'] {
|
||||
width: 20%;
|
||||
background-color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
&[data-score='1'] {
|
||||
width: 40%;
|
||||
background-color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
&[data-score='2'] {
|
||||
width: 60%;
|
||||
background-color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
&[data-score='3'] {
|
||||
width: 80%;
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&[data-score='4'] {
|
||||
width: 100%;
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--mini > &__bar {
|
||||
border-radius: var(--el-border-radius-small);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
import LocaleDropdown from './src/LocaleDropdown.vue'
|
||||
|
||||
export { LocaleDropdown }
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, unref } from 'vue'
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||
import { useLocaleStore } from '@/store/modules/locale'
|
||||
import { useLocale } from '@/hooks/web/useLocale'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('locale-dropdown')
|
||||
|
||||
defineProps({
|
||||
color: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const localeStore = useLocaleStore()
|
||||
|
||||
const langMap = computed(() => localeStore.getLocaleMap)
|
||||
|
||||
const currentLang = computed(() => localeStore.getCurrentLocale)
|
||||
|
||||
const setLang = (lang: LocaleType) => {
|
||||
if (lang === unref(currentLang).lang) return
|
||||
// 需要重新加载页面让整个语言多初始化
|
||||
window.location.reload()
|
||||
localeStore.setCurrentLocale({
|
||||
lang
|
||||
})
|
||||
const { changeLocale } = useLocale()
|
||||
changeLocale(lang)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDropdown :class="prefixCls" trigger="click" @command="setLang">
|
||||
<Icon
|
||||
:size="18"
|
||||
icon="ion:language-sharp"
|
||||
class="cursor-pointer"
|
||||
:class="$attrs.class"
|
||||
:color="color"
|
||||
/>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem v-for="item in langMap" :key="item.lang" :command="item.lang">
|
||||
{{ item.name }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/Logo/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Logo/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Logo from './src/Logo.vue'
|
||||
|
||||
export { Logo }
|
||||
88
yudao-ui-admin-vue3/src/components/Logo/src/Logo.vue
Normal file
88
yudao-ui-admin-vue3/src/components/Logo/src/Logo.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, onMounted, unref } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('logo')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const show = ref(true)
|
||||
|
||||
const title = computed(() => appStore.getTitle)
|
||||
|
||||
const layout = computed(() => appStore.getLayout)
|
||||
|
||||
const collapse = computed(() => appStore.getCollapse)
|
||||
|
||||
onMounted(() => {
|
||||
if (unref(collapse)) show.value = false
|
||||
})
|
||||
|
||||
watch(
|
||||
() => collapse.value,
|
||||
(collapse: boolean) => {
|
||||
if (unref(layout) === 'topLeft' || unref(layout) === 'cutMenu') {
|
||||
show.value = true
|
||||
return
|
||||
}
|
||||
if (!collapse) {
|
||||
setTimeout(() => {
|
||||
show.value = !collapse
|
||||
}, 400)
|
||||
} else {
|
||||
show.value = !collapse
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => layout.value,
|
||||
(layout) => {
|
||||
if (layout === 'top' || layout === 'cutMenu') {
|
||||
show.value = true
|
||||
} else {
|
||||
if (unref(collapse)) {
|
||||
show.value = false
|
||||
} else {
|
||||
show.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<router-link
|
||||
:class="[
|
||||
prefixCls,
|
||||
layout !== 'classic' ? `${prefixCls}__Top` : '',
|
||||
'flex !h-[var(--logo-height)] items-center cursor-pointer pl-8px relative',
|
||||
'dark:bg-[var(--el-bg-color)]'
|
||||
]"
|
||||
to="/"
|
||||
>
|
||||
<img
|
||||
src="@/assets/imgs/logo.png"
|
||||
class="w-[calc(var(--logo-height)-10px)] h-[calc(var(--logo-height)-10px)]"
|
||||
alt=""
|
||||
/>
|
||||
<div
|
||||
v-if="show"
|
||||
:class="[
|
||||
'ml-10px text-16px font-700',
|
||||
{
|
||||
'text-[var(--logo-title-text-color)]': layout === 'classic',
|
||||
'text-[var(--top-header-text-color)]':
|
||||
layout === 'topLeft' || layout === 'top' || layout === 'cutMenu'
|
||||
}
|
||||
]"
|
||||
>
|
||||
{{ title }}
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/Menu/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Menu/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Menu from './src/Menu.vue'
|
||||
|
||||
export { Menu }
|
||||
299
yudao-ui-admin-vue3/src/components/Menu/src/Menu.vue
Normal file
299
yudao-ui-admin-vue3/src/components/Menu/src/Menu.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<script lang="tsx">
|
||||
import { computed, defineComponent, unref, PropType } from 'vue'
|
||||
import { ElMenu, ElScrollbar } from 'element-plus'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import type { LayoutType } from '@/config/app'
|
||||
import { useRenderMenuItem } from './components/useRenderMenuItem'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { isUrl } from '@/utils/is'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('menu')
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Menu',
|
||||
props: {
|
||||
menuSelect: {
|
||||
type: Function as PropType<(index: string) => void>,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const appStore = useAppStore()
|
||||
|
||||
const layout = computed(() => appStore.getLayout)
|
||||
|
||||
const { push, currentRoute } = useRouter()
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const menuMode = computed((): 'vertical' | 'horizontal' => {
|
||||
// 竖
|
||||
const vertical: LayoutType[] = ['classic', 'topLeft', 'cutMenu']
|
||||
|
||||
if (vertical.includes(unref(layout))) {
|
||||
return 'vertical'
|
||||
} else {
|
||||
return 'horizontal'
|
||||
}
|
||||
})
|
||||
|
||||
const routers = computed(() =>
|
||||
unref(layout) === 'cutMenu' ? permissionStore.getMenuTabRouters : permissionStore.getRouters
|
||||
)
|
||||
|
||||
const collapse = computed(() => appStore.getCollapse)
|
||||
|
||||
const uniqueOpened = computed(() => appStore.getUniqueOpened)
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = unref(currentRoute)
|
||||
// if set path, the sidebar will highlight the path you set
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu as string
|
||||
}
|
||||
return path
|
||||
})
|
||||
|
||||
const menuSelect = (index: string) => {
|
||||
if (props.menuSelect) {
|
||||
props.menuSelect(index)
|
||||
}
|
||||
// 自定义事件
|
||||
if (isUrl(index)) {
|
||||
window.open(index)
|
||||
} else {
|
||||
push(index)
|
||||
}
|
||||
}
|
||||
|
||||
const renderMenuWrap = () => {
|
||||
if (unref(layout) === 'top') {
|
||||
return renderMenu()
|
||||
} else {
|
||||
return <ElScrollbar>{renderMenu()}</ElScrollbar>
|
||||
}
|
||||
}
|
||||
|
||||
const renderMenu = () => {
|
||||
return (
|
||||
<ElMenu
|
||||
defaultActive={unref(activeMenu)}
|
||||
mode={unref(menuMode)}
|
||||
collapse={
|
||||
unref(layout) === 'top' || unref(layout) === 'cutMenu' ? false : unref(collapse)
|
||||
}
|
||||
uniqueOpened={unref(layout) === 'top' ? false : unref(uniqueOpened)}
|
||||
backgroundColor="var(--left-menu-bg-color)"
|
||||
textColor="var(--left-menu-text-color)"
|
||||
activeTextColor="var(--left-menu-text-active-color)"
|
||||
onSelect={menuSelect}
|
||||
>
|
||||
{{
|
||||
default: () => {
|
||||
const { renderMenuItem } = useRenderMenuItem(unref(routers), unref(menuMode))
|
||||
return renderMenuItem()
|
||||
}
|
||||
}}
|
||||
</ElMenu>
|
||||
)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div
|
||||
id={prefixCls}
|
||||
class={[
|
||||
`${prefixCls} ${prefixCls}__${unref(menuMode)}`,
|
||||
'h-[100%] overflow-hidden flex-col bg-[var(--left-menu-bg-color)]',
|
||||
{
|
||||
'w-[var(--left-menu-min-width)]': unref(collapse) && unref(layout) !== 'cutMenu',
|
||||
'w-[var(--left-menu-max-width)]': !unref(collapse) && unref(layout) !== 'cutMenu'
|
||||
}
|
||||
]}
|
||||
>
|
||||
{renderMenuWrap()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-menu';
|
||||
|
||||
.is-active--after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background-color: var(--el-color-primary);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.@{prefix-cls} {
|
||||
position: relative;
|
||||
transition: width var(--transition-time-02);
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
border-left: 1px solid var(--left-menu-border-color);
|
||||
content: '';
|
||||
}
|
||||
|
||||
:deep(.@{elNamespace}-menu) {
|
||||
width: 100% !important;
|
||||
border-right: none;
|
||||
|
||||
// 设置选中时子标题的颜色
|
||||
.is-active {
|
||||
& > .@{elNamespace}-sub-menu__title {
|
||||
color: var(--left-menu-text-active-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置子菜单悬停的高亮和背景色
|
||||
.@{elNamespace}-sub-menu__title,
|
||||
.@{elNamespace}-menu-item {
|
||||
&:hover {
|
||||
color: var(--left-menu-text-active-color) !important;
|
||||
background-color: var(--left-menu-bg-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置选中时的高亮背景和高亮颜色
|
||||
.@{elNamespace}-sub-menu.is-active,
|
||||
.@{elNamespace}-menu-item.is-active {
|
||||
color: var(--left-menu-text-active-color) !important;
|
||||
background-color: var(--left-menu-bg-active-color) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--left-menu-bg-active-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.@{elNamespace}-menu-item.is-active {
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
.is-active--after;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置子菜单的背景颜色
|
||||
.@{elNamespace}-menu {
|
||||
.@{elNamespace}-sub-menu__title,
|
||||
.@{elNamespace}-menu-item:not(.is-active) {
|
||||
background-color: var(--left-menu-bg-light-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠时的最小宽度
|
||||
:deep(.@{elNamespace}-menu--collapse) {
|
||||
width: var(--left-menu-min-width);
|
||||
|
||||
& > .is-active,
|
||||
& > .is-active > .@{elNamespace}-sub-menu__title {
|
||||
position: relative;
|
||||
background-color: var(--left-menu-collapse-bg-active-color) !important;
|
||||
|
||||
&:after {
|
||||
.is-active--after;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠动画的时候,就需要把文字给隐藏掉
|
||||
:deep(.horizontal-collapse-transition) {
|
||||
// transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out !important;
|
||||
.@{prefix-cls}__title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 水平菜单
|
||||
&__horizontal {
|
||||
height: calc(~'var( - -top-tool-height)') !important;
|
||||
|
||||
:deep(.@{elNamespace}-menu--horizontal) {
|
||||
height: calc(~'var( - -top-tool-height)');
|
||||
border-bottom: none;
|
||||
// 重新设置底部高亮颜色
|
||||
& > .@{elNamespace}-sub-menu.is-active {
|
||||
.@{elNamespace}-sub-menu__title {
|
||||
border-bottom-color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.@{elNamespace}-menu-item.is-active {
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls}__title {
|
||||
/* stylelint-disable-next-line */
|
||||
max-height: calc(~'var(--top-tool-height) - 2px') !important;
|
||||
/* stylelint-disable-next-line */
|
||||
line-height: calc(~'var(--top-tool-height) - 2px');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-menu-popper';
|
||||
|
||||
.is-active--after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background-color: var(--el-color-primary);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.@{prefix-cls}--vertical,
|
||||
.@{prefix-cls}--horizontal {
|
||||
// 设置选中时子标题的颜色
|
||||
.is-active {
|
||||
& > .el-sub-menu__title {
|
||||
color: var(--left-menu-text-active-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置子菜单悬停的高亮和背景色
|
||||
.el-sub-menu__title,
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
color: var(--left-menu-text-active-color) !important;
|
||||
background-color: var(--left-menu-bg-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置选中时的高亮背景
|
||||
.el-menu-item.is-active {
|
||||
position: relative;
|
||||
background-color: var(--left-menu-bg-active-color) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--left-menu-bg-active-color) !important;
|
||||
}
|
||||
|
||||
&:after {
|
||||
.is-active--after;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ElSubMenu, ElMenuItem } from 'element-plus'
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
import { getAllParentPath, hasOneShowingChild } from '../helper'
|
||||
import { isUrl } from '@/utils/is'
|
||||
import { useRenderMenuTitle } from './useRenderMenuTitle'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { pathResolve } from '@/utils/routerHelper'
|
||||
|
||||
export const useRenderMenuItem = (
|
||||
allRouters: AppRouteRecordRaw[] = [],
|
||||
menuMode: 'vertical' | 'horizontal'
|
||||
) => {
|
||||
const renderMenuItem = (routers?: AppRouteRecordRaw[]) => {
|
||||
return (routers || allRouters).map((v) => {
|
||||
const meta = (v.meta ?? {}) as RouteMeta
|
||||
if (!meta.hidden) {
|
||||
const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
|
||||
const fullPath = isUrl(v.path)
|
||||
? v.path
|
||||
: getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
|
||||
|
||||
const { renderMenuTitle } = useRenderMenuTitle()
|
||||
|
||||
if (
|
||||
oneShowingChild &&
|
||||
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
|
||||
!meta?.alwaysShow
|
||||
) {
|
||||
return (
|
||||
<ElMenuItem index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}>
|
||||
{{
|
||||
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
|
||||
}}
|
||||
</ElMenuItem>
|
||||
)
|
||||
} else {
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const preFixCls = getPrefixCls('menu-popper')
|
||||
return (
|
||||
<ElSubMenu
|
||||
index={fullPath}
|
||||
popperClass={
|
||||
menuMode === 'vertical' ? `${preFixCls}--vertical` : `${preFixCls}--horizontal`
|
||||
}
|
||||
>
|
||||
{{
|
||||
title: () => renderMenuTitle(meta),
|
||||
default: () => renderMenuItem(v.children)
|
||||
}}
|
||||
</ElSubMenu>
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
renderMenuItem
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
|
||||
export const useRenderMenuTitle = () => {
|
||||
const renderMenuTitle = (meta: RouteMeta) => {
|
||||
const { t } = useI18n()
|
||||
const { title = 'Please set title', icon } = meta
|
||||
|
||||
return icon ? (
|
||||
<>
|
||||
<Icon icon={meta.icon}></Icon>
|
||||
<span class="v-menu__title">{t(title as string)}</span>
|
||||
</>
|
||||
) : (
|
||||
<span class="v-menu__title">{t(title as string)}</span>
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
renderMenuTitle
|
||||
}
|
||||
}
|
||||
55
yudao-ui-admin-vue3/src/components/Menu/src/helper.ts
Normal file
55
yudao-ui-admin-vue3/src/components/Menu/src/helper.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
import { ref, unref } from 'vue'
|
||||
import { findPath } from '@/utils/tree'
|
||||
|
||||
type OnlyOneChildType = AppRouteRecordRaw & { noShowingChildren?: boolean }
|
||||
|
||||
interface HasOneShowingChild {
|
||||
oneShowingChild?: boolean
|
||||
onlyOneChild?: OnlyOneChildType
|
||||
}
|
||||
|
||||
export const getAllParentPath = <T = Recordable>(treeData: T[], path: string) => {
|
||||
const menuList = findPath(treeData, (n) => n.path === path) as AppRouteRecordRaw[]
|
||||
return (menuList || []).map((item) => item.path)
|
||||
}
|
||||
|
||||
export const hasOneShowingChild = (
|
||||
children: AppRouteRecordRaw[] = [],
|
||||
parent: AppRouteRecordRaw
|
||||
): HasOneShowingChild => {
|
||||
const onlyOneChild = ref<OnlyOneChildType>()
|
||||
|
||||
const showingChildren = children.filter((v) => {
|
||||
const meta = (v.meta ?? {}) as RouteMeta
|
||||
if (meta.hidden) {
|
||||
return false
|
||||
} else {
|
||||
// Temp set(will be used if only has one showing child)
|
||||
onlyOneChild.value = v
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// When there is only one child router, the child router is displayed by default
|
||||
if (showingChildren.length === 1) {
|
||||
return {
|
||||
oneShowingChild: true,
|
||||
onlyOneChild: unref(onlyOneChild)
|
||||
}
|
||||
}
|
||||
|
||||
// Show parent if there are no child router to display
|
||||
if (!showingChildren.length) {
|
||||
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
|
||||
return {
|
||||
oneShowingChild: true,
|
||||
onlyOneChild: unref(onlyOneChild)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
oneShowingChild: false,
|
||||
onlyOneChild: unref(onlyOneChild)
|
||||
}
|
||||
}
|
||||
3
yudao-ui-admin-vue3/src/components/Qrcode/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Qrcode/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Qrcode from './src/Qrcode.vue'
|
||||
|
||||
export { Qrcode }
|
||||
251
yudao-ui-admin-vue3/src/components/Qrcode/src/Qrcode.vue
Normal file
251
yudao-ui-admin-vue3/src/components/Qrcode/src/Qrcode.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<script setup lang="ts">
|
||||
import { PropType, nextTick, ref, watch, computed, unref } from 'vue'
|
||||
import QRCode from 'qrcode'
|
||||
import { QRCodeRenderersOptions } from 'qrcode'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { isString } from '@/utils/is'
|
||||
|
||||
const props = defineProps({
|
||||
// img 或者 canvas,img不支持logo嵌套
|
||||
tag: propTypes.string.validate((v: string) => ['canvas', 'img'].includes(v)).def('canvas'),
|
||||
// 二维码内容
|
||||
text: {
|
||||
type: [String, Array] as PropType<string | Recordable[]>,
|
||||
default: null
|
||||
},
|
||||
// qrcode.js配置项
|
||||
options: {
|
||||
type: Object as PropType<QRCodeRenderersOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
// 宽度
|
||||
width: propTypes.number.def(200),
|
||||
// logo
|
||||
logo: {
|
||||
type: [String, Object] as PropType<Partial<QrcodeLogo> | string>,
|
||||
default: ''
|
||||
},
|
||||
// 是否过期
|
||||
disabled: propTypes.bool.def(false),
|
||||
// 过期提示内容
|
||||
disabledText: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const emit = defineEmits(['done', 'click', 'disabled-click'])
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('qrcode')
|
||||
|
||||
const { toCanvas, toDataURL } = QRCode
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
const wrapRef = ref<Nullable<HTMLCanvasElement | HTMLImageElement>>(null)
|
||||
|
||||
const renderText = computed(() => String(props.text))
|
||||
|
||||
const wrapStyle = computed(() => {
|
||||
return {
|
||||
width: props.width + 'px',
|
||||
height: props.width + 'px'
|
||||
}
|
||||
})
|
||||
|
||||
const initQrcode = async () => {
|
||||
await nextTick()
|
||||
const options = cloneDeep(props.options || {})
|
||||
if (props.tag === 'canvas') {
|
||||
// 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
|
||||
options.errorCorrectionLevel =
|
||||
options.errorCorrectionLevel || getErrorCorrectionLevel(unref(renderText))
|
||||
const _width: number = await getOriginWidth(unref(renderText), options)
|
||||
options.scale = props.width === 0 ? undefined : (props.width / _width) * 4
|
||||
const canvasRef: HTMLCanvasElement = await toCanvas(
|
||||
unref(wrapRef) as HTMLCanvasElement,
|
||||
unref(renderText),
|
||||
options
|
||||
)
|
||||
if (props.logo) {
|
||||
const url = await createLogoCode(canvasRef)
|
||||
emit('done', url)
|
||||
loading.value = false
|
||||
} else {
|
||||
emit('done', canvasRef.toDataURL())
|
||||
loading.value = false
|
||||
}
|
||||
} else {
|
||||
const url = await toDataURL(renderText.value, {
|
||||
errorCorrectionLevel: 'H',
|
||||
width: props.width,
|
||||
...options
|
||||
})
|
||||
;(unref(wrapRef) as HTMLImageElement).src = url
|
||||
emit('done', url)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => renderText.value,
|
||||
(val) => {
|
||||
if (!val) return
|
||||
initQrcode()
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
const createLogoCode = (canvasRef: HTMLCanvasElement) => {
|
||||
const canvasWidth = canvasRef.width
|
||||
const logoOptions: QrcodeLogo = Object.assign(
|
||||
{
|
||||
logoSize: 0.15,
|
||||
bgColor: '#ffffff',
|
||||
borderSize: 0.05,
|
||||
crossOrigin: 'anonymous',
|
||||
borderRadius: 8,
|
||||
logoRadius: 0
|
||||
},
|
||||
isString(props.logo) ? {} : props.logo
|
||||
)
|
||||
const {
|
||||
logoSize = 0.15,
|
||||
bgColor = '#ffffff',
|
||||
borderSize = 0.05,
|
||||
crossOrigin = 'anonymous',
|
||||
borderRadius = 8,
|
||||
logoRadius = 0
|
||||
} = logoOptions
|
||||
const logoSrc = isString(props.logo) ? props.logo : props.logo.src
|
||||
const logoWidth = canvasWidth * logoSize
|
||||
const logoXY = (canvasWidth * (1 - logoSize)) / 2
|
||||
const logoBgWidth = canvasWidth * (logoSize + borderSize)
|
||||
const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2
|
||||
|
||||
const ctx = canvasRef.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
// logo 底色
|
||||
canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius)
|
||||
ctx.fillStyle = bgColor
|
||||
ctx.fill()
|
||||
|
||||
// logo
|
||||
const image = new Image()
|
||||
if (crossOrigin || logoRadius) {
|
||||
image.setAttribute('crossOrigin', crossOrigin)
|
||||
}
|
||||
;(image as any).src = logoSrc
|
||||
|
||||
// 使用image绘制可以避免某些跨域情况
|
||||
const drawLogoWithImage = (image: HTMLImageElement) => {
|
||||
ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
|
||||
}
|
||||
|
||||
// 使用canvas绘制以获得更多的功能
|
||||
const drawLogoWithCanvas = (image: HTMLImageElement) => {
|
||||
const canvasImage = document.createElement('canvas')
|
||||
canvasImage.width = logoXY + logoWidth
|
||||
canvasImage.height = logoXY + logoWidth
|
||||
const imageCanvas = canvasImage.getContext('2d')
|
||||
if (!imageCanvas || !ctx) return
|
||||
imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
|
||||
|
||||
canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius)
|
||||
if (!ctx) return
|
||||
const fillStyle = ctx.createPattern(canvasImage, 'no-repeat')
|
||||
if (fillStyle) {
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
// 将 logo绘制到 canvas上
|
||||
return new Promise((resolve: any) => {
|
||||
image.onload = () => {
|
||||
logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image)
|
||||
resolve(canvasRef.toDataURL())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 得到原QrCode的大小,以便缩放得到正确的QrCode大小
|
||||
const getOriginWidth = async (content: string, options: QRCodeRenderersOptions) => {
|
||||
const _canvas = document.createElement('canvas')
|
||||
await toCanvas(_canvas, content, options)
|
||||
return _canvas.width
|
||||
}
|
||||
|
||||
// 对于内容少的QrCode,增大容错率
|
||||
const getErrorCorrectionLevel = (content: string) => {
|
||||
if (content.length > 36) {
|
||||
return 'M'
|
||||
} else if (content.length > 16) {
|
||||
return 'Q'
|
||||
} else {
|
||||
return 'H'
|
||||
}
|
||||
}
|
||||
|
||||
// copy来的方法,用于绘制圆角
|
||||
const canvasRoundRect = (ctx: CanvasRenderingContext2D) => {
|
||||
return (x: number, y: number, w: number, h: number, r: number) => {
|
||||
const minSize = Math.min(w, h)
|
||||
if (r > minSize / 2) {
|
||||
r = minSize / 2
|
||||
}
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + r, y)
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r)
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r)
|
||||
ctx.arcTo(x, y + h, x, y, r)
|
||||
ctx.arcTo(x, y, x + w, y, r)
|
||||
ctx.closePath()
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
|
||||
const clickCode = () => {
|
||||
emit('click')
|
||||
}
|
||||
|
||||
const disabledClick = () => {
|
||||
emit('disabled-click')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" :class="[prefixCls, 'relative inline-block']" :style="wrapStyle">
|
||||
<component :is="tag" ref="wrapRef" @click="clickCode" />
|
||||
<div
|
||||
v-if="disabled"
|
||||
:class="`${prefixCls}--disabled`"
|
||||
class="absolute top-0 left-0 flex w-full h-full items-center justify-center"
|
||||
@click="disabledClick"
|
||||
>
|
||||
<div class="absolute top-[50%] left-[50%] font-bold">
|
||||
<Icon icon="ep:refresh-right" :size="30" color="var(--el-color-primary)" />
|
||||
<div>{{ disabledText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-qrcode';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&--disabled {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
|
||||
& > div {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
yudao-ui-admin-vue3/src/components/Screenfull/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Screenfull/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Screenfull from './src/Screenfull.vue'
|
||||
|
||||
export { Screenfull }
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('screenfull')
|
||||
|
||||
defineProps({
|
||||
color: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const { toggle, isFullscreen } = useFullscreen()
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
toggle()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="prefixCls" @click="toggleFullscreen">
|
||||
<Icon
|
||||
:size="18"
|
||||
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
|
||||
:color="color"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/Search/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Search/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Search from './src/Search.vue'
|
||||
|
||||
export { Search }
|
||||
139
yudao-ui-admin-vue3/src/components/Search/src/Search.vue
Normal file
139
yudao-ui-admin-vue3/src/components/Search/src/Search.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { Form } from '@/components/Form'
|
||||
import { PropType, computed, unref, ref } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { ElButton } from 'element-plus'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useForm } from '@/hooks/web/useForm'
|
||||
import { findIndex } from '@/utils'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
// 生成Form的布局结构数组
|
||||
schema: {
|
||||
type: Array as PropType<FormSchema[]>,
|
||||
default: () => []
|
||||
},
|
||||
// 是否需要栅格布局
|
||||
isCol: propTypes.bool.def(false),
|
||||
// 表单label宽度
|
||||
labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
|
||||
// 操作按钮风格位置
|
||||
layout: propTypes.string.validate((v: string) => ['inline', 'bottom'].includes(v)).def('inline'),
|
||||
// 底部按钮的对齐方式
|
||||
buttomPosition: propTypes.string
|
||||
.validate((v: string) => ['left', 'center', 'right'].includes(v))
|
||||
.def('center'),
|
||||
showSearch: propTypes.bool.def(true),
|
||||
showReset: propTypes.bool.def(true),
|
||||
// 是否显示伸缩
|
||||
expand: propTypes.bool.def(false),
|
||||
// 伸缩的界限字段
|
||||
expandField: propTypes.string.def(''),
|
||||
inline: propTypes.bool.def(true)
|
||||
})
|
||||
|
||||
const emit = defineEmits(['search', 'reset'])
|
||||
|
||||
const visible = ref(true)
|
||||
|
||||
const newSchema = computed(() => {
|
||||
let schema: FormSchema[] = cloneDeep(props.schema)
|
||||
if (props.expand && props.expandField && !unref(visible)) {
|
||||
const index = findIndex(schema, (v: FormSchema) => v.field === props.expandField)
|
||||
if (index > -1) {
|
||||
const length = schema.length
|
||||
schema.splice(index + 1, length)
|
||||
}
|
||||
}
|
||||
if (props.layout === 'inline') {
|
||||
schema = schema.concat([
|
||||
{
|
||||
field: 'action',
|
||||
formItemProps: {
|
||||
labelWidth: '0px'
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
return schema
|
||||
})
|
||||
|
||||
const { register, elFormRef, methods } = useForm()
|
||||
|
||||
const search = async () => {
|
||||
await unref(elFormRef)?.validate(async (isValid) => {
|
||||
if (isValid) {
|
||||
const { getFormData } = methods
|
||||
const model = await getFormData()
|
||||
emit('search', model)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const reset = async () => {
|
||||
unref(elFormRef)?.resetFields()
|
||||
const { getFormData } = methods
|
||||
const model = await getFormData()
|
||||
emit('reset', model)
|
||||
}
|
||||
|
||||
const bottonButtonStyle = computed(() => {
|
||||
return {
|
||||
textAlign: props.buttomPosition as unknown as 'left' | 'center' | 'right'
|
||||
}
|
||||
})
|
||||
|
||||
const setVisible = () => {
|
||||
unref(elFormRef)?.resetFields()
|
||||
visible.value = !unref(visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
:is-custom="false"
|
||||
:label-width="labelWidth"
|
||||
hide-required-asterisk
|
||||
:inline="inline"
|
||||
:is-col="isCol"
|
||||
:schema="newSchema"
|
||||
@register="register"
|
||||
>
|
||||
<template #action>
|
||||
<div v-if="layout === 'inline'">
|
||||
<ElButton v-if="showSearch" type="primary" @click="search">
|
||||
<Icon icon="ep:search" class="mr-5px" />
|
||||
{{ t('common.query') }}
|
||||
</ElButton>
|
||||
<ElButton v-if="showReset" @click="reset">
|
||||
<Icon icon="ep:refresh-right" class="mr-5px" />
|
||||
{{ t('common.reset') }}
|
||||
</ElButton>
|
||||
<ElButton v-if="expand" text @click="setVisible">
|
||||
{{ t(visible ? 'common.shrink' : 'common.expand') }}
|
||||
<Icon :icon="visible ? 'ep:arrow-up' : 'ep:arrow-down'" />
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
|
||||
<template v-if="layout === 'bottom'">
|
||||
<div :style="bottonButtonStyle">
|
||||
<ElButton v-if="showSearch" type="primary" @click="search">
|
||||
<Icon icon="ep:search" class="mr-5px" />
|
||||
{{ t('common.query') }}
|
||||
</ElButton>
|
||||
<ElButton v-if="showReset" @click="reset">
|
||||
<Icon icon="ep:refresh-right" class="mr-5px" />
|
||||
{{ t('common.reset') }}
|
||||
</ElButton>
|
||||
<ElButton v-if="expand" text @click="setVisible">
|
||||
{{ t(visible ? 'common.shrink' : 'common.expand') }}
|
||||
<Icon :icon="visible ? 'ep:arrow-up' : 'ep:arrow-down'" />
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/Setting/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Setting/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Setting from './src/Setting.vue'
|
||||
|
||||
export { Setting }
|
||||
301
yudao-ui-admin-vue3/src/components/Setting/src/Setting.vue
Normal file
301
yudao-ui-admin-vue3/src/components/Setting/src/Setting.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<script setup lang="ts">
|
||||
import { ElDrawer, ElDivider, ElMessage } from 'element-plus'
|
||||
import { ref, unref, computed, watch } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { ThemeSwitch } from '@/components/ThemeSwitch'
|
||||
import { colorIsDark, lighten, hexToRGB } from '@/utils/color'
|
||||
import { useCssVar } from '@vueuse/core'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { trim, setCssVar } from '@/utils'
|
||||
import ColorRadioPicker from './components/ColorRadioPicker.vue'
|
||||
import InterfaceDisplay from './components/InterfaceDisplay.vue'
|
||||
import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('setting')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const layout = computed(() => appStore.getLayout)
|
||||
|
||||
const drawer = ref(false)
|
||||
|
||||
// 主题色相关
|
||||
const systemTheme = ref(appStore.getTheme.elColorPrimary)
|
||||
|
||||
const setSystemTheme = (color: string) => {
|
||||
setCssVar('--el-color-primary', color)
|
||||
appStore.setTheme({ elColorPrimary: color })
|
||||
const leftMenuBgColor = useCssVar('--left-menu-bg-color', document.documentElement)
|
||||
setMenuTheme(trim(unref(leftMenuBgColor)))
|
||||
}
|
||||
|
||||
// 头部主题相关
|
||||
const headerTheme = ref(appStore.getTheme.topHeaderBgColor || '')
|
||||
|
||||
const setHeaderTheme = (color: string) => {
|
||||
const isDarkColor = colorIsDark(color)
|
||||
const textColor = isDarkColor ? '#fff' : 'inherit'
|
||||
const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
|
||||
const topToolBorderColor = isDarkColor ? color : '#eee'
|
||||
setCssVar('--top-header-bg-color', color)
|
||||
setCssVar('--top-header-text-color', textColor)
|
||||
setCssVar('--top-header-hover-color', textHoverColor)
|
||||
setCssVar('--top-tool-border-color', topToolBorderColor)
|
||||
appStore.setTheme({
|
||||
topHeaderBgColor: color,
|
||||
topHeaderTextColor: textColor,
|
||||
topHeaderHoverColor: textHoverColor,
|
||||
topToolBorderColor
|
||||
})
|
||||
if (unref(layout) === 'top') {
|
||||
setMenuTheme(color)
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单主题相关
|
||||
const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '')
|
||||
|
||||
const setMenuTheme = (color: string) => {
|
||||
const primaryColor = useCssVar('--el-color-primary', document.documentElement)
|
||||
const isDarkColor = colorIsDark(color)
|
||||
const theme: Recordable = {
|
||||
// 左侧菜单边框颜色
|
||||
leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
|
||||
// 左侧菜单背景颜色
|
||||
leftMenuBgColor: color,
|
||||
// 左侧菜单浅色背景颜色
|
||||
leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color,
|
||||
// 左侧菜单选中背景颜色
|
||||
leftMenuBgActiveColor: isDarkColor
|
||||
? 'var(--el-color-primary)'
|
||||
: hexToRGB(unref(primaryColor), 0.1),
|
||||
// 左侧菜单收起选中背景颜色
|
||||
leftMenuCollapseBgActiveColor: isDarkColor
|
||||
? 'var(--el-color-primary)'
|
||||
: hexToRGB(unref(primaryColor), 0.1),
|
||||
// 左侧菜单字体颜色
|
||||
leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
|
||||
// 左侧菜单选中字体颜色
|
||||
leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)',
|
||||
// logo字体颜色
|
||||
logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
|
||||
// logo边框颜色
|
||||
logoBorderColor: isDarkColor ? color : '#eee'
|
||||
}
|
||||
appStore.setTheme(theme)
|
||||
appStore.setCssVarTheme()
|
||||
}
|
||||
if (layout.value === 'top' && !appStore.getIsDark) {
|
||||
headerTheme.value = '#fff'
|
||||
setHeaderTheme('#fff')
|
||||
}
|
||||
|
||||
// 监听layout变化,重置一些主题色
|
||||
watch(
|
||||
() => layout.value,
|
||||
(n) => {
|
||||
if (n === 'top' && !appStore.getIsDark) {
|
||||
headerTheme.value = '#fff'
|
||||
setHeaderTheme('#fff')
|
||||
} else {
|
||||
setMenuTheme(unref(menuTheme))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 拷贝
|
||||
const copyConfig = async () => {
|
||||
const { copy, copied, isSupported } = useClipboard({
|
||||
source: `
|
||||
// 面包屑
|
||||
breadcrumb: ${appStore.getBreadcrumb},
|
||||
// 面包屑图标
|
||||
breadcrumbIcon: ${appStore.getBreadcrumbIcon},
|
||||
// 折叠图标
|
||||
hamburger: ${appStore.getHamburger},
|
||||
// 全屏图标
|
||||
screenfull: ${appStore.getScreenfull},
|
||||
// 尺寸图标
|
||||
size: ${appStore.getSize},
|
||||
// 多语言图标
|
||||
locale: ${appStore.getLocale},
|
||||
// 标签页
|
||||
tagsView: ${appStore.getTagsView},
|
||||
// 标签页图标
|
||||
getTagsViewIcon: ${appStore.getTagsViewIcon},
|
||||
// logo
|
||||
logo: ${appStore.getLogo},
|
||||
// 菜单手风琴
|
||||
uniqueOpened: ${appStore.getUniqueOpened},
|
||||
// 固定header
|
||||
fixedHeader: ${appStore.getFixedHeader},
|
||||
// 页脚
|
||||
footer: ${appStore.getFooter},
|
||||
// 灰色模式
|
||||
greyMode: ${appStore.getGreyMode},
|
||||
// layout布局
|
||||
layout: '${appStore.getLayout}',
|
||||
// 暗黑模式
|
||||
isDark: ${appStore.getIsDark},
|
||||
// 组件尺寸
|
||||
currentSize: '${appStore.getCurrentSize}',
|
||||
// 主题相关
|
||||
theme: {
|
||||
// 主题色
|
||||
elColorPrimary: '${appStore.getTheme.elColorPrimary}',
|
||||
// 左侧菜单边框颜色
|
||||
leftMenuBorderColor: '${appStore.getTheme.leftMenuBorderColor}',
|
||||
// 左侧菜单背景颜色
|
||||
leftMenuBgColor: '${appStore.getTheme.leftMenuBgColor}',
|
||||
// 左侧菜单浅色背景颜色
|
||||
leftMenuBgLightColor: '${appStore.getTheme.leftMenuBgLightColor}',
|
||||
// 左侧菜单选中背景颜色
|
||||
leftMenuBgActiveColor: '${appStore.getTheme.leftMenuBgActiveColor}',
|
||||
// 左侧菜单收起选中背景颜色
|
||||
leftMenuCollapseBgActiveColor: '${appStore.getTheme.leftMenuCollapseBgActiveColor}',
|
||||
// 左侧菜单字体颜色
|
||||
leftMenuTextColor: '${appStore.getTheme.leftMenuTextColor}',
|
||||
// 左侧菜单选中字体颜色
|
||||
leftMenuTextActiveColor: '${appStore.getTheme.leftMenuTextActiveColor}',
|
||||
// logo字体颜色
|
||||
logoTitleTextColor: '${appStore.getTheme.logoTitleTextColor}',
|
||||
// logo边框颜色
|
||||
logoBorderColor: '${appStore.getTheme.logoBorderColor}',
|
||||
// 头部背景颜色
|
||||
topHeaderBgColor: '${appStore.getTheme.topHeaderBgColor}',
|
||||
// 头部字体颜色
|
||||
topHeaderTextColor: '${appStore.getTheme.topHeaderTextColor}',
|
||||
// 头部悬停颜色
|
||||
topHeaderHoverColor: '${appStore.getTheme.topHeaderHoverColor}',
|
||||
// 头部边框颜色
|
||||
topToolBorderColor: '${appStore.getTheme.topToolBorderColor}'
|
||||
}
|
||||
`
|
||||
})
|
||||
if (!isSupported) {
|
||||
ElMessage.error(t('setting.copyFailed'))
|
||||
} else {
|
||||
await copy()
|
||||
if (unref(copied)) {
|
||||
ElMessage.success(t('setting.copySuccess'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清空缓存
|
||||
const clear = () => {
|
||||
const { wsCache } = useCache()
|
||||
wsCache.delete('layout')
|
||||
wsCache.delete('theme')
|
||||
wsCache.delete('isDark')
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="prefixCls"
|
||||
class="fixed top-[45%] right-0 w-40px h-40px text-center leading-40px bg-[var(--el-color-primary)] cursor-pointer"
|
||||
@click="drawer = true"
|
||||
>
|
||||
<Icon icon="ep:setting" color="#fff" />
|
||||
</div>
|
||||
|
||||
<ElDrawer v-model="drawer" direction="rtl" size="350px">
|
||||
<template #header>
|
||||
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
|
||||
</template>
|
||||
|
||||
<div class="text-center">
|
||||
<!-- 主题 -->
|
||||
<ElDivider>{{ t('setting.theme') }}</ElDivider>
|
||||
<ThemeSwitch />
|
||||
|
||||
<!-- 布局 -->
|
||||
<ElDivider>{{ t('setting.layout') }}</ElDivider>
|
||||
<LayoutRadioPicker />
|
||||
|
||||
<!-- 系统主题 -->
|
||||
<ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
|
||||
<ColorRadioPicker
|
||||
v-model="systemTheme"
|
||||
:schema="[
|
||||
'#409eff',
|
||||
'#009688',
|
||||
'#536dfe',
|
||||
'#ff5c93',
|
||||
'#ee4f12',
|
||||
'#0096c7',
|
||||
'#9c27b0',
|
||||
'#ff9800'
|
||||
]"
|
||||
@change="setSystemTheme"
|
||||
/>
|
||||
|
||||
<!-- 头部主题 -->
|
||||
<ElDivider>{{ t('setting.headerTheme') }}</ElDivider>
|
||||
<ColorRadioPicker
|
||||
v-model="headerTheme"
|
||||
:schema="[
|
||||
'#fff',
|
||||
'#151515',
|
||||
'#5172dc',
|
||||
'#e74c3c',
|
||||
'#24292e',
|
||||
'#394664',
|
||||
'#009688',
|
||||
'#383f45'
|
||||
]"
|
||||
@change="setHeaderTheme"
|
||||
/>
|
||||
|
||||
<!-- 菜单主题 -->
|
||||
<template v-if="layout !== 'top'">
|
||||
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
|
||||
<ColorRadioPicker
|
||||
v-model="menuTheme"
|
||||
:schema="[
|
||||
'#fff',
|
||||
'#001529',
|
||||
'#212121',
|
||||
'#273352',
|
||||
'#191b24',
|
||||
'#383f45',
|
||||
'#001628',
|
||||
'#344058'
|
||||
]"
|
||||
@change="setMenuTheme"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 界面显示 -->
|
||||
<ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
|
||||
<InterfaceDisplay />
|
||||
|
||||
<ElDivider />
|
||||
<div>
|
||||
<ElButton type="primary" class="w-full" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
|
||||
</div>
|
||||
<div class="mt-5px">
|
||||
<ElButton type="danger" class="w-full" @click="clear">
|
||||
{{ t('setting.clearAndReset') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElDrawer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-setting';
|
||||
|
||||
.@{prefix-cls} {
|
||||
border-radius: 6px 0 0 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { PropType, watch, unref, ref } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('color-radio-picker')
|
||||
|
||||
const props = defineProps({
|
||||
schema: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => []
|
||||
},
|
||||
modelValue: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const colorVal = ref(props.modelValue)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val: string) => {
|
||||
if (val === unref(colorVal)) return
|
||||
colorVal.value = val
|
||||
}
|
||||
)
|
||||
|
||||
// 监听
|
||||
watch(
|
||||
() => colorVal.value,
|
||||
(val: string) => {
|
||||
emit('update:modelValue', val)
|
||||
emit('change', val)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="prefixCls" class="flex flex-wrap space-x-14px">
|
||||
<span
|
||||
v-for="(item, i) in schema"
|
||||
:key="`radio-${i}`"
|
||||
class="w-20px h-20px cursor-pointer rounded-2px border-solid border-gray-300 border-2px text-center leading-20px mb-5px"
|
||||
:class="{ 'is-active': colorVal === item }"
|
||||
:style="{
|
||||
background: item
|
||||
}"
|
||||
@click="colorVal = item"
|
||||
>
|
||||
<Icon v-if="colorVal === item" color="#fff" icon="ep:check" :size="16" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-color-radio-picker';
|
||||
|
||||
.@{prefix-cls} {
|
||||
.is-active {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,189 @@
|
||||
<script setup lang="ts">
|
||||
import { ElSwitch } from 'element-plus'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { setCssVar } from '@/utils'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('interface-display')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 面包屑
|
||||
const breadcrumb = ref(appStore.getBreadcrumb)
|
||||
|
||||
const breadcrumbChange = (show: boolean) => {
|
||||
appStore.setBreadcrumb(show)
|
||||
}
|
||||
|
||||
// 面包屑图标
|
||||
const breadcrumbIcon = ref(appStore.getBreadcrumbIcon)
|
||||
|
||||
const breadcrumbIconChange = (show: boolean) => {
|
||||
appStore.setBreadcrumbIcon(show)
|
||||
}
|
||||
|
||||
// 折叠图标
|
||||
const hamburger = ref(appStore.getHamburger)
|
||||
|
||||
const hamburgerChange = (show: boolean) => {
|
||||
appStore.setHamburger(show)
|
||||
}
|
||||
|
||||
// 全屏图标
|
||||
const screenfull = ref(appStore.getScreenfull)
|
||||
|
||||
const screenfullChange = (show: boolean) => {
|
||||
appStore.setScreenfull(show)
|
||||
}
|
||||
|
||||
// 尺寸图标
|
||||
const size = ref(appStore.getSize)
|
||||
|
||||
const sizeChange = (show: boolean) => {
|
||||
appStore.setSize(show)
|
||||
}
|
||||
|
||||
// 多语言图标
|
||||
const locale = ref(appStore.getLocale)
|
||||
|
||||
const localeChange = (show: boolean) => {
|
||||
appStore.setLocale(show)
|
||||
}
|
||||
|
||||
// 标签页
|
||||
const tagsView = ref(appStore.getTagsView)
|
||||
|
||||
const tagsViewChange = (show: boolean) => {
|
||||
// 切换标签栏显示时,同步切换标签栏的高度
|
||||
setCssVar('--tags-view-height', show ? '35px' : '0px')
|
||||
appStore.setTagsView(show)
|
||||
}
|
||||
|
||||
// 标签页图标
|
||||
const tagsViewIcon = ref(appStore.getTagsViewIcon)
|
||||
|
||||
const tagsViewIconChange = (show: boolean) => {
|
||||
appStore.setTagsViewIcon(show)
|
||||
}
|
||||
|
||||
// logo
|
||||
const logo = ref(appStore.getLogo)
|
||||
|
||||
const logoChange = (show: boolean) => {
|
||||
appStore.setLogo(show)
|
||||
}
|
||||
|
||||
// 菜单手风琴
|
||||
const uniqueOpened = ref(appStore.getUniqueOpened)
|
||||
|
||||
const uniqueOpenedChange = (uniqueOpened: boolean) => {
|
||||
appStore.setUniqueOpened(uniqueOpened)
|
||||
}
|
||||
|
||||
// 固定头部
|
||||
const fixedHeader = ref(appStore.getFixedHeader)
|
||||
|
||||
const fixedHeaderChange = (show: boolean) => {
|
||||
appStore.setFixedHeader(show)
|
||||
}
|
||||
|
||||
// 页脚
|
||||
const footer = ref(appStore.getFooter)
|
||||
|
||||
const footerChange = (show: boolean) => {
|
||||
appStore.setFooter(show)
|
||||
}
|
||||
|
||||
// 灰色模式
|
||||
const greyMode = ref(appStore.getGreyMode)
|
||||
|
||||
const greyModeChange = (show: boolean) => {
|
||||
appStore.setGreyMode(show)
|
||||
}
|
||||
|
||||
const layout = computed(() => appStore.getLayout)
|
||||
|
||||
watch(
|
||||
() => layout.value,
|
||||
(n) => {
|
||||
if (n === 'top') {
|
||||
appStore.setCollapse(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.breadcrumb') }}</span>
|
||||
<ElSwitch v-model="breadcrumb" @change="breadcrumbChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.breadcrumbIcon') }}</span>
|
||||
<ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.hamburgerIcon') }}</span>
|
||||
<ElSwitch v-model="hamburger" @change="hamburgerChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.screenfullIcon') }}</span>
|
||||
<ElSwitch v-model="screenfull" @change="screenfullChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.sizeIcon') }}</span>
|
||||
<ElSwitch v-model="size" @change="sizeChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.localeIcon') }}</span>
|
||||
<ElSwitch v-model="locale" @change="localeChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.tagsView') }}</span>
|
||||
<ElSwitch v-model="tagsView" @change="tagsViewChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.tagsViewIcon') }}</span>
|
||||
<ElSwitch v-model="tagsViewIcon" @change="tagsViewIconChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.logo') }}</span>
|
||||
<ElSwitch v-model="logo" @change="logoChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.uniqueOpened') }}</span>
|
||||
<ElSwitch v-model="uniqueOpened" @change="uniqueOpenedChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.fixedHeader') }}</span>
|
||||
<ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.footer') }}</span>
|
||||
<ElSwitch v-model="footer" @change="footerChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-14px">{{ t('setting.greyMode') }}</span>
|
||||
<ElSwitch v-model="greyMode" @change="greyModeChange" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,171 @@
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { computed } from 'vue'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('layout-radio-picker')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const layout = computed(() => appStore.getLayout)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="prefixCls" class="flex flex-wrap space-x-14px">
|
||||
<div
|
||||
:class="[
|
||||
`${prefixCls}__classic`,
|
||||
'relative w-56px h-48px cursor-pointer bg-gray-300',
|
||||
{
|
||||
'is-acitve': layout === 'classic'
|
||||
}
|
||||
]"
|
||||
@click="appStore.setLayout('classic')"
|
||||
></div>
|
||||
<div
|
||||
:class="[
|
||||
`${prefixCls}__top-left`,
|
||||
'relative w-56px h-48px cursor-pointer bg-gray-300',
|
||||
{
|
||||
'is-acitve': layout === 'topLeft'
|
||||
}
|
||||
]"
|
||||
@click="appStore.setLayout('topLeft')"
|
||||
></div>
|
||||
<div
|
||||
:class="[
|
||||
`${prefixCls}__top`,
|
||||
'relative w-56px h-48px cursor-pointer bg-gray-300',
|
||||
{
|
||||
'is-acitve': layout === 'top'
|
||||
}
|
||||
]"
|
||||
@click="appStore.setLayout('top')"
|
||||
></div>
|
||||
<div
|
||||
:class="[
|
||||
`${prefixCls}__cut-menu`,
|
||||
'relative w-56px h-48px cursor-pointer bg-gray-300',
|
||||
{
|
||||
'is-acitve': layout === 'cutMenu'
|
||||
}
|
||||
]"
|
||||
@click="appStore.setLayout('cutMenu')"
|
||||
>
|
||||
<div class="absolute h-full w-[33%] top-0 left-[10%] bg-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-layout-radio-picker';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&__classic {
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
background-color: #273352;
|
||||
border-radius: 4px 0 0 4px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
background-color: #fff;
|
||||
border-radius: 4px 4px 0 4px;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&__top-left {
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 33%;
|
||||
background-color: #273352;
|
||||
border-radius: 4px 4px 0 0;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 4px 0 0 4px;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&__top {
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 33%;
|
||||
background-color: #273352;
|
||||
border-radius: 4px 4px 0 0;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&__cut-menu {
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 33%;
|
||||
background-color: #273352;
|
||||
border-radius: 4px 4px 0 0;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 10%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 4px 0 0 4px;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.is-acitve {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
yudao-ui-admin-vue3/src/components/SizeDropdown/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/SizeDropdown/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import SizeDropdown from './src/SizeDropdown.vue'
|
||||
|
||||
export { SizeDropdown }
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('size-dropdown')
|
||||
|
||||
defineProps({
|
||||
color: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const sizeMap = computed(() => appStore.sizeMap)
|
||||
|
||||
const setCurrentSize = (size: ElememtPlusSize) => {
|
||||
appStore.setCurrentSize(size)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDropdown :class="prefixCls" trigger="click" @command="setCurrentSize">
|
||||
<Icon :size="18" icon="mdi:format-size" :color="color" class="cursor-pointer" />
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem v-for="item in sizeMap" :key="item" :command="item">
|
||||
{{ t(`size.${item}`) }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/Sticky/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Sticky/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Sticky from './src/Sticky.vue'
|
||||
|
||||
export { Sticky }
|
||||
141
yudao-ui-admin-vue3/src/components/Sticky/src/Sticky.vue
Normal file
141
yudao-ui-admin-vue3/src/components/Sticky/src/Sticky.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { ref, onMounted, onActivated, shallowRef } from 'vue'
|
||||
import { useEventListener, useWindowSize, isClient } from '@vueuse/core'
|
||||
import type { CSSProperties } from 'vue'
|
||||
const props = defineProps({
|
||||
// 距离顶部或者底部的距离(单位px)
|
||||
offset: propTypes.number.def(0),
|
||||
// 设置元素的堆叠顺序
|
||||
zIndex: propTypes.number.def(999),
|
||||
// 设置指定的class
|
||||
className: propTypes.string.def(''),
|
||||
// 定位方式,默认为(top),表示距离顶部位置,可以设置为top或者bottom
|
||||
position: {
|
||||
type: String,
|
||||
validator: function (value: string) {
|
||||
return ['top', 'bottom'].indexOf(value) !== -1
|
||||
},
|
||||
default: 'top'
|
||||
}
|
||||
})
|
||||
const width = ref('auto' as string)
|
||||
const height = ref('auto' as string)
|
||||
const isSticky = ref(false)
|
||||
const refSticky = shallowRef<HTMLElement>()
|
||||
const scrollContainer = shallowRef<HTMLElement | Window>()
|
||||
const { height: windowHeight } = useWindowSize()
|
||||
onMounted(() => {
|
||||
height.value = refSticky.value?.getBoundingClientRect().height + 'px'
|
||||
|
||||
scrollContainer.value = getScrollContainer(refSticky.value!, true)
|
||||
useEventListener(scrollContainer, 'scroll', handleScroll)
|
||||
useEventListener('resize', handleReize)
|
||||
handleScroll()
|
||||
})
|
||||
onActivated(() => {
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
const camelize = (str: string): string => {
|
||||
return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
|
||||
}
|
||||
|
||||
const getStyle = (element: HTMLElement, styleName: keyof CSSProperties): string => {
|
||||
if (!isClient || !element || !styleName) return ''
|
||||
|
||||
let key = camelize(styleName)
|
||||
if (key === 'float') key = 'cssFloat'
|
||||
try {
|
||||
const style = element.style[styleName]
|
||||
if (style) return style
|
||||
const computed = document.defaultView?.getComputedStyle(element, '')
|
||||
return computed ? computed[styleName] : ''
|
||||
} catch {
|
||||
return element.style[styleName]
|
||||
}
|
||||
}
|
||||
const isScroll = (el: HTMLElement, isVertical?: boolean): boolean => {
|
||||
if (!isClient) return false
|
||||
const key = (
|
||||
{
|
||||
undefined: 'overflow',
|
||||
true: 'overflow-y',
|
||||
false: 'overflow-x'
|
||||
} as const
|
||||
)[String(isVertical)]!
|
||||
const overflow = getStyle(el, key)
|
||||
return ['scroll', 'auto', 'overlay'].some((s) => overflow.includes(s))
|
||||
}
|
||||
|
||||
const getScrollContainer = (
|
||||
el: HTMLElement,
|
||||
isVertical: boolean
|
||||
): Window | HTMLElement | undefined => {
|
||||
if (!isClient) return
|
||||
let parent = el
|
||||
while (parent) {
|
||||
if ([window, document, document.documentElement].includes(parent)) return window
|
||||
if (isScroll(parent, isVertical)) return parent
|
||||
parent = parent.parentNode as HTMLElement
|
||||
}
|
||||
return parent
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
width.value = refSticky.value!.getBoundingClientRect().width! + 'px'
|
||||
if (props.position === 'top') {
|
||||
const offsetTop = refSticky.value?.getBoundingClientRect().top
|
||||
if (offsetTop !== undefined && offsetTop < props.offset) {
|
||||
sticky()
|
||||
return
|
||||
}
|
||||
reset()
|
||||
} else {
|
||||
const offsetBottom = refSticky.value?.getBoundingClientRect().bottom
|
||||
|
||||
if (offsetBottom !== undefined && offsetBottom > windowHeight.value - props.offset) {
|
||||
sticky()
|
||||
return
|
||||
}
|
||||
reset()
|
||||
}
|
||||
}
|
||||
const handleReize = () => {
|
||||
if (isSticky.value && refSticky.value) {
|
||||
width.value = refSticky.value.getBoundingClientRect().width + 'px'
|
||||
}
|
||||
}
|
||||
const sticky = () => {
|
||||
if (isSticky.value) {
|
||||
return
|
||||
}
|
||||
isSticky.value = true
|
||||
}
|
||||
const reset = () => {
|
||||
if (!isSticky.value) {
|
||||
return
|
||||
}
|
||||
width.value = 'auto'
|
||||
isSticky.value = false
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div :style="{ height: height, zIndex: zIndex }" ref="refSticky">
|
||||
<div
|
||||
:class="className"
|
||||
:style="{
|
||||
top: position === 'top' ? offset + 'px' : '',
|
||||
bottom: position !== 'top' ? offset + 'px' : '',
|
||||
zIndex: zIndex,
|
||||
position: isSticky ? 'fixed' : 'static',
|
||||
width: width,
|
||||
height: height
|
||||
}"
|
||||
>
|
||||
<slot>
|
||||
<div>sticky</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/TabMenu/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/TabMenu/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import TabMenu from './src/TabMenu.vue'
|
||||
|
||||
export { TabMenu }
|
||||
226
yudao-ui-admin-vue3/src/components/TabMenu/src/TabMenu.vue
Normal file
226
yudao-ui-admin-vue3/src/components/TabMenu/src/TabMenu.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<script lang="tsx">
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { computed, unref, defineComponent, watch, ref } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { ElScrollbar } from 'element-plus'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { Menu } from '@/components/Menu'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { pathResolve } from '@/utils/routerHelper'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { filterMenusPath, initTabMap, tabPathMap } from './helper'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { isUrl } from '@/utils/is'
|
||||
|
||||
const { getPrefixCls, variables } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('tab-menu')
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TabMenu',
|
||||
setup() {
|
||||
const { push, currentRoute } = useRouter()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const collapse = computed(() => appStore.getCollapse)
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const routers = computed(() => permissionStore.getRouters)
|
||||
|
||||
const tabRouters = computed(() => unref(routers).filter((v) => !v?.meta?.hidden))
|
||||
|
||||
const setCollapse = () => {
|
||||
appStore.setCollapse(!unref(collapse))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => routers.value,
|
||||
(routers: AppRouteRecordRaw[]) => {
|
||||
initTabMap(routers)
|
||||
filterMenusPath(routers, routers)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
const showTitle = ref(true)
|
||||
|
||||
watch(
|
||||
() => collapse.value,
|
||||
(collapse: boolean) => {
|
||||
if (!collapse) {
|
||||
setTimeout(() => {
|
||||
showTitle.value = !collapse
|
||||
}, 200)
|
||||
} else {
|
||||
showTitle.value = !collapse
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 是否显示菜单
|
||||
const showMenu = ref(false)
|
||||
|
||||
// tab高亮
|
||||
const tabActive = ref('')
|
||||
|
||||
// tab点击事件
|
||||
const tabClick = (item: AppRouteRecordRaw) => {
|
||||
if (isUrl(item.path)) {
|
||||
window.open(item.path)
|
||||
return
|
||||
}
|
||||
tabActive.value = item.children ? item.path : item.path.split('/')[0]
|
||||
if (item.children) {
|
||||
showMenu.value = !unref(showMenu)
|
||||
if (unref(showMenu)) {
|
||||
permissionStore.setMenuTabRouters(
|
||||
cloneDeep(item.children).map((v) => {
|
||||
v.path = pathResolve(unref(tabActive), v.path)
|
||||
return v
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
push(item.path)
|
||||
permissionStore.setMenuTabRouters([])
|
||||
showMenu.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 设置高亮
|
||||
const isActice = (currentPath: string) => {
|
||||
const { path } = unref(currentRoute)
|
||||
if (tabPathMap[currentPath].includes(path)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const mouseleave = () => {
|
||||
if (!unref(showMenu)) return
|
||||
showMenu.value = false
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div
|
||||
id={`${variables.namespace}-menu`}
|
||||
class={[
|
||||
prefixCls,
|
||||
'relative bg-[var(--left-menu-bg-color)] top-1px z-999',
|
||||
{
|
||||
'w-[var(--tab-menu-max-width)]': !unref(collapse),
|
||||
'w-[var(--tab-menu-min-width)]': unref(collapse)
|
||||
}
|
||||
]}
|
||||
onMouseleave={mouseleave}
|
||||
>
|
||||
<ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height)-1px)]">
|
||||
<div>
|
||||
{() => {
|
||||
return unref(tabRouters).map((v) => {
|
||||
const item = (
|
||||
v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)
|
||||
? v
|
||||
: {
|
||||
...(v?.children && v?.children[0]),
|
||||
path: pathResolve(v.path, (v?.children && v?.children[0])?.path as string)
|
||||
}
|
||||
) as AppRouteRecordRaw
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls}__item`,
|
||||
'text-center text-12px relative py-12px cursor-pointer',
|
||||
{
|
||||
'is-active': isActice(v.path)
|
||||
}
|
||||
]}
|
||||
onClick={() => {
|
||||
tabClick(item)
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Icon icon={item?.meta?.icon}></Icon>
|
||||
</div>
|
||||
{!unref(showTitle) ? undefined : (
|
||||
<p class="break-words mt-5px px-2px">{t(item.meta?.title)}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls}--collapse`,
|
||||
'text-center h-[var(--tab-menu-collapse-height)] leading-[var(--tab-menu-collapse-height)] cursor-pointer'
|
||||
]}
|
||||
onClick={setCollapse}
|
||||
>
|
||||
<Icon icon={unref(collapse) ? 'ep:d-arrow-right' : 'ep:d-arrow-left'}></Icon>
|
||||
</div>
|
||||
<Menu
|
||||
class={[
|
||||
'!absolute top-0 border-left-1 border-solid border-[var(--left-menu-bg-light-color)]',
|
||||
{
|
||||
'!left-[var(--tab-menu-min-width)]': unref(collapse),
|
||||
'!left-[var(--tab-menu-max-width)]': !unref(collapse),
|
||||
'!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu),
|
||||
'!w-0': !unref(showMenu)
|
||||
}
|
||||
]}
|
||||
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
|
||||
></Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-tab-menu';
|
||||
|
||||
.@{prefix-cls} {
|
||||
transition: all var(--transition-time-02);
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
border-left: 1px solid var(--left-menu-border-color);
|
||||
content: '';
|
||||
}
|
||||
|
||||
&__item {
|
||||
color: var(--left-menu-text-color);
|
||||
transition: all var(--transition-time-02);
|
||||
|
||||
&:hover {
|
||||
color: var(--left-menu-text-active-color);
|
||||
// background-color: var(--left-menu-bg-active-color);
|
||||
}
|
||||
}
|
||||
|
||||
&--collapse {
|
||||
color: var(--left-menu-text-color);
|
||||
background-color: var(--left-menu-bg-light-color);
|
||||
border-top: 1px solid var(--left-menu-border-color);
|
||||
}
|
||||
|
||||
.is-active {
|
||||
color: var(--left-menu-text-active-color);
|
||||
background-color: var(--left-menu-bg-active-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
yudao-ui-admin-vue3/src/components/TabMenu/src/helper.ts
Normal file
52
yudao-ui-admin-vue3/src/components/TabMenu/src/helper.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { getAllParentPath } from '@/components/Menu/src/helper'
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
import { isUrl } from '@/utils/is'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export type TabMapTypes = {
|
||||
[key: string]: string[]
|
||||
}
|
||||
|
||||
export const tabPathMap = reactive<TabMapTypes>({})
|
||||
|
||||
export const initTabMap = (routes: AppRouteRecordRaw[]) => {
|
||||
for (const v of routes) {
|
||||
const meta = (v.meta ?? {}) as RouteMeta
|
||||
if (!meta?.hidden) {
|
||||
tabPathMap[v.path] = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const filterMenusPath = (
|
||||
routes: AppRouteRecordRaw[],
|
||||
allRoutes: AppRouteRecordRaw[]
|
||||
): AppRouteRecordRaw[] => {
|
||||
const res: AppRouteRecordRaw[] = []
|
||||
for (const v of routes) {
|
||||
let data: Nullable<AppRouteRecordRaw> = null
|
||||
const meta = (v.meta ?? {}) as RouteMeta
|
||||
if (!meta.hidden || meta.canTo) {
|
||||
const allParentPaht = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
|
||||
|
||||
const fullPath = isUrl(v.path) ? v.path : allParentPaht.join('/')
|
||||
|
||||
data = cloneDeep(v)
|
||||
data.path = fullPath
|
||||
if (v.children && data) {
|
||||
data.children = filterMenusPath(v.children, allRoutes)
|
||||
}
|
||||
|
||||
if (data) {
|
||||
res.push(data)
|
||||
}
|
||||
|
||||
if (allParentPaht.length && Reflect.has(tabPathMap, allParentPaht[0])) {
|
||||
tabPathMap[allParentPaht[0]].push(fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
9
yudao-ui-admin-vue3/src/components/Table/index.ts
Normal file
9
yudao-ui-admin-vue3/src/components/Table/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import Table from './src/Table.vue'
|
||||
|
||||
export interface TableExpose {
|
||||
setProps: (props: Recordable) => void
|
||||
setColumn: (columnProps: TableSetPropsType[]) => void
|
||||
selections: Recordable[]
|
||||
}
|
||||
|
||||
export { Table }
|
||||
302
yudao-ui-admin-vue3/src/components/Table/src/Table.vue
Normal file
302
yudao-ui-admin-vue3/src/components/Table/src/Table.vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<script lang="tsx">
|
||||
import { ElTable, ElTableColumn, ElPagination } from 'element-plus'
|
||||
import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { setIndex } from './helper'
|
||||
import { getSlot } from '@/utils/tsxHelper'
|
||||
import type { TableProps } from './types'
|
||||
import { set } from 'lodash-es'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Table',
|
||||
props: {
|
||||
pageSize: propTypes.number.def(10),
|
||||
currentPage: propTypes.number.def(1),
|
||||
// 是否多选
|
||||
selection: propTypes.bool.def(true),
|
||||
// 是否所有的超出隐藏,优先级低于schema中的showOverflowTooltip,
|
||||
showOverflowTooltip: propTypes.bool.def(true),
|
||||
// 表头
|
||||
columns: {
|
||||
type: Array as PropType<TableColumn[]>,
|
||||
default: () => []
|
||||
},
|
||||
// 展开行
|
||||
expand: propTypes.bool.def(false),
|
||||
// 是否展示分页
|
||||
pagination: {
|
||||
type: Object as PropType<Pagination>,
|
||||
default: (): Pagination | undefined => undefined
|
||||
},
|
||||
// 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key)
|
||||
reserveSelection: propTypes.bool.def(false),
|
||||
// 加载状态
|
||||
loading: propTypes.bool.def(false),
|
||||
// 是否叠加索引
|
||||
reserveIndex: propTypes.bool.def(false),
|
||||
// 对齐方式
|
||||
align: propTypes.string
|
||||
.validate((v: string) => ['left', 'center', 'right'].includes(v))
|
||||
.def('center'),
|
||||
// 表头对齐方式
|
||||
headerAlign: propTypes.string
|
||||
.validate((v: string) => ['left', 'center', 'right'].includes(v))
|
||||
.def('center'),
|
||||
data: {
|
||||
type: Array as PropType<Recordable[]>,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['update:pageSize', 'update:currentPage', 'register'],
|
||||
setup(props, { attrs, slots, emit, expose }) {
|
||||
const elTableRef = ref<ComponentRef<typeof ElTable>>()
|
||||
|
||||
// 注册
|
||||
onMounted(() => {
|
||||
const tableRef = unref(elTableRef)
|
||||
emit('register', tableRef?.$parent, elTableRef)
|
||||
})
|
||||
|
||||
const pageSizeRef = ref(props.pageSize)
|
||||
|
||||
const currentPageRef = ref(props.currentPage)
|
||||
|
||||
// useTable传入的props
|
||||
const outsideProps = ref<TableProps>({})
|
||||
|
||||
const mergeProps = ref<TableProps>({})
|
||||
|
||||
const getProps = computed(() => {
|
||||
const propsObj = { ...props }
|
||||
Object.assign(propsObj, unref(mergeProps))
|
||||
return propsObj
|
||||
})
|
||||
|
||||
const setProps = (props: TableProps = {}) => {
|
||||
mergeProps.value = Object.assign(unref(mergeProps), props)
|
||||
outsideProps.value = props
|
||||
}
|
||||
|
||||
const setColumn = (columnProps: TableSetPropsType[], columnsChildren?: TableColumn[]) => {
|
||||
const { columns } = unref(getProps)
|
||||
for (const v of columnsChildren || columns) {
|
||||
for (const item of columnProps) {
|
||||
if (v.field === item.field) {
|
||||
set(v, item.path, item.value)
|
||||
} else if (v.children?.length) {
|
||||
setColumn(columnProps, v.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selections = ref<Recordable[]>([])
|
||||
|
||||
const selectionChange = (selection: Recordable[]) => {
|
||||
selections.value = selection
|
||||
}
|
||||
|
||||
expose({
|
||||
setProps,
|
||||
setColumn,
|
||||
selections
|
||||
})
|
||||
|
||||
const pagination = computed(() => {
|
||||
return Object.assign(
|
||||
{
|
||||
small: false,
|
||||
background: true,
|
||||
pagerCount: 5,
|
||||
layout: 'total, sizes, prev, pager, next, jumper',
|
||||
pageSizes: [10, 20, 30, 50, 100],
|
||||
disabled: false,
|
||||
hideOnSinglePage: false,
|
||||
total: 10
|
||||
},
|
||||
unref(getProps).pagination
|
||||
)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => unref(getProps).pageSize,
|
||||
(val: number) => {
|
||||
pageSizeRef.value = val
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => unref(getProps).currentPage,
|
||||
(val: number) => {
|
||||
currentPageRef.value = val
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => pageSizeRef.value,
|
||||
(val: number) => {
|
||||
emit('update:pageSize', val)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => currentPageRef.value,
|
||||
(val: number) => {
|
||||
emit('update:currentPage', val)
|
||||
}
|
||||
)
|
||||
|
||||
const getBindValue = computed(() => {
|
||||
const bindValue: Recordable = { ...attrs, ...props }
|
||||
delete bindValue.columns
|
||||
delete bindValue.data
|
||||
return bindValue
|
||||
})
|
||||
|
||||
const renderTableSelection = () => {
|
||||
const { selection, reserveSelection, align, headerAlign } = unref(getProps)
|
||||
// 渲染多选
|
||||
return selection ? (
|
||||
<ElTableColumn
|
||||
type="selection"
|
||||
reserveSelection={reserveSelection}
|
||||
align={align}
|
||||
headerAlign={headerAlign}
|
||||
width="50"
|
||||
></ElTableColumn>
|
||||
) : undefined
|
||||
}
|
||||
|
||||
const renderTableExpand = () => {
|
||||
const { align, headerAlign, expand } = unref(getProps)
|
||||
// 渲染展开行
|
||||
return expand ? (
|
||||
<ElTableColumn type="expand" align={align} headerAlign={headerAlign}>
|
||||
{{
|
||||
// @ts-ignore
|
||||
default: (data: TableSlotDefault) => getSlot(slots, 'expand', data)
|
||||
}}
|
||||
</ElTableColumn>
|
||||
) : undefined
|
||||
}
|
||||
|
||||
const rnderTreeTableColumn = (columnsChildren: TableColumn[]) => {
|
||||
const { align, headerAlign, showOverflowTooltip } = unref(getProps)
|
||||
return columnsChildren.map((v) => {
|
||||
const props = { ...v }
|
||||
if (props.children) delete props.children
|
||||
return (
|
||||
<ElTableColumn
|
||||
showOverflowTooltip={showOverflowTooltip}
|
||||
align={align}
|
||||
headerAlign={headerAlign}
|
||||
{...props}
|
||||
prop={v.field}
|
||||
>
|
||||
{{
|
||||
default: (data: TableSlotDefault) =>
|
||||
v.children && v.children.length
|
||||
? rnderTableColumn(v.children)
|
||||
: // @ts-ignore
|
||||
getSlot(slots, v.field, data) ||
|
||||
v?.formatter?.(data.row, data.column, data.row[v.field], data.$index) ||
|
||||
data.row[v.field],
|
||||
// @ts-ignore
|
||||
header: getSlot(slots, `${v.field}-header`)
|
||||
}}
|
||||
</ElTableColumn>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const rnderTableColumn = (columnsChildren?: TableColumn[]) => {
|
||||
const {
|
||||
columns,
|
||||
reserveIndex,
|
||||
pageSize,
|
||||
currentPage,
|
||||
align,
|
||||
headerAlign,
|
||||
showOverflowTooltip
|
||||
} = unref(getProps)
|
||||
return [...[renderTableExpand()], ...[renderTableSelection()]].concat(
|
||||
(columnsChildren || columns).map((v) => {
|
||||
// 自定生成序号
|
||||
if (v.type === 'index') {
|
||||
return (
|
||||
<ElTableColumn
|
||||
type="index"
|
||||
index={
|
||||
v.index
|
||||
? v.index
|
||||
: (index) => setIndex(reserveIndex, index, pageSize, currentPage)
|
||||
}
|
||||
align={v.align || align}
|
||||
headerAlign={v.headerAlign || headerAlign}
|
||||
label={v.label}
|
||||
width="65px"
|
||||
></ElTableColumn>
|
||||
)
|
||||
} else {
|
||||
const props = { ...v }
|
||||
if (props.children) delete props.children
|
||||
return (
|
||||
<ElTableColumn
|
||||
showOverflowTooltip={showOverflowTooltip}
|
||||
align={align}
|
||||
headerAlign={headerAlign}
|
||||
{...props}
|
||||
prop={v.field}
|
||||
>
|
||||
{{
|
||||
default: (data: TableSlotDefault) =>
|
||||
v.children && v.children.length
|
||||
? rnderTreeTableColumn(v.children)
|
||||
: // @ts-ignore
|
||||
getSlot(slots, v.field, data) ||
|
||||
v?.formatter?.(data.row, data.column, data.row[v.field], data.$index) ||
|
||||
data.row[v.field],
|
||||
// @ts-ignore
|
||||
header: () => getSlot(slots, `${v.field}-header`) || v.label
|
||||
}}
|
||||
</ElTableColumn>
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div v-loading={unref(getProps).loading}>
|
||||
<ElTable
|
||||
// @ts-ignore
|
||||
ref={elTableRef}
|
||||
data={unref(getProps).data}
|
||||
onSelection-change={selectionChange}
|
||||
{...unref(getBindValue)}
|
||||
>
|
||||
{{
|
||||
default: () => rnderTableColumn(),
|
||||
// @ts-ignore
|
||||
append: () => getSlot(slots, 'append')
|
||||
}}
|
||||
</ElTable>
|
||||
{unref(getProps).pagination ? (
|
||||
<ElPagination
|
||||
v-model:pageSize={pageSizeRef.value}
|
||||
v-model:currentPage={currentPageRef.value}
|
||||
class="mt-10px"
|
||||
{...unref(pagination)}
|
||||
></ElPagination>
|
||||
) : undefined}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
:deep(.el-button.is-text) {
|
||||
margin-left: 0;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
</style>
|
||||
8
yudao-ui-admin-vue3/src/components/Table/src/helper.ts
Normal file
8
yudao-ui-admin-vue3/src/components/Table/src/helper.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const setIndex = (reserveIndex: boolean, index: number, size: number, current: number) => {
|
||||
const newIndex = index + 1
|
||||
if (reserveIndex) {
|
||||
return size * (current - 1) + newIndex
|
||||
} else {
|
||||
return newIndex
|
||||
}
|
||||
}
|
||||
24
yudao-ui-admin-vue3/src/components/Table/src/types.ts
Normal file
24
yudao-ui-admin-vue3/src/components/Table/src/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export type TableProps = {
|
||||
pageSize?: number
|
||||
currentPage?: number
|
||||
// 是否多选
|
||||
selection?: boolean
|
||||
// 是否所有的超出隐藏,优先级低于schema中的showOverflowTooltip,
|
||||
showOverflowTooltip?: boolean
|
||||
// 表头
|
||||
columns?: TableColumn[]
|
||||
// 是否展示分页
|
||||
pagination?: Pagination | undefined
|
||||
// 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key)
|
||||
reserveSelection?: boolean
|
||||
// 加载状态
|
||||
loading?: boolean
|
||||
// 是否叠加索引
|
||||
reserveIndex?: boolean
|
||||
// 对齐方式
|
||||
align?: 'left' | 'center' | 'right'
|
||||
// 表头对齐方式
|
||||
headerAlign?: 'left' | 'center' | 'right'
|
||||
data?: Recordable
|
||||
expand?: boolean
|
||||
} & Recordable
|
||||
3
yudao-ui-admin-vue3/src/components/TagsView/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/TagsView/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import TagsView from './src/TagsView.vue'
|
||||
|
||||
export { TagsView }
|
||||
573
yudao-ui-admin-vue3/src/components/TagsView/src/TagsView.vue
Normal file
573
yudao-ui-admin-vue3/src/components/TagsView/src/TagsView.vue
Normal file
@@ -0,0 +1,573 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch, computed, unref, ref, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { filterAffixTags } from './helper'
|
||||
import { ContextMenu, ContextMenuExpose } from '@/components/ContextMenu'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { useTemplateRefsList } from '@vueuse/core'
|
||||
import { ElScrollbar } from 'element-plus'
|
||||
import { useScrollTo } from '@/hooks/event/useScrollTo'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('tags-view')
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { currentRoute, push, replace } = useRouter()
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const routers = computed(() => permissionStore.getRouters)
|
||||
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
|
||||
const visitedViews = computed(() => tagsViewStore.getVisitedViews)
|
||||
|
||||
const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
|
||||
|
||||
// 初始化tag
|
||||
const initTags = () => {
|
||||
affixTagArr.value = filterAffixTags(unref(routers))
|
||||
for (const tag of unref(affixTagArr)) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
tagsViewStore.addVisitedView(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectedTag = ref<RouteLocationNormalizedLoaded>()
|
||||
|
||||
// 新增tag
|
||||
const addTags = () => {
|
||||
const { name } = unref(currentRoute)
|
||||
if (name) {
|
||||
selectedTag.value = unref(currentRoute)
|
||||
tagsViewStore.addView(unref(currentRoute))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 关闭选中的tag
|
||||
const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
|
||||
if (view?.meta?.affix) return
|
||||
tagsViewStore.delView(view)
|
||||
if (isActive(view)) {
|
||||
toLastView()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭全部
|
||||
const closeAllTags = () => {
|
||||
tagsViewStore.delAllViews()
|
||||
toLastView()
|
||||
}
|
||||
|
||||
// 关闭其他
|
||||
const closeOthersTags = () => {
|
||||
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
|
||||
}
|
||||
|
||||
// 重新加载
|
||||
const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
|
||||
if (!view) return
|
||||
tagsViewStore.delCachedView()
|
||||
const { path, query } = view
|
||||
await nextTick()
|
||||
await replace({
|
||||
path: '/redirect' + path,
|
||||
query: query
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭左侧
|
||||
const closeLeftTags = () => {
|
||||
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
|
||||
}
|
||||
|
||||
// 关闭右侧
|
||||
const closeRightTags = () => {
|
||||
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
|
||||
}
|
||||
|
||||
// 跳转到最后一个
|
||||
const toLastView = () => {
|
||||
const visitedViews = tagsViewStore.getVisitedViews
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
if (latestView) {
|
||||
push(latestView)
|
||||
} else {
|
||||
push('/')
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到选中的tag
|
||||
const moveToCurrentTag = async () => {
|
||||
await nextTick()
|
||||
for (const v of unref(visitedViews)) {
|
||||
if (v.fullPath === unref(currentRoute).path) {
|
||||
moveToTarget(v)
|
||||
if (v.fullPath !== unref(currentRoute).fullPath) {
|
||||
tagsViewStore.updateVisitedView(unref(currentRoute))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tagLinksRefs = useTemplateRefsList<RouterLinkProps>()
|
||||
|
||||
const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
|
||||
const wrap$ = unref(scrollbarRef)?.wrap$
|
||||
let firstTag: Nullable<RouterLinkProps> = null
|
||||
let lastTag: Nullable<RouterLinkProps> = null
|
||||
|
||||
const tagList = unref(tagLinksRefs)
|
||||
// find first tag and last tag
|
||||
if (tagList.length > 0) {
|
||||
firstTag = tagList[0]
|
||||
lastTag = tagList[tagList.length - 1]
|
||||
}
|
||||
if ((firstTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
|
||||
// 直接滚动到0的位置
|
||||
const { start } = useScrollTo({
|
||||
el: wrap$!,
|
||||
position: 'scrollLeft',
|
||||
to: 0,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
} else if ((lastTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
|
||||
// 滚动到最后的位置
|
||||
const { start } = useScrollTo({
|
||||
el: wrap$!,
|
||||
position: 'scrollLeft',
|
||||
to: wrap$!.scrollWidth - wrap$!.offsetWidth,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
} else {
|
||||
// find preTag and nextTag
|
||||
const currentIndex: number = tagList.findIndex(
|
||||
(item) => (item?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath
|
||||
)
|
||||
const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`)
|
||||
|
||||
const prevTag = tgsRefs[currentIndex - 1] as HTMLElement
|
||||
const nextTag = tgsRefs[currentIndex + 1] as HTMLElement
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4
|
||||
|
||||
if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$!.offsetWidth) {
|
||||
const { start } = useScrollTo({
|
||||
el: wrap$!,
|
||||
position: 'scrollLeft',
|
||||
to: afterNextTagOffsetLeft - wrap$!.offsetWidth,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
} else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) {
|
||||
const { start } = useScrollTo({
|
||||
el: wrap$!,
|
||||
position: 'scrollLeft',
|
||||
to: beforePrevTagOffsetLeft,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 是否是当前tag
|
||||
const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
|
||||
return route.path === unref(currentRoute).path
|
||||
}
|
||||
|
||||
// 所有右键菜单组件的元素
|
||||
const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
|
||||
|
||||
// 右键菜单装填改变的时候
|
||||
const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => {
|
||||
if (visible) {
|
||||
for (const v of unref(itemRefs)) {
|
||||
const elDropdownMenuRef = v.elDropdownMenuRef
|
||||
if (tagItem.fullPath !== v.tagItem.fullPath) {
|
||||
elDropdownMenuRef?.handleClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// elscroll 实例
|
||||
const scrollbarRef = ref<ComponentRef<typeof ElScrollbar>>()
|
||||
|
||||
// 保存滚动位置
|
||||
const scrollLeftNumber = ref(0)
|
||||
|
||||
const scroll = ({ scrollLeft }) => {
|
||||
scrollLeftNumber.value = scrollLeft as number
|
||||
}
|
||||
|
||||
// 移动到某个位置
|
||||
const move = (to: number) => {
|
||||
const wrap$ = unref(scrollbarRef)?.wrap$
|
||||
const { start } = useScrollTo({
|
||||
el: wrap$!,
|
||||
position: 'scrollLeft',
|
||||
to: unref(scrollLeftNumber) + to,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTags()
|
||||
addTags()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
() => {
|
||||
addTags()
|
||||
moveToCurrentTag()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:id="prefixCls"
|
||||
:class="prefixCls"
|
||||
class="flex w-full relative bg-[#fff] dark:bg-[var(--el-bg-color)]"
|
||||
>
|
||||
<span
|
||||
:class="`${prefixCls}__tool`"
|
||||
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer"
|
||||
@click="move(-200)"
|
||||
>
|
||||
<Icon
|
||||
icon="ep:d-arrow-left"
|
||||
:color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'"
|
||||
/>
|
||||
</span>
|
||||
<div class="overflow-hidden flex-1">
|
||||
<ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll">
|
||||
<div class="flex h-full">
|
||||
<ContextMenu
|
||||
:ref="itemRefs.set"
|
||||
:schema="[
|
||||
{
|
||||
icon: 'ep:refresh',
|
||||
label: t('common.reload'),
|
||||
disabled: selectedTag?.fullPath !== item.fullPath,
|
||||
command: () => {
|
||||
refreshSelectedTag(item)
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ep:close',
|
||||
label: t('common.closeTab'),
|
||||
disabled: !!visitedViews?.length && selectedTag?.meta.affix,
|
||||
command: () => {
|
||||
closeSelectedTag(item)
|
||||
}
|
||||
},
|
||||
{
|
||||
divided: true,
|
||||
icon: 'ep:d-arrow-left',
|
||||
label: t('common.closeTheLeftTab'),
|
||||
disabled:
|
||||
!!visitedViews?.length &&
|
||||
(item.fullPath === visitedViews[0].fullPath ||
|
||||
selectedTag?.fullPath !== item.fullPath),
|
||||
command: () => {
|
||||
closeLeftTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ep:d-arrow-right',
|
||||
label: t('common.closeTheRightTab'),
|
||||
disabled:
|
||||
!!visitedViews?.length &&
|
||||
(item.fullPath === visitedViews[visitedViews.length - 1].fullPath ||
|
||||
selectedTag?.fullPath !== item.fullPath),
|
||||
command: () => {
|
||||
closeRightTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
divided: true,
|
||||
icon: 'ep:discount',
|
||||
label: t('common.closeOther'),
|
||||
disabled: selectedTag?.fullPath !== item.fullPath,
|
||||
command: () => {
|
||||
closeOthersTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ep:minus',
|
||||
label: t('common.closeAll'),
|
||||
command: () => {
|
||||
closeAllTags()
|
||||
}
|
||||
}
|
||||
]"
|
||||
v-for="item in visitedViews"
|
||||
:key="item.fullPath"
|
||||
:tag-item="item"
|
||||
:class="[
|
||||
`${prefixCls}__item`,
|
||||
item?.meta?.affix ? `${prefixCls}__item--affix` : '',
|
||||
{
|
||||
'is-active': isActive(item)
|
||||
}
|
||||
]"
|
||||
@visible-change="visibleChange"
|
||||
>
|
||||
<div>
|
||||
<router-link :ref="tagLinksRefs.set" :to="{ ...item }" custom v-slot="{ navigate }">
|
||||
<div
|
||||
@click="navigate"
|
||||
class="h-full flex justify-center items-center whitespace-nowrap pl-15px"
|
||||
>
|
||||
<Icon
|
||||
v-if="
|
||||
item?.matched &&
|
||||
item?.matched[1] &&
|
||||
item?.matched[1]?.meta?.icon &&
|
||||
tagsViewIcon
|
||||
"
|
||||
:icon="item?.matched[1]?.meta?.icon"
|
||||
:size="12"
|
||||
class="mr-5px"
|
||||
/>
|
||||
{{ t(item?.meta?.title as string) }}
|
||||
<Icon
|
||||
:class="`${prefixCls}__item--close`"
|
||||
color="#333"
|
||||
icon="ep:close"
|
||||
:size="12"
|
||||
@click.prevent.stop="closeSelectedTag(item)"
|
||||
/>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
<span
|
||||
:class="`${prefixCls}__tool`"
|
||||
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer"
|
||||
@click="move(200)"
|
||||
>
|
||||
<Icon
|
||||
icon="ep:d-arrow-right"
|
||||
:color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
:class="`${prefixCls}__tool`"
|
||||
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer"
|
||||
@click="refreshSelectedTag(selectedTag)"
|
||||
>
|
||||
<Icon
|
||||
icon="ep:refresh-right"
|
||||
:color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'"
|
||||
/>
|
||||
</span>
|
||||
<ContextMenu
|
||||
trigger="click"
|
||||
:schema="[
|
||||
{
|
||||
icon: 'ep:refresh',
|
||||
label: t('common.reload'),
|
||||
command: () => {
|
||||
refreshSelectedTag(selectedTag)
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ep:close',
|
||||
label: t('common.closeTab'),
|
||||
disabled: !!visitedViews?.length && selectedTag?.meta.affix
|
||||
},
|
||||
{
|
||||
divided: true,
|
||||
icon: 'ep:d-arrow-left',
|
||||
label: t('common.closeTheLeftTab'),
|
||||
disabled: !!visitedViews?.length && selectedTag?.fullPath === visitedViews[0].fullPath,
|
||||
command: () => {
|
||||
closeLeftTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ep:d-arrow-right',
|
||||
label: t('common.closeTheRightTab'),
|
||||
disabled:
|
||||
!!visitedViews?.length &&
|
||||
selectedTag?.fullPath === visitedViews[visitedViews.length - 1].fullPath,
|
||||
command: () => {
|
||||
closeRightTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
divided: true,
|
||||
icon: 'ep:discount',
|
||||
label: t('common.closeOther'),
|
||||
command: () => {
|
||||
closeOthersTags()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'ep:minus',
|
||||
label: t('common.closeAll'),
|
||||
command: () => {
|
||||
closeAllTags()
|
||||
}
|
||||
}
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="`${prefixCls}__tool`"
|
||||
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer block"
|
||||
>
|
||||
<Icon
|
||||
icon="ep:setting"
|
||||
:color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'"
|
||||
/>
|
||||
</span>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-tags-view';
|
||||
|
||||
.@{prefix-cls} {
|
||||
:deep(.@{elNamespace}-scrollbar__view) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__tool {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
:deep(span) {
|
||||
color: var(--el-color-black) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(~'100% - 1px');
|
||||
border-right: 1px solid var(--tags-view-border-color);
|
||||
border-left: 1px solid var(--tags-view-border-color);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&__item + &__item {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
height: calc(~'100% - 4px');
|
||||
padding-right: 25px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #d9d9d9;
|
||||
|
||||
&--close {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 5px;
|
||||
display: none;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
&:not(.@{prefix-cls}__item--affix):hover {
|
||||
.@{prefix-cls}__item--close {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__item:not(.is-active) {
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__item.is-active {
|
||||
color: var(--el-color-white);
|
||||
background-color: var(--el-color-primary);
|
||||
.@{prefix-cls}__item--close {
|
||||
:deep(span) {
|
||||
color: var(--el-color-white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.@{prefix-cls} {
|
||||
&__tool {
|
||||
&:hover {
|
||||
:deep(span) {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
border-left: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
height: calc(~'100% - 4px');
|
||||
padding-right: 25px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
&__item:not(.is-active) {
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__item.is-active {
|
||||
color: var(--el-color-white);
|
||||
background-color: var(--el-color-primary);
|
||||
.@{prefix-cls}__item--close {
|
||||
:deep(span) {
|
||||
color: var(--el-color-white) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
21
yudao-ui-admin-vue3/src/components/TagsView/src/helper.ts
Normal file
21
yudao-ui-admin-vue3/src/components/TagsView/src/helper.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { RouteMeta, RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { pathResolve } from '@/utils/routerHelper'
|
||||
|
||||
export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => {
|
||||
let tags: RouteLocationNormalizedLoaded[] = []
|
||||
routes.forEach((route) => {
|
||||
const meta = route.meta as RouteMeta
|
||||
const tagPath = pathResolve(parentPath, route.path)
|
||||
if (meta?.affix) {
|
||||
tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded)
|
||||
}
|
||||
if (route.children) {
|
||||
const tempTags: RouteLocationNormalizedLoaded[] = filterAffixTags(route.children, tagPath)
|
||||
if (tempTags.length >= 1) {
|
||||
tags = [...tags, ...tempTags]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return tags
|
||||
}
|
||||
3
yudao-ui-admin-vue3/src/components/ThemeSwitch/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/ThemeSwitch/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ThemeSwitch from './src/ThemeSwitch.vue'
|
||||
|
||||
export { ThemeSwitch }
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { ElSwitch } from 'element-plus'
|
||||
import { useIcon } from '@/hooks/web/useIcon'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('theme-switch')
|
||||
|
||||
const Sun = useIcon({ icon: 'emojione-monotone:sun', color: '#fde047' })
|
||||
|
||||
const CrescentMoon = useIcon({ icon: 'emojione-monotone:crescent-moon', color: '#fde047' })
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 初始化获取是否是暗黑主题
|
||||
const isDark = ref(appStore.getIsDark)
|
||||
|
||||
// 设置switch的背景颜色
|
||||
const blackColor = 'var(--el-color-black)'
|
||||
|
||||
const themeChange = (val: boolean) => {
|
||||
appStore.setIsDark(val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElSwitch
|
||||
:class="prefixCls"
|
||||
v-model="isDark"
|
||||
inline-prompt
|
||||
:border-color="blackColor"
|
||||
:inactive-color="blackColor"
|
||||
:active-color="blackColor"
|
||||
:active-icon="Sun"
|
||||
:inactive-icon="CrescentMoon"
|
||||
@change="themeChange"
|
||||
/>
|
||||
</template>
|
||||
3
yudao-ui-admin-vue3/src/components/Tooltip/index.ts
Normal file
3
yudao-ui-admin-vue3/src/components/Tooltip/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Tooltip from './src/Tooltip.vue'
|
||||
|
||||
export { Tooltip }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user