@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.
Files changed (61) hide show
  1. package/README.md +260 -0
  2. package/dist/element-go.cjs +1 -0
  3. package/dist/element-go.d.ts +1 -0
  4. package/dist/element-go.mjs +2994 -0
  5. package/dist/resolver.cjs +1 -0
  6. package/dist/resolver.d.ts +1 -0
  7. package/dist/resolver.mjs +16 -0
  8. package/dist/styles/index.css +2 -0
  9. package/package.json +133 -0
  10. package/src/assets/avatar.png +0 -0
  11. package/src/assets/icon.png +0 -0
  12. package/src/components/ExAssetPreview.vue +55 -0
  13. package/src/components/ExButton.vue +47 -0
  14. package/src/components/ExEmpty.vue +26 -0
  15. package/src/components/ExForm.vue +95 -0
  16. package/src/components/ExFormField.vue +49 -0
  17. package/src/components/ExFormSearch.vue +50 -0
  18. package/src/components/ExFormViewer.vue +51 -0
  19. package/src/components/ExIcon.vue +33 -0
  20. package/src/components/ExInputPercentage.vue +36 -0
  21. package/src/components/ExLayout/account.vue +33 -0
  22. package/src/components/ExLayout/aside.vue +58 -0
  23. package/src/components/ExLayout/lang.vue +27 -0
  24. package/src/components/ExLayout.vue +91 -0
  25. package/src/components/ExLoading.vue +18 -0
  26. package/src/components/ExMenu.vue +80 -0
  27. package/src/components/ExPage.vue +66 -0
  28. package/src/components/ExPageHeader.vue +34 -0
  29. package/src/components/ExPagination.vue +34 -0
  30. package/src/components/ExSelect.vue +28 -0
  31. package/src/components/ExTable.vue +237 -0
  32. package/src/components/ExTableColumn.vue +160 -0
  33. package/src/components/ExUpload.vue +91 -0
  34. package/src/components/ExUploadAsset.vue +299 -0
  35. package/src/components/vIcon.vue +23 -0
  36. package/src/env.d.ts +7 -0
  37. package/src/hooks/useBreak.ts +23 -0
  38. package/src/hooks/useChat.ts +135 -0
  39. package/src/hooks/useIcon.ts +8 -0
  40. package/src/hooks/useMessage.ts +22 -0
  41. package/src/hooks/useNanoid.ts +9 -0
  42. package/src/hooks/useUpload.ts +60 -0
  43. package/src/index.ts +94 -0
  44. package/src/libs/auto-imports.d.ts +94 -0
  45. package/src/libs/components.d.ts +171 -0
  46. package/src/locale/en-US.ts +49 -0
  47. package/src/locale/index.ts +73 -0
  48. package/src/locale/zh-CN.ts +49 -0
  49. package/src/resolver.ts +26 -0
  50. package/src/styles/arco.css +179 -0
  51. package/src/styles/index.css +53 -0
  52. package/src/types/index.ts +77 -0
  53. package/src/utils/datetime.ts +42 -0
  54. package/src/utils/download.ts +11 -0
  55. package/src/utils/formatter.ts +42 -0
  56. package/src/utils/get.ts +10 -0
  57. package/src/utils/index.ts +8 -0
  58. package/src/utils/params.ts +18 -0
  59. package/src/utils/platform.ts +38 -0
  60. package/src/utils/request.ts +144 -0
  61. 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>