@puresnake/snake-ui 0.0.0-stage → 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.
Files changed (279) hide show
  1. package/LICENSE +21 -0
  2. package/changelog.md +1 -0
  3. package/components/snake-activity-list/snake-activity-list.vue +365 -0
  4. package/components/snake-activity-list/types.ts +29 -0
  5. package/components/snake-auxiliary-segmentation/snake-auxiliary-segmentation.vue +44 -0
  6. package/components/snake-auxiliary-segmentation/types.ts +5 -0
  7. package/components/snake-button/snake-button.vue +357 -0
  8. package/components/snake-button/types.ts +103 -0
  9. package/components/snake-button-sort/snake-button-sort.vue +72 -0
  10. package/components/snake-button-sort/types.ts +22 -0
  11. package/components/snake-calendar/snake-calendar.vue +998 -0
  12. package/components/snake-calendar/types.ts +189 -0
  13. package/components/snake-cell-group/snake-cell-group.vue +55 -0
  14. package/components/snake-cell-group/types.ts +19 -0
  15. package/components/snake-cell-item/snake-cell-item.vue +159 -0
  16. package/components/snake-cell-item/types.ts +66 -0
  17. package/components/snake-checkbox/snake-checkbox.vue +264 -0
  18. package/components/snake-checkbox/types.ts +70 -0
  19. package/components/snake-checkbox-group/snake-checkbox-group.vue +154 -0
  20. package/components/snake-checkbox-group/types.ts +52 -0
  21. package/components/snake-cms/blocks/v2/cms-v2-carousel.vue +87 -0
  22. package/components/snake-cms/blocks/v2/cms-v2-coupon.vue +25 -0
  23. package/components/snake-cms/blocks/v2/cms-v2-custom.vue +12 -0
  24. package/components/snake-cms/blocks/v2/cms-v2-divider.vue +33 -0
  25. package/components/snake-cms/blocks/v2/cms-v2-floatbutton.vue +28 -0
  26. package/components/snake-cms/blocks/v2/cms-v2-goods-list.vue +150 -0
  27. package/components/snake-cms/blocks/v2/cms-v2-hotzone.vue +41 -0
  28. package/components/snake-cms/blocks/v2/cms-v2-images.vue +41 -0
  29. package/components/snake-cms/blocks/v2/cms-v2-magiccube.vue +93 -0
  30. package/components/snake-cms/blocks/v2/cms-v2-notice.vue +80 -0
  31. package/components/snake-cms/blocks/v2/cms-v2-popup.vue +46 -0
  32. package/components/snake-cms/blocks/v2/cms-v2-richtext.vue +18 -0
  33. package/components/snake-cms/blocks/v2/cms-v2-search.vue +115 -0
  34. package/components/snake-cms/blocks/v2/cms-v2-video.vue +25 -0
  35. package/components/snake-cms/blocks/v2/shared.ts +84 -0
  36. package/components/snake-cms/snake-cms.vue +409 -0
  37. package/components/snake-cms/types.ts +50 -0
  38. package/components/snake-commodity-search/snake-commodity-search.vue +234 -0
  39. package/components/snake-commodity-search/types.ts +11 -0
  40. package/components/snake-config-provider/snake-config-provider.vue +61 -0
  41. package/components/snake-config-provider/types.ts +22 -0
  42. package/components/snake-count-down/snake-count-down.vue +231 -0
  43. package/components/snake-count-down/types.ts +103 -0
  44. package/components/snake-coupon/snake-coupon.vue +209 -0
  45. package/components/snake-coupon/types.ts +21 -0
  46. package/components/snake-custom-module/snake-custom-module.vue +25 -0
  47. package/components/snake-custom-module/types.ts +11 -0
  48. package/components/snake-drop-menu/snake-drop-menu.vue +123 -0
  49. package/components/snake-drop-menu/types.ts +95 -0
  50. package/components/snake-drop-menu-item/snake-drop-menu-item.vue +181 -0
  51. package/components/snake-drop-menu-item/types.ts +48 -0
  52. package/components/snake-empty/snake-empty.vue +100 -0
  53. package/components/snake-empty/types.ts +48 -0
  54. package/components/snake-filter-sorts/filter-page.vue +695 -0
  55. package/components/snake-filter-sorts/snake-filter-sorts.vue +185 -0
  56. package/components/snake-filter-sorts/types.ts +57 -0
  57. package/components/snake-floating-bubble/snake-floating-bubble.vue +157 -0
  58. package/components/snake-floating-bubble/types.ts +43 -0
  59. package/components/snake-follow/snake-follow.vue +18 -0
  60. package/components/snake-follow/types.ts +2 -0
  61. package/components/snake-form/snake-form.vue +124 -0
  62. package/components/snake-form/types.ts +96 -0
  63. package/components/snake-form-item/snake-form-item.vue +331 -0
  64. package/components/snake-form-item/types.ts +70 -0
  65. package/components/snake-goods-list/snake-goods-list.vue +792 -0
  66. package/components/snake-goods-list/types.ts +55 -0
  67. package/components/snake-graphic-navigation/snake-graphic-navigation.vue +198 -0
  68. package/components/snake-graphic-navigation/types.ts +43 -0
  69. package/components/snake-icon/icon-names.ts +222 -0
  70. package/components/snake-icon/snake-icon.vue +220 -0
  71. package/components/snake-icon/types.ts +45 -0
  72. package/components/snake-image/snake-image.vue +230 -0
  73. package/components/snake-image/types.ts +70 -0
  74. package/components/snake-images/snake-images.vue +80 -0
  75. package/components/snake-images/types.ts +5 -0
  76. package/components/snake-index-anchor/snake-index-anchor.vue +83 -0
  77. package/components/snake-index-anchor/types.ts +14 -0
  78. package/components/snake-index-list/anchor-registry.ts +80 -0
  79. package/components/snake-index-list/snake-index-list.vue +259 -0
  80. package/components/snake-index-list/types.ts +115 -0
  81. package/components/snake-index-list-panel/snake-index-list-panel.vue +277 -0
  82. package/components/snake-index-list-panel/types.ts +10 -0
  83. package/components/snake-input/snake-input.vue +225 -0
  84. package/components/snake-input/types.ts +99 -0
  85. package/components/snake-jump-applet/snake-jump-applet.vue +99 -0
  86. package/components/snake-jump-applet/types.ts +17 -0
  87. package/components/snake-line/snake-line.vue +42 -0
  88. package/components/snake-line/types.ts +30 -0
  89. package/components/snake-line-progress/snake-line-progress.vue +105 -0
  90. package/components/snake-line-progress/types.ts +26 -0
  91. package/components/snake-link/snake-link.vue +74 -0
  92. package/components/snake-link/types.ts +30 -0
  93. package/components/snake-list-switching/components/tree-select/index.vue +156 -0
  94. package/components/snake-list-switching/snake-list-switching.vue +1285 -0
  95. package/components/snake-list-switching/types.ts +11 -0
  96. package/components/snake-loading/snake-loading.vue +241 -0
  97. package/components/snake-loading/types.ts +34 -0
  98. package/components/snake-loadmore/snake-loadmore.vue +103 -0
  99. package/components/snake-loadmore/types.ts +56 -0
  100. package/components/snake-magic-cube/snake-magic-cube.vue +622 -0
  101. package/components/snake-magic-cube/types.ts +5 -0
  102. package/components/snake-modal/snake-modal.vue +434 -0
  103. package/components/snake-modal/types.ts +71 -0
  104. package/components/snake-navbar/snake-navbar.vue +194 -0
  105. package/components/snake-navbar/types.ts +54 -0
  106. package/components/snake-no-network/snake-no-network.vue +125 -0
  107. package/components/snake-no-network/types.ts +27 -0
  108. package/components/snake-notice/snake-notice.vue +44 -0
  109. package/components/snake-notice/types.ts +5 -0
  110. package/components/snake-notice-bar/snake-notice-bar.vue +336 -0
  111. package/components/snake-notice-bar/types.ts +82 -0
  112. package/components/snake-overlay/snake-overlay.vue +73 -0
  113. package/components/snake-overlay/types.ts +35 -0
  114. package/components/snake-page-popup/snake-page-popup.vue +146 -0
  115. package/components/snake-page-popup/types.ts +28 -0
  116. package/components/snake-parse/audio/audio.vue +286 -0
  117. package/components/snake-parse/audio/context.js +11 -0
  118. package/components/snake-parse/audio/index.js +34 -0
  119. package/components/snake-parse/node/node.vue +830 -0
  120. package/components/snake-parse/parser.js +1480 -0
  121. package/components/snake-parse/snake-parse.vue +548 -0
  122. package/components/snake-parse/static/app-plus/snake-parse/js/handler.js +109 -0
  123. package/components/snake-parse/static/app-plus/snake-parse/js/uni.webview.js +0 -0
  124. package/components/snake-parse/static/app-plus/snake-parse/js/uni.webview.min.js +1 -0
  125. package/components/snake-parse/static/app-plus/snake-parse/local.html +3 -0
  126. package/components/snake-parse/style/index.js +132 -0
  127. package/components/snake-parse/style/parser.js +175 -0
  128. package/components/snake-parse/types.ts +49 -0
  129. package/components/snake-picture-ads/snake-picture-ads.vue +292 -0
  130. package/components/snake-picture-ads/types.ts +5 -0
  131. package/components/snake-popup/snake-popup.vue +348 -0
  132. package/components/snake-popup/types.ts +88 -0
  133. package/components/snake-qr-code/qrcode.ts +1214 -0
  134. package/components/snake-qr-code/snake-qr-code.vue +408 -0
  135. package/components/snake-qr-code/types.ts +78 -0
  136. package/components/snake-radio/snake-radio.vue +271 -0
  137. package/components/snake-radio/types.ts +64 -0
  138. package/components/snake-radio-group/snake-radio-group.vue +98 -0
  139. package/components/snake-radio-group/types.ts +48 -0
  140. package/components/snake-read-more/snake-read-more.vue +140 -0
  141. package/components/snake-read-more/types.ts +53 -0
  142. package/components/snake-rich-text/snake-rich-text.vue +24 -0
  143. package/components/snake-rich-text/types.ts +5 -0
  144. package/components/snake-root-portal/snake-root-portal.vue +92 -0
  145. package/components/snake-root-portal/types.ts +9 -0
  146. package/components/snake-search/snake-search.vue +202 -0
  147. package/components/snake-search/types.ts +84 -0
  148. package/components/snake-select/snake-select.vue +371 -0
  149. package/components/snake-select/types.ts +75 -0
  150. package/components/snake-skeleton/snake-skeleton.vue +104 -0
  151. package/components/snake-skeleton/types.ts +32 -0
  152. package/components/snake-skeleton-item/snake-skeleton-item.vue +90 -0
  153. package/components/snake-skeleton-item/types.ts +30 -0
  154. package/components/snake-slider/snake-slider.vue +249 -0
  155. package/components/snake-slider/types.ts +59 -0
  156. package/components/snake-sticky/snake-sticky.vue +178 -0
  157. package/components/snake-sticky/types.ts +48 -0
  158. package/components/snake-subsection/snake-subsection.vue +188 -0
  159. package/components/snake-subsection/types.ts +54 -0
  160. package/components/snake-swiper/snake-swiper.vue +229 -0
  161. package/components/snake-swiper/types.ts +79 -0
  162. package/components/snake-switch/snake-switch.vue +100 -0
  163. package/components/snake-switch/types.ts +41 -0
  164. package/components/snake-tabbar/snake-tabbar.vue +306 -0
  165. package/components/snake-tabbar/types.ts +93 -0
  166. package/components/snake-tabs/snake-tabs.vue +305 -0
  167. package/components/snake-tabs/types.ts +79 -0
  168. package/components/snake-text/snake-text.vue +292 -0
  169. package/components/snake-text/types.ts +100 -0
  170. package/components/snake-textarea/snake-textarea.vue +171 -0
  171. package/components/snake-textarea/types.ts +79 -0
  172. package/components/snake-toast/snake-toast.vue +330 -0
  173. package/components/snake-toast/types.ts +55 -0
  174. package/components/snake-tooltip/position.ts +33 -0
  175. package/components/snake-tooltip/snake-tooltip.vue +339 -0
  176. package/components/snake-tooltip/types.ts +80 -0
  177. package/components/snake-transition/snake-transition.vue +315 -0
  178. package/components/snake-transition/types.ts +65 -0
  179. package/components/snake-upload/snake-upload.vue +1801 -0
  180. package/components/snake-upload/types.ts +256 -0
  181. package/components/snake-videos/snake-videos.vue +122 -0
  182. package/components/snake-videos/types.ts +7 -0
  183. package/iconfont.css +881 -0
  184. package/index.d.ts +7 -0
  185. package/index.scss +26 -0
  186. package/index.ts +94 -0
  187. package/libs/cms/blocks.ts +103 -0
  188. package/libs/cms/config.ts +25 -0
  189. package/libs/cms/index.ts +7 -0
  190. package/libs/cms/normalize.ts +135 -0
  191. package/libs/cms/resolve.ts +12 -0
  192. package/libs/cms/types.ts +164 -0
  193. package/libs/cms/use-commerce.ts +385 -0
  194. package/libs/cms/v2.ts +193 -0
  195. package/libs/config/index.ts +4 -0
  196. package/libs/config/modal.ts +93 -0
  197. package/libs/config/state.ts +33 -0
  198. package/libs/config/theme.ts +351 -0
  199. package/libs/config/toast.ts +71 -0
  200. package/libs/config/types.ts +135 -0
  201. package/libs/config/zIndex.ts +44 -0
  202. package/libs/css/color.scss +105 -0
  203. package/libs/css/common.scss +171 -0
  204. package/libs/css/style.h5.scss +7 -0
  205. package/libs/css/style.mp.scss +7 -0
  206. package/libs/css/style.theme.scss +70 -0
  207. package/libs/css/style.vue.scss +140 -0
  208. package/libs/form/async-validator.min.d.ts +75 -0
  209. package/libs/form/async-validator.min.js +3 -0
  210. package/libs/form/index.ts +3 -0
  211. package/libs/form/path.ts +40 -0
  212. package/libs/form/types.ts +51 -0
  213. package/libs/form/validate.ts +149 -0
  214. package/libs/foundation.ts +96 -0
  215. package/libs/function/$parent.ts +33 -0
  216. package/libs/function/clipboard.ts +13 -0
  217. package/libs/function/color.ts +55 -0
  218. package/libs/function/debounce.ts +34 -0
  219. package/libs/function/deepClone.ts +30 -0
  220. package/libs/function/deepMerge.ts +25 -0
  221. package/libs/function/emitter.ts +30 -0
  222. package/libs/function/format.ts +27 -0
  223. package/libs/function/getParent.ts +53 -0
  224. package/libs/function/getRect.ts +29 -0
  225. package/libs/function/guid.ts +11 -0
  226. package/libs/function/index.ts +31 -0
  227. package/libs/function/queryParams.ts +73 -0
  228. package/libs/function/random.ts +6 -0
  229. package/libs/function/randomArray.ts +14 -0
  230. package/libs/function/route.ts +155 -0
  231. package/libs/function/sleep.ts +7 -0
  232. package/libs/function/style.ts +58 -0
  233. package/libs/function/styleUtils.ts +83 -0
  234. package/libs/function/system.ts +14 -0
  235. package/libs/function/test.ts +202 -0
  236. package/libs/function/throttle.ts +36 -0
  237. package/libs/function/timeFormat.ts +27 -0
  238. package/libs/function/timeFrom.ts +27 -0
  239. package/libs/function/toast.ts +10 -0
  240. package/libs/function/trim.ts +11 -0
  241. package/libs/function/type2icon.ts +11 -0
  242. package/libs/function/unit.ts +45 -0
  243. package/libs/hooks/index.ts +11 -0
  244. package/libs/hooks/useChildren.ts +125 -0
  245. package/libs/hooks/useColor.ts +5 -0
  246. package/libs/hooks/useDebounce.ts +5 -0
  247. package/libs/hooks/useLocale.ts +13 -0
  248. package/libs/hooks/useModal.ts +131 -0
  249. package/libs/hooks/useParent.ts +62 -0
  250. package/libs/hooks/useRect.ts +44 -0
  251. package/libs/hooks/useRouter.ts +5 -0
  252. package/libs/hooks/useTheme.ts +31 -0
  253. package/libs/hooks/useThrottle.ts +5 -0
  254. package/libs/hooks/useToast.ts +116 -0
  255. package/libs/index.ts +18 -0
  256. package/libs/request/auto-http.ts +1 -0
  257. package/libs/request/index.ts +343 -0
  258. package/libs/runtime/accessors.ts +55 -0
  259. package/libs/runtime/context.ts +40 -0
  260. package/libs/runtime/index.ts +18 -0
  261. package/libs/runtime/types.ts +49 -0
  262. package/libs/util/calendar.ts +6 -0
  263. package/locale/index.ts +130 -0
  264. package/locale/lang/en-US.ts +67 -0
  265. package/locale/lang/zh-CN.ts +67 -0
  266. package/package.json +130 -5
  267. package/plugins/index.ts +1 -0
  268. package/plugins/root/README.md +86 -0
  269. package/plugins/root/index.ts +123 -0
  270. package/plugins/root/page.ts +77 -0
  271. package/plugins/root/root.ts +53 -0
  272. package/plugins/root/utils.ts +137 -0
  273. package/readme.md +79 -0
  274. package/theme.scss +118 -0
  275. package/types/components.d.ts +236 -0
  276. package/types/global.d.ts +21 -0
  277. package/types/index.d.ts +6 -0
  278. package/types/uni-app.d.ts +16 -0
  279. package/README.md +0 -3
