Skip to content

Commit b1a8a30

Browse files
author
Zerone
committed
feat: add Web Components and Vue Components
1 parent 3184394 commit b1a8a30

37 files changed

+1553
-727
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
import { defineConfig } from 'vite';
22
import vue from '@vitejs/plugin-vue';
33
import vueJsx from '@vitejs/plugin-vue-jsx';
4-
import autoprefixer from 'autoprefixer';
5-
import { resolve } from 'path';
4+
import { fileURLToPath } from 'node:url';
65

7-
// https://vitejs.dev/config/
86
export default defineConfig({
97
plugins: [vue(), vueJsx()],
10-
css: {
11-
postcss: {
12-
plugins: [autoprefixer()],
13-
},
14-
},
158
build: {
9+
copyPublicDir: false,
10+
rollupOptions: {
11+
external: ['lodash'],
12+
},
1613
lib: {
17-
entry: resolve(__dirname, 'src/index.ts'),
14+
entry: fileURLToPath(new URL('../src/index.ts', import.meta.url)),
1815
formats: ['es', 'cjs'],
1916
fileName: 'index',
2017
},
21-
copyPublicDir: false,
22-
rollupOptions: {
23-
external: ['vue', 'lodash'],
24-
},
2518
},
2619
});

config/vite.config.ce.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { defineConfig, mergeConfig } from 'vite';
2+
import baseConfig from './vite.config.base';
3+
import { fileURLToPath } from 'node:url';
4+
5+
export default mergeConfig(
6+
baseConfig,
7+
defineConfig({
8+
build: {
9+
outDir: 'dist/web-components',
10+
lib: {
11+
entry: fileURLToPath(
12+
new URL('../src/web-components/index.ts', import.meta.url)
13+
),
14+
},
15+
},
16+
})
17+
);

config/vite.config.core.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defineConfig, mergeConfig } from 'vite';
2+
import baseConfig from './vite.config.base';
3+
4+
export default mergeConfig(
5+
baseConfig,
6+
defineConfig({
7+
build: {
8+
rollupOptions: {
9+
external: ['vue'],
10+
},
11+
},
12+
})
13+
);

package.json

