tiddy 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 +64 -0
- package/package.json +61 -0
- package/src/components/dialog/dialog.vue +73 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/form/array-field.vue +157 -0
- package/src/components/form/form-field.vue +89 -0
- package/src/components/form/form-item.vue +417 -0
- package/src/components/form/form-label-wrap.tsx +105 -0
- package/src/components/form/form.vue +99 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/form/interface.d.ts +61 -0
- package/src/components/form/layout-field.vue +33 -0
- package/src/components/form/object-field.vue +30 -0
- package/src/components/form/util.ts +4 -0
- package/src/components/form/widget-field.vue +88 -0
- package/src/components/form/widget.ts +28 -0
- package/src/components/slot-nest/index.ts +1 -0
- package/src/components/slot-nest/slot-nest.vue +50 -0
- package/src/components/table/index.ts +1 -0
- package/src/components/table/interface.d.ts +3 -0
- package/src/components/table/table-col.vue +42 -0
- package/src/components/table/table.vue +42 -0
- package/src/components/table/util.ts +4 -0
- package/src/components/utils/index.ts +50 -0
- package/src/index.d.ts +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# tiddy
|
|
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
|
+
## Type Support for `.vue` Imports in TS
|
|
10
|
+
|
|
11
|
+
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
|
12
|
+
|
|
13
|
+
## Customize configuration
|
|
14
|
+
|
|
15
|
+
See [Vite Configuration Reference](https://vite.dev/config/).
|
|
16
|
+
|
|
17
|
+
## Project Setup
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pnpm install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Compile and Hot-Reload for Development
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pnpm dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Type-Check, Compile and Minify for Production
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pnpm build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pnpm test:unit
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Run End-to-End Tests with [Playwright](https://playwright.dev)
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
# Install browsers for the first run
|
|
45
|
+
npx playwright install
|
|
46
|
+
|
|
47
|
+
# When testing on CI, must build the project first
|
|
48
|
+
pnpm build
|
|
49
|
+
|
|
50
|
+
# Runs the end-to-end tests
|
|
51
|
+
pnpm test:e2e
|
|
52
|
+
# Runs the tests only on Chromium
|
|
53
|
+
pnpm test:e2e --project=chromium
|
|
54
|
+
# Runs the tests of a specific file
|
|
55
|
+
pnpm test:e2e tests/example.spec.ts
|
|
56
|
+
# Runs the tests in debug mode
|
|
57
|
+
pnpm test:e2e --debug
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Lint with [ESLint](https://eslint.org/)
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
pnpm lint
|
|
64
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tiddy",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"typings": "./src/index.d.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "run-p type-check \"build-only {@}\" --",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"test:unit": "vitest",
|
|
11
|
+
"test:e2e": "playwright test",
|
|
12
|
+
"build-only": "vite build",
|
|
13
|
+
"type-check": "vue-tsc --build",
|
|
14
|
+
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
|
|
15
|
+
"lint:eslint": "eslint . --fix",
|
|
16
|
+
"lint": "run-s lint:*",
|
|
17
|
+
"format": "biome format --write src/"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
"./*": "./src/components/*"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@vueuse/core": "^12.0.0",
|
|
24
|
+
"async-validator": "^4.2.5",
|
|
25
|
+
"vue": "^3.5.13",
|
|
26
|
+
"yatter": "^1.8.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@biomejs/biome": "^1.9.4",
|
|
30
|
+
"@playwright/test": "^1.49.1",
|
|
31
|
+
"@tsconfig/node22": "^22.0.0",
|
|
32
|
+
"@types/jsdom": "^21.1.7",
|
|
33
|
+
"@types/node": "^22.10.2",
|
|
34
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
35
|
+
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
|
36
|
+
"@vitest/eslint-plugin": "1.1.10",
|
|
37
|
+
"@vue/eslint-config-prettier": "^10.1.0",
|
|
38
|
+
"@vue/eslint-config-typescript": "^14.1.4",
|
|
39
|
+
"@vue/test-utils": "^2.4.6",
|
|
40
|
+
"@vue/tsconfig": "^0.7.0",
|
|
41
|
+
"element-plus": "^2.9.0",
|
|
42
|
+
"eslint": "^9.16.0",
|
|
43
|
+
"eslint-plugin-oxlint": "^0.11.1",
|
|
44
|
+
"eslint-plugin-playwright": "^2.1.0",
|
|
45
|
+
"eslint-plugin-vue": "^9.32.0",
|
|
46
|
+
"jsdom": "^25.0.1",
|
|
47
|
+
"npm-run-all2": "^7.0.1",
|
|
48
|
+
"oxlint": "^0.11.1",
|
|
49
|
+
"prettier": "^3.4.2",
|
|
50
|
+
"sass-embedded": "^1.83.0",
|
|
51
|
+
"typescript": "~5.6.3",
|
|
52
|
+
"vite": "^6.0.3",
|
|
53
|
+
"vite-plugin-vue-devtools": "^7.6.8",
|
|
54
|
+
"vitest": "^2.1.8",
|
|
55
|
+
"vue-tsc": "^2.1.10"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"element-plus": "^2.9.0"
|
|
59
|
+
},
|
|
60
|
+
"files": ["src/components", "src/index.d.ts"]
|
|
61
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ElDialog v-bind="props" ref="dialog" v-model="visible" @close="close">
|
|
3
|
+
<template v-for="(Slot, name) in $slots" :key="name" #[name]="scope">
|
|
4
|
+
<slot :name="name" v-bind="scope" :ok="confirm" :close="close" :form="form" />
|
|
5
|
+
</template>
|
|
6
|
+
</ElDialog>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import { dialogProps, ElDialog, type FormInstance } from 'element-plus';
|
|
11
|
+
import { ref, useTemplateRef } from 'vue';
|
|
12
|
+
|
|
13
|
+
defineOptions({
|
|
14
|
+
name: 'SeDialog',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const props = defineProps(dialogProps);
|
|
18
|
+
|
|
19
|
+
const visible = ref(false);
|
|
20
|
+
|
|
21
|
+
const waiting: Partial<PromiseWithResolvers<any>> = {};
|
|
22
|
+
const dialogRef = useTemplateRef('dialog');
|
|
23
|
+
|
|
24
|
+
async function waitPreStep(preStep?: any, data?: any) {
|
|
25
|
+
let res = data;
|
|
26
|
+
if (typeof preStep === 'function') {
|
|
27
|
+
res = await preStep(data);
|
|
28
|
+
}
|
|
29
|
+
return res;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function open<T = any>(): Promise<T> {
|
|
33
|
+
visible.value = true;
|
|
34
|
+
Object.assign(waiting, Promise.withResolvers());
|
|
35
|
+
return waiting.promise!;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function close() {
|
|
39
|
+
visible.value = false;
|
|
40
|
+
waiting.reject?.();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function confirm(preStep?: any, data?: any) {
|
|
44
|
+
const res = await waitPreStep(preStep, data);
|
|
45
|
+
visible.value = false;
|
|
46
|
+
waiting.resolve?.(res);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function form(instance?: FormInstance, preStep?: any) {
|
|
50
|
+
await instance?.validate?.();
|
|
51
|
+
await confirm(preStep, instance?.model);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
defineExpose(
|
|
55
|
+
new Proxy(
|
|
56
|
+
{
|
|
57
|
+
open,
|
|
58
|
+
close,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
get(target, key) {
|
|
62
|
+
// @ts-expect-error 外部传值
|
|
63
|
+
return target[key] || dialogRef.value?.[key];
|
|
64
|
+
},
|
|
65
|
+
has(target, key) {
|
|
66
|
+
return Object.hasOwn(target, key) || Reflect.has(dialogRef.value!, key);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<style lang="scss" scoped></style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dialog.vue';
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FormItem v-bind="labelProps" class="array-item" name-space="layout-form-item">
|
|
3
|
+
<component
|
|
4
|
+
v-if="EmptySlot && !values.length"
|
|
5
|
+
:is="EmptySlot.component"
|
|
6
|
+
:add="lineAction.add.bind(null, -1)"
|
|
7
|
+
:empty="true"
|
|
8
|
+
/>
|
|
9
|
+
<!-- TODO: 添加原始字符串的key支持 -->
|
|
10
|
+
<div v-for="(v, vi) in values" :key="getKey(v)" class="list-row" :style="lineStyle">
|
|
11
|
+
<div class="row-item">
|
|
12
|
+
<FormField
|
|
13
|
+
name-space="layout-form-item"
|
|
14
|
+
v-bind="fieldProps"
|
|
15
|
+
:label="indexProp(label, vi)"
|
|
16
|
+
:label-width="indexProp(labelWidth, vi)"
|
|
17
|
+
:label-position="indexProp(labelPosition, vi)"
|
|
18
|
+
:type="undefined"
|
|
19
|
+
:full-prop="`${fullProp}[${vi}]`"
|
|
20
|
+
:prop="String(vi)"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="row-action">
|
|
24
|
+
<component
|
|
25
|
+
v-if="RowSlot"
|
|
26
|
+
:is="RowSlot.component"
|
|
27
|
+
:up="lineAction.up.bind(null, vi)"
|
|
28
|
+
:down="lineAction.down.bind(null, vi)"
|
|
29
|
+
:add="lineAction.add.bind(null, vi)"
|
|
30
|
+
:remove="lineAction.remove.bind(null, vi)"
|
|
31
|
+
:index="vi"
|
|
32
|
+
:first="!vi"
|
|
33
|
+
:last="vi === values.length - 1"
|
|
34
|
+
:empty="false"
|
|
35
|
+
:single="values.length === 1"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</FormItem>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { computed, inject, onMounted, readonly, useAttrs } from 'vue';
|
|
44
|
+
import FormItem from './form-item.vue';
|
|
45
|
+
import type { ArrayFieldProps, FormContext } from './interface';
|
|
46
|
+
import { cut, getDeepValue, isFunction, isNullOrUndef, pick, setDeepValue } from 'yatter';
|
|
47
|
+
import { formCtxKey } from './util';
|
|
48
|
+
import { getKey } from '../utils';
|
|
49
|
+
import FormField from './form-field.vue';
|
|
50
|
+
|
|
51
|
+
type Writeable<T> = {
|
|
52
|
+
-readonly [K in keyof T]: T[K];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const props = defineProps<ArrayFieldProps>();
|
|
56
|
+
const attrs = useAttrs();
|
|
57
|
+
|
|
58
|
+
const fieldProps = computed(() => {
|
|
59
|
+
return {
|
|
60
|
+
...cut(props, [k => k.endsWith('Action') || ['lineStyle'].includes(k)]),
|
|
61
|
+
...attrs,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function indexProp(v: OrFunction<any>, index: number, value?: any) {
|
|
66
|
+
if (isFunction(v)) {
|
|
67
|
+
return v(index);
|
|
68
|
+
}
|
|
69
|
+
if (isNullOrUndef(value)) {
|
|
70
|
+
return v;
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const formCtx = inject<FormContext>(formCtxKey)!;
|
|
76
|
+
|
|
77
|
+
const values = computed(() => {
|
|
78
|
+
return getDeepValue<unknown[]>(formCtx.model, props.fullProp!, {
|
|
79
|
+
fallback: [],
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const labelProps = computed<any>(() => {
|
|
84
|
+
const p = pick(props, [/^label/]) as Writeable<typeof props>;
|
|
85
|
+
if (isFunction(p.hideLabel)) {
|
|
86
|
+
p.label = '';
|
|
87
|
+
p.labelWidth = '0';
|
|
88
|
+
}
|
|
89
|
+
p.label = indexProp(p.label, -1, '');
|
|
90
|
+
p.labelWidth = indexProp(p.labelWidth, -1, '0');
|
|
91
|
+
p.labelPosition = indexProp(p.labelPosition, -1);
|
|
92
|
+
return p;
|
|
93
|
+
});
|
|
94
|
+
const EmptySlot = formCtx.getParentSlots([props.emptyAction || 'empty_action'])[0];
|
|
95
|
+
const RowSlot = formCtx.getParentSlots([props.rowAction || 'row_action'])[0];
|
|
96
|
+
|
|
97
|
+
const lineAction = {
|
|
98
|
+
up(index: number) {
|
|
99
|
+
if (!index) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const temp = values.value.slice();
|
|
103
|
+
const [current] = temp.splice(index, 1);
|
|
104
|
+
temp.splice(index - 1, 0, current);
|
|
105
|
+
updateValue(temp);
|
|
106
|
+
},
|
|
107
|
+
down(index: number) {
|
|
108
|
+
const temp = values.value.slice();
|
|
109
|
+
if (index >= temp.length) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const [current] = temp.splice(index, 1);
|
|
113
|
+
temp.splice(index + 1, 0, current);
|
|
114
|
+
updateValue(temp);
|
|
115
|
+
},
|
|
116
|
+
add(index: number) {
|
|
117
|
+
const temp = values.value.slice();
|
|
118
|
+
const rawValue = props.rawValue?.() || {};
|
|
119
|
+
temp.splice(index + 1, 0, rawValue);
|
|
120
|
+
updateValue(temp);
|
|
121
|
+
},
|
|
122
|
+
remove(index: number) {
|
|
123
|
+
const temp = values.value.slice();
|
|
124
|
+
temp.splice(index, 1);
|
|
125
|
+
updateValue(temp);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function updateValue(newValue: any) {
|
|
130
|
+
setDeepValue(formCtx.model, props.fullProp!, newValue);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
onMounted(() => {
|
|
134
|
+
if (props.mandatory && !values.value?.length) {
|
|
135
|
+
lineAction.add(-1);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
</script>
|
|
139
|
+
|
|
140
|
+
<style lang="scss" scoped>
|
|
141
|
+
.list-row {
|
|
142
|
+
display: flex;
|
|
143
|
+
|
|
144
|
+
.row-item {
|
|
145
|
+
flex: 1;
|
|
146
|
+
:deep(> .el-layout-form-item > .el-layout-form-item__content) {
|
|
147
|
+
flex: 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
.row-action {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.array-item > :deep(.el-layout-form-item__content) {
|
|
155
|
+
flex: 1;
|
|
156
|
+
}
|
|
157
|
+
</style>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Fragment v-if="isHiddenField"></Fragment>
|
|
3
|
+
<LayoutField v-else-if="isLayoutField" v-bind="fieldAttrs" />
|
|
4
|
+
<ArrayField v-else-if="isArrayField" v-bind="fieldAttrs" />
|
|
5
|
+
<ObjectField v-else-if="isObjectField" v-bind="fieldAttrs" />
|
|
6
|
+
<WidgetField v-else-if="isWidgetField" v-bind="fieldAttrs" />
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import { computed, Fragment, useAttrs } from 'vue';
|
|
11
|
+
import WidgetField from './widget-field.vue';
|
|
12
|
+
import ObjectField from './object-field.vue';
|
|
13
|
+
import ArrayField from './array-field.vue';
|
|
14
|
+
import LayoutField from './layout-field.vue';
|
|
15
|
+
import { isNullOrUndef } from 'yatter';
|
|
16
|
+
|
|
17
|
+
const attrs = useAttrs();
|
|
18
|
+
const fieldAttrs = computed<any>(() => {
|
|
19
|
+
return {
|
|
20
|
+
...attrs,
|
|
21
|
+
fullProp: attrs.fullProp || attrs.prop,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const isHiddenField = computed(() => !!attrs.hide);
|
|
26
|
+
|
|
27
|
+
const isLayoutField = computed(() => attrs.type === 'layout');
|
|
28
|
+
|
|
29
|
+
const isArrayField = computed(() => attrs.type === 'array');
|
|
30
|
+
|
|
31
|
+
const isObjectField = computed(() => {
|
|
32
|
+
if (attrs.type === 'object') {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (!isNullOrUndef(attrs.type)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return !!attrs.fields;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const isWidgetField = computed(() => {
|
|
42
|
+
if (attrs.type === 'widget') {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (!isNullOrUndef(attrs.type)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return !!attrs.component;
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<style lang="scss" scoped>
|
|
53
|
+
.el-layout-form-item {
|
|
54
|
+
display: flex;
|
|
55
|
+
--font-size: 14px;
|
|
56
|
+
|
|
57
|
+
:deep(.el-layout-form-item__label) {
|
|
58
|
+
align-items: flex-start;
|
|
59
|
+
box-sizing: border-box;
|
|
60
|
+
color: var(--el-text-color-regular);
|
|
61
|
+
display: inline-flex;
|
|
62
|
+
flex: 0 0 auto;
|
|
63
|
+
font-size: var(--el-form-label-font-size);
|
|
64
|
+
height: 32px;
|
|
65
|
+
justify-content: flex-end;
|
|
66
|
+
line-height: 32px;
|
|
67
|
+
padding: 0 12px 0 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.el-layout-form-item--label-left {
|
|
72
|
+
:deep(.el-layout-form-item__label) {
|
|
73
|
+
justify-content: flex-start;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.el-layout-form-item--label-top {
|
|
78
|
+
display: block;
|
|
79
|
+
|
|
80
|
+
:deep(.el-layout-form-item__label) {
|
|
81
|
+
display: inline-block;
|
|
82
|
+
height: auto;
|
|
83
|
+
line-height: 22px;
|
|
84
|
+
margin-bottom: 8px;
|
|
85
|
+
text-align: left;
|
|
86
|
+
vertical-align: middle;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
</style>
|