@@ -0,0 +1,51 @@
1
+ export type FormTrigger = 'change' | 'blur';
2
+ export type FormRuleType =
3
+ | 'string'
4
+ | 'number'
5
+ | 'boolean'
6
+ | 'method'
7
+ | 'regexp'
8
+ | 'integer'
9
+ | 'float'
10
+ | 'array'
11
+ | 'object'
12
+ | 'date'
13
+ | 'url'
14
+ | 'hex'
15
+ | 'email'
16
+ | 'any';
17
+ export type FormValidatorResult = boolean | string | Error | void;
18
+ export type FormValidatorCallback = (error?: string | Error) => void;
19
+
20
+ export interface FormRule {
21
+ required?: boolean;
22
+ type?: FormRuleType;
23
+ min?: number;
24
+ max?: number;
25
+ len?: number;
26
+ enum?: unknown[];
27
+ pattern?: RegExp | string;
28
+ whitespace?: boolean;
29
+ message?: string;
30
+ trigger?: FormTrigger | FormTrigger[];
31
+ transform?: (value: unknown) => unknown;
32
+ validator?: (
33
+ rule: FormRule,
34
+ value: unknown,
35
+ callback: FormValidatorCallback
36
+ ) => FormValidatorResult | Promise<FormValidatorResult>;
37
+ asyncValidator?: (
38
+ rule: FormRule,
39
+ value: unknown,
40
+ callback: FormValidatorCallback
41
+ ) => FormValidatorResult | Promise<FormValidatorResult>;
42
+ fields?: FormRules;
43
+ defaultField?: FormRule | FormRule[];
44
+ }
45
+
46
+ export type FormRules = Record<string, FormRule | FormRule[]>;
47
+ export interface FormError {
48
+ prop: string;
49
+ message: string;
50
+ rule?: FormRule;
51
+ }
@@ -0,0 +1,149 @@
1
+ import type {
2
+ FormError,
3
+ FormRule,
4
+ FormRules,
5
+ FormTrigger,
6
+ FormValidatorCallback,
7
+ FormValidatorResult
8
+ } from './types';
9
+
10
+ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
11
+ const urlPattern = /^(https?|ftp):\/\/[^\s]+$/i;
12
+ const hexPattern = /^#?(?:[\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i;
13
+
14
+ function empty(value: unknown): boolean {
15
+ return value == null || value === '' || (Array.isArray(value) && value.length === 0);
16
+ }
17
+ function lengthOf(value: unknown): number | undefined {
18
+ if (typeof value === 'number') return value;
19
+ if (typeof value === 'string' || Array.isArray(value)) return value.length;
20
+ return undefined;
21
+ }
22
+ function defaultMessage(rule: FormRule): string {
23
+ if (rule.required) return '此项为必填项';
24
+ if (rule.len != null) return `长度必须为 ${rule.len}`;
25
+ if (rule.min != null && rule.max != null) return `长度或数值应在 ${rule.min}-${rule.max} 之间`;
26
+ if (rule.min != null) return `长度或数值不能小于 ${rule.min}`;
27
+ if (rule.max != null) return `长度或数值不能大于 ${rule.max}`;
28
+ return '格式不正确';
29
+ }
30
+ function typeMatches(type: FormRule['type'], value: unknown): boolean {
31
+ if (!type || type === 'any') return true;
32
+ if (type === 'array') return Array.isArray(value);
33
+ if (type === 'date') {
34
+ const date = typeof value === 'number' ? new Date(value) : value;
35
+ return date instanceof Date && !Number.isNaN(date.getTime());
36
+ }
37
+ if (type === 'regexp') return value instanceof RegExp;
38
+ if (type === 'integer') return typeof value === 'number' && Number.isInteger(value);
39
+ if (type === 'float') return typeof value === 'number' && Number.isFinite(value);
40
+ if (type === 'method') return typeof value === 'function';
41
+ if (type === 'url') return typeof value === 'string' && urlPattern.test(value);
42
+ if (type === 'email') return typeof value === 'string' && emailPattern.test(value);
43
+ if (type === 'hex') return typeof value === 'string' && hexPattern.test(value);
44
+ if (type === 'number') return typeof value === 'number' && !Number.isNaN(value);
45
+ return typeof value === type;
46
+ }
47
+ function normalizeCustomResult(result: FormValidatorResult, fallback: string): string | undefined {
48
+ if (result === false) return fallback;
49
+ if (typeof result === 'string') return result;
50
+ if (result instanceof Error) return result.message || fallback;
51
+ return undefined;
52
+ }
53
+
54
+ async function runCustom(rule: FormRule, value: unknown): Promise<string | undefined> {
55
+ const validator = rule.asyncValidator || rule.validator;
56
+ if (!validator) return undefined;
57
+ return new Promise(resolve => {
58
+ let completed = false;
59
+ const done: FormValidatorCallback = error => {
60
+ if (completed) return;
61
+ completed = true;
62
+ resolve(typeof error === 'string' ? error : error?.message);
63
+ };
64
+ try {
65
+ const result = validator(rule, value, done);
66
+ if (result && typeof (result as Promise<FormValidatorResult>).then === 'function') {
67
+ void (result as Promise<FormValidatorResult>)
68
+ .then(value => {
69
+ if (!completed) {
70
+ completed = true;
71
+ resolve(normalizeCustomResult(value, rule.message || '校验失败'));
72
+ }
73
+ })
74
+ .catch(error => done(error instanceof Error ? error : String(error)));
75
+ } else if (result !== undefined) {
76
+ completed = true;
77
+ resolve(normalizeCustomResult(result as FormValidatorResult, rule.message || '校验失败'));
78
+ } else if (validator.length < 3) {
79
+ completed = true;
80
+ resolve(undefined);
81
+ }
82
+ } catch (error) {
83
+ done(error instanceof Error ? error : String(error));
84
+ }
85
+ });
86
+ }
87
+
88
+ export function ruleMatchesTrigger(rule: FormRule, trigger?: FormTrigger): boolean {
89
+ if (!trigger || !rule.trigger) return true;
90
+ return (Array.isArray(rule.trigger) ? rule.trigger : [rule.trigger]).includes(trigger);
91
+ }
92
+
93
+ export async function validateFormValue(
94
+ prop: string,
95
+ rawValue: unknown,
96
+ rules: FormRule | FormRule[],
97
+ trigger?: FormTrigger
98
+ ): Promise<FormError[]> {
99
+ const list = (Array.isArray(rules) ? rules : [rules]).filter(rule => ruleMatchesTrigger(rule, trigger));
100
+ const errors: FormError[] = [];
101
+ for (const rule of list) {
102
+ const value = rule.transform ? rule.transform(rawValue) : rawValue;
103
+ let message: string | undefined;
104
+ if (rule.required && empty(value)) message = rule.message || defaultMessage(rule);
105
+ else if (!empty(value)) {
106
+ const length = lengthOf(value);
107
+ if (!typeMatches(rule.type, value)) message = rule.message || defaultMessage(rule);
108
+ else if (rule.len != null && length !== rule.len) message = rule.message || defaultMessage(rule);
109
+ else if (rule.min != null && length != null && length < rule.min)
110
+ message = rule.message || defaultMessage(rule);
111
+ else if (rule.max != null && length != null && length > rule.max)
112
+ message = rule.message || defaultMessage(rule);
113
+ else if (rule.enum && !rule.enum.includes(value)) message = rule.message || defaultMessage(rule);
114
+ else if (rule.pattern) {
115
+ const pattern = typeof rule.pattern === 'string' ? new RegExp(rule.pattern) : rule.pattern;
116
+ pattern.lastIndex = 0;
117
+ if (!pattern.test(String(value))) message = rule.message || defaultMessage(rule);
118
+ } else if (rule.whitespace && typeof value === 'string' && /^\s+$/.test(value)) {
119
+ message = rule.message || defaultMessage(rule);
120
+ } else message = await runCustom(rule, value);
121
+
122
+ if (!message && value && (rule.fields || rule.defaultField) && typeof value === 'object') {
123
+ const childRules: FormRules = { ...(rule.fields || {}) };
124
+ if (rule.defaultField) {
125
+ for (const key of Object.keys(value as Record<string, unknown>)) {
126
+ if (!childRules[key]) childRules[key] = rule.defaultField;
127
+ }
128
+ }
129
+ for (const [key, childRule] of Object.entries(childRules)) {
130
+ const childErrors = await validateFormValue(
131
+ `${prop}.${key}`,
132
+ (value as Record<string, unknown>)[key],
133
+ childRule,
134
+ trigger
135
+ );
136
+ if (childErrors.length) {
137
+ message = childErrors[0].message;
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ if (message) {
144
+ errors.push({ prop, message, rule });
145
+ break;
146
+ }
147
+ }
148
+ return errors;
149
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Snake UI 全局 API 聚合与插件初始化。
3
+ *
4
+ * foundation 汇总工具函数、hooks、主题、国际化、HTTP 与全局配置,
5
+ * 经根入口挂载为 $snake;宿主注入的 request / cache 等运行时适配器
6
+ * 由 libs/runtime 负责,不经此文件。
7
+ */
8
+ import type { App } from 'vue';
9
+ import type { RuntimeOptions } from './runtime';
10
+ import { config, setConfig, SNAKE_UI_VERSION } from './config/state';
11
+ import zIndex from './config/zIndex';
12
+ import {
13
+ currentTheme,
14
+ initTheme,
15
+ isDarkMode,
16
+ setDarkMode,
17
+ setTheme,
18
+ setThemeColor,
19
+ color,
20
+ cssVars,
21
+ darkMode,
22
+ themes,
23
+ toggleDarkMode
24
+ } from './config/theme';
25
+ import * as functions from './function';
26
+ import { emitter } from './function/emitter';
27
+ import * as hooks from './hooks';
28
+ import { currentLocale, initLocale, setLocale, locales, translate } from '../locale';
29
+ import { configureHttp, http } from './request';
30
+ import { configureCms } from './cms';
31
+
32
+ /**
33
+ * 全局 API 聚合:工具函数与 hooks 全量展开,再显式补上配置、
34
+ * 主题、国际化、HTTP 等具名成员,并保留 date / isInDarkMode / t 等短别名。
35
+ */
36
+ export const foundation = {
37
+ ...functions,
38
+ ...hooks,
39
+ color: color,
40
+ config: config,
41
+ zIndex,
42
+ version: SNAKE_UI_VERSION,
43
+ /** 语义色类型清单,toast / modal 等组件按它约束 type 取值。 */
44
+ type: ['primary', 'success', 'error', 'warning', 'info'],
45
+ /** date 是 formatTime 的短别名。 */
46
+ date: functions.formatTime,
47
+ emitter: emitter,
48
+ http,
49
+ themes: themes,
50
+ currentTheme: currentTheme,
51
+ darkMode: darkMode,
52
+ cssVars: cssVars,
53
+ locales: locales,
54
+ currentLocale: currentLocale,
55
+ initTheme: initTheme,
56
+ setTheme: setTheme,
57
+ setThemeColor: setThemeColor,
58
+ setDarkMode: setDarkMode,
59
+ toggleDarkMode: toggleDarkMode,
60
+ isInDarkMode: isDarkMode,
61
+ initLocales: initLocale,
62
+ setLocale: setLocale,
63
+ /** t 是 translate 的短别名。 */
64
+ t: translate
65
+ };
66
+
67
+ /** foundation 的公开类型视图,供宿主对 $snake 做类型标注。 */
68
+ export type FoundationAPI = typeof foundation;
69
+ /** Snake UI 的唯一全局 API 聚合。运行时适配器不在这里安装。 */
70
+ export const snake = foundation;
71
+
72
+ /**
73
+ * 安装 UI 基座:由根入口(index.ts 的 install)调用,在 $snake
74
+ * 挂载到全局属性之前完成配置、主题、语言、HTTP 与 CMS 的初始化。
75
+ *
76
+ * - `$http`:请求全局属性,与 snake.http 同一单例。
77
+ * - `$themeStyle`:用 getter 定义而非直接赋值,模板每次读取都取
78
+ * cssVars 当前值,主题切换后不需要重新挂属性、也不会固化快照。
79
+ * - provide 的 theme / locale 面向宿主业务组件 inject 使用;
80
+ * snake-ui 自身组件直接消费 config/theme 与 locale 模块,
81
+ * 不依赖这两个注入。
82
+ */
83
+ export function installUIFoundation(app: App, options: RuntimeOptions = {}): void {
84
+ setConfig({ debugMode: options.debugMode ?? false });
85
+ initTheme(options.theme);
86
+ initLocale(options.locale);
87
+ configureHttp(options.http, options.request);
88
+ (app.config.globalProperties as Record<string, unknown>).$http = http;
89
+ configureCms(options.cms);
90
+ Object.defineProperty(app.config.globalProperties, '$themeStyle', {
91
+ configurable: true,
92
+ get: () => cssVars.value
93
+ });
94
+ app.provide('snake-ui-theme', currentTheme);
95
+ app.provide('snake-ui-locale', currentLocale);
96
+ }
@@ -0,0 +1,33 @@
1
+ // 获取父组件实例,因为支付宝小程序不支持 provide/inject 的写法
2
+ // 在非 H5 中 instance.parent 可以准确获取到父组件,但在 H5 中需要多层 parent.parent.xxx
3
+ // componentName 默认 undefined 有其含义:最顶层元素的 parent 就是 undefined,
4
+ // 不传 name 即查找最顶层的父实例。
5
+ //
6
+ // 注意:本文件属于 function/ 基础层,会被 node 直载测试加载,禁止 import 'vue';
7
+ // 调用方(组件内)自行传入 getCurrentInstance() 结果,与 getRect(selector, instance) 约定一致。
8
+
9
+ export interface ParentLike {
10
+ parent?: ParentLike | null;
11
+ type?: { name?: string };
12
+ }
13
+
14
+ export function $parent(
15
+ componentName?: string,
16
+ instance?: ParentLike | null
17
+ ): ParentLike | null | undefined {
18
+ let parent = instance?.parent ?? null;
19
+
20
+ if (!componentName) return parent;
21
+
22
+ while (parent) {
23
+ const name = parent.type?.name;
24
+ if (name === componentName) {
25
+ return parent;
26
+ }
27
+ parent = parent.parent ?? null;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ // 兼容默认导入习惯:import $parent from '.../$parent'
33
+ export default $parent;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 复制文本到剪贴板(Promise 版):成功 resolve,平台无该 API 或调用失败 reject。
3
+ * 不接管提示——小程序端由 uni 默认行为弹「已复制」。
4
+ */
5
+ export function clipboard(value: string): Promise<void> {
6
+ return new Promise((resolve, reject) => {
7
+ if (typeof uni === 'undefined' || typeof uni.setClipboardData !== 'function') {
8
+ reject(new Error('[Snake UI] clipboard is unavailable on this platform.'));
9
+ return;
10
+ }
11
+ uni.setClipboardData({ data: value, success: () => resolve(), fail: reject });
12
+ });
13
+ }
@@ -0,0 +1,55 @@
1
+ /** 归一化 hex:接受 3 / 6 位(可带 #),3 位展开为 6 位,其余返回 undefined。 */
2
+ function normalizeHex(value: string): string | undefined {
3
+ const color = value.trim().replace('#', '');
4
+ if (/^[0-9a-f]{3}$/i.test(color))
5
+ return color
6
+ .split('')
7
+ .map(item => item.repeat(2))
8
+ .join('');
9
+ return /^[0-9a-f]{6}$/i.test(color) ? color : undefined;
10
+ }
11
+
12
+ /** hex → { r, g, b };无法解析时返回 undefined。 */
13
+ export function hexToRgb(value: string): { r: number; g: number; b: number } | undefined {
14
+ const color = normalizeHex(value);
15
+ if (!color) return undefined;
16
+ return {
17
+ r: Number.parseInt(color.slice(0, 2), 16),
18
+ g: Number.parseInt(color.slice(2, 4), 16),
19
+ b: Number.parseInt(color.slice(4, 6), 16)
20
+ };
21
+ }
22
+
23
+ /** rgb → #rrggbb;各通道先收敛到 [0, 255] 再取整,输出恒为 6 位。 */
24
+ export function rgbToHex(r: number, g: number, b: number): string {
25
+ const channel = (value: number) =>
26
+ Math.min(255, Math.max(0, Math.round(value)))
27
+ .toString(16)
28
+ .padStart(2, '0');
29
+ return `#${channel(r)}${channel(g)}${channel(b)}`;
30
+ }
31
+
32
+ /** hex → rgba(...):alpha 收敛到 [0, 1],无法解析的输入原样返回。 */
33
+ export function colorToRgba(value: string, alpha = 1): string {
34
+ const rgb = hexToRgb(value);
35
+ return rgb ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${Math.min(1, Math.max(0, alpha))})` : value;
36
+ }
37
+
38
+ /**
39
+ * 两色之间的线性渐变阶梯,首尾正好是起止色。
40
+ * @param steps 阶梯数:<=0 返回空数组,1 只返回起点色
41
+ */
42
+ export function colorGradient(start: string, end: string, steps: number): string[] {
43
+ const from = hexToRgb(start);
44
+ const to = hexToRgb(end);
45
+ if (!from || !to || steps <= 0) return [];
46
+ if (steps === 1) return [rgbToHex(from.r, from.g, from.b)];
47
+ return Array.from({ length: steps }, (_, index) => {
48
+ const ratio = index / (steps - 1);
49
+ return rgbToHex(
50
+ from.r + (to.r - from.r) * ratio,
51
+ from.g + (to.g - from.g) * ratio,
52
+ from.b + (to.b - from.b) * ratio
53
+ );
54
+ });
55
+ }
@@ -0,0 +1,34 @@
1
+ /** 包装后的函数:保留原调用能力,另提供 cancel() 取消未触发的定时执行。 */
2
+ export interface CancelableFunction<Args extends unknown[]> {
3
+ (...args: Args): void;
4
+ cancel(): void;
5
+ }
6
+
7
+ /**
8
+ * 防抖:wait 毫秒内只有最后一次调用生效,静默期结束后执行。
9
+ * @param callback 目标回调,参数由包装函数透传
10
+ * @param wait 防抖窗口,单位 ms,默认 300
11
+ * @param immediate true 时首次调用立即执行并进入冷却,默认 false
12
+ * @returns 带 cancel() 的包装函数
13
+ */
14
+ export function debounce<Args extends unknown[]>(
15
+ callback: (...args: Args) => void,
16
+ wait = 300,
17
+ immediate = false
18
+ ): CancelableFunction<Args> {
19
+ let timer: ReturnType<typeof setTimeout> | undefined;
20
+ const wrapped = ((...args: Args) => {
21
+ const callNow = immediate && !timer;
22
+ if (timer) clearTimeout(timer);
23
+ timer = setTimeout(() => {
24
+ timer = undefined;
25
+ if (!immediate) callback(...args);
26
+ }, wait);
27
+ if (callNow) callback(...args);
28
+ }) as CancelableFunction<Args>;
29
+ wrapped.cancel = () => {
30
+ if (timer) clearTimeout(timer);
31
+ timer = undefined;
32
+ };
33
+ return wrapped;
34
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * 深拷贝:WeakMap 记录「原对象 → 克隆」以支持循环引用,
3
+ * Reflect.ownKeys 连 Symbol 键与不可枚举键一起复制,
4
+ * Date / Map / Set 重建新实例,其余引用类型按对象 / 数组重构。
5
+ * @param value 任意值,原始类型直接返回
6
+ * @param seen 循环引用缓存,递归内部使用
7
+ */
8
+ export function deepClone<T>(value: T, seen = new WeakMap<object, unknown>()): T {
9
+ if (value == null || typeof value !== 'object') return value;
10
+ if (seen.has(value as object)) return seen.get(value as object) as T;
11
+ if (value instanceof Date) return new Date(value.getTime()) as T;
12
+ if (value instanceof Map) {
13
+ const result = new Map();
14
+ seen.set(value, result);
15
+ value.forEach((item, key) => result.set(deepClone(key, seen), deepClone(item, seen)));
16
+ return result as T;
17
+ }
18
+ if (value instanceof Set) {
19
+ const result = new Set();
20
+ seen.set(value, result);
21
+ value.forEach(item => result.add(deepClone(item, seen)));
22
+ return result as T;
23
+ }
24
+ const result: unknown[] | Record<PropertyKey, unknown> = Array.isArray(value) ? [] : {};
25
+ seen.set(value as object, result);
26
+ Reflect.ownKeys(value as object).forEach(key => {
27
+ (result as Record<PropertyKey, unknown>)[key] = deepClone((value as Record<PropertyKey, unknown>)[key], seen);
28
+ });
29
+ return result as T;
30
+ }
@@ -0,0 +1,25 @@
1
+ import { deepClone } from './deepClone';
2
+
3
+ /** 纯对象判定(原型为 null 或 Object.prototype),非纯对象不参与递归合并。 */
4
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
5
+ if (!value || Object.prototype.toString.call(value) !== '[object Object]') return false;
6
+ const prototype = Object.getPrototypeOf(value);
7
+ return prototype === null || prototype === Object.prototype;
8
+ }
9
+
10
+ /**
11
+ * 多源深合并:target 深拷贝后作为基底(不改动入参),sources 依次覆盖。
12
+ * 只有两侧都是纯对象才递归合并;数组等其余值整体替换为深拷贝。
13
+ */
14
+ export function deepMerge<T extends Record<string, any>>(target: T, ...sources: Array<Partial<T>>): T {
15
+ const result = deepClone(target);
16
+ sources.forEach(source => {
17
+ Object.entries(source || {}).forEach(([key, value]) => {
18
+ const previous = result[key];
19
+ result[key as keyof T] = (
20
+ isPlainObject(previous) && isPlainObject(value) ? deepMerge(previous, value) : deepClone(value)
21
+ ) as T[keyof T];
22
+ });
23
+ });
24
+ return result;
25
+ }
@@ -0,0 +1,30 @@
1
+ export type EventHandler<Payload = unknown> = (payload: Payload) => void;
2
+
3
+ /**
4
+ * 轻量事件总线:同一 handler 用 Set 去重,on 返回解绑函数,
5
+ * off 不传 handler 时清空整个事件,clear() 清空全部事件。
6
+ */
7
+ export function createEmitter() {
8
+ const events = new Map<string, Set<EventHandler>>();
9
+ return {
10
+ on<Payload>(name: string, handler: EventHandler<Payload>) {
11
+ const handlers = events.get(name) || new Set();
12
+ handlers.add(handler as EventHandler);
13
+ events.set(name, handlers);
14
+ return () => handlers.delete(handler as EventHandler);
15
+ },
16
+ off<Payload>(name: string, handler?: EventHandler<Payload>) {
17
+ if (!handler) events.delete(name);
18
+ else events.get(name)?.delete(handler as EventHandler);
19
+ },
20
+ emit<Payload>(name: string, payload: Payload) {
21
+ events.get(name)?.forEach(handler => handler(payload));
22
+ },
23
+ clear() {
24
+ events.clear();
25
+ }
26
+ };
27
+ }
28
+
29
+ /** 库内共享的事件总线单例。 */
30
+ export const emitter = createEmitter();
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 价格格式化:剥离非数字字符后按 decimals 保留小数、整数部分加千分位;
3
+ * 非有限数值按 0 处理。
4
+ * @param value 数字或数字字符串
5
+ * @param decimals 小数位数,默认 0
6
+ * @param decimalPoint 小数点符号,默认 '.'
7
+ * @param thousandsSeparator 千分位符号,默认 ','
8
+ */
9
+ export function formatPrice(
10
+ value: string | number,
11
+ decimals = 0,
12
+ decimalPoint = '.',
13
+ thousandsSeparator = ','
14
+ ): string {
15
+ const numeric = Number(String(value).replace(/[^0-9+\-Ee.]/g, ''));
16
+ const safe = Number.isFinite(numeric) ? numeric : 0;
17
+ const fixed = safe.toFixed(Math.max(0, decimals)).split('.');
18
+ fixed[0] = fixed[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator);
19
+ return fixed.join(decimalPoint);
20
+ }
21
+
22
+ /** 姓名脱敏:单字原样,两字保首打 *,三字以上首尾保留、中间打 *。 */
23
+ export function formatName(value: string): string {
24
+ if (value.length <= 1) return value;
25
+ if (value.length === 2) return `${value[0]}*`;
26
+ return `${value[0]}${'*'.repeat(value.length - 2)}${value.at(-1)}`;
27
+ }
@@ -0,0 +1,53 @@
1
+ // 获取父组件的参数,因为支付宝小程序不支持 provide/inject 的写法
2
+ // this.$parent 在非 H5 中可以准确获取到父组件,但在 H5 中需要多层 this.$parent.$parent.xxx
3
+ interface VueInstance {
4
+ $parent?: VueInstance;
5
+ $options?: { name?: string };
6
+ [key: string]: any;
7
+ }
8
+
9
+ /**
10
+ * 沿 $parent 链查找指定 name 的父组件并按 keys 取参。
11
+ * @param this 当前组件实例(Options API 调用方用 getParent.call(this, ...))
12
+ * @param name 父组件 name
13
+ * @param keys 需要获取的参数名数组或对象;对象形态时子组件已有值优先(false 也算已传)
14
+ * @returns 父组件参数对象
15
+ */
16
+ export function getParent(
17
+ this: VueInstance,
18
+ name: string,
19
+ keys: string[] | Record<string, any>
20
+ ): Record<string, any> {
21
+ let parent = this.$parent;
22
+ // 通过 while 历遍,这里主要是为了 H5 需要多层解析的问题
23
+ while (parent) {
24
+ if (parent.$options?.name !== name) {
25
+ // 如果组件的 name 不相等,继续上一级寻找
26
+ parent = parent.$parent;
27
+ } else {
28
+ const data: Record<string, any> = {};
29
+ // 传数组时直接用数组元素值当做键值去父组件寻找
30
+ if (Array.isArray(keys)) {
31
+ keys.forEach(val => {
32
+ data[val] = parent?.[val] ? parent[val] : '';
33
+ });
34
+ } else {
35
+ for (const key in keys) {
36
+ const childValue = keys[key];
37
+ // 空数组/空对象视为未传,回落父组件的值
38
+ if (Array.isArray(childValue) || (childValue && childValue.constructor === Object)) {
39
+ data[key] = Object.keys(childValue).length ? childValue : parent[key];
40
+ } else {
41
+ // 只要子组件有传值,即使是 false 值也算「传值」,覆盖父组件的同名参数
42
+ data[key] = childValue || childValue === false ? childValue : parent[key];
43
+ }
44
+ }
45
+ }
46
+ return data;
47
+ }
48
+ }
49
+ return {};
50
+ }
51
+
52
+ // 兼容默认导入习惯:import getParent from '.../getParent'
53
+ export default getParent;
@@ -0,0 +1,29 @@
1
+ export type SelectorRect = UniApp.NodeInfo & { width: number; height: number; top: number; left: number };
2
+
3
+ /**
4
+ * 查询节点布局信息(boundingClientRect)。
5
+ * 查不到节点不 reject:单值返回全 0 兜底 rect,all=true 返回空数组,调用方无需判空。
6
+ * @param selector CSS 选择器
7
+ * @param instance 组件实例(自动取 .proxy)或上下文对象;不传则页面级查询
8
+ * @param all 是否查询全部匹配节点(返回数组)
9
+ */
10
+ export function getRect(selector: string, instance?: unknown, all?: false): Promise<SelectorRect>;
11
+ export function getRect(selector: string, instance: unknown | undefined, all: true): Promise<SelectorRect[]>;
12
+ export function getRect(selector: string, instance?: unknown, all = false): Promise<SelectorRect | SelectorRect[]> {
13
+ return new Promise(resolve => {
14
+ let query = uni.createSelectorQuery();
15
+ // instance 可能是组件实例(取 .proxy)也可能是上下文对象;部分平台无 query.in,降级为页面级查询
16
+ const target = (instance as { proxy?: unknown } | undefined)?.proxy ?? instance;
17
+ if (target && typeof query.in === 'function') query = query.in(target);
18
+ const builder = all ? query.selectAll(selector) : query.select(selector);
19
+ builder
20
+ .boundingClientRect(rect =>
21
+ resolve(
22
+ all
23
+ ? ((rect || []) as SelectorRect[])
24
+ : ((rect || { width: 0, height: 0, top: 0, left: 0 }) as SelectorRect)
25
+ )
26
+ )
27
+ .exec();
28
+ });
29
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 生成带前缀的轻量唯一 id:`前缀-时间戳36进制-随机串`。
3
+ * 时间戳保证趋势递增,随机段兜底同毫秒碰撞,适合做组件内部 key、
4
+ * v-for 列表 id(避免用 index 导致复用错乱)。
5
+ * 非加密强度,勿用于令牌等安全场景。
6
+ * @param prefix id 前缀,默认 'snake'
7
+ */
8
+ export function guid(prefix = 'snake'): string {
9
+ const random = Math.random().toString(36).slice(2, 10);
10
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
11
+ }