@cyberpunk-vue/hooks 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cyberpunk Vue Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const d=require("@cyberpunk-vue/constants"),f=require("vue"),h=d.CSS_NAMESPACE;function P(e){const r=h;return{namespace:r,b:()=>`${r}-${e}`,e:n=>n?`${r}-${e}__${n}`:"",m:n=>n?`${r}-${e}--${n}`:"",bem:(n,u,$)=>{let l=`${r}-${e}`;return n&&(l+=`-${n}`),u&&(l+=`__${u}`),$&&(l+=`--${$}`),l},is:(n,u=!0)=>u?`is-${n}`:""}}function z(e,r){const i=f.inject(d.DEFAULTS_KEY,{})[r]??{};return f.computed(()=>{const o={...e};for(const s in i)e[s]===void 0&&(o[s]=i[s]);return o})}function p(e){return e===void 0||e===""?"":typeof e=="number"?`${e}ms`:e}function b(e){return f.computed(()=>p(e.value))}function D(e){return/^-?\d+(\.\d+)?$/.test(e.trim())}function a(e,r){if(e===void 0||e==="")return"";if(typeof e=="number")return`${e}px`;const t=e.trim();return r&&t in r?`${r[t]}px`:D(t)?`${t}px`:t}function _(e,r=["sm","md","lg"]){return typeof e=="string"&&r.includes(e)}function j(e,r){return f.computed(()=>a(e.value,r))}function q(e){return typeof e=="string"?e.replace(/^[?&]/,""):Array.isArray(e)?e.join("&"):typeof e=="object"&&e!==null?Object.entries(e).filter(([,r])=>r!=null).map(([r,t])=>`${encodeURIComponent(r)}=${encodeURIComponent(String(t))}`).join("&"):""}const g=(e,r)=>{if(!r)return e;const t=q(r);if(!t)return e;const i=e.includes("?")?"&":"?";return`${e}${i}${t}`},m=(e,r)=>r?typeof r=="function"?r(e):r.replace(/\{src\}/g,e):e,y=(e,r)=>{if(!r)return e;const t=[];if(r.width||r.height){const o=["resize"];r.mode&&o.push(`m_${r.mode}`),r.width&&o.push(`w_${r.width}`),r.height&&o.push(`h_${r.height}`),t.push(o.join(","))}if(r.quality&&t.push(`quality,q_${r.quality}`),r.format&&t.push(`format,${r.format}`),t.length===0)return e;const i=e.includes("?")?"&":"?";return`${e}${i}x-tos-process=image/${t.join("/")}`},S={append:g,replace:m,tos:y};function w(e){return{processedSrc:f.computed(()=>{var n;const t=typeof e.src=="string"?e.src:e.src.value,i=typeof e.processor=="function"||typeof e.processor=="string"?e.processor:(n=e.processor)==null?void 0:n.value,o=typeof e.params=="object"&&"value"in e.params?e.params.value:e.params,s=i??(o?"append":void 0);if(!s)return t;let c;if(typeof s=="string"){if(c=S[s],!c)return console.warn(`[useImageSrc] Unknown processor: ${s}`),t}else c=s;try{return c(t,o)||t}catch(u){return console.error("[useImageSrc] Processor error:",u),t}})}}exports.appendProcessor=g;exports.builtinProcessors=S;exports.isPresetSize=_;exports.normalizeDuration=p;exports.normalizeSize=a;exports.replaceProcessor=m;exports.tosProcessor=y;exports.useDefaults=z;exports.useDuration=b;exports.useImageSrc=w;exports.useNamespace=P;exports.useSize=j;
package/dist/index.mjs ADDED
@@ -0,0 +1,105 @@
1
+ import { CSS_NAMESPACE as d, DEFAULTS_KEY as p } from "@cyberpunk-vue/constants";
2
+ import { inject as g, computed as $ } from "vue";
3
+ const h = d;
4
+ function w(e) {
5
+ const r = h;
6
+ return {
7
+ namespace: r,
8
+ b: () => `${r}-${e}`,
9
+ e: (n) => n ? `${r}-${e}__${n}` : "",
10
+ m: (n) => n ? `${r}-${e}--${n}` : "",
11
+ bem: (n, u, l) => {
12
+ let f = `${r}-${e}`;
13
+ return n && (f += `-${n}`), u && (f += `__${u}`), l && (f += `--${l}`), f;
14
+ },
15
+ is: (n, u = !0) => u ? `is-${n}` : ""
16
+ };
17
+ }
18
+ function A(e, r) {
19
+ const s = g(p, {})[r] ?? {};
20
+ return $(() => {
21
+ const o = { ...e };
22
+ for (const i in s)
23
+ e[i] === void 0 && (o[i] = s[i]);
24
+ return o;
25
+ });
26
+ }
27
+ function m(e) {
28
+ return e === void 0 || e === "" ? "" : typeof e == "number" ? `${e}ms` : e;
29
+ }
30
+ function I(e) {
31
+ return $(() => m(e.value));
32
+ }
33
+ function y(e) {
34
+ return /^-?\d+(\.\d+)?$/.test(e.trim());
35
+ }
36
+ function S(e, r) {
37
+ if (e === void 0 || e === "") return "";
38
+ if (typeof e == "number")
39
+ return `${e}px`;
40
+ const t = e.trim();
41
+ return r && t in r ? `${r[t]}px` : y(t) ? `${t}px` : t;
42
+ }
43
+ function q(e, r = ["sm", "md", "lg"]) {
44
+ return typeof e == "string" && r.includes(e);
45
+ }
46
+ function x(e, r) {
47
+ return $(() => S(e.value, r));
48
+ }
49
+ function a(e) {
50
+ return typeof e == "string" ? e.replace(/^[?&]/, "") : Array.isArray(e) ? e.join("&") : typeof e == "object" && e !== null ? Object.entries(e).filter(([, r]) => r != null).map(([r, t]) => `${encodeURIComponent(r)}=${encodeURIComponent(String(t))}`).join("&") : "";
51
+ }
52
+ const P = (e, r) => {
53
+ if (!r) return e;
54
+ const t = a(r);
55
+ if (!t) return e;
56
+ const s = e.includes("?") ? "&" : "?";
57
+ return `${e}${s}${t}`;
58
+ }, _ = (e, r) => r ? typeof r == "function" ? r(e) : r.replace(/\{src\}/g, e) : e, b = (e, r) => {
59
+ if (!r) return e;
60
+ const t = [];
61
+ if (r.width || r.height) {
62
+ const o = ["resize"];
63
+ r.mode && o.push(`m_${r.mode}`), r.width && o.push(`w_${r.width}`), r.height && o.push(`h_${r.height}`), t.push(o.join(","));
64
+ }
65
+ if (r.quality && t.push(`quality,q_${r.quality}`), r.format && t.push(`format,${r.format}`), t.length === 0) return e;
66
+ const s = e.includes("?") ? "&" : "?";
67
+ return `${e}${s}x-tos-process=image/${t.join("/")}`;
68
+ }, j = {
69
+ append: P,
70
+ replace: _,
71
+ tos: b
72
+ };
73
+ function C(e) {
74
+ return { processedSrc: $(() => {
75
+ var n;
76
+ const t = typeof e.src == "string" ? e.src : e.src.value, s = typeof e.processor == "function" || typeof e.processor == "string" ? e.processor : (n = e.processor) == null ? void 0 : n.value, o = typeof e.params == "object" && "value" in e.params ? e.params.value : e.params, i = s ?? (o ? "append" : void 0);
77
+ if (!i)
78
+ return t;
79
+ let c;
80
+ if (typeof i == "string") {
81
+ if (c = j[i], !c)
82
+ return console.warn(`[useImageSrc] Unknown processor: ${i}`), t;
83
+ } else
84
+ c = i;
85
+ try {
86
+ return c(t, o) || t;
87
+ } catch (u) {
88
+ return console.error("[useImageSrc] Processor error:", u), t;
89
+ }
90
+ }) };
91
+ }
92
+ export {
93
+ P as appendProcessor,
94
+ j as builtinProcessors,
95
+ q as isPresetSize,
96
+ m as normalizeDuration,
97
+ S as normalizeSize,
98
+ _ as replaceProcessor,
99
+ b as tosProcessor,
100
+ A as useDefaults,
101
+ I as useDuration,
102
+ C as useImageSrc,
103
+ w as useNamespace,
104
+ x as useSize
105
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@cyberpunk-vue/hooks",
3
+ "version": "0.1.0",
4
+ "description": "Cyberpunk Vue composables - Reusable Vue 3 composition functions",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./*": "./*"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src"
20
+ ],
21
+ "sideEffects": false,
22
+ "peerDependencies": {
23
+ "vue": "^3.5.0"
24
+ },
25
+ "dependencies": {
26
+ "@cyberpunk-vue/constants": "0.1.0"
27
+ },
28
+ "author": "Juxest",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/user/cyberpunk-vue.git",
33
+ "directory": "packages/hooks"
34
+ },
35
+ "homepage": "https://github.com/user/cyberpunk-vue#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/user/cyberpunk-vue/issues"
38
+ },
39
+ "keywords": [
40
+ "vue",
41
+ "vue3",
42
+ "composables",
43
+ "hooks",
44
+ "cyberpunk"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "build": "vite build"
51
+ }
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ export { useNamespace } from './use-namespace'
2
+ export type { UseNamespaceReturn } from './use-namespace'
3
+
4
+ export { useDefaults } from './use-defaults'
5
+
6
+ export { useDuration, normalizeDuration } from './use-duration'
7
+ export type { DurationValue } from './use-duration'
8
+
9
+ export { useSize, normalizeSize, isPresetSize } from './use-size'
10
+ export type { SizeValue, CommonSize, Size } from './use-size'
11
+
12
+ export {
13
+ useImageSrc,
14
+ appendProcessor,
15
+ replaceProcessor,
16
+ tosProcessor,
17
+ builtinProcessors,
18
+ } from './use-image-src'
19
+ export type {
20
+ ImageSrcProcessor,
21
+ ImageProcessorParams,
22
+ TosProcessorParams,
23
+ BuiltinProcessorName,
24
+ UseImageSrcOptions,
25
+ UseImageSrcReturn,
26
+ } from './use-image-src'
27
+
@@ -0,0 +1,29 @@
1
+ /**
2
+ * useDefaults - 全局默认值合并
3
+ * 合并优先级: 显式 Props > 全局配置 > 组件默认值
4
+ */
5
+ import { inject, computed, type ComputedRef } from 'vue'
6
+ import { DEFAULTS_KEY } from '@cyberpunk-vue/constants'
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- 泛型约束需要 any
9
+ export function useDefaults<T extends Record<string, any>>(
10
+ props: T,
11
+ name: string
12
+ ): ComputedRef<T> {
13
+ const globalDefaults = inject(DEFAULTS_KEY, {}) as Record<string, Partial<T>>
14
+ const componentDefaults = globalDefaults[name] ?? {}
15
+
16
+ return computed(() => {
17
+ const result = { ...props }
18
+
19
+ // 只覆盖未显式传递的 props
20
+ for (const key in componentDefaults) {
21
+ if (props[key] === undefined) {
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- 动态属性赋值需要类型断言
23
+ ; (result as any)[key] = componentDefaults[key]
24
+ }
25
+ }
26
+
27
+ return result
28
+ })
29
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * useDuration - 动画时间单位标准化工具
3
+ *
4
+ * 统一处理动画时间值,支持数字和字符串两种格式:
5
+ * - 数字:默认视为毫秒 (ms),转为 CSS 时间字符串
6
+ * - 字符串:保持原样,用户可自行指定单位 (如 '1.5s' 或 '500ms')
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * normalizeDuration(300) // => '300ms'
11
+ * normalizeDuration('1.5s') // => '1.5s'
12
+ * normalizeDuration('500ms') // => '500ms'
13
+ * normalizeDuration('') // => ''
14
+ * ```
15
+ */
16
+
17
+ import { computed, type Ref, type ComputedRef } from 'vue'
18
+
19
+ export type DurationValue = number | string | undefined
20
+
21
+ /**
22
+ * 将时间值标准化为 CSS 可用的时间字符串
23
+ * @param value - 时间值(数字默认 ms,字符串保持原样)
24
+ * @returns CSS 时间字符串,如 '300ms' 或 '1.5s'
25
+ */
26
+ export function normalizeDuration(value: DurationValue): string {
27
+ if (value === undefined || value === '') return ''
28
+ if (typeof value === 'number') return `${value}ms`
29
+ return value
30
+ }
31
+
32
+ /**
33
+ * 响应式版本的时间标准化
34
+ * @param valueRef - 响应式时间值引用
35
+ * @returns 响应式 CSS 时间字符串
36
+ */
37
+ export function useDuration(valueRef: Ref<DurationValue>): ComputedRef<string> {
38
+ return computed(() => normalizeDuration(valueRef.value))
39
+ }
@@ -0,0 +1,216 @@
1
+ import { computed, type Ref, type ComputedRef } from 'vue'
2
+
3
+ /**
4
+ * 图片 URL 处理器函数签名
5
+ * @param src - 原始图片 URL
6
+ * @param params - 处理参数(可选)
7
+ * @returns 处理后的 URL
8
+ */
9
+ export type ImageSrcProcessor<T = unknown> = (src: string, params?: T) => string
10
+
11
+ /**
12
+ * 处理器参数类型
13
+ * 支持字符串、对象、数组等
14
+ */
15
+ export type ImageProcessorParams = string | Record<string, unknown> | unknown[]
16
+
17
+ /**
18
+ * TOS 图像处理参数
19
+ */
20
+ export interface TosProcessorParams {
21
+ /** 目标宽度 */
22
+ width?: number
23
+ /** 目标高度 */
24
+ height?: number
25
+ /** 图片质量 (1-100) */
26
+ quality?: number
27
+ /** 输出格式 */
28
+ format?: 'jpg' | 'png' | 'webp' | 'avif' | 'gif'
29
+ /** 缩放模式 */
30
+ mode?: 'fit' | 'fill' | 'pad'
31
+ }
32
+
33
+ // ============================================
34
+ // 内置处理器
35
+ // ============================================
36
+
37
+ /**
38
+ * 将查询参数序列化为字符串
39
+ */
40
+ function serializeParams(params: unknown): string {
41
+ if (typeof params === 'string') {
42
+ // 移除开头的 ? 或 &
43
+ return params.replace(/^[?&]/, '')
44
+ }
45
+ if (Array.isArray(params)) {
46
+ return params.join('&')
47
+ }
48
+ if (typeof params === 'object' && params !== null) {
49
+ return Object.entries(params as Record<string, unknown>)
50
+ .filter(([, value]) => value !== undefined && value !== null)
51
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
52
+ .join('&')
53
+ }
54
+ return ''
55
+ }
56
+
57
+ /**
58
+ * append 处理器 - 追加查询参数
59
+ * 智能处理 URL 中的 ? 和 &
60
+ */
61
+ export const appendProcessor: ImageSrcProcessor<ImageProcessorParams> = (src, params) => {
62
+ if (!params) return src
63
+
64
+ const serialized = serializeParams(params)
65
+ if (!serialized) return src
66
+
67
+ // 检查 URL 是否已有查询参数
68
+ const separator = src.includes('?') ? '&' : '?'
69
+ return `${src}${separator}${serialized}`
70
+ }
71
+
72
+ /**
73
+ * replace 处理器 - 替换 URL
74
+ * 支持字符串模板或函数
75
+ */
76
+ export const replaceProcessor: ImageSrcProcessor<string | ((src: string) => string)> = (src, params) => {
77
+ if (!params) return src
78
+
79
+ if (typeof params === 'function') {
80
+ return params(src)
81
+ }
82
+
83
+ // 字符串模板,支持 {src} 占位符
84
+ return params.replace(/\{src\}/g, src)
85
+ }
86
+
87
+ /**
88
+ * TOS 处理器 - 火山引擎图像处理
89
+ * 格式: ~tplv-xxx~image/resize,w_100,h_100/quality,q_80/format,webp
90
+ */
91
+ export const tosProcessor: ImageSrcProcessor<TosProcessorParams> = (src, params) => {
92
+ if (!params) return src
93
+
94
+ const operations: string[] = []
95
+
96
+ // 缩放
97
+ if (params.width || params.height) {
98
+ const resizeParts = ['resize']
99
+ if (params.mode) resizeParts.push(`m_${params.mode}`)
100
+ if (params.width) resizeParts.push(`w_${params.width}`)
101
+ if (params.height) resizeParts.push(`h_${params.height}`)
102
+ operations.push(resizeParts.join(','))
103
+ }
104
+
105
+ // 质量
106
+ if (params.quality) {
107
+ operations.push(`quality,q_${params.quality}`)
108
+ }
109
+
110
+ // 格式
111
+ if (params.format) {
112
+ operations.push(`format,${params.format}`)
113
+ }
114
+
115
+ if (operations.length === 0) return src
116
+
117
+ // TOS URL 格式
118
+ const separator = src.includes('?') ? '&' : '?'
119
+ return `${src}${separator}x-tos-process=image/${operations.join('/')}`
120
+ }
121
+
122
+ /**
123
+ * 内置处理器映射
124
+ */
125
+ export const builtinProcessors: Record<string, ImageSrcProcessor<unknown>> = {
126
+ append: appendProcessor as ImageSrcProcessor<unknown>,
127
+ replace: replaceProcessor as ImageSrcProcessor<unknown>,
128
+ tos: tosProcessor as ImageSrcProcessor<unknown>,
129
+ }
130
+
131
+ /**
132
+ * 预设处理器名称类型
133
+ */
134
+ export type BuiltinProcessorName = keyof typeof builtinProcessors
135
+
136
+ // ============================================
137
+ // Hook
138
+ // ============================================
139
+
140
+ export interface UseImageSrcOptions {
141
+ /** 原始 URL */
142
+ src: Ref<string> | string
143
+ /** 处理器:预设名称或自定义函数 */
144
+ processor?: Ref<string | ImageSrcProcessor | undefined> | string | ImageSrcProcessor
145
+ /** 处理器参数 */
146
+ params?: Ref<ImageProcessorParams | undefined> | ImageProcessorParams
147
+ }
148
+
149
+ export interface UseImageSrcReturn {
150
+ /** 处理后的 URL */
151
+ processedSrc: ComputedRef<string>
152
+ }
153
+
154
+ /**
155
+ * 图片 URL 处理 Hook
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * // 使用预设处理器
160
+ * const { processedSrc } = useImageSrc({
161
+ * src: 'https://example.com/image.jpg',
162
+ * processor: 'append',
163
+ * params: { width: 100 }
164
+ * })
165
+ *
166
+ * // 使用自定义处理器
167
+ * const { processedSrc } = useImageSrc({
168
+ * src: imageUrl,
169
+ * processor: (src, params) => `${src}?custom=${params.id}`
170
+ * })
171
+ * ```
172
+ */
173
+ export function useImageSrc(options: UseImageSrcOptions): UseImageSrcReturn {
174
+ const processedSrc = computed(() => {
175
+ // 获取原始值
176
+ const srcValue = typeof options.src === 'string' ? options.src : options.src.value
177
+ const processorValue = typeof options.processor === 'function' || typeof options.processor === 'string'
178
+ ? options.processor
179
+ : options.processor?.value
180
+ const paramsValue = typeof options.params === 'object' && 'value' in (options.params as object)
181
+ ? (options.params as Ref<ImageProcessorParams | undefined>).value
182
+ : options.params as ImageProcessorParams | undefined
183
+
184
+ // 无处理器但有参数时,默认使用 append
185
+ const effectiveProcessor = processorValue ?? (paramsValue ? 'append' : undefined)
186
+
187
+ if (!effectiveProcessor) {
188
+ return srcValue
189
+ }
190
+
191
+ // 获取处理器函数
192
+ let processorFn: ImageSrcProcessor<unknown>
193
+
194
+ if (typeof effectiveProcessor === 'string') {
195
+ processorFn = builtinProcessors[effectiveProcessor]
196
+ if (!processorFn) {
197
+ console.warn(`[useImageSrc] Unknown processor: ${effectiveProcessor}`)
198
+ return srcValue
199
+ }
200
+ } else {
201
+ processorFn = effectiveProcessor
202
+ }
203
+
204
+ // 执行处理
205
+ try {
206
+ return processorFn(srcValue, paramsValue) || srcValue
207
+ } catch (e) {
208
+ console.error('[useImageSrc] Processor error:', e)
209
+ return srcValue
210
+ }
211
+ })
212
+
213
+ return { processedSrc }
214
+ }
215
+
216
+ export default useImageSrc
@@ -0,0 +1,60 @@
1
+ /**
2
+ * useNamespace - BEM 命名空间生成器
3
+ * 用于生成符合 BEM 规范的 CSS 类名
4
+ */
5
+
6
+ import { CSS_NAMESPACE } from '@cyberpunk-vue/constants'
7
+
8
+ const defaultNamespace = CSS_NAMESPACE
9
+
10
+ export type UseNamespaceReturn = {
11
+
12
+ /** 命名空间 */
13
+ namespace: string
14
+ /** 块类名 cp-{block} */
15
+ b: () => string
16
+ /** 元素类名 cp-{block}__{element} */
17
+ e: (element: string) => string
18
+ /** 修饰符类名 cp-{block}--{modifier} */
19
+ m: (modifier: string) => string
20
+ /** 块+元素+修饰符 */
21
+ bem: (block?: string, element?: string, modifier?: string) => string
22
+ /** 状态类名 is-{state} */
23
+ is: (state: string, value?: boolean) => string
24
+ }
25
+
26
+ export function useNamespace(block: string): UseNamespaceReturn {
27
+ const namespace = defaultNamespace
28
+
29
+ const b = () => `${namespace}-${block}`
30
+
31
+ const e = (element: string) =>
32
+ element ? `${namespace}-${block}__${element}` : ''
33
+
34
+ const m = (modifier: string) =>
35
+ modifier ? `${namespace}-${block}--${modifier}` : ''
36
+
37
+ const bem = (
38
+ blockSuffix?: string,
39
+ element?: string,
40
+ modifier?: string
41
+ ) => {
42
+ let cls = `${namespace}-${block}`
43
+ if (blockSuffix) cls += `-${blockSuffix}`
44
+ if (element) cls += `__${element}`
45
+ if (modifier) cls += `--${modifier}`
46
+ return cls
47
+ }
48
+
49
+ const is = (state: string, value = true) =>
50
+ value ? `is-${state}` : ''
51
+
52
+ return {
53
+ namespace,
54
+ b,
55
+ e,
56
+ m,
57
+ bem,
58
+ is,
59
+ }
60
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * useSize - 尺寸值标准化工具
3
+ *
4
+ * 统一处理组件尺寸值,支持预设值、数字和带单位字符串:
5
+ * - 数字:默认视为像素 (px)
6
+ * - 纯数字字符串:默认视为像素 (px)
7
+ * - 带单位字符串:保持原样 (如 '2rem', '100%')
8
+ * - 预设字符串:通过 sizeMap 查表转换
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * normalizeSize(32) // => '32px'
13
+ * normalizeSize('32') // => '32px'
14
+ * normalizeSize('2rem') // => '2rem'
15
+ * normalizeSize('md', { md: 40 }) // => '40px'
16
+ * normalizeSize('100%') // => '100%'
17
+ * ```
18
+ */
19
+
20
+ import { computed, type Ref, type ComputedRef } from 'vue'
21
+
22
+ /**
23
+ * 尺寸值类型
24
+ * - 数字:直接作为像素值
25
+ * - 字符串:可以是预设值、带单位的值或纯数字
26
+ */
27
+ export type SizeValue = string | number | undefined
28
+
29
+ /**
30
+ * 通用尺寸预设类型
31
+ * 所有组件基础尺寸,组件可扩展为专属 Size
32
+ */
33
+ export type CommonSize = 'sm' | 'md' | 'lg'
34
+
35
+ /**
36
+ * 通用 Size 类型 - 预设值 + 自定义值
37
+ * 可直接用于大部分组件的 size prop
38
+ */
39
+ export type Size = CommonSize | number | string
40
+
41
+ /**
42
+ * 检测字符串是否为纯数字(不含单位)
43
+ */
44
+ function isNumericString(value: string): boolean {
45
+ return /^-?\d+(\.\d+)?$/.test(value.trim())
46
+ }
47
+
48
+ /**
49
+ * 检测字符串是否已包含 CSS 单位
50
+ */
51
+ function hasUnit(value: string): boolean {
52
+ return /^-?\d+(\.\d+)?(px|em|rem|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc)$/i.test(value.trim())
53
+ }
54
+
55
+ /**
56
+ * 将尺寸值标准化为 CSS 可用的尺寸字符串
57
+ *
58
+ * @param value - 尺寸值(数字、字符串或预设)
59
+ * @param sizeMap - 预设值映射表(如 { sm: 24, md: 32, lg: 40 })
60
+ * @returns CSS 尺寸字符串,如 '32px' 或 '2rem'
61
+ */
62
+ export function normalizeSize(
63
+ value: SizeValue,
64
+ sizeMap?: Record<string, number>
65
+ ): string {
66
+ // 空值返回空字符串
67
+ if (value === undefined || value === '') return ''
68
+
69
+ // 数字直接转 px
70
+ if (typeof value === 'number') {
71
+ return `${value}px`
72
+ }
73
+
74
+ // 字符串处理
75
+ const trimmed = value.trim()
76
+
77
+ // 1. 检查是否为预设值
78
+ if (sizeMap && trimmed in sizeMap) {
79
+ return `${sizeMap[trimmed]}px`
80
+ }
81
+
82
+ // 2. 纯数字字符串,添加 px
83
+ if (isNumericString(trimmed)) {
84
+ return `${trimmed}px`
85
+ }
86
+
87
+ // 3. 已有单位或其他有效 CSS 值,保持原样
88
+ return trimmed
89
+ }
90
+
91
+ /**
92
+ * 检测 size 值是否为 CommonSize 预设值
93
+ * 用于组件判断是否应用 CSS 类名还是内联样式
94
+ *
95
+ * @param value - size 值
96
+ * @param presets - 预设值列表,默认 ['sm', 'md', 'lg']
97
+ * @returns 是否为预设值
98
+ *
99
+ * @example
100
+ * isPresetSize('md') // true
101
+ * isPresetSize('xl', ['sm','md','lg','xl']) // true
102
+ * isPresetSize(32) // false
103
+ * isPresetSize('2rem') // false
104
+ */
105
+ export function isPresetSize(
106
+ value: SizeValue,
107
+ presets: readonly string[] = ['sm', 'md', 'lg']
108
+ ): value is string {
109
+ return typeof value === 'string' && presets.includes(value)
110
+ }
111
+
112
+ /**
113
+ * 响应式版本的尺寸标准化
114
+ *
115
+ * @param valueRef - 响应式尺寸值引用
116
+ * @param sizeMap - 预设值映射表
117
+ * @returns 响应式 CSS 尺寸字符串
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * const size = ref<SizeValue>('md')
122
+ * const normalizedSize = useSize(size, { sm: 24, md: 32, lg: 40 })
123
+ * // normalizedSize.value === '32px'
124
+ * ```
125
+ */
126
+ export function useSize(
127
+ valueRef: Ref<SizeValue>,
128
+ sizeMap?: Record<string, number>
129
+ ): ComputedRef<string> {
130
+ return computed(() => normalizeSize(valueRef.value, sizeMap))
131
+ }