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 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>