+11-4
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,19 @@
2020
},
2121
"type": "module",
2222
"files": [
23-
"dist"
23+
"dist",
24+
"web-components"
2425
],
2526
"main": "dist/index.cjs",
2627
"module": "dist/index.js",
2728
"types": "dist/types/index.d.ts",
2829
"style": "dist/style.css",
2930
"exports": {
31+
"./web-components": {
32+
"import": "./dist/web-components/index.js",
33+
"require": "./dist/web-components/index.cjs",
34+
"types": "./dist/web-components/index.d.ts"
35+
},
3036
".": {
3137
"import": "./dist/index.js",
3238
"require": "./dist/index.cjs",
@@ -35,13 +41,14 @@
3541
"./index.css": "./dist/style.css"
3642
},
3743
"scripts": {
38-
"dev": "vite",
39-
"build": "run-p type-check \"build-only {@}\" --",
44+
"dev": "vite -c=./config/vite.config.base.ts",
45+
"build": "run-s build:core build:ce type-check",
4046
"preview": "vite preview",
4147
"test:unit": "vitest",
4248
"test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
4349
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
44-
"build-only": "vite build",
50+
"build:core": "vite build -c=./config/vite.config.core.ts",
51+
"build:ce": "vite build -c=./config/vite.config.ce.ts",
4552
"type-check": "vue-tsc --build --force",
4653
"lint": "eslint \"**/*.{js,ts,jsx,tsx,vue}\" --fix",
4754
"format": "prettier \"**/*.{js,ts,jsx,tsx,vue,css,less}\" --write",

src/components/core/Core.vue

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<script lang="ts" setup>
2+
import { computed, useSlots } from 'vue';
3+
import {
4+
defaults,
5+
endsWith,
6+
isBoolean,
7+
isObject,
8+
isUndefined,
9+
mapValues,
10+
omit,
11+
pick,
12+
} from 'lodash';
13+
import { getPrefix } from '../../utils';
14+
import IconErrorMini from '../icon/IconErrorMini.vue';
15+
import type { CamelName, CoreProps, TargetProps } from './type';
16+
import { PUBLIC_PROPS_KEYS } from './utils';
17+
import type { PictureBaseProps } from '../picture/type';
18+
import Picture from '../picture/Picture.vue';
19+
import Skeleton from '../skeleton/Skeleton.vue';
20+
import IconLoading from '../icon/IconLoading.vue';
21+
import IconImage from '../icon/IconImage.vue';
22+
import IconEmptyMini from '../icon/IconEmptyMini.vue';
23+
24+
const props = defineProps<CoreProps>();
25+
26+
const priority: CamelName[] = [
27+
'loading',
28+
'skeleton',
29+
'skeletonAvatar',
30+
'skeletonList',
31+
'error',
32+
'empty',
33+
];
34+
const formatStyle = computed(() => {
35+
const key = priority.find((item) => props[item]);
36+
const showProps = key ? props[`${key}Props`] : {};
37+
const { zIndex, background } = { ...props, ...showProps };
38+
return {
39+
background,
40+
zIndex,
41+
};
42+
});
43+
44+
const formatProps = computed<TargetProps>(() =>
45+
mapValues(props, (value, key: keyof TargetProps) =>
46+
endsWith(key, 'Props') && isObject(value)
47+
? omit(value, PUBLIC_PROPS_KEYS)
48+
: value
49+
)
50+
);
51+
52+
const pictureBaseProps: (keyof PictureBaseProps)[] = [
53+
'icon',
54+
'iconMaxSize',
55+
'iconShowText',
56+
'miniIcon',
57+
'textColor',
58+
];
59+
60+
// loading
61+
const formatLoadingProps = computed(() => {
62+
const { loadingProps = {} } = formatProps.value;
63+
return defaults(
64+
{ ...loadingProps },
65+
{
66+
text: 'Loading…',
67+
miniIcon: true,
68+
iconMaxSize: 24,
69+
}
70+
);
71+
});
72+
const formatLoadingBind = computed(() =>
73+
pick(formatLoadingProps.value, pictureBaseProps)
74+
);
75+
76+
// skeleton
77+
const formatShowSkeleton = computed(() => {
78+
const { skeleton, skeletonList, skeletonAvatar } = props;
79+
return skeleton || skeletonList || skeletonAvatar;
80+
});
81+
const formatSkeletonProps = computed(() => {
82+
const {
83+
skeletonProps,
84+
skeletonAvatarProps: { animation: avatar, ...avatarProps } = {},
85+
skeletonListProps: { animation: list, ...listProps } = {},
86+
skeletonAvatar,
87+
skeletonList,
88+
} = formatProps.value;
89+
return {
90+
...skeletonProps,
91+
...avatarProps,
92+
...listProps,
93+
animation: isUndefined(avatar ?? list)
94+
? skeletonProps?.animation
95+
: { avatar, list },
96+
disabledAvatar: !skeletonAvatar && skeletonList,
97+
disabledList: !skeletonList && skeletonAvatar,
98+
};
99+
});
100+
101+
// error
102+
const formatError = computed(() => {
103+
const { error } = props;
104+
return isBoolean(error) ? ([error] as const) : error;
105+
});
106+
const formatRefreshText = computed(() => {
107+
const { refreshText = true } = props.errorProps || {};
108+
const onRefresh = formatError.value?.[1];
109+
if (!onRefresh || !refreshText) return '';
110+
if (refreshText === true) return ', Click to Refresh';
111+
if (/^|,/.test(refreshText)) return refreshText;
112+
return `, ${refreshText}`;
113+
});
114+
const formatErrorProps = computed(() => {
115+
const { errorProps = {} } = formatProps.value;
116+
const { text = 'Network Error' } = errorProps;
117+
return defaults(
118+
{ ...errorProps },
119+
{
120+
formatText: text + formatRefreshText.value,
121+
miniIcon: true,
122+
}
123+
);
124+
});
125+
const formatErrorBind = computed(() =>
126+
pick(formatErrorProps.value, pictureBaseProps)
127+
);
128+
129+
// empty
130+
const formatEmptyProps = computed(() => {
131+
const { emptyProps = {} } = formatProps.value;
132+
return defaults(
133+
{ ...emptyProps },
134+
{
135+
text: 'No Data',
136+
}
137+
);
138+
});
139+
const formatEmptyBind = computed(() =>
140+
pick(formatEmptyProps.value, pictureBaseProps)
141+
);
142+
143+
const slots = useSlots();
144+
const isIcon = (name: string, props: PictureBaseProps) => {
145+
if (slots[name]) return true;
146+
if (name.split('-').includes('mini')) return props.miniIcon === true;
147+
return !props.icon;
148+
};
149+
</script>
150+
151+
<template>
152+
<div
153+
:class="[getPrefix('core'), getPrefix('center'), getPrefix('main')]"
154+
:style="formatStyle"
155+
>
156+
<!-- 👆 getPrefix('main') 兼容 1.0.2 之前版本 -->
157+
<Picture v-if="loading" v-bind="formatLoadingBind">
158+
<template v-if="isIcon('loading-icon', formatLoadingProps)" #icon>
159+
<slot name="loading-icon">
160+
<IconLoading :stroke="formatLoadingProps.iconColor" />
161+
</slot>
162+
</template>
163+
<template
164+
v-if="isIcon('loading-mini-icon', formatLoadingProps)"
165+
#mini-icon
166+
>
167+
<slot name="loading-mini-icon">
168+
<IconLoading :stroke="formatLoadingProps.miniIconColor" />
169+
</slot>
170+
</template>
171+
{{ formatLoadingProps.text }}
172+
</Picture>
173+
<Skeleton v-else-if="formatShowSkeleton" v-bind="formatSkeletonProps" />
174+
<Picture
175+
v-else-if="formatError?.[0]"
176+
v-bind="formatErrorBind"
177+
:class="{ [getPrefix('pointer')]: formatError[1] }"
178+
@click="formatError[1]"
179+
>
180+
<template v-if="isIcon('error-icon', formatErrorProps)" #icon>
181+
<slot name="error-icon">
182+
<IconImage type="error" />
183+
</slot>
184+
</template>
185+
<template v-if="isIcon('error-mini-icon', formatErrorProps)" #mini-icon>
186+
<slot name="error-mini-icon">
187+
<IconErrorMini />
188+
</slot>
189+
</template>
190+
{{ formatErrorProps.formatText }}
191+
</Picture>
192+
<Picture v-else-if="empty" v-bind="formatEmptyBind">
193+
<template v-if="isIcon('empty-icon', formatEmptyProps)" #icon>
194+
<slot name="empty-icon">
195+
<IconImage type="empty" />
196+
</slot>
197+
</template>
198+
<template v-if="isIcon('empty-mini-icon', formatEmptyProps)" #mini-icon>
199+
<slot name="empty-mini-icon">
200+
<IconEmptyMini />
201+
</slot>
202+
</template>
203+
{{ formatEmptyProps.text }}
204+
</Picture>
205+
</div>
206+
</template>

src/components/core/index.less

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// :host 兼容 Web Components 写法
2+
:root,
3+
:host {
4+
--vdp-core-z-index: 100;
5+
--vdp-core-padding: 10px;
6+
--vdp-core-bg: #fff;
7+
}
8+
9+
.vdp-core {
10+
position: absolute;
11+
inset: 0;
12+
// --vdp-main 兼容 1.0.2 之前版本
13+
z-index: var(--vdp-main-z-index, var(--vdp-core-z-index));
14+
padding: var(--vdp-main-padding, var(--vdp-core-padding));
15+
background: var(--vdp-main-bg, var(--vdp-core-bg));
16+
}

0 commit comments

Comments
 (0)