@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
package/package.json
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gindow/element-go",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "基于 Element Plus 的桌面端扩展组件库",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/element-go.cjs",
|
|
8
|
+
"types": "./dist/element-go.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/element-go.d.ts",
|
|
12
|
+
"import": "./dist/element-go.mjs",
|
|
13
|
+
"require": "./dist/element-go.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./resolver": {
|
|
16
|
+
"types": "./dist/resolver.d.ts",
|
|
17
|
+
"import": "./dist/resolver.mjs",
|
|
18
|
+
"require": "./dist/resolver.cjs"
|
|
19
|
+
},
|
|
20
|
+
"./styles/index.css": "./dist/styles/index.css",
|
|
21
|
+
"./styles/arco.css": "./src/styles/arco.css",
|
|
22
|
+
"./src/*": "./src/*",
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": [
|
|
31
|
+
"**/*.css"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "vue-tsc --noEmit && vite build"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@iconify/vue": "^5.0.0",
|
|
38
|
+
"@gindow/vue": "^1.0.3",
|
|
39
|
+
"@vueuse/core": "^12.0.0",
|
|
40
|
+
"axios": "^1.6.0",
|
|
41
|
+
"compressorjs": "^1.2.0",
|
|
42
|
+
"dayjs": "^1.11.0",
|
|
43
|
+
"element-plus": "^2.7.0",
|
|
44
|
+
"file-type-checker": "^1.0.0",
|
|
45
|
+
"heic2any": "^0.0.4",
|
|
46
|
+
"js-cookie": "^3.0.0",
|
|
47
|
+
"lodash": "^4.17.0",
|
|
48
|
+
"nanoid": "^5.0.0",
|
|
49
|
+
"rxjs": "^7.0.0",
|
|
50
|
+
"sortablejs": "^1.15.0",
|
|
51
|
+
"vue": "^3.5.0",
|
|
52
|
+
"vue-request": "^2.0.0",
|
|
53
|
+
"vue-router": "^4.0.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"@iconify/vue": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"@vueuse/core": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"axios": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"compressorjs": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"dayjs": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"file-type-checker": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"heic2any": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"js-cookie": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"lodash": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"nanoid": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"rxjs": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"sortablejs": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"vue-request": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"vue-router": {
|
|
96
|
+
"optional": true
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"devDependencies": {
|
|
100
|
+
"@iconify/vue": "^5.0.1",
|
|
101
|
+
"@tailwindcss/vite": "^4.3.1",
|
|
102
|
+
"@types/js-cookie": "^3.0.6",
|
|
103
|
+
"@types/lodash": "^4.17.24",
|
|
104
|
+
"@types/node": "^22.20.0",
|
|
105
|
+
"@types/sortablejs": "^1.15.9",
|
|
106
|
+
"@vitejs/plugin-vue": "^6.0.7",
|
|
107
|
+
"@vitejs/plugin-vue-jsx": "^5.1.6",
|
|
108
|
+
"@vue/tsconfig": "^0.9.1",
|
|
109
|
+
"@vueuse/core": "^12.8.2",
|
|
110
|
+
"axios": "^1.18.1",
|
|
111
|
+
"compressorjs": "^1.3.0",
|
|
112
|
+
"dayjs": "^1.11.21",
|
|
113
|
+
"element-plus": "^2.14.2",
|
|
114
|
+
"file-type-checker": "^1.1.7",
|
|
115
|
+
"heic2any": "^0.0.4",
|
|
116
|
+
"js-cookie": "^3.0.8",
|
|
117
|
+
"lodash": "^4.18.1",
|
|
118
|
+
"nanoid": "^5.1.15",
|
|
119
|
+
"rxjs": "^7.8.2",
|
|
120
|
+
"sortablejs": "^1.15.7",
|
|
121
|
+
"tailwindcss": "^4.3.1",
|
|
122
|
+
"typescript": "^6.0.3",
|
|
123
|
+
"unplugin-auto-import": "^21.0.0",
|
|
124
|
+
"unplugin-vue-components": "^32.1.0",
|
|
125
|
+
"vite": "^8.1.0",
|
|
126
|
+
"vite-plugin-dts": "^4.5.4",
|
|
127
|
+
"vue": "^3.5.38",
|
|
128
|
+
"vue-i18n": "^11.4.6",
|
|
129
|
+
"vue-request": "^2.0.4",
|
|
130
|
+
"vue-router": "^4.6.4",
|
|
131
|
+
"vue-tsc": "^3.3.5"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-image-viewer v-if="show"
|
|
3
|
+
:url-list="assetUrls"
|
|
4
|
+
:initial-index="initialIndex"
|
|
5
|
+
:infinite="false"
|
|
6
|
+
@close="onClose"
|
|
7
|
+
@setActiveItem="setActiveItem">
|
|
8
|
+
<template #viewer-error>
|
|
9
|
+
<div class="flex-col flex-center bg-white p-10 z-999 rounded-4xl">
|
|
10
|
+
<ex-icon icon="file-question" :size="40" />
|
|
11
|
+
<div class="mt-10">{{ currentAsset?.title }}</div>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
<template #toolbar="{ actions }">
|
|
15
|
+
<ex-icon icon="zoom-out" @click="actions('zoomOut')" />
|
|
16
|
+
<ex-icon icon="zoom-in" @click="actions('zoomIn')" />
|
|
17
|
+
<ex-icon icon="redo" @click="actions('clockwise')" />
|
|
18
|
+
<ex-icon icon="undo" @click="actions('anticlockwise')" />
|
|
19
|
+
<ex-icon icon="download" @click="onDownload" />
|
|
20
|
+
</template>
|
|
21
|
+
</el-image-viewer>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import type { PropType } from 'vue'
|
|
26
|
+
import type { IAsset } from '../types'
|
|
27
|
+
import { useMessage } from '../hooks/useMessage'
|
|
28
|
+
import { useLocale } from '../locale'
|
|
29
|
+
import { download } from '../utils/download'
|
|
30
|
+
import ExIcon from './ExIcon.vue'
|
|
31
|
+
|
|
32
|
+
const props = defineProps({
|
|
33
|
+
asset: { type: Object as PropType<IAsset>, default: () => ({}) },
|
|
34
|
+
assets: { type: Array as PropType<IAsset[]>, default: () => [] },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const show = defineModel('show', { type: Boolean, default: false })
|
|
38
|
+
const assetUrls = computed(() => props.assets.map(item => item.shrink ?? item?.url))
|
|
39
|
+
const initialIndex = computed(() => props.assets.findIndex(item => item.id === props.asset?.id) ?? 0)
|
|
40
|
+
|
|
41
|
+
const onClose = () => show.value = false
|
|
42
|
+
const index = ref(initialIndex.value === -1 ? 0 : initialIndex.value)
|
|
43
|
+
const currentAsset = computed(() => props.assets[index.value])
|
|
44
|
+
const setActiveItem = (value: number) => index.value = value
|
|
45
|
+
|
|
46
|
+
const { t } = useLocale()
|
|
47
|
+
const { error } = useMessage()
|
|
48
|
+
const onDownload = () => {
|
|
49
|
+
if (!currentAsset.value) return
|
|
50
|
+
fetch(currentAsset.value.url)
|
|
51
|
+
.then(response => response.blob())
|
|
52
|
+
.then(blob => download(blob, currentAsset.value!.title))
|
|
53
|
+
.catch(() => error(t('core.message.downloadFailed')))
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-tooltip v-if="tooltip" :content="tooltip" :effect :rawContent :show-after>
|
|
3
|
+
<el-button v-bind="$attrs" :class="{ 'is-on': on }" :icon :link :circle>
|
|
4
|
+
<template v-if="$slots.default" #default>
|
|
5
|
+
<slot />
|
|
6
|
+
</template>
|
|
7
|
+
</el-button>
|
|
8
|
+
</el-tooltip>
|
|
9
|
+
<el-button v-else v-bind="$attrs" :class="{ 'is-on': on }" :icon :link :circle>
|
|
10
|
+
<template v-if="$slots.default" #default>
|
|
11
|
+
<slot />
|
|
12
|
+
</template>
|
|
13
|
+
</el-button>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { useIcon } from '../hooks/useIcon'
|
|
18
|
+
|
|
19
|
+
defineOptions({ inheritAttrs: false })
|
|
20
|
+
|
|
21
|
+
const { iconVendor: vendor, iconColor, strokeWidth, ...props } = defineProps({
|
|
22
|
+
icon: { type: String },
|
|
23
|
+
iconVendor: { type: String, default: 'icon-park-outline' },
|
|
24
|
+
iconColor: { type: String },
|
|
25
|
+
iconSize: { type: Number, default: 16 },
|
|
26
|
+
strokeWidth: { type: Number, default: 3 },
|
|
27
|
+
link: { type: Boolean, default: true },
|
|
28
|
+
circle: { type: Boolean, default: false },
|
|
29
|
+
on: { type: Boolean, default: false },
|
|
30
|
+
tooltip: { type: String },
|
|
31
|
+
effect: { type: String, default: 'dark' },
|
|
32
|
+
rawContent: { type: Boolean, default: false },
|
|
33
|
+
showAfter: { type: Number, default: 500 },
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const { i } = useIcon()
|
|
37
|
+
|
|
38
|
+
const color = computed(() => iconColor ?? (props.on ? 'var(--el-color-primary)' : undefined))
|
|
39
|
+
const icon = computed(() => props.icon ? i(props.icon, { vendor, color: color.value, strokeWidth }) : undefined)
|
|
40
|
+
const iconSize = computed(() => props.iconSize + 'px')
|
|
41
|
+
const link = computed(() => props.circle ? false : props.link)
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<style scoped>
|
|
45
|
+
.el-button :deep(.el-icon) { font-size: v-bind(iconSize); }
|
|
46
|
+
.el-button.is-on { color: var(--el-color-primary); }
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-empty>
|
|
3
|
+
<template #image>
|
|
4
|
+
<ex-icon icon="topic-discussion" :size="72" :stroke-width="1" />
|
|
5
|
+
</template>
|
|
6
|
+
<template #description>
|
|
7
|
+
<div class="mb-[10px]" v-if="title">{{ title }}</div>
|
|
8
|
+
<div class="text-sm text-gray-500">{{ description || t('empty.noData') }}</div>
|
|
9
|
+
</template>
|
|
10
|
+
</el-empty>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { useLocale } from '../locale'
|
|
15
|
+
import ExIcon from './ExIcon.vue'
|
|
16
|
+
|
|
17
|
+
defineProps({
|
|
18
|
+
title: { type: String },
|
|
19
|
+
description: { type: String },
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const { t } = useLocale()
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<style scoped>
|
|
26
|
+
</style>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dialog v-if="onShow" v-model="show" align-center :title :show-close="false" :width>
|
|
3
|
+
<el-form v-bind="$attrs" ref="formRef" class="ex-form mt-5!" label-suffix=":" :labelWidth :labelPosition :model :rules @submit.prevent="submit">
|
|
4
|
+
<slot />
|
|
5
|
+
<el-row :gutter="20">
|
|
6
|
+
<el-col v-for="{ label, rules, ...field } in fields" :span :hidden="field.hidden" class="pb-4.5">
|
|
7
|
+
<el-form-item :prop="field.name" :label :rules :required="field.required">
|
|
8
|
+
<slot name="field" :model :field>
|
|
9
|
+
<ex-form-field v-model="model" :field />
|
|
10
|
+
</slot>
|
|
11
|
+
</el-form-item>
|
|
12
|
+
</el-col>
|
|
13
|
+
</el-row>
|
|
14
|
+
</el-form>
|
|
15
|
+
<template v-if="loaded" #footer>
|
|
16
|
+
<el-button type="primary" @click="submit" :disabled :loading>{{ submitText || t('core.save') }}</el-button>
|
|
17
|
+
<el-button @click="cancel">{{ cancelText || t('core.cancel') }}</el-button>
|
|
18
|
+
</template>
|
|
19
|
+
</el-dialog>
|
|
20
|
+
<el-form v-else v-bind="$attrs" ref="formRef" class="ex-form" label-suffix=":" :labelWidth :labelPosition :model :rules @submit.prevent="submit">
|
|
21
|
+
<slot />
|
|
22
|
+
<el-row :gutter="20">
|
|
23
|
+
<el-col v-for="{ label, rules, required, hidden, props, ...field } in fields" :span :hidden class="pb-4.5">
|
|
24
|
+
<el-form-item :prop="field.name" :label :rules :required>
|
|
25
|
+
<ex-form-field v-model="model" :field />
|
|
26
|
+
</el-form-item>
|
|
27
|
+
</el-col>
|
|
28
|
+
</el-row>
|
|
29
|
+
<el-form-item v-if="loaded">
|
|
30
|
+
<template #label><div /></template>
|
|
31
|
+
<el-button type="primary" native-type="submit" :disabled :loading>{{ submitText || t('core.save') }}</el-button>
|
|
32
|
+
<el-button native-type="reset" @click="reset">{{ resetText || t('core.reset') }}</el-button>
|
|
33
|
+
</el-form-item>
|
|
34
|
+
</el-form>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
import type { FormInstance, FormRules } from 'element-plus'
|
|
39
|
+
import type { IModel } from '../types'
|
|
40
|
+
import { useLocale } from '../locale'
|
|
41
|
+
import ExFormField from './ExFormField.vue'
|
|
42
|
+
|
|
43
|
+
defineOptions({ inheritAttrs: false })
|
|
44
|
+
|
|
45
|
+
const { fields: fieldsData, formColumns, labelPosition, destroyOnClose, ...props } = defineProps({
|
|
46
|
+
fields: { type: [Array, Object], default: () => ({}) },
|
|
47
|
+
rules: { type: Object as PropType<FormRules>, default: () => {} },
|
|
48
|
+
title: { type: String, default: 'Dialog' },
|
|
49
|
+
submitText: { type: String },
|
|
50
|
+
cancelText: { type: String },
|
|
51
|
+
resetText: { type: String },
|
|
52
|
+
loaded: { type: Boolean, default: true },
|
|
53
|
+
loading: { type: Boolean, default: false },
|
|
54
|
+
disabled: { type: Boolean, default: false },
|
|
55
|
+
formColumns: { type: Number, default: 1 },
|
|
56
|
+
labelWidth: { type: String, default: 'auto' },
|
|
57
|
+
labelPosition: { type: String as PropType<'top' | 'left' | 'right'> },
|
|
58
|
+
destroyOnClose: { type: Boolean, default: false },
|
|
59
|
+
width: { type: String },
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const emit = defineEmits(['submit', 'reset', 'cancel'])
|
|
63
|
+
|
|
64
|
+
const model = defineModel({ type: Object as PropType<IModel>, default: () => ({}) })
|
|
65
|
+
const show = defineModel('show', { type: Boolean, default: false })
|
|
66
|
+
|
|
67
|
+
const { t } = useLocale()
|
|
68
|
+
|
|
69
|
+
const span = computed(() => 24 / formColumns)
|
|
70
|
+
const width = computed(() => props.width || (labelPosition === 'top' ? 320 : 400) * formColumns)
|
|
71
|
+
const fields = computed(() => Array.isArray(fieldsData) ? fieldsData : Object.entries(fieldsData).map(([key, value]) => ({ ...value, name: key })))
|
|
72
|
+
|
|
73
|
+
// 表单及校验
|
|
74
|
+
const formRef = useTemplateRef<FormInstance>('formRef')
|
|
75
|
+
const clearValidate = () => formRef.value!.clearValidate()
|
|
76
|
+
const submit = () => formRef.value!.validate().then(() => emit('submit', model.value)).catch(() => {})
|
|
77
|
+
const reset = () => {
|
|
78
|
+
formRef.value!.resetFields()
|
|
79
|
+
emit('reset')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 操作按钮
|
|
83
|
+
const instance = getCurrentInstance()
|
|
84
|
+
const onShow = instance?.vnode.props?.['onUpdate:show']
|
|
85
|
+
const cancel = () => {
|
|
86
|
+
if (destroyOnClose) reset() // 清除表单
|
|
87
|
+
show.value = false
|
|
88
|
+
emit('cancel')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
defineExpose({ submit, reset, clearValidate, validate: () => formRef.value!.validate() })
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<style scoped>
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component v-if="custom" :is="custom" v-model="model[name]" v-bind="attrs" :disabled class="w-full" @change="emit('change', $event)" />
|
|
3
|
+
<el-select v-else-if="type === 'select'" v-model="model[name]" v-bind="attrs" :disabled clearable class="w-full" @change="emit('change', $event)">
|
|
4
|
+
<template v-for="{ label, value, options } in field.options" :key="value">
|
|
5
|
+
<el-option-group v-if="options" :label>
|
|
6
|
+
<el-option v-for="{ label, value } in options" :key="value" :label :value />
|
|
7
|
+
</el-option-group>
|
|
8
|
+
<el-option v-else :label :value />
|
|
9
|
+
</template>
|
|
10
|
+
</el-select>
|
|
11
|
+
<el-cascader v-else-if="type === 'cascader'" v-model="model[name]" v-bind="attrs" :disabled :options="field.options" collapse-tags collapse-tags-tooltip :maxCollapseTags clearable filterable class="w-full" @change="emit('change', $event)" />
|
|
12
|
+
<el-radio-group v-else-if="type === 'radio'" v-model="model[name]" v-bind="attrs" :disabled @change="emit('change', $event)">
|
|
13
|
+
<el-radio v-for="{ label, value } in field.options" :key="value" :value="value">{{ label }}</el-radio>
|
|
14
|
+
</el-radio-group>
|
|
15
|
+
<el-date-picker v-else-if="type === 'date' || type === 'daterange'" v-model="model[name]" v-bind="attrs" :type :disabled clearable format="YYYY-MM-DD" value-format="YYYY-MM-DD" :start-placeholder="t('core.datetime.start')" :end-placeholder="t('core.datetime.end')" class="w-full" @change="emit('change', $event)" />
|
|
16
|
+
<!-- <ex-price v-else-if="type === 'currency'" v-model="model[name]" v-bind="attrs" :selectable="false" /> -->
|
|
17
|
+
<el-switch v-else-if="type === 'switch'" v-model="model[name]" v-bind="attrs" :disabled />
|
|
18
|
+
<ex-input-percentage v-else-if="type === 'percentage'" v-model="model[name]" v-bind="attrs" class="w-full" />
|
|
19
|
+
<el-input-number v-else-if="type === 'number'" v-model="model[name]" v-bind="attrs" :disabled clearable class="w-full" />
|
|
20
|
+
<el-tree-select v-else-if="type === 'tree'" v-model="model[name]" v-bind="attrs" :disabled clearable filterable class="w-full" @change="emit('change', $event)" />
|
|
21
|
+
<el-input v-else-if="type === 'password'" v-model="model[name]" v-bind="attrs" :type :disabled clearable autocomplete="new-password" />
|
|
22
|
+
<el-input v-else v-model="model[name]" v-bind="attrs" :type :disabled clearable />
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import type { IField } from '../types'
|
|
27
|
+
import { useLocale } from '../locale'
|
|
28
|
+
import ExInputPercentage from './ExInputPercentage.vue'
|
|
29
|
+
|
|
30
|
+
const props = defineProps({
|
|
31
|
+
field: { type: Object as PropType<IField>, required: true },
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const model = defineModel({ type: Object as PropType<any> })
|
|
35
|
+
|
|
36
|
+
const emit = defineEmits(['change'])
|
|
37
|
+
|
|
38
|
+
const { t } = useLocale()
|
|
39
|
+
|
|
40
|
+
const name = computed(() => props.field.name)
|
|
41
|
+
const type = computed(() => props.field.type)
|
|
42
|
+
const disabled = computed(() => !!props.field.disabled || !!props.field.readonly)
|
|
43
|
+
const custom = computed(() => props.field.component ? markRaw(toRaw(props.field.component)) : null) // 自定义组件
|
|
44
|
+
const maxCollapseTags = computed(() => props.field.maxCollapseTags || 2)
|
|
45
|
+
const attrs = computed(() => {
|
|
46
|
+
const { name, type, readonly, options, component, ...rest } = props.field
|
|
47
|
+
return rest
|
|
48
|
+
})
|
|
49
|
+
</script>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-form :model="params" inline @submit.prevent="onSearch" @reset.prevent="onReset">
|
|
3
|
+
<slot />
|
|
4
|
+
<template v-for="{ label, rules, ...field } in fields.filter(field => !field.hidden)">
|
|
5
|
+
<el-form-item :prop="['filter', field.name]" :label :rules :required="field.required">
|
|
6
|
+
<ex-form-field v-model="params.filter" :field @change="onSearch" />
|
|
7
|
+
</el-form-item>
|
|
8
|
+
</template>
|
|
9
|
+
<el-form-item v-if="searching" prop="search" :label="t('core.keywords')">
|
|
10
|
+
<el-input v-model="params.search" :placeholder clearable @keyup.enter="onSearch" />
|
|
11
|
+
</el-form-item>
|
|
12
|
+
<el-form-item>
|
|
13
|
+
<el-button native-type="submit" type="primary">{{ t('core.search') }}</el-button>
|
|
14
|
+
<el-button native-type="reset">{{ t('core.reset') }}</el-button>
|
|
15
|
+
</el-form-item>
|
|
16
|
+
</el-form>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import type { IField, IFilter, IParams } from '../types'
|
|
21
|
+
import { useLocale } from '../locale'
|
|
22
|
+
import ExFormField from './ExFormField.vue'
|
|
23
|
+
|
|
24
|
+
const props = defineProps({
|
|
25
|
+
filter: { type: Object as PropType<Record<string, IFilter>>, default: () => ({}) },
|
|
26
|
+
search: { type: Boolean, default: true },
|
|
27
|
+
placeholder: { type: String },
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits(['search', 'reset'])
|
|
31
|
+
|
|
32
|
+
const params = defineModel<IParams>('params', { default: () => ({ filter: {}, search: '' }) })
|
|
33
|
+
|
|
34
|
+
const { t } = useLocale()
|
|
35
|
+
|
|
36
|
+
const searching = computed(() => Object.keys(params.value).includes('search') && props.search)
|
|
37
|
+
const fields = computed(() => Object.keys(props.filter).map(name => ({ ...props.filter[name], name }) as IField))
|
|
38
|
+
|
|
39
|
+
const onReset = () => emit('reset')
|
|
40
|
+
const onSearch = () => {
|
|
41
|
+
params.value.search = params.value.search?.trim() || ''
|
|
42
|
+
emit('search', params.value)
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<style scoped>
|
|
47
|
+
.el-form :deep(.el-form-item) { display: inline-block; vertical-align: bottom; margin-bottom: 20px; margin-right: 10px; max-width: 250px; }
|
|
48
|
+
.el-form :deep(.el-input) { --el-input-width: 160px; }
|
|
49
|
+
.el-form :deep(.el-select) { --el-select-width: 160px; }
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dialog v-if="onShow" v-model="show" :title="topic" align-center :width :fullscreen="!isDesktop" @close="close">
|
|
3
|
+
<el-descriptions :border :title :extra :column :labelWidth v-bind="$attrs">
|
|
4
|
+
<template #extra><slot name="extra" /></template>
|
|
5
|
+
<el-descriptions-item v-for="{ label, value, span, align, VNode } in items" :label :span>
|
|
6
|
+
<component v-if="VNode" :is="VNode" />
|
|
7
|
+
<div v-else v-html="value" :class="`text-${align}`" />
|
|
8
|
+
</el-descriptions-item>
|
|
9
|
+
</el-descriptions>
|
|
10
|
+
</el-dialog>
|
|
11
|
+
<el-descriptions :border :title :extra :column :labelWidth v-bind="$attrs" v-else>
|
|
12
|
+
<template #extra><slot name="extra" /></template>
|
|
13
|
+
<el-descriptions-item v-for="{ label, value, span, align, VNode } in items" :label :span>
|
|
14
|
+
<component v-if="VNode" :is="VNode" />
|
|
15
|
+
<div v-else v-html="value" :class="`text-${align}`" />
|
|
16
|
+
</el-descriptions-item>
|
|
17
|
+
</el-descriptions>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import type { PropType, VNode } from 'vue'
|
|
22
|
+
import { computed, getCurrentInstance } from 'vue'
|
|
23
|
+
import { useBreak } from '../hooks/useBreak'
|
|
24
|
+
|
|
25
|
+
defineOptions({ inheritAttrs: false })
|
|
26
|
+
|
|
27
|
+
const props = defineProps({
|
|
28
|
+
topic: { type: String, default: '详情' },
|
|
29
|
+
width: { type: String, default: '800px' },
|
|
30
|
+
title: { type: String },
|
|
31
|
+
extra: { type: String },
|
|
32
|
+
items: { type: Array as PropType<Array<{ label: string; value?: string; align?: string; span?: number; VNode?: VNode }>>, default: () => [] },
|
|
33
|
+
column: { type: Number, default: 2 },
|
|
34
|
+
border: { type: Boolean, default: true },
|
|
35
|
+
labelWidth: { type: String, default: '120px' },
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const show = defineModel('show', { type: Boolean, default: false })
|
|
39
|
+
|
|
40
|
+
const { isDesktop } = useBreak()
|
|
41
|
+
|
|
42
|
+
const onShow = getCurrentInstance()?.vnode.props?.['onUpdate:show']
|
|
43
|
+
const close = () => show.value = false
|
|
44
|
+
|
|
45
|
+
const column = computed(() => isDesktop.value ? props.column : 1)
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<style scoped>
|
|
49
|
+
.el-descriptions :deep(.el-descriptions__label) { min-width: v-bind(labelWidth); }
|
|
50
|
+
.el-descriptions :deep(.el-descriptions__content) { width: calc(100% / v-bind(column)); }
|
|
51
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-icon v-if="icon" :class="{ 'cursor-pointer': hasClick, circle, 'is-loading': loading }" :size>
|
|
3
|
+
<v-icon :icon :vendor :strokeWidth :color />
|
|
4
|
+
</el-icon>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import vIcon from './vIcon.vue'
|
|
9
|
+
|
|
10
|
+
const { size } = defineProps({
|
|
11
|
+
icon: { type: String, default: '' },
|
|
12
|
+
size: { type: Number, default: 16 },
|
|
13
|
+
color: { type: String },
|
|
14
|
+
strokeWidth: { type: Number },
|
|
15
|
+
loading: { type: Boolean, default: false },
|
|
16
|
+
circle: { type: Boolean, default: false },
|
|
17
|
+
vendor: { type: String },
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const attrs = useAttrs()
|
|
21
|
+
const hasClick = computed(() => 'onClick' in attrs)
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<style scoped>
|
|
25
|
+
.circle {
|
|
26
|
+
width: v-bind(size + 16 + 'px');
|
|
27
|
+
height: v-bind(size + 16 + 'px');
|
|
28
|
+
border: 1px solid var(--el-border-color);
|
|
29
|
+
background-color: var(--el-fill-color-blank);
|
|
30
|
+
border-radius: 50%;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
}
|
|
33
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-input-number v-model="percentage" controls-position="right" :step="0.01" :precision :controls clearable @change="change">
|
|
3
|
+
<template #prefix v-if="symbol">
|
|
4
|
+
<span>{{ symbol }}</span>
|
|
5
|
+
</template>
|
|
6
|
+
</el-input-number>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
modelValue: { type: [Number, String], default: '' },
|
|
12
|
+
symbol: { type: String, default: '%' },
|
|
13
|
+
precision: { type: Number, default: 2 },
|
|
14
|
+
controls: { type: Boolean, default: false },
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits(['update:modelValue', 'change'])
|
|
18
|
+
|
|
19
|
+
const round = (v: number, p: number) => Math.round(v * 10 ** p) / 10 ** p // 保留 N 位小数
|
|
20
|
+
|
|
21
|
+
const percentage = computed({
|
|
22
|
+
get: (): number | undefined => {
|
|
23
|
+
const n = parseFloat(props.modelValue as string)
|
|
24
|
+
return Number.isFinite(n) ? round(n * 100, props.precision) : undefined
|
|
25
|
+
},
|
|
26
|
+
set: (v: number | undefined) => {
|
|
27
|
+
const next = v == null ? undefined : round(v / 100, props.precision + 2)
|
|
28
|
+
emit('update:modelValue', next)
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const change = (current: number | undefined) => {
|
|
33
|
+
const next = current == null ? undefined : round(current / 100, props.precision + 2)
|
|
34
|
+
emit('change', next)
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dropdown placement="bottom-end">
|
|
3
|
+
<!-- <el-badge :is-dot="!user.secured">
|
|
4
|
+
<el-avatar :src :size="32">
|
|
5
|
+
<el-image :src="avatar" />
|
|
6
|
+
</el-avatar>
|
|
7
|
+
</el-badge>
|
|
8
|
+
<template #dropdown>
|
|
9
|
+
<el-dropdown-menu>
|
|
10
|
+
<el-dropdown-item @click="web('/account/profile')">{{ t('core.profile') }}</el-dropdown-item>
|
|
11
|
+
<el-badge :is-dot="!user.secured" :offset="[-10, 10]">
|
|
12
|
+
<el-dropdown-item @click="web('/account/password')">{{ t('core.password') }}</el-dropdown-item>
|
|
13
|
+
</el-badge>
|
|
14
|
+
<el-dropdown-item @click="logout">{{ t('core.logout') }}</el-dropdown-item>
|
|
15
|
+
</el-dropdown-menu>
|
|
16
|
+
</template> -->
|
|
17
|
+
</el-dropdown>
|
|
18
|
+
</template>
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
// import avatar from '@packages/core/assets/avatar.png'
|
|
21
|
+
|
|
22
|
+
// const { t } = useI18n()
|
|
23
|
+
// const { push } = useRouter()
|
|
24
|
+
// const { web } = useApplication()
|
|
25
|
+
// const { user } = storeToRefs(useMainStore())
|
|
26
|
+
|
|
27
|
+
// // 退出登录
|
|
28
|
+
// const src = computed(() => user.value.avatar?.shrink ?? avatar)
|
|
29
|
+
// const logout = () => useMainStore().logout().then(() => push('/login'))
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<style scoped>
|
|
33
|
+
</style>
|