@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 +21 -0
- package/dist/index.cjs +1 -0
- package/dist/index.mjs +105 -0
- package/package.json +52 -0
- package/src/index.ts +27 -0
- package/src/use-defaults/index.ts +29 -0
- package/src/use-duration/index.ts +39 -0
- package/src/use-image-src/index.ts +216 -0
- package/src/use-namespace/index.ts +60 -0
- package/src/use-size/index.ts +131 -0
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
|
+
}
|