vue-2024-ui 0.0.1
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 +29 -0
- package/package.json +30 -0
- package/src/components/g-ctrl.vue +168 -0
- package/src/components/g-form.vue +266 -0
- package/src/components/g-table.vue +143 -0
- package/src/components/index.js +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# vue-ele-ui
|
|
2
|
+
|
|
3
|
+
This template should help get you started developing with Vue 3 in Vite.
|
|
4
|
+
|
|
5
|
+
## Recommended IDE Setup
|
|
6
|
+
|
|
7
|
+
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
|
8
|
+
|
|
9
|
+
## Customize configuration
|
|
10
|
+
|
|
11
|
+
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
|
12
|
+
|
|
13
|
+
## Project Setup
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Compile and Hot-Reload for Development
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm run dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Compile and Minify for Production
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm run build
|
|
29
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue-2024-ui",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/components/index.js",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "guoyuanchao",
|
|
9
|
+
"email": "xg816620@163.com"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/components"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "vite",
|
|
16
|
+
"build": "vite build",
|
|
17
|
+
"preview": "vite preview"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"element-plus": "^2.8.4",
|
|
21
|
+
"vue": "^3.4.29"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@vitejs/plugin-vue": "^5.0.5",
|
|
26
|
+
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
|
27
|
+
"vite": "^5.3.1",
|
|
28
|
+
"vite-plugin-vue-devtools": "^7.3.1"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div style="display: flex; width: 100%;" class="g-ctrl-container">
|
|
3
|
+
<!--radio-->
|
|
4
|
+
<el-radio-group v-model="ctrlValue" v-bind="itemInfo" v-if="ctrlType == 'radio'">
|
|
5
|
+
<slot :name="`${ctrlKey}-ctrl`" :data="itemInfo">
|
|
6
|
+
<el-radio v-bind="option" v-for="option in itemInfo.options">
|
|
7
|
+
<slot :name="`${ctrlKey}-ctrl-options`" :option></slot>
|
|
8
|
+
</el-radio>
|
|
9
|
+
</slot>
|
|
10
|
+
</el-radio-group>
|
|
11
|
+
<!--radio-button-->
|
|
12
|
+
<el-radio-group v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'radio-button'">
|
|
13
|
+
<slot :name="`${ctrlKey}-ctrl`" :data="itemInfo">
|
|
14
|
+
<el-radio-button v-bind="option" v-for="option in itemInfo.options">
|
|
15
|
+
<slot :name="`${ctrlKey}-ctrl-options`" :option></slot>
|
|
16
|
+
</el-radio-button>
|
|
17
|
+
</slot>
|
|
18
|
+
</el-radio-group>
|
|
19
|
+
<!--checkbox-->
|
|
20
|
+
<el-checkbox v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'checkbox'">
|
|
21
|
+
<slot :name="`${ctrlKey}-ctrl`" :data="itemInfo">{{ " " }}</slot>
|
|
22
|
+
</el-checkbox>
|
|
23
|
+
<!--checkbox-group-->
|
|
24
|
+
<el-checkbox-group v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'checkbox-group'">
|
|
25
|
+
<slot :name="`${ctrlKey}-ctrl`" :data="itemInfo">
|
|
26
|
+
<el-checkbox v-bind="option" v-for="option in itemInfo.options">
|
|
27
|
+
<slot :name="`${ctrlKey}-ctrl-options`" :option>{{ option.label }}</slot>
|
|
28
|
+
</el-checkbox>
|
|
29
|
+
</slot>
|
|
30
|
+
</el-checkbox-group>
|
|
31
|
+
<!--checkbox-group-button-->
|
|
32
|
+
<el-checkbox-group v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'checkbox-group-button'">
|
|
33
|
+
<slot :name="`${ctrlKey}-ctrl`" :data="itemInfo">
|
|
34
|
+
<el-checkbox-button v-bind="option" v-for="option in itemInfo.options">
|
|
35
|
+
<slot :name="`${ctrlKey}-ctrl-options`" :option></slot>
|
|
36
|
+
</el-checkbox-button>
|
|
37
|
+
</slot>
|
|
38
|
+
</el-checkbox-group>
|
|
39
|
+
<!--select-->
|
|
40
|
+
<el-select v-model="ctrlValue" v-bind="selectModel" v-else-if="ctrlType == 'select'">
|
|
41
|
+
<template #default v-if="itemInfo?.options?.some(a => a.options)">
|
|
42
|
+
<slot :name="`${ctrlKey}-ctrl`" :options="itemInfo.options">
|
|
43
|
+
<el-option-group v-for="group in itemInfo.options" :label="group.label">
|
|
44
|
+
<el-option v-bind="option" v-for="option in group.options"
|
|
45
|
+
@click.native="selectModel.change && selectModel.change(option)">
|
|
46
|
+
<slot :name="`${ctrlKey}-ctrl-options`" :option></slot>
|
|
47
|
+
</el-option>
|
|
48
|
+
</el-option-group>
|
|
49
|
+
</slot>
|
|
50
|
+
</template>
|
|
51
|
+
<template #default v-else>
|
|
52
|
+
<slot :name="`${ctrlKey}-ctrl`" :options="itemInfo.options">
|
|
53
|
+
<el-option v-bind="option" v-for="option in itemInfo.options"
|
|
54
|
+
@click.native="selectModel.change && selectModel.change(option)">
|
|
55
|
+
<slot :name="`${ctrlKey}-ctrl-options`" :option></slot>
|
|
56
|
+
</el-option>
|
|
57
|
+
</slot>
|
|
58
|
+
</template>
|
|
59
|
+
<template v-for="(slot, index) in slots()" :key="index" #[slot]="data">
|
|
60
|
+
<slot :name="`${ctrlKey}-ctrl-${slot}`" :data></slot>
|
|
61
|
+
</template>
|
|
62
|
+
</el-select>
|
|
63
|
+
<!--tree-select-->
|
|
64
|
+
<el-tree-select v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'tree-select'">
|
|
65
|
+
<template v-for="(slot, index) in slots()" :key="index" #[slot]="data">
|
|
66
|
+
<slot :name="`${ctrlKey}-ctrl-${slot}`" :data></slot>
|
|
67
|
+
</template>
|
|
68
|
+
</el-tree-select>
|
|
69
|
+
<!--time-->
|
|
70
|
+
<el-time-picker v-model="ctrlValue" arrow-control v-bind="itemInfo" v-else-if="ctrlType == 'time'" />
|
|
71
|
+
<!--time-select-->
|
|
72
|
+
<el-time-select v-model="ctrlValue" start="06:30" step="00:15" end="23:30" v-bind="itemInfo"
|
|
73
|
+
v-else-if="ctrlType == 'time-select'" />
|
|
74
|
+
<!--date-->
|
|
75
|
+
<el-date-picker v-model="ctrlValue" type="date" v-bind="itemInfo" v-else-if="ctrlType == 'date'">
|
|
76
|
+
<template v-for="(slot, index) in slots()" :key="index" #[slot]="data">
|
|
77
|
+
<slot :name="`${ctrlKey}-ctrl-${slot}`" :data></slot>
|
|
78
|
+
</template>
|
|
79
|
+
</el-date-picker>
|
|
80
|
+
<!--datetime-->
|
|
81
|
+
<el-date-picker v-model="ctrlValue" type="datetime" v-bind="itemInfo" v-else-if="ctrlType == 'datetime'">
|
|
82
|
+
<template v-for="(slot, index) in slots()" :key="index" #[slot]="data">
|
|
83
|
+
<slot :name="`${ctrlKey}-ctrl-${slot}`" :data></slot>
|
|
84
|
+
</template>
|
|
85
|
+
</el-date-picker>
|
|
86
|
+
<!--color-->
|
|
87
|
+
<el-color-picker v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'color'" />
|
|
88
|
+
<!--rate-->
|
|
89
|
+
<el-rate v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'rate'" />
|
|
90
|
+
<!--slider-->
|
|
91
|
+
<el-slider v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'slider'" />
|
|
92
|
+
<!--switch-->
|
|
93
|
+
<el-switch v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'switch'">
|
|
94
|
+
<template v-for="(slot, index) in slots()" :key="index" #[slot]="data">
|
|
95
|
+
<slot :name="`${ctrlKey}-ctrl-${slot}`" :data></slot>
|
|
96
|
+
</template>
|
|
97
|
+
</el-switch>
|
|
98
|
+
<!--transfer-->
|
|
99
|
+
<el-transfer v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'transfer'">
|
|
100
|
+
<template v-for="(slot, index) in slots()" :key="index" #[slot]="data">
|
|
101
|
+
<slot :name="`${ctrlKey}-ctrl-${slot}`" :data></slot>
|
|
102
|
+
</template>
|
|
103
|
+
</el-transfer>
|
|
104
|
+
<!--cascader-->
|
|
105
|
+
<el-cascader v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'cascader'">
|
|
106
|
+
<template v-for="(slot, index) in slots()" :key="index" #[slot]="data">
|
|
107
|
+
<slot :name="`${ctrlKey}-ctrl-${slot}`" :data></slot>
|
|
108
|
+
</template>
|
|
109
|
+
</el-cascader>
|
|
110
|
+
<!--upload-->
|
|
111
|
+
<el-upload v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'upload'">
|
|
112
|
+
<template v-for="(slot, index) in slots()" :key="index" #[slot]="data">
|
|
113
|
+
<slot :name="`${ctrlKey}-ctrl-${slot}`" :data></slot>
|
|
114
|
+
</template>
|
|
115
|
+
</el-upload>
|
|
116
|
+
<!--autocomplete-->
|
|
117
|
+
<el-autocomplete v-model="ctrlValue" v-bind="itemInfo" v-else-if="ctrlType == 'autocomplete'">
|
|
118
|
+
<template v-for="(slot, index) in slots()" :key="index" #[slot]="data">
|
|
119
|
+
<slot :name="`${ctrlKey}-ctrl-${slot}`" :data></slot>
|
|
120
|
+
</template>
|
|
121
|
+
</el-autocomplete>
|
|
122
|
+
<!--input-number-->
|
|
123
|
+
<el-input-number v-model="ctrlValue" :min="0" :max="100000000" v-bind="itemInfo"
|
|
124
|
+
v-else-if="ctrlType == 'input-number'">
|
|
125
|
+
<template v-for="(slot, index) in slots()" :key="index" #[slot]="data">
|
|
126
|
+
<slot :name="`${ctrlKey}-ctrl-${slot}`" :data></slot>
|
|
127
|
+
</template>
|
|
128
|
+
</el-input-number>
|
|
129
|
+
<!--input-->
|
|
130
|
+
<el-input v-model="ctrlValue" show-word-limit :maxlength="itemInfo.max || 25" :clearable="true"
|
|
131
|
+
v-bind="itemInfo" v-else>
|
|
132
|
+
<template v-for="(slot, index) in slots()" :key="index" #[slot]="data">
|
|
133
|
+
<slot :name="`${ctrlKey}-ctrl-${slot}`" :data></slot>
|
|
134
|
+
</template>
|
|
135
|
+
</el-input>
|
|
136
|
+
</div>
|
|
137
|
+
</template>
|
|
138
|
+
|
|
139
|
+
<script setup>
|
|
140
|
+
defineOptions({ inheritAttrs: false });
|
|
141
|
+
import { onMounted, useAttrs, useSlots } from "vue";
|
|
142
|
+
const ctrlValue = defineModel({ required: true });
|
|
143
|
+
const itemInfo = useAttrs().item;
|
|
144
|
+
const ctrlKey = useAttrs().ctrlKey;
|
|
145
|
+
const ctrlType = itemInfo.type;
|
|
146
|
+
const model = useAttrs().model;
|
|
147
|
+
const slots = () => Object.keys(useSlots()).filter(a => a.startsWith(ctrlKey)).filter(a => a.includes('-ctrl-')).map(a => a.split("-ctrl-")[1]);
|
|
148
|
+
|
|
149
|
+
//select 默认属性
|
|
150
|
+
const selectModel = {
|
|
151
|
+
...{
|
|
152
|
+
filterable: true,
|
|
153
|
+
remote: itemInfo["remote-method"] ? true : false,
|
|
154
|
+
}, ...itemInfo
|
|
155
|
+
};
|
|
156
|
+
const selectRemoteMethod = (query) => {
|
|
157
|
+
itemInfo["remote-method"](query, (a) => {
|
|
158
|
+
model[ctrlKey].options = a;
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
onMounted(() => {
|
|
163
|
+
if (itemInfo["remote-method"]) {
|
|
164
|
+
selectModel['remote-method'] = selectRemoteMethod;
|
|
165
|
+
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
</script>
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-form ref="gform" :model="model" v-bind="model.form" class="g-form-container">
|
|
3
|
+
<slot>
|
|
4
|
+
<el-form-item :label="item.label" :prop="`${key}.value`" :rules="getItemRulesByItem(item)"
|
|
5
|
+
v-for="(item, key) of items" :key="key" v-bind="{ ...model.formItems, ...item.formItem }"
|
|
6
|
+
:style="{ ...model.formItems.style, ...item?.formItem?.style }">
|
|
7
|
+
<slot :name="key" :item :model>
|
|
8
|
+
<g-ctrl v-model="model[key].value" :item :ctrlKey="key" :model class="g-ctrl-container">
|
|
9
|
+
<template v-for="(slot, slotKey, index) in $slots" :key="index" #[slotKey]="data">
|
|
10
|
+
<slot :name="slotKey" :data :item :model></slot>
|
|
11
|
+
</template>
|
|
12
|
+
</g-ctrl>
|
|
13
|
+
</slot>
|
|
14
|
+
<template #label="data">
|
|
15
|
+
<slot :name="`${key}-label`" :data :item :model>{{ item.label }}</slot>
|
|
16
|
+
</template>
|
|
17
|
+
</el-form-item>
|
|
18
|
+
<el-form-item style="width: 100%;">
|
|
19
|
+
<slot name="footer">
|
|
20
|
+
<div style="display: flex; width:100%;align-items: center;" :style="model.form.footer">
|
|
21
|
+
<slot name="footer-left"></slot>
|
|
22
|
+
<el-button @click="submitForm(model)" v-bind="model.form.submit"
|
|
23
|
+
v-if="!model.form.submit.hidden">
|
|
24
|
+
{{ model.form.submit.submitText }}
|
|
25
|
+
</el-button>
|
|
26
|
+
<el-button @click="resetForm(ruleFormRef)" v-bind="model.form.reset"
|
|
27
|
+
v-if="!model.form.reset.hidden">{{
|
|
28
|
+
model.form.reset.resetText
|
|
29
|
+
}}</el-button>
|
|
30
|
+
<slot name="footer-right"></slot>
|
|
31
|
+
</div>
|
|
32
|
+
</slot>
|
|
33
|
+
</el-form-item>
|
|
34
|
+
</slot>
|
|
35
|
+
</el-form>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script setup>
|
|
39
|
+
import { computed, onMounted, ref, useAttrs, watch } from "vue";
|
|
40
|
+
import gCtrl from "./g-ctrl.vue";
|
|
41
|
+
defineOptions({
|
|
42
|
+
name: "g-form",
|
|
43
|
+
inheritAttrs: false
|
|
44
|
+
})
|
|
45
|
+
const model = defineModel({ required: true });
|
|
46
|
+
const emits = defineEmits(["submit", "reset"]);
|
|
47
|
+
const gform = ref(null);
|
|
48
|
+
const columnsWidth = ref(0);
|
|
49
|
+
let marginRight = ref(32);
|
|
50
|
+
//合并表单默认属性
|
|
51
|
+
model.value.form = {
|
|
52
|
+
inline: true,
|
|
53
|
+
type: "hidden",
|
|
54
|
+
"scroll-to-error": true,
|
|
55
|
+
"label-width": "100px",
|
|
56
|
+
"status-icon": true,
|
|
57
|
+
"scroll-into-view-options": true,
|
|
58
|
+
columns: 2,
|
|
59
|
+
...model.value.form,
|
|
60
|
+
style: {
|
|
61
|
+
width: "600px",
|
|
62
|
+
border: "1px solid rgba(0, 0, 0,0.09)",
|
|
63
|
+
borderRadius: "10px",
|
|
64
|
+
padding: "0px",
|
|
65
|
+
paddingTop: "20px",
|
|
66
|
+
margin: "0px",
|
|
67
|
+
boxShadow: "0 0 5px rgba(0, 0, 0, 0.1)",
|
|
68
|
+
...model.value.form?.style,
|
|
69
|
+
...useAttrs().style
|
|
70
|
+
},
|
|
71
|
+
// 表单底部属性
|
|
72
|
+
footer: {
|
|
73
|
+
justifyContent: "center",
|
|
74
|
+
...model.value.form?.footer
|
|
75
|
+
},
|
|
76
|
+
//表单提交按钮属性
|
|
77
|
+
submit: {
|
|
78
|
+
type: "primary",
|
|
79
|
+
disabled: false,
|
|
80
|
+
submitText: "提交",
|
|
81
|
+
plain: true,
|
|
82
|
+
hidden: false,
|
|
83
|
+
...model.value.form?.submit
|
|
84
|
+
},
|
|
85
|
+
//表单重置按钮属性
|
|
86
|
+
reset: {
|
|
87
|
+
disabled: false,
|
|
88
|
+
resetText: "重置",
|
|
89
|
+
hidden: false,
|
|
90
|
+
plain: true,
|
|
91
|
+
...model.value.form?.reset
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
//合并表单项默认属性
|
|
95
|
+
model.value.formItems = {
|
|
96
|
+
type: "hidden",
|
|
97
|
+
...model.value.formItems,
|
|
98
|
+
style: {
|
|
99
|
+
width: "100%",
|
|
100
|
+
...model.value.formItems?.style
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// 提交表单
|
|
105
|
+
const submitForm = async (model) => {
|
|
106
|
+
if (!gform.value) return
|
|
107
|
+
await gform.value.validate((valid, fields) => {
|
|
108
|
+
if (valid) {
|
|
109
|
+
emits("submit", formInfo.value, model);
|
|
110
|
+
} else {
|
|
111
|
+
console.log('error submit!', fields);
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 重置表单
|
|
117
|
+
const resetForm = () => {
|
|
118
|
+
if (!gform.value) return
|
|
119
|
+
const isreset = true;
|
|
120
|
+
emits("reset", (flag) => {
|
|
121
|
+
isreset = flag;
|
|
122
|
+
});
|
|
123
|
+
if (isreset) {
|
|
124
|
+
gform.value.resetFields();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 定义对外暴露的函数
|
|
129
|
+
defineExpose({
|
|
130
|
+
submit: submitForm, reset: resetForm
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
//获取表单项验证规则
|
|
134
|
+
const getItemRulesByItem = (item) => {
|
|
135
|
+
// 组装验证规则
|
|
136
|
+
let itemRules = [];
|
|
137
|
+
if (item.rules) {
|
|
138
|
+
itemRules = item.rules;
|
|
139
|
+
} else {
|
|
140
|
+
//默认验证规则
|
|
141
|
+
const itemDefaultRule = {
|
|
142
|
+
required: undefined,
|
|
143
|
+
max: undefined,
|
|
144
|
+
min: undefined,
|
|
145
|
+
email: undefined,
|
|
146
|
+
url: undefined,
|
|
147
|
+
pattern: undefined,
|
|
148
|
+
validate: undefined
|
|
149
|
+
}
|
|
150
|
+
// 获取item属性并合并规则
|
|
151
|
+
const itemInfo = { ...itemDefaultRule, ...item };
|
|
152
|
+
|
|
153
|
+
//必填验证
|
|
154
|
+
if (itemInfo.required) {
|
|
155
|
+
itemRules.push({ required: true, message: `${itemInfo.label}不能为空!`, trigger: 'blur' });
|
|
156
|
+
}
|
|
157
|
+
// 数字范围验证 和 字符串长度验证
|
|
158
|
+
if (itemInfo.max && itemInfo.min) {
|
|
159
|
+
itemRules.push({
|
|
160
|
+
validator: (rule, value, callback) => {
|
|
161
|
+
if (value && item.type == "number" && (value < itemInfo.min || value > itemInfo.max)) {
|
|
162
|
+
callback(new Error(`${itemInfo.label} 必须在 ${itemInfo.min} 和 ${itemInfo.max} 之间!`));
|
|
163
|
+
} else if (value && item.type !== "number" && (value.toString().length < itemInfo.min || value.toString().length > itemInfo.max)) {
|
|
164
|
+
callback(new Error(`字符长度必须在 ${itemInfo.min} 和 ${itemInfo.max} 之间!`));
|
|
165
|
+
} else {
|
|
166
|
+
callback();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
//验证数字不能大于一个值
|
|
173
|
+
if (itemInfo.max && !itemInfo.min && item.type == "number") {
|
|
174
|
+
itemRules.push({
|
|
175
|
+
validator: (rule, value, callback) => {
|
|
176
|
+
if (value && (value > itemInfo.max)) {
|
|
177
|
+
callback(new Error(`${itemInfo.label}不能大于${itemInfo.max}!`));
|
|
178
|
+
} else {
|
|
179
|
+
callback();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
//验证数字不能小于一个值
|
|
185
|
+
if (itemInfo.min && !itemInfo.max && item.type == "number") {
|
|
186
|
+
itemRules.push({
|
|
187
|
+
validator: (rule, value, callback) => {
|
|
188
|
+
if (value && (value < itemInfo.min)) {
|
|
189
|
+
callback(new Error(`${itemInfo.label}不能小于${itemInfo.min}!`));
|
|
190
|
+
} else {
|
|
191
|
+
callback();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
//验证邮箱格式
|
|
197
|
+
if (itemInfo.type == "email") {
|
|
198
|
+
itemRules.push({ type: "email", message: "不是一个正确的邮箱地址!" });
|
|
199
|
+
}
|
|
200
|
+
//验证URL格式
|
|
201
|
+
if (itemInfo.type == "url") {
|
|
202
|
+
itemRules.push({ type: "url", message: "不是一个合正确的URL!" });
|
|
203
|
+
}
|
|
204
|
+
// 正则验证
|
|
205
|
+
if (itemInfo.pattern) {
|
|
206
|
+
itemRules.push({ pattern: itemInfo?.pattern, message: itemInfo?.message || "格式不正确!" });
|
|
207
|
+
}
|
|
208
|
+
// 自定义验证规则
|
|
209
|
+
if (itemInfo.validate) {
|
|
210
|
+
itemRules.push({
|
|
211
|
+
validator: (rule, value, callback) => itemInfo.validate(rule, value, callback)
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return itemRules;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
onMounted(() => {
|
|
219
|
+
load();
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const load = () => {
|
|
223
|
+
if (!model.value.form.inline) {
|
|
224
|
+
model.value.form.columns = 1;
|
|
225
|
+
}
|
|
226
|
+
const width = getComputedStyle(document.querySelector(".g-form-container")).width;
|
|
227
|
+
console.log(width);
|
|
228
|
+
if (width === "600px") {
|
|
229
|
+
columnsWidth.value = 300;
|
|
230
|
+
model.value.form.style.width = columns.value * 300 + 'px';
|
|
231
|
+
} else {
|
|
232
|
+
columnsWidth.value = removePx(width) / model.value.form.columns;
|
|
233
|
+
model.value.form.style.width = columns.value * columnsWidth.value + 'px';
|
|
234
|
+
}
|
|
235
|
+
for (const [key, value] of Object.entries(items.value)) {
|
|
236
|
+
model.value[key].formItem = { ...model.value[key].formItem, ...value.formItem }
|
|
237
|
+
model.value[key].formItem.style = { ...model.value[key].formItem.style, ...value.formItem.style };
|
|
238
|
+
if (value.columns && value.columns > 1) {
|
|
239
|
+
if (model.value.form.inline) {
|
|
240
|
+
let column = columnsWidth.value * value.columns - (value.columns - 1) * marginRight.value;
|
|
241
|
+
model.value[key].formItem.style.width = `calc(${column}px)`;
|
|
242
|
+
} else {
|
|
243
|
+
model.value[key].formItem.style.width = `calc(${columnsWidth.value - marginRight.value}px)`;
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
model.value[key].formItem.style.width = `calc(${columnsWidth.value - marginRight.value}px)`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
watch(() => model.value.form.columns, () => load());
|
|
252
|
+
watch(() => model.value.form.inline, () => load());
|
|
253
|
+
|
|
254
|
+
const removePx = (el) => parseFloat(el.replace("px", ""));
|
|
255
|
+
|
|
256
|
+
//表单项
|
|
257
|
+
const items = computed(() => Object.fromEntries(Object.entries(model.value).filter(a => a[1].type != "hidden").filter(a => (a[1].edit !== false)).map(key => [key[0], key[1]])));
|
|
258
|
+
//表单提交对象
|
|
259
|
+
const formInfo = computed(() => Object.fromEntries(Object.entries(model.value).filter(a => a[1].type != "hidden").map(key => [key[0], key[1].value])));
|
|
260
|
+
//表单列数
|
|
261
|
+
const columns = computed(() => model.value.form.columns);
|
|
262
|
+
//表单列的宽度减去右边距
|
|
263
|
+
const formItemsWidth = computed(() => columnsWidth.value - marginRight.value);
|
|
264
|
+
//表单宽度
|
|
265
|
+
const formWidth = computed(() => columns.value * columnsWidth.value);
|
|
266
|
+
</script>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-table class="g-table-container" v-bind="{ ...$attrs, ...model.table }">
|
|
3
|
+
<slot>
|
|
4
|
+
<el-table-column :prop="key" v-for="(value, key) of tableColumns" :key="key"
|
|
5
|
+
v-bind="{ ...itemValue, ...value }" header-align="center">
|
|
6
|
+
<template #default="{ row, column, $index }"
|
|
7
|
+
v-if="value.type != 'selection' && value.type != 'index' && value.type != 'expand'">
|
|
8
|
+
<slot :name="`${key}`" :row :column :$index>
|
|
9
|
+
<div v-html="formatterExtends(value, column.formatter(row, column, row[key], $index))"></div>
|
|
10
|
+
</slot>
|
|
11
|
+
</template>
|
|
12
|
+
<template #header="{ column, $index }">
|
|
13
|
+
<slot :name="`${key}-header`" :column :$index></slot>
|
|
14
|
+
</template>
|
|
15
|
+
<template #filter-icon="data">
|
|
16
|
+
<slot :name="`${key}-filter-icon`" :data></slot>
|
|
17
|
+
</template>
|
|
18
|
+
</el-table-column>
|
|
19
|
+
</slot>
|
|
20
|
+
<template #append>
|
|
21
|
+
<slot name="append"></slot>
|
|
22
|
+
</template>
|
|
23
|
+
<template #empty>
|
|
24
|
+
<slot name="empty"></slot>
|
|
25
|
+
</template>
|
|
26
|
+
</el-table>
|
|
27
|
+
<slot name="page">
|
|
28
|
+
<el-pagination v-model:page-size="model.table.page.pageSize" v-model:current-page="model.table.page.currentPage"
|
|
29
|
+
v-bind="model.table.page">
|
|
30
|
+
<slot name="table-page"></slot>
|
|
31
|
+
</el-pagination>
|
|
32
|
+
</slot>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup>
|
|
36
|
+
import dayjs from 'dayjs';
|
|
37
|
+
import { computed, onMounted, reactive, watchEffect } from 'vue';
|
|
38
|
+
|
|
39
|
+
defineOptions({
|
|
40
|
+
name: "g-table",
|
|
41
|
+
inheritAttrs: false
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const emits = defineEmits(['load']);
|
|
45
|
+
|
|
46
|
+
const model = defineModel({ required: true });
|
|
47
|
+
|
|
48
|
+
//table default attrs
|
|
49
|
+
model.value.table = {
|
|
50
|
+
data: [],
|
|
51
|
+
border: true,
|
|
52
|
+
stripe: true,
|
|
53
|
+
emptyText: '暂无数据',
|
|
54
|
+
fit: false,
|
|
55
|
+
type: "hidden",
|
|
56
|
+
height: "calc(100vh - 55px)",
|
|
57
|
+
|
|
58
|
+
...model.value.table,
|
|
59
|
+
load: () => {
|
|
60
|
+
let { currentPage, pageSize } = model.value.table.page;
|
|
61
|
+
emits('load', { currentPage, pageSize }, (data) => {
|
|
62
|
+
model.value.table.data = data;
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
page: {
|
|
66
|
+
background: true,
|
|
67
|
+
total: 0,
|
|
68
|
+
pageSize: 10,
|
|
69
|
+
currentPage: 1,
|
|
70
|
+
pageSizes: [5, 10, 20, 30, 40, 50],
|
|
71
|
+
layout: 'total, sizes, prev, pager, next, jumper',
|
|
72
|
+
...model.value.table.page,
|
|
73
|
+
style: {
|
|
74
|
+
display: 'flex',
|
|
75
|
+
justifyContent: 'flex-end',
|
|
76
|
+
padding: "10px",
|
|
77
|
+
...model.value.table?.page?.style
|
|
78
|
+
},
|
|
79
|
+
onChange: (a, b) => {
|
|
80
|
+
console.log(a, b);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//table column default attrs
|
|
86
|
+
const itemValue = reactive({
|
|
87
|
+
width: 100, align: "center", formatter: (row, column, cellValue, index) => {
|
|
88
|
+
return cellValue;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
//扩展formatter方法
|
|
93
|
+
const formatterExtends = (item, str) => {
|
|
94
|
+
let trueValue = str;
|
|
95
|
+
if (item.decimals && !isNaN(str)) {
|
|
96
|
+
if (typeof item.decimals == 'number') {
|
|
97
|
+
trueValue = parseFloat(trueValue).toFixed(item.decimals);
|
|
98
|
+
}
|
|
99
|
+
else if (typeof item.decimals == 'object') {
|
|
100
|
+
if (item.decimals.num) {
|
|
101
|
+
if (item.decimals.pad) {
|
|
102
|
+
trueValue = parseFloat(trueValue).toFixed(item.decimals.num);
|
|
103
|
+
} else {
|
|
104
|
+
trueValue = parseFloat(parseFloat(trueValue).toFixed(item.decimals.num));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (item.dateFormat && dayjs(str).isValid()) {
|
|
110
|
+
if (item.dateFormat == 'date') {
|
|
111
|
+
trueValue = dayjs(trueValue).format('YYYY-MM-DD');
|
|
112
|
+
} else if (item.dateFormat == 'time') {
|
|
113
|
+
trueValue = dayjs(trueValue).format('HH:mm:ss');
|
|
114
|
+
}
|
|
115
|
+
else if (item.dateFormat == 'datetime') {
|
|
116
|
+
trueValue = dayjs(trueValue).format('YYYY-MM-DD HH:mm:ss');
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
trueValue = dayjs(trueValue).format(item.dateFormat)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (item.prefix && trueValue) {
|
|
123
|
+
trueValue = `<span>${item.prefix}${trueValue}</span>`;
|
|
124
|
+
}
|
|
125
|
+
if (item.suffix && trueValue) {
|
|
126
|
+
trueValue += `<span>${item.suffix}</span>`
|
|
127
|
+
}
|
|
128
|
+
return trueValue
|
|
129
|
+
}
|
|
130
|
+
onMounted(() => {
|
|
131
|
+
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
watchEffect(() => {
|
|
135
|
+
model.value.table.load();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
//表单项
|
|
139
|
+
const tableColumns = computed(() => Object.fromEntries(Object.entries(model.value).filter(a => a[1].type != "hidden").filter(a => a[1].visible !== false).map(key => [key[0], key[1]])));
|
|
140
|
+
//const formInfo = computed(() => Object.fromEntries(Object.entries(model.value).filter(a => a[1].type != "hidden").map(key => [key[0], key[1].value])));
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<style lang="scss" scoped></style>
|