@gindow/element-go 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +260 -0
- package/dist/element-go.cjs +1 -0
- package/dist/element-go.d.ts +1 -0
- package/dist/element-go.mjs +2994 -0
- package/dist/resolver.cjs +1 -0
- package/dist/resolver.d.ts +1 -0
- package/dist/resolver.mjs +16 -0
- package/dist/styles/index.css +2 -0
- package/package.json +133 -0
- package/src/assets/avatar.png +0 -0
- package/src/assets/icon.png +0 -0
- package/src/components/ExAssetPreview.vue +55 -0
- package/src/components/ExButton.vue +47 -0
- package/src/components/ExEmpty.vue +26 -0
- package/src/components/ExForm.vue +95 -0
- package/src/components/ExFormField.vue +49 -0
- package/src/components/ExFormSearch.vue +50 -0
- package/src/components/ExFormViewer.vue +51 -0
- package/src/components/ExIcon.vue +33 -0
- package/src/components/ExInputPercentage.vue +36 -0
- package/src/components/ExLayout/account.vue +33 -0
- package/src/components/ExLayout/aside.vue +58 -0
- package/src/components/ExLayout/lang.vue +27 -0
- package/src/components/ExLayout.vue +91 -0
- package/src/components/ExLoading.vue +18 -0
- package/src/components/ExMenu.vue +80 -0
- package/src/components/ExPage.vue +66 -0
- package/src/components/ExPageHeader.vue +34 -0
- package/src/components/ExPagination.vue +34 -0
- package/src/components/ExSelect.vue +28 -0
- package/src/components/ExTable.vue +237 -0
- package/src/components/ExTableColumn.vue +160 -0
- package/src/components/ExUpload.vue +91 -0
- package/src/components/ExUploadAsset.vue +299 -0
- package/src/components/vIcon.vue +23 -0
- package/src/env.d.ts +7 -0
- package/src/hooks/useBreak.ts +23 -0
- package/src/hooks/useChat.ts +135 -0
- package/src/hooks/useIcon.ts +8 -0
- package/src/hooks/useMessage.ts +22 -0
- package/src/hooks/useNanoid.ts +9 -0
- package/src/hooks/useUpload.ts +60 -0
- package/src/index.ts +94 -0
- package/src/libs/auto-imports.d.ts +94 -0
- package/src/libs/components.d.ts +171 -0
- package/src/locale/en-US.ts +49 -0
- package/src/locale/index.ts +73 -0
- package/src/locale/zh-CN.ts +49 -0
- package/src/resolver.ts +26 -0
- package/src/styles/arco.css +179 -0
- package/src/styles/index.css +53 -0
- package/src/types/index.ts +77 -0
- package/src/utils/datetime.ts +42 -0
- package/src/utils/download.ts +11 -0
- package/src/utils/formatter.ts +42 -0
- package/src/utils/get.ts +10 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/params.ts +18 -0
- package/src/utils/platform.ts +38 -0
- package/src/utils/request.ts +144 -0
- package/src/utils/validate.ts +23 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-aside class="main min-h-screen" :style="{ width: collapse ? 'auto' : `${width}px`}">
|
|
3
|
+
<ex-menu :menu="topMenu" :menu-width="width" :active :unique-opened :collapse>
|
|
4
|
+
<div class="logo gap-2 flex-center">
|
|
5
|
+
<el-image :src="logo ?? icon" class="size-6" />
|
|
6
|
+
<span class="title">{{ title }}</span>
|
|
7
|
+
</div>
|
|
8
|
+
</ex-menu>
|
|
9
|
+
</el-aside>
|
|
10
|
+
<el-aside v-if="doubleColumn && subMenu" class="min-h-screen">
|
|
11
|
+
<ex-menu class="secondary" :menu="subMenu" :menu-width>
|
|
12
|
+
<div class="current h-15 px-5 flex-center-items">{{ current.key ? t(current.key) : current.title }}</div>
|
|
13
|
+
</ex-menu>
|
|
14
|
+
</el-aside>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import type { IMenu } from '../../types'
|
|
19
|
+
import { useLocale } from '../../locale'
|
|
20
|
+
import icon from '../../assets/icon.png'
|
|
21
|
+
import ExMenu from '../ExMenu.vue'
|
|
22
|
+
|
|
23
|
+
const { menu, menuWidth, doubleColumn } = defineProps({
|
|
24
|
+
title: { type: String },
|
|
25
|
+
logo: { type: String },
|
|
26
|
+
menu: { type: Array as PropType<IMenu[]>, default: ()=> ([]) },
|
|
27
|
+
menuWidth: { type: Number, default: 160 },
|
|
28
|
+
uniqueOpened: { type: Boolean },
|
|
29
|
+
doubleColumn: { type: Boolean, default: false }, // 双列模式
|
|
30
|
+
collapse: { type: Boolean, default: false },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const { t } = useLocale()
|
|
34
|
+
|
|
35
|
+
// 菜单
|
|
36
|
+
const route = useRoute()
|
|
37
|
+
const width = computed(() => doubleColumn ? undefined : menuWidth)
|
|
38
|
+
const active = computed(() => doubleColumn ? current.value.children?.[0]?.path || current.value.path : undefined) // 双列模式,激活第一个子项
|
|
39
|
+
const current = computed(() => menu.reduce((next, curr) => route.path.startsWith(curr.path) && curr.path.length > next.path.length ? curr : next, { path: '' } as IMenu))
|
|
40
|
+
const subMenu = computed(() => current.value.children?.map((item: any) => ({ ...item, isGroup: true })))
|
|
41
|
+
const topMenu = computed(() => menu.map(item => ({
|
|
42
|
+
...item,
|
|
43
|
+
path: item.children?.[0]?.path || item.path,
|
|
44
|
+
children: doubleColumn ? undefined : item.children,
|
|
45
|
+
})))
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<style scoped>
|
|
49
|
+
.el-aside { --el-aside-width: auto; }
|
|
50
|
+
.el-aside .logo { height: 60px; }
|
|
51
|
+
.el-aside .el-menu .title { font-weight: 500; }
|
|
52
|
+
.el-aside .el-menu--collapse .title { display: none; }
|
|
53
|
+
.el-aside .secondary .current { border-bottom: 1px solid var(--el-border-color-light); }
|
|
54
|
+
.el-aside .secondary :deep(.el-menu-item-group) { margin: 10px 0; }
|
|
55
|
+
.el-aside.main .el-menu { min-width: 120px; }
|
|
56
|
+
.el-aside.main .el-menu.el-menu--collapse { min-width: auto; }
|
|
57
|
+
</style>
|
|
58
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dropdown v-if="langs.length > 1" class="cursor-pointer" placement="bottom-end" @command="toggle">
|
|
3
|
+
<ex-icon icon="translate" />
|
|
4
|
+
<template #dropdown>
|
|
5
|
+
<el-dropdown-menu>
|
|
6
|
+
<el-dropdown-item v-for="{ label, value } in langs" :disabled="locale === value" :command="value">{{ label }}</el-dropdown-item>
|
|
7
|
+
</el-dropdown-menu>
|
|
8
|
+
</template>
|
|
9
|
+
</el-dropdown>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { useLocale, setLocale } from '../../locale'
|
|
14
|
+
import ExIcon from '../ExIcon.vue'
|
|
15
|
+
|
|
16
|
+
const props = defineProps({
|
|
17
|
+
langs: { type: Array as PropType<{ label: string, value: string }[]>, default: () => [] },
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const { locale } = useLocale()
|
|
21
|
+
|
|
22
|
+
// 切换语言
|
|
23
|
+
const toggle = (value: string) => {
|
|
24
|
+
setLocale(value)
|
|
25
|
+
localStorage.setItem('locale', value)
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-container class="min-h-screen">
|
|
3
|
+
<ex-aside v-if="!meta.wide && isDesktop" v-bind="$attrs" :logo :collapse />
|
|
4
|
+
<el-drawer v-else v-model="show" direction="ltr" :with-header="false" size="auto" body-class="p-0!">
|
|
5
|
+
<ex-aside v-bind="$attrs" :logo :collapse="false" :double-column="false" />
|
|
6
|
+
</el-drawer>
|
|
7
|
+
<el-container class="min-h-full">
|
|
8
|
+
<el-header>
|
|
9
|
+
<el-row class="h-full">
|
|
10
|
+
<el-col :span="8" class="flex-center-items">
|
|
11
|
+
<ex-icon icon="hamburger-button" @click="toggle" />
|
|
12
|
+
</el-col>
|
|
13
|
+
<el-col :span="16" class="flex-center-end gap-4">
|
|
14
|
+
<slot />
|
|
15
|
+
<template v-for="item in layouts" :key="item">
|
|
16
|
+
<div v-if="item === 'div'" class="h-4 w-px bg-color-page" />
|
|
17
|
+
<ex-lang v-else-if="item === 'lang' && layout.langs" :langs />
|
|
18
|
+
<ex-icon v-else-if="item === 'dark'" :icon="isDark ? 'sun' : 'moon'" @click="toggleDark()" />
|
|
19
|
+
<!-- <ex-account v-else-if="item === 'account'" /> -->
|
|
20
|
+
</template>
|
|
21
|
+
<ex-icon v-if="onClose" icon="close" @click="emit('close')" />
|
|
22
|
+
</el-col>
|
|
23
|
+
</el-row>
|
|
24
|
+
</el-header>
|
|
25
|
+
<el-main :class="{ 'border-transparent!': isDark }">
|
|
26
|
+
<ex-empty v-if="!hasPermission" :description="t('core.noPermission')" />
|
|
27
|
+
<router-view v-else-if="meta.alive" v-slot="{ Component }">
|
|
28
|
+
<keep-alive><component :is="Component" /></keep-alive>
|
|
29
|
+
</router-view>
|
|
30
|
+
<router-view v-else />
|
|
31
|
+
</el-main>
|
|
32
|
+
</el-container>
|
|
33
|
+
</el-container>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { useLocale } from '../locale'
|
|
38
|
+
import { useBreak } from '../hooks/useBreak'
|
|
39
|
+
import ExAside from './ExLayout/aside.vue'
|
|
40
|
+
// import ExAccount from './ExLayout/account.vue'
|
|
41
|
+
import ExLang from './ExLayout/lang.vue'
|
|
42
|
+
import ExEmpty from './ExEmpty.vue'
|
|
43
|
+
import ExIcon from './ExIcon.vue'
|
|
44
|
+
|
|
45
|
+
defineOptions({ inheritAttrs: false })
|
|
46
|
+
|
|
47
|
+
const props = defineProps({
|
|
48
|
+
layout: { type: String, default: 'lang,dark,account' }, // 布局顺序
|
|
49
|
+
hasPermission: { type: Boolean, default: true },
|
|
50
|
+
logo: { type: String },
|
|
51
|
+
langs: { type: Array as PropType<{ label: string, value: string }[]>, default: () => [] },
|
|
52
|
+
agent: { type: Object, default: () => ({}) },
|
|
53
|
+
onClose: { type: Function },
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits(['close'])
|
|
57
|
+
|
|
58
|
+
const { t } = useLocale()
|
|
59
|
+
const { isDesktop } = useBreak()
|
|
60
|
+
|
|
61
|
+
// 菜单
|
|
62
|
+
const route = useRoute()
|
|
63
|
+
const meta = computed(() => (route?.meta ?? {}) as { alive?: boolean, wide?: boolean, permission?: string })
|
|
64
|
+
const collapse = ref(localStorage.getItem('collapse') === 'true')
|
|
65
|
+
const toggle = () => isDesktop.value
|
|
66
|
+
? localStorage.setItem('collapse', (collapse.value = !collapse.value).toString())
|
|
67
|
+
: show.value = true
|
|
68
|
+
|
|
69
|
+
const layout = computed(() => ({
|
|
70
|
+
agent: props.agent.state,
|
|
71
|
+
langs: props.langs.length > 1,
|
|
72
|
+
}))
|
|
73
|
+
|
|
74
|
+
const layouts = computed(() => {
|
|
75
|
+
const layouts = props.layout.split(',') || []
|
|
76
|
+
if (layout.value.langs && !layouts.includes('lang')) layouts.push('lang')
|
|
77
|
+
return layouts
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// 手机菜单
|
|
81
|
+
const show = ref(false)
|
|
82
|
+
|
|
83
|
+
// 暗黑模式
|
|
84
|
+
const isDark = useDark()
|
|
85
|
+
const toggleDark = useToggle(isDark)
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<style scoped>
|
|
89
|
+
.el-header { border-bottom: 1px solid var(--el-border-color-lighter); }
|
|
90
|
+
.el-main { background-color: var(--el-bg-color-page); }
|
|
91
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="ex-loading flex-center" :style="{ padding }">
|
|
3
|
+
<ex-icon icon="loading" loading :size />
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import ExIcon from './ExIcon.vue'
|
|
9
|
+
|
|
10
|
+
defineProps({
|
|
11
|
+
size: { type: Number, default: 24 },
|
|
12
|
+
padding: { type: String, default: '20px' },
|
|
13
|
+
})
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<style scoped>
|
|
17
|
+
.ex-loading { color: var(--el-text-color-placeholder); }
|
|
18
|
+
</style>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-menu router :unique-opened :default-active :collapse-transition="false">
|
|
3
|
+
<slot />
|
|
4
|
+
<template v-for="{ path: index, title, icon, disabled, children, isGroup } in filter(menu)">
|
|
5
|
+
<el-menu-item-group v-if="children && isGroup" :title>
|
|
6
|
+
<ex-menu-item v-for="{ path: index, title, disabled } in children" :index :title :width :disabled />
|
|
7
|
+
</el-menu-item-group>
|
|
8
|
+
<el-sub-menu v-else-if="children" :index :disabled>
|
|
9
|
+
<template #title>
|
|
10
|
+
<ex-menu-text :title :width :icon />
|
|
11
|
+
</template>
|
|
12
|
+
<template v-for="{ path: index, title, disabled, children: groups } in children">
|
|
13
|
+
<el-menu-item-group v-if="groups?.length" :title>
|
|
14
|
+
<ex-menu-item v-for="{ path: index, title, disabled } in groups" :index :title :width :disabled />
|
|
15
|
+
</el-menu-item-group>
|
|
16
|
+
<ex-menu-item v-else :index :title :width :disabled />
|
|
17
|
+
</template>
|
|
18
|
+
</el-sub-menu>
|
|
19
|
+
<ex-menu-item v-else :index :title :width :icon :disabled />
|
|
20
|
+
</template>
|
|
21
|
+
</el-menu>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="tsx">
|
|
25
|
+
import type { IMenu } from '../types'
|
|
26
|
+
import { useLocale } from '../locale'
|
|
27
|
+
|
|
28
|
+
const { menu, menuWidth, active } = defineProps({
|
|
29
|
+
menu: { type: Array as PropType<IMenu[]>, default: () => [] },
|
|
30
|
+
menuWidth: { type: Number },
|
|
31
|
+
active: { type: String },
|
|
32
|
+
uniqueOpened: { type: Boolean },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const { t } = useLocale()
|
|
36
|
+
|
|
37
|
+
const route = useRoute()
|
|
38
|
+
const width = computed(() => menuWidth ? (menuWidth - 93) + 'px' : undefined)
|
|
39
|
+
const paths = computed(() => menu.flatMap(item => item.children || item).map(({ path }) => path))
|
|
40
|
+
const defaultActive = computed(() => active || paths.value.reduce((next, curr) => route.path.startsWith(curr) && curr.length > next.length ? curr : next, ''))
|
|
41
|
+
|
|
42
|
+
const filter = (items: IMenu[]) => items.filter(({ hidden }) => !hidden).map(item => {
|
|
43
|
+
item.title = item.key ? t(item.key) : item.title
|
|
44
|
+
if (item.children) item.children = filter(item.children)
|
|
45
|
+
return item
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const ExMenuText = defineComponent({
|
|
49
|
+
props: {
|
|
50
|
+
title: { type: String, required: true },
|
|
51
|
+
width: { type: String },
|
|
52
|
+
icon: { type: String },
|
|
53
|
+
},
|
|
54
|
+
setup(props) {
|
|
55
|
+
return () => <>
|
|
56
|
+
<ex-icon icon={props.icon} />
|
|
57
|
+
<span style={{ width: props.width }}>{ props.title }</span>
|
|
58
|
+
</>
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const ExMenuItem = defineComponent({
|
|
63
|
+
props: {
|
|
64
|
+
index: { type: String, required: true },
|
|
65
|
+
title: { type: String, required: true },
|
|
66
|
+
width: { type: String },
|
|
67
|
+
icon: { type: String },
|
|
68
|
+
disabled: { type: Boolean },
|
|
69
|
+
},
|
|
70
|
+
setup(props) {
|
|
71
|
+
return () => <el-menu-item index={props.index} disabled={props.disabled}>
|
|
72
|
+
<ExMenuText title={props.title} width={props.width} icon={props.icon} />
|
|
73
|
+
</el-menu-item>
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<style scoped>
|
|
79
|
+
.el-menu { --el-menu-level-padding: 29px; --el-menu-item-height: 44px; --el-menu-sub-item-height: 40px; min-height: 100%; --el-menu-border-color: var(--el-border-color-lighter); background-color: transparent; }
|
|
80
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-container class="ex-page">
|
|
3
|
+
<el-aside v-if="menu.length" width="auto">
|
|
4
|
+
<el-menu :default-active="active">
|
|
5
|
+
<el-menu-item v-for="(item, index) in menu" :index="item.name || index.toString()" @click="onClick(item)">
|
|
6
|
+
<ex-icon v-if="item.icon" :icon="item.icon" />
|
|
7
|
+
<span>{{ item.label }}</span>
|
|
8
|
+
</el-menu-item>
|
|
9
|
+
</el-menu>
|
|
10
|
+
</el-aside>
|
|
11
|
+
<el-container>
|
|
12
|
+
<el-header v-if="title || $slots.title || $slots.header">
|
|
13
|
+
<slot v-if="$slots.header" name="header" />
|
|
14
|
+
<ex-page-header v-else-if="title || $slots.title" :back :icon :title>
|
|
15
|
+
<template v-if="$slots.title" #title><slot name="title" /></template>
|
|
16
|
+
<slot name="extra" />
|
|
17
|
+
</ex-page-header>
|
|
18
|
+
</el-header>
|
|
19
|
+
<el-main>
|
|
20
|
+
<slot />
|
|
21
|
+
</el-main>
|
|
22
|
+
</el-container>
|
|
23
|
+
</el-container>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import ExPageHeader from './ExPageHeader.vue'
|
|
28
|
+
import ExIcon from './ExIcon.vue'
|
|
29
|
+
|
|
30
|
+
interface IMenuItem {
|
|
31
|
+
label: string
|
|
32
|
+
name?: string
|
|
33
|
+
icon?: string
|
|
34
|
+
to?: string
|
|
35
|
+
onclick?: () => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const props = defineProps({
|
|
39
|
+
back: { type: Boolean, default: false },
|
|
40
|
+
title: { type: String },
|
|
41
|
+
icon: { type: String },
|
|
42
|
+
menu: { type: Array as PropType<IMenuItem[]>, default: () => [] },
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const active = defineModel('active', { type: String })
|
|
46
|
+
|
|
47
|
+
const { push } = useRouter()
|
|
48
|
+
|
|
49
|
+
function onClick(item: IMenuItem) {
|
|
50
|
+
if (item.to) push(item.to)
|
|
51
|
+
item.onclick?.()
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<style scoped>
|
|
56
|
+
.ex-page { min-height: 100%; }
|
|
57
|
+
.ex-page .el-aside {
|
|
58
|
+
--el-menu-bg-color: transparent;
|
|
59
|
+
--el-menu-border-color: transparent;
|
|
60
|
+
--el-menu-item-height: 40px;
|
|
61
|
+
margin-right: 20px;
|
|
62
|
+
}
|
|
63
|
+
.ex-page .el-container { background-color: var(--el-bg-color); border-radius: var(--ex-page-border-radius, 0); }
|
|
64
|
+
.ex-page .el-header { --el-header-height: auto; }
|
|
65
|
+
.ex-page .el-main { --el-main-padding: var(--ex-page-padding, 20px); }
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-row class="h-15">
|
|
3
|
+
<el-col :md="8" class="flex-center-items">
|
|
4
|
+
<el-space class="mr-3" v-if="showBack">
|
|
5
|
+
<el-button link :icon="i('arrow-left')" @click="onBack">{{ t('core.back') }}</el-button>
|
|
6
|
+
<el-divider direction="vertical" />
|
|
7
|
+
</el-space>
|
|
8
|
+
<span class="text-lg mr-2 flex-center-items" v-if="icon"><ex-icon :icon /></span>
|
|
9
|
+
<slot v-if="$slots.title" name="title" />
|
|
10
|
+
<span v-else class="text-lg font-bold">{{ title }}</span>
|
|
11
|
+
</el-col>
|
|
12
|
+
<el-col :md="16" class="flex-center-end"><slot /></el-col>
|
|
13
|
+
</el-row>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { useLocale } from '../locale'
|
|
18
|
+
import { useIcon } from '../hooks/useIcon'
|
|
19
|
+
import ExIcon from './ExIcon.vue'
|
|
20
|
+
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
title: { type: String, default: '' },
|
|
23
|
+
back: { type: Boolean, default: false },
|
|
24
|
+
icon: { type: String },
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const { i } = useIcon()
|
|
28
|
+
const { t } = useLocale()
|
|
29
|
+
const router = useRouter()
|
|
30
|
+
|
|
31
|
+
const canBack = computed(() => typeof window !== 'undefined' && !!window.history.state?.back)
|
|
32
|
+
const showBack = computed(() => props.back && canBack.value)
|
|
33
|
+
const onBack = () => router?.back()
|
|
34
|
+
</script>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-pagination class="my-8 justify-center" background
|
|
3
|
+
:layout :pageSizes :pager-count="5"
|
|
4
|
+
:current-page="page" @current-change="currentChange"
|
|
5
|
+
:page-size="size" @size-change="sizeChange" />
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import { computed } from 'vue'
|
|
10
|
+
import { useBreak } from '../hooks/useBreak'
|
|
11
|
+
|
|
12
|
+
const { isDesktop } = useBreak()
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits(['currentChange', 'sizeChange'])
|
|
15
|
+
const page = defineModel('page', { type: Number, required: true })
|
|
16
|
+
const size = defineModel('size', { type: Number, required: true })
|
|
17
|
+
|
|
18
|
+
const layout = computed(() => isDesktop.value ? 'total,sizes,prev,pager,next' : 'total,prev,next')
|
|
19
|
+
const pageSizes = computed(() => {
|
|
20
|
+
const base = [15, 30, 60, 120, 240]
|
|
21
|
+
if (!base.includes(size.value)) {
|
|
22
|
+
base.push(size.value)
|
|
23
|
+
base.sort((a, b) => a - b)
|
|
24
|
+
}
|
|
25
|
+
return base
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const currentChange = (value: number) => emit('currentChange', page.value = value)
|
|
29
|
+
const sizeChange = (value: number) => emit('sizeChange', size.value = value)
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<style scoped>
|
|
33
|
+
:deep(.el-pagination__sizes .el-select) { width: 108px; }
|
|
34
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-select filterable clearable>
|
|
3
|
+
<el-option v-for="{ label, value } in data" :label :value>
|
|
4
|
+
<slot :row="{ label, value }" />
|
|
5
|
+
</el-option>
|
|
6
|
+
</el-select>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import type { IModel } from '../types'
|
|
11
|
+
|
|
12
|
+
const { options, props, params, onSelect } = defineProps({
|
|
13
|
+
options: { type: Array as PropType<any>, default: () => [] },
|
|
14
|
+
props: { type: Object, default: () => ({}) },
|
|
15
|
+
params: { type: Object, default: () => ({}) },
|
|
16
|
+
onSelect: { type: Function },
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const label = computed(() => props?.label || 'label')
|
|
20
|
+
const value = computed(() => props?.value || 'value')
|
|
21
|
+
|
|
22
|
+
const items = ref<IModel[]>(options)
|
|
23
|
+
const data = computed(() => items.value.map(item => ({ label: item[label.value], value: item[value.value] })))
|
|
24
|
+
|
|
25
|
+
watch(() => options, value => items.value = value)
|
|
26
|
+
|
|
27
|
+
onSelect ? onSelect(params).then(({ data }: { data: IModel[] }) => items.value = data) : null
|
|
28
|
+
</script>
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ex-form-search v-if="filter" v-model:params="params" :filter :search :placeholder @search="onSearch" @reset="onSearch" />
|
|
3
|
+
<ex-loading v-if="!loaded" />
|
|
4
|
+
<ex-empty v-else-if="!data.length" />
|
|
5
|
+
<template v-else>
|
|
6
|
+
<!-- 数据表格 -->
|
|
7
|
+
<el-table ref="refTable" class="ex-table" v-bind="$attrs" v-loading="loading" :data :size="density" row-key="id">
|
|
8
|
+
<ex-table-column v-if="sorting" type="sort" /><!-- 排序字段 -->
|
|
9
|
+
<ex-table-column v-for="col in columns" v-bind="col">
|
|
10
|
+
<template v-if="col.customRender" #default="{ row }">
|
|
11
|
+
<div v-html="col.customRender(row, col)" />
|
|
12
|
+
</template>
|
|
13
|
+
<template v-else-if="!col.type" #default="{ row }">
|
|
14
|
+
<slot name="cell" :row :col>{{ col.value && typeof col.value === 'function' ? col.value(row, col) : col.value ?? get(row, col.prop) ?? col.defaultValue }}</slot>
|
|
15
|
+
</template>
|
|
16
|
+
</ex-table-column>
|
|
17
|
+
<slot />
|
|
18
|
+
</el-table>
|
|
19
|
+
<!-- 批量操作 -->
|
|
20
|
+
<el-space class="mt-5">
|
|
21
|
+
<el-checkbox v-if="sortable" v-model="sorting" :label="t('core.sort')" border />
|
|
22
|
+
<slot v-if="$slots.batch" name="batch" />
|
|
23
|
+
</el-space>
|
|
24
|
+
<!-- 分页操作 -->
|
|
25
|
+
<ex-pagination v-if="pager && total" v-model:size="size" v-model:page="page" :total />
|
|
26
|
+
</template>
|
|
27
|
+
<!-- 编辑表单 -->
|
|
28
|
+
<ex-form v-model:show="open" v-model="model" :title :fields :rules :formColumns :loading="saving" destroyOnClose :labelPosition :width @submit="onSubmit" >
|
|
29
|
+
<slot name="formItem" />
|
|
30
|
+
<template #field="{ field, model }: { field: any, model: IModel}">
|
|
31
|
+
<slot name="formField" :field :model />
|
|
32
|
+
</template>
|
|
33
|
+
</ex-form>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import type { FormRules, TableInstance } from 'element-plus'
|
|
38
|
+
import type { IResult, IModel } from '../types'
|
|
39
|
+
import { watchOnce } from '@vueuse/core'
|
|
40
|
+
import { cloneDeep } from 'lodash'
|
|
41
|
+
import { usePagination } from 'vue-request'
|
|
42
|
+
import { useLocale } from '../locale'
|
|
43
|
+
import { useMessage } from '../hooks/useMessage'
|
|
44
|
+
import ExTableColumn from './ExTableColumn.vue'
|
|
45
|
+
import ExEmpty from './ExEmpty.vue'
|
|
46
|
+
import ExForm from './ExForm.vue'
|
|
47
|
+
import ExFormSearch from './ExFormSearch.vue'
|
|
48
|
+
import ExLoading from './ExLoading.vue'
|
|
49
|
+
import ExPagination from './ExPagination.vue'
|
|
50
|
+
import Sortablejs from 'sortablejs'
|
|
51
|
+
import get from 'lodash/get'
|
|
52
|
+
|
|
53
|
+
defineOptions({ inheritAttrs: false })
|
|
54
|
+
|
|
55
|
+
const {
|
|
56
|
+
columns: columnData, actions: actionData, model: modelData, actionShow, formColumns, formWidth,
|
|
57
|
+
onSelect, onDelete, onUpdate, onInsert
|
|
58
|
+
} = defineProps({
|
|
59
|
+
filter: { type: Object as PropType<any>, default: () => {} },
|
|
60
|
+
search: { type: Boolean, default: true },
|
|
61
|
+
placeholder: { type: String },
|
|
62
|
+
pager: { type: Boolean, default: true },
|
|
63
|
+
columns: { type: Array as PropType<IModel[]>, default: () => [] },
|
|
64
|
+
actions: { type: Array as PropType<IModel[]>, default: () => [] },
|
|
65
|
+
actionShow: { type: Number, default: 1 },
|
|
66
|
+
sortable: { type: Boolean, default: false },
|
|
67
|
+
formColumns: { type: Number, default: 1 }, // 表单列数
|
|
68
|
+
formWidth: { type: String }, // 表单宽度
|
|
69
|
+
labelPosition: { type: String as PropType<'top' | 'left' | 'right'>, default: 'top' },
|
|
70
|
+
model: { type: Object as PropType<any>, default: () => {} },
|
|
71
|
+
fields: { type: Object as PropType<any>, default: () => {} },
|
|
72
|
+
rules: { type: Object as PropType<FormRules>, default: () => {} }, // 校验规则
|
|
73
|
+
density: { type: String as PropType<'small' | 'default' | 'large'> },
|
|
74
|
+
onSelect: { type: Function as PropType<any> },
|
|
75
|
+
onDelete: { type: Function },
|
|
76
|
+
onInsert: { type: Function },
|
|
77
|
+
onUpdate: { type: Function },
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const emit = defineEmits(['search', 'sorted'])
|
|
81
|
+
|
|
82
|
+
const loaded = defineModel('loaded', { type: Boolean, default: false })
|
|
83
|
+
const loading = defineModel('loading', { type: Boolean, default: false })
|
|
84
|
+
const page = defineModel('page', { type: Number, default: 1 })
|
|
85
|
+
const size = defineModel('size', { type: Number, default: 15 })
|
|
86
|
+
const data = defineModel('data', { type: Array as PropType<IModel[]>, default: () => [] })
|
|
87
|
+
const total = defineModel('total', { type: Number, default: 0 })
|
|
88
|
+
const params = defineModel('params', { type: Object as PropType<any>, default: () => ({ filter: {}, search: '' }) })
|
|
89
|
+
|
|
90
|
+
const { t } = useLocale()
|
|
91
|
+
const { confirm, success, error } = useMessage()
|
|
92
|
+
|
|
93
|
+
const actions = computed(() => {
|
|
94
|
+
const actions = [ ...actionData ]
|
|
95
|
+
if (onUpdate) {
|
|
96
|
+
const indexUpdate = actions.findIndex(({ type }) => type === 'update')
|
|
97
|
+
const valueUpdate = { label: t('core.edit'), click: (row: IModel) => modify(row), ...actions[indexUpdate] }
|
|
98
|
+
indexUpdate === -1 ? actions.push(valueUpdate) : actions.splice(indexUpdate, 1, valueUpdate)
|
|
99
|
+
}
|
|
100
|
+
if (onDelete) {
|
|
101
|
+
const indexDelete = actions.findIndex(({ type }) => type === 'delete')
|
|
102
|
+
const valueDelete = { label: t('core.delete'), click: (row: IModel) => remove(row), ...actions[indexDelete] }
|
|
103
|
+
indexDelete === -1 ? actions.push(valueDelete) : actions.splice(indexDelete, 1, valueDelete)
|
|
104
|
+
}
|
|
105
|
+
return actions
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const show = computed(() => actions.value.filter((_, index) => index < actionShow))
|
|
109
|
+
const more = computed(() => actions.value.filter((_, index) => index >= actionShow))
|
|
110
|
+
const columns = computed(() => {
|
|
111
|
+
const columns = [ ...columnData ]
|
|
112
|
+
if (actions.value.length) {
|
|
113
|
+
const index = columns.findIndex(({ type }) => type === 'action')
|
|
114
|
+
const width = show.value.length * 80 + Number(more.value.length > 0) * 30
|
|
115
|
+
const value = { type: 'action', align: 'right', width, ...columns.find(({ type }) => type === 'action') }
|
|
116
|
+
index === -1 ? columns.push(value) : columns.splice(index, 1, value)
|
|
117
|
+
}
|
|
118
|
+
return columns
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
provide('show', show)
|
|
122
|
+
provide('more', more)
|
|
123
|
+
|
|
124
|
+
const refTable = useTemplateRef<TableInstance>('refTable')
|
|
125
|
+
const clearSelection = () => refTable.value?.clearSelection()
|
|
126
|
+
const toggleRowSelection = (row: any, selected?: boolean, ignoreSelectable?: boolean) => refTable.value?.toggleRowSelection(row, selected, ignoreSelectable)
|
|
127
|
+
|
|
128
|
+
// 查询指令
|
|
129
|
+
const { run, refresh } = onSelect ? usePagination(onSelect, {
|
|
130
|
+
defaultParams: [{ page: 1, size: size.value, ...params.value }],
|
|
131
|
+
onBefore: () => loading.value = true,
|
|
132
|
+
onSuccess: (result: IResult) => {
|
|
133
|
+
data.value = result.data as IModel[]
|
|
134
|
+
size.value = result.meta?.pagination?.per_page || 15
|
|
135
|
+
page.value = result.meta?.pagination?.current_page || 1
|
|
136
|
+
total.value = result.meta?.pagination?.total || 0
|
|
137
|
+
loading.value = false
|
|
138
|
+
loaded.value = true
|
|
139
|
+
},
|
|
140
|
+
pagination: {
|
|
141
|
+
currentKey: 'page',
|
|
142
|
+
pageSizeKey: 'size',
|
|
143
|
+
},
|
|
144
|
+
}) : { run: () => {}, refresh: () => {} }
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
const reload = () => run({ ...params.value, size: size.value, page: 1 })
|
|
148
|
+
|
|
149
|
+
const onSearch = () => onSelect ? reload() : emit('search')
|
|
150
|
+
|
|
151
|
+
watch(() => [page.value, size.value], () => run({ ...params.value, size: size.value, page: page.value }))
|
|
152
|
+
|
|
153
|
+
// 编辑指令
|
|
154
|
+
const width = computed(() => formWidth ?? Math.max(formColumns * 320, 500) + 'px')
|
|
155
|
+
const open = ref(false)
|
|
156
|
+
const model = ref()
|
|
157
|
+
const modify = (row: IModel) => {
|
|
158
|
+
model.value = { ...row }
|
|
159
|
+
isNew.value = false
|
|
160
|
+
open.value = true
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 添加指令
|
|
164
|
+
const create = () => {
|
|
165
|
+
model.value = { ...modelData }
|
|
166
|
+
isNew.value = true
|
|
167
|
+
open.value = true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 提交表单
|
|
171
|
+
const saving = ref(false)
|
|
172
|
+
const isNew = ref(false)
|
|
173
|
+
const title = computed(() => isNew.value ? t('core.create') : t('core.edit'))
|
|
174
|
+
const store = () => isNew.value ? onInsert!(model.value) : onUpdate!(model.value.id, model.value)
|
|
175
|
+
const onSubmit = () => {
|
|
176
|
+
saving.value = true
|
|
177
|
+
store().then(() => {
|
|
178
|
+
success(t('core.message.successful'))
|
|
179
|
+
open.value = false
|
|
180
|
+
isNew.value ? reload() : refresh()
|
|
181
|
+
}).finally(() => saving.value = false)
|
|
182
|
+
.catch(({ message }: { message: string }) => error(message))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 删除指令
|
|
186
|
+
const remove = ({ id, name ='' }: IModel) => confirm(t('core.message.delConfirm', { name }), t('core.message.confirm'), { type: 'warning' })
|
|
187
|
+
.then(() => onDelete!(id).then(() => refresh()).catch((res: { message: string }) => error(res.message)))
|
|
188
|
+
.catch(() => {})
|
|
189
|
+
|
|
190
|
+
// 排序功能
|
|
191
|
+
const sorting = ref(false)
|
|
192
|
+
const sorted = ref(false)
|
|
193
|
+
const sorter = shallowRef<Sortablejs>()
|
|
194
|
+
const sort = () => nextTick(() => {
|
|
195
|
+
const el = document.querySelector('.el-table__body-wrapper tbody') as HTMLElement
|
|
196
|
+
sorter.value = Sortablejs.create(el, {
|
|
197
|
+
animation: 150,
|
|
198
|
+
preventOnFilter: false,
|
|
199
|
+
onEnd: ({ oldIndex, newIndex }) => {
|
|
200
|
+
const value = cloneDeep(data.value)
|
|
201
|
+
const movedItem = value.splice(oldIndex!, 1)[0]
|
|
202
|
+
if (!movedItem) return
|
|
203
|
+
value.splice(newIndex!, 0, movedItem)
|
|
204
|
+
data.value = []
|
|
205
|
+
sorted.value = true
|
|
206
|
+
nextTick(() => {
|
|
207
|
+
data.value = value
|
|
208
|
+
sort()
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
onBeforeUnmount(() => sorter.value?.destroy())
|
|
215
|
+
|
|
216
|
+
watchOnce(() => sorting.value, () => sort())
|
|
217
|
+
|
|
218
|
+
watch(() => sorting.value, () => {
|
|
219
|
+
if (sorting.value || !sorted.value) return
|
|
220
|
+
sorted.value = false
|
|
221
|
+
emit('sorted', data.value)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
defineExpose({
|
|
225
|
+
create,
|
|
226
|
+
reload,
|
|
227
|
+
refresh,
|
|
228
|
+
clearSelection,
|
|
229
|
+
toggleRowSelection,
|
|
230
|
+
})
|
|
231
|
+
</script>
|
|
232
|
+
|
|
233
|
+
<style scoped>
|
|
234
|
+
/*.el-table { --el-table-header-bg-color: transparent; --el-table-tr-bg-color: transparent; } */
|
|
235
|
+
.el-dropdown { vertical-align: middle; }
|
|
236
|
+
.el-dropdown .el-button { font-size: 18px; margin-left: 12px; }
|
|
237
|
+
</style>
|