@pubinfo/core 2.0.0-rc.4 → 2.0.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/dist/{AppSetting-BI-oNc4e.js → AppSetting-DqVYDIHj.js} +15 -15
- package/dist/{HCheckList.vue_vue_type_script_setup_true_lang-BdLpkcoh.js → HCheckList.vue_vue_type_script_setup_true_lang-SrNklW3P.js} +1 -1
- package/dist/{HToggle-DxdWLgp-.js → HToggle-DGTP9jYA.js} +1 -1
- package/dist/{PreferencesContent-CCYkZeCT.js → PreferencesContent-5NtwK9RQ.js} +4 -4
- package/dist/{SettingBreadcrumb-BTyfiy4k.js → SettingBreadcrumb-BudqQsuJ.js} +3 -3
- package/dist/{SettingCopyright-g6UHi8pZ.js → SettingCopyright-VUberG4R.js} +2 -2
- package/dist/{SettingEnableTransition-Ci-5bhbR.js → SettingEnableTransition-C6NYf021.js} +2 -2
- package/dist/SettingHome-BTaeKgwN.js +46 -0
- package/dist/{SettingMenu-BYLWzA5i.js → SettingMenu-D9Aon2LP.js} +3 -3
- package/dist/{SettingMode-tRisyKtg.js → SettingMode-DaqVd9Mq.js} +1 -1
- package/dist/{SettingNavSearch-CSM6mPf8.js → SettingNavSearch-N4JIheIk.js} +2 -2
- package/dist/{SettingOther-Bj5KF_vC.js → SettingOther-tLulcors.js} +2 -2
- package/dist/{SettingPage-CFjmrVI7.js → SettingPage-CEjWB45R.js} +2 -2
- package/dist/{SettingTabbar-uFYiaZhK.js → SettingTabbar-DyeLhcCT.js} +3 -3
- package/dist/{SettingThemes-C-tMq9o5.js → SettingThemes-C2M3tsVl.js} +1 -1
- package/dist/{SettingToolbar-BfDzijNU.js → SettingToolbar-DI7de6i0.js} +24 -31
- package/dist/{SettingTopbar-DTDv4NXD.js → SettingTopbar-BgIoXeAq.js} +3 -3
- package/dist/{SettingWidthMode-PkiwrHe3.js → SettingWidthMode-DIAU4s5e.js} +1 -1
- package/dist/{TopThinMode-BrvA8pV0.js → TopThinMode-JNUHrJI2.js} +1 -1
- package/dist/built-in/index.d.ts +1 -0
- package/dist/built-in/layout-component/components/Tools/SearchPanel.vue.d.ts +7 -2
- package/dist/built-in/layout-component/composables/useGlobalSearch.d.ts +7 -0
- package/dist/built-in/system-info/components/SystemInfo.vue.d.ts +2 -0
- package/dist/built-in/system-info/index.d.ts +5 -0
- package/dist/{colors-VoaDbOhe.js → colors-DxWfHM_v.js} +1 -1
- package/dist/features/components/PubinfoIcon/PrismBox.vue.d.ts +21 -0
- package/dist/features/components/PubinfoIcon/SquareBox.vue.d.ts +17 -0
- package/dist/features/components/PubinfoIcon/index.vue.d.ts +13 -9
- package/dist/features/components/PubinfoIcon/props.d.ts +58 -0
- package/dist/features/components/index.d.ts +2 -0
- package/dist/{index-BfGqLWFB.js → index-5fRpGyLW.js} +4 -4
- package/dist/{index-ConeY38N.js → index-BFRIv97x.js} +2 -2
- package/dist/{index-Dv9ndBoi.js → index-BH-vHGvk.js} +1 -1
- package/dist/{index-BSevJVD5.js → index-C7xIGcDc.js} +2 -2
- package/dist/{index-CYoFRwvw.js → index-CNVn3Ubv.js} +2 -2
- package/dist/{index-DV3hkzKA.js → index-Cf-u1Zqh.js} +1 -1
- package/dist/{index-Ddw98rJ5.js → index-D4v4g8FJ.js} +112 -98
- package/dist/{index-DrC787X_.js → index-DQGnbEGS.js} +2 -2
- package/dist/{index-IAYhIBQH.js → index-Dv7UUFkD.js} +23446 -23082
- package/dist/index.d.ts +1 -1
- package/dist/index.js +55 -48
- package/dist/{pick-vpv9EEvu.js → pick-VFuUwFn-.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/utils/global.d.ts +33 -0
- package/dist/utils/index.d.ts +2 -1
- package/package.json +8 -5
- package/src/built-in/index.ts +1 -0
- package/src/built-in/layout-component/components/Header/TopMode/index.vue +27 -6
- package/src/built-in/layout-component/components/Menu/item.vue +41 -6
- package/src/built-in/layout-component/components/SettingBar/components/SettingHome.vue +1 -4
- package/src/built-in/layout-component/components/SettingBar/components/SettingToolbar.vue +0 -6
- package/src/built-in/layout-component/components/Tools/SearchPanel.vue +113 -37
- package/src/built-in/layout-component/components/Tools/index.vue +1 -1
- package/src/built-in/layout-component/components/Topbar/Tabbar/MoreAction.vue +58 -2
- package/src/built-in/layout-component/components/Topbar/Tabbar/index.vue +64 -6
- package/src/built-in/layout-component/composables/useGlobalSearch.ts +40 -1
- package/src/built-in/system-info/components/SystemInfo.vue +53 -0
- package/src/built-in/system-info/index.ts +16 -0
- package/src/core/ctx.ts +7 -1
- package/src/core/resolver/icon.ts +9 -5
- package/src/features/components/PubinfoIcon/PrismBox.vue +203 -0
- package/src/features/components/PubinfoIcon/SquareBox.vue +59 -0
- package/src/features/components/PubinfoIcon/index.vue +128 -37
- package/src/features/components/PubinfoIcon/props.ts +54 -0
- package/src/features/components/index.ts +4 -1
- package/src/features/context/index.ts +1 -16
- package/src/features/settings/index.ts +0 -1
- package/src/index.ts +7 -0
- package/src/utils/global.ts +161 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/proxy.ts +7 -8
- package/types/global.d.ts +7 -0
- package/types/menu.d.ts +10 -0
- package/types/settings.d.ts +0 -7
- package/dist/SettingHome-K4Iel0Hr.js +0 -55
|
@@ -1,9 +1,44 @@
|
|
|
1
|
-
import { ref } from 'vue';
|
|
1
|
+
import { ref, shallowRef } from 'vue';
|
|
2
2
|
|
|
3
3
|
type SearchType = 'menu' | 'tab';
|
|
4
4
|
|
|
5
5
|
const isShow = ref(false);
|
|
6
6
|
const searchType = ref<SearchType>('menu');
|
|
7
|
+
const panelRegistry = new Map<symbol, number>();
|
|
8
|
+
const activePanelId = shallowRef<symbol | null>(null);
|
|
9
|
+
|
|
10
|
+
function updateActivePanel() {
|
|
11
|
+
let candidate: symbol | null = null;
|
|
12
|
+
let highestPriority = Number.NEGATIVE_INFINITY;
|
|
13
|
+
panelRegistry.forEach((priority, id) => {
|
|
14
|
+
if (priority > highestPriority) {
|
|
15
|
+
highestPriority = priority;
|
|
16
|
+
candidate = id;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
activePanelId.value = candidate;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function registerPanel(id: symbol, priority = 0) {
|
|
23
|
+
panelRegistry.set(id, priority);
|
|
24
|
+
updateActivePanel();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function unregisterPanel(id: symbol) {
|
|
28
|
+
if (!panelRegistry.has(id)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
panelRegistry.delete(id);
|
|
32
|
+
updateActivePanel();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setPanelPriority(id: symbol, priority: number) {
|
|
36
|
+
if (!panelRegistry.has(id)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
panelRegistry.set(id, priority);
|
|
40
|
+
updateActivePanel();
|
|
41
|
+
}
|
|
7
42
|
|
|
8
43
|
function open(type?: SearchType) {
|
|
9
44
|
if (type) {
|
|
@@ -39,9 +74,13 @@ export function useGlobalSearch() {
|
|
|
39
74
|
return {
|
|
40
75
|
isShow,
|
|
41
76
|
searchType,
|
|
77
|
+
activePanelId,
|
|
42
78
|
open,
|
|
43
79
|
close,
|
|
44
80
|
toggle,
|
|
81
|
+
registerPanel,
|
|
82
|
+
unregisterPanel,
|
|
83
|
+
setPanelPriority,
|
|
45
84
|
};
|
|
46
85
|
}
|
|
47
86
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useToggle } from '@vueuse/core';
|
|
3
|
+
import hotkeys from 'hotkeys-js';
|
|
4
|
+
import HSlideover from '@/built-in/layout-component/components/ui/HSlideover.vue';
|
|
5
|
+
|
|
6
|
+
const [visible, toggle] = useToggle(false);
|
|
7
|
+
|
|
8
|
+
const { pkg, buildTime } = __SYSTEM_INFO__;
|
|
9
|
+
hotkeys('alt+i', () => toggle());
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<HSlideover v-model="visible" title="系统信息">
|
|
14
|
+
<div class="px-4">
|
|
15
|
+
<h2 class="m-0 text-lg font-bold">
|
|
16
|
+
最后编译时间
|
|
17
|
+
</h2>
|
|
18
|
+
<div class="my-4 text-center text-lg font-sans">
|
|
19
|
+
{{ buildTime }}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="px-4">
|
|
23
|
+
<h2 class="m-0 text-lg font-bold">
|
|
24
|
+
生产环境依赖
|
|
25
|
+
</h2>
|
|
26
|
+
<ul class="list-none pl-0 text-sm">
|
|
27
|
+
<li v-for="(val, key) in (pkg.dependencies as object)" :key="key" class="flex items-center justify-between rounded px-2 py-1.5 hover:bg-stone-1 dark:hover:bg-stone-9">
|
|
28
|
+
<div class="font-bold">
|
|
29
|
+
{{ key }}
|
|
30
|
+
</div>
|
|
31
|
+
<div class="font-sans">
|
|
32
|
+
{{ val }}
|
|
33
|
+
</div>
|
|
34
|
+
</li>
|
|
35
|
+
</ul>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="px-4">
|
|
38
|
+
<h2 class="m-0 text-lg font-bold">
|
|
39
|
+
开发环境依赖
|
|
40
|
+
</h2>
|
|
41
|
+
<ul class="list-none pl-0 text-sm">
|
|
42
|
+
<li v-for="(val, key) in (pkg.devDependencies as object)" :key="key" class="flex items-center justify-between rounded px-2 py-1.5 hover:bg-stone-1 dark:hover:bg-stone-9">
|
|
43
|
+
<div class="font-bold">
|
|
44
|
+
{{ key }}
|
|
45
|
+
</div>
|
|
46
|
+
<div class="font-sans">
|
|
47
|
+
{{ val }}
|
|
48
|
+
</div>
|
|
49
|
+
</li>
|
|
50
|
+
</ul>
|
|
51
|
+
</div>
|
|
52
|
+
</HSlideover>
|
|
53
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ModuleOptions } from '@/core';
|
|
2
|
+
import SystemInfoComponent from './components/SystemInfo.vue';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 系统信息
|
|
6
|
+
*/
|
|
7
|
+
export function SystemInfo(): ModuleOptions {
|
|
8
|
+
return {
|
|
9
|
+
name: 'built-in:system-info',
|
|
10
|
+
setup() {
|
|
11
|
+
const container = document.createElement('div');
|
|
12
|
+
document.body.appendChild(container);
|
|
13
|
+
createApp(SystemInfoComponent).mount(container);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
package/src/core/ctx.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import { createHooks } from 'hookable';
|
|
2
|
+
import { getPubinfoNamespace } from '@/utils';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
const HOOKS_KEY = '__pubinfo_core_hooks__';
|
|
5
|
+
type HookStore = ReturnType<typeof createHooks>;
|
|
6
|
+
|
|
7
|
+
const namespace = getPubinfoNamespace() as Record<string, HookStore | undefined>;
|
|
8
|
+
|
|
9
|
+
export const hooks = namespace[HOOKS_KEY] || (namespace[HOOKS_KEY] = createHooks());
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { ModuleRecord } from './resolver';
|
|
2
|
-
import {
|
|
2
|
+
import { getPubinfoNamespace } from '@/utils';
|
|
3
|
+
import { loadProjectModules, readProjectModules } from './resolver';
|
|
3
4
|
|
|
4
5
|
// 持久化图标模块,支持 HMR 后恢复
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const iconModuleMap = g.__PUBINFO__.iconModules as Map<string, ModuleRecord>;
|
|
6
|
+
const namespace = getPubinfoNamespace();
|
|
7
|
+
namespace.iconModules = namespace.iconModules || new Map<string, ModuleRecord>();
|
|
8
|
+
const iconModuleMap = namespace.iconModules as Map<string, ModuleRecord>;
|
|
9
9
|
|
|
10
10
|
export function defineIconModule(id: string, map: ModuleRecord) {
|
|
11
11
|
if (iconModuleMap.has(id)) {
|
|
@@ -31,6 +31,10 @@ export function getAllIconModules(): Map<string, ModuleRecord> {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export async function setupIcon(name: string, id?: string) {
|
|
34
|
+
if (!readProjectModules()) {
|
|
35
|
+
await loadProjectModules().catch(() => {});
|
|
36
|
+
}
|
|
37
|
+
|
|
34
38
|
const modules = readProjectModules();
|
|
35
39
|
const iconMap = modules?.icons ?? {};
|
|
36
40
|
const moduleMap = id ? (getIconModule(id) ?? {}) : {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PrismBox(六边形容器)
|
|
6
|
+
*
|
|
7
|
+
* 目标:
|
|
8
|
+
* - 默认使用纯 CSS clip-path 生成六边形
|
|
9
|
+
* - 当检测到 CSS 变量 `--pubinfo-box-radius` > 0 且容器为“像素正方形”时,
|
|
10
|
+
* 通过运行时按需生成圆角六边形 SVG mask(mask-image / -webkit-mask-image),
|
|
11
|
+
* 兼容浏览器差异,保持与 SquareBox 一致的主题变量接口。
|
|
12
|
+
*
|
|
13
|
+
* 注:圆角六边形目前纯 CSS 难以稳定跨浏览器实现(clip-path 不支持圆角),
|
|
14
|
+
* 因此采用极薄的运行时以 CSS 变量为输入生成 mask;未满足条件时退回纯 CSS 六边形。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const el = ref<HTMLElement | null>(null);
|
|
18
|
+
let ro: ResizeObserver | null = null;
|
|
19
|
+
let mo: MutationObserver | null = null;
|
|
20
|
+
let pending = false;
|
|
21
|
+
|
|
22
|
+
function scheduleApply() {
|
|
23
|
+
if (pending) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
pending = true;
|
|
27
|
+
requestAnimationFrame(() => {
|
|
28
|
+
pending = false;
|
|
29
|
+
applyMask();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parsePx(input: string | null | undefined): number | null {
|
|
34
|
+
if (!input) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const n = Number.parseFloat(input);
|
|
38
|
+
return Number.isFinite(n) ? n : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function genRoundedHexMask(size: number, rPx: number): string | null {
|
|
42
|
+
if (!(size > 0 && rPx > 0)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const maxR = size * 0.35;
|
|
46
|
+
const R = Math.min(rPx, maxR);
|
|
47
|
+
const rp = (R / size) * 100;
|
|
48
|
+
// 移除衰减系数以提高圆角的可见度
|
|
49
|
+
const pts = [
|
|
50
|
+
{ x: 50, y: 0 },
|
|
51
|
+
{ x: 93, y: 25 },
|
|
52
|
+
{ x: 93, y: 75 },
|
|
53
|
+
{ x: 50, y: 100 },
|
|
54
|
+
{ x: 7, y: 75 },
|
|
55
|
+
{ x: 7, y: 25 },
|
|
56
|
+
];
|
|
57
|
+
const normEdge = 43;
|
|
58
|
+
const t = Math.min(rp / normEdge, 0.5);
|
|
59
|
+
const lerp = (a: any, b: any, tt: number) => ({ x: a.x + (b.x - a.x) * tt, y: a.y + (b.y - a.y) * tt });
|
|
60
|
+
const before: any[] = [];
|
|
61
|
+
const after: any[] = [];
|
|
62
|
+
for (let i = 0; i < pts.length; i++) {
|
|
63
|
+
const p = pts[i];
|
|
64
|
+
const pPrev = pts[(i - 1 + pts.length) % pts.length];
|
|
65
|
+
const pNext = pts[(i + 1) % pts.length];
|
|
66
|
+
before.push(lerp(p, pPrev, t));
|
|
67
|
+
after.push(lerp(p, pNext, t));
|
|
68
|
+
}
|
|
69
|
+
let d = `M ${after[0].x} ${after[0].y}`;
|
|
70
|
+
for (let i = 1; i < pts.length; i++) {
|
|
71
|
+
d += ` L ${before[i].x} ${before[i].y} Q ${pts[i].x} ${pts[i].y} ${after[i].x} ${after[i].y}`;
|
|
72
|
+
}
|
|
73
|
+
d += ` L ${before[0].x} ${before[0].y} Q ${pts[0].x} ${pts[0].y} ${after[0].x} ${after[0].y} Z`;
|
|
74
|
+
const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><path fill='white' d='${d}'/></svg>`;
|
|
75
|
+
const encoded = encodeURIComponent(svg).replace(/'/g, '%27').replace(/"/g, '%22');
|
|
76
|
+
return `url("data:image/svg+xml,${encoded}")`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function applyMask() {
|
|
80
|
+
const node = el.value;
|
|
81
|
+
if (!node) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const cs = window.getComputedStyle(node);
|
|
85
|
+
|
|
86
|
+
const w = parsePx(cs.width);
|
|
87
|
+
const h = parsePx(cs.height);
|
|
88
|
+
// 允许通过 CSS 变量传递 px 值(推荐 px),非 px 或缺省将视作 0
|
|
89
|
+
const rVar = cs.getPropertyValue('--pubinfo-box-radius').trim();
|
|
90
|
+
let rPx: number = 0;
|
|
91
|
+
if (rVar) {
|
|
92
|
+
const numeric = parsePx(rVar);
|
|
93
|
+
rPx = numeric ?? (rVar.endsWith('px') ? parsePx(rVar) ?? 0 : 0);
|
|
94
|
+
}
|
|
95
|
+
// 变量取不到时,回退解析 border-radius 的左上角值(px)
|
|
96
|
+
if (!rPx) {
|
|
97
|
+
const br = cs.borderTopLeftRadius || cs.borderRadius;
|
|
98
|
+
const brNum = parsePx(br);
|
|
99
|
+
if (brNum) {
|
|
100
|
+
rPx = brNum;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 宽高可解析且 r>0 时启用圆角 mask(非正方形也允许,形状会按元素比例拉伸)
|
|
105
|
+
if (w && h && rPx > 0) {
|
|
106
|
+
const size = Math.min(w, h);
|
|
107
|
+
const maskUrl = genRoundedHexMask(size, rPx);
|
|
108
|
+
if (maskUrl) {
|
|
109
|
+
const supportsMask = typeof CSS !== 'undefined' && (
|
|
110
|
+
CSS.supports('mask-image', 'url("data:image/svg+xml,%3Csvg%3E%3C/svg%3E")')
|
|
111
|
+
|| CSS.supports('-webkit-mask-image', 'url("data:image/svg+xml,%3Csvg%3E%3C/svg%3E")')
|
|
112
|
+
);
|
|
113
|
+
node.style.webkitMaskImage = maskUrl as any;
|
|
114
|
+
node.style.maskImage = maskUrl as any;
|
|
115
|
+
node.style.webkitMaskRepeat = 'no-repeat';
|
|
116
|
+
node.style.maskRepeat = 'no-repeat';
|
|
117
|
+
node.style.webkitMaskSize = '100% 100%';
|
|
118
|
+
node.style.maskSize = '100% 100%';
|
|
119
|
+
node.style.webkitMaskPosition = 'center';
|
|
120
|
+
node.style.maskPosition = 'center';
|
|
121
|
+
if (supportsMask) {
|
|
122
|
+
// 移除 clip-path,避免其直角边缘覆盖圆角效果
|
|
123
|
+
node.style.clipPath = 'none';
|
|
124
|
+
(node.style as any).webkitClipPath = 'none';
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// 不支持 mask 时回退 clip-path 六边形
|
|
128
|
+
node.style.clipPath = 'polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)';
|
|
129
|
+
(node.style as any).webkitClipPath = 'polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)';
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 回退:去掉 mask,仅使用 clip-path 六边形
|
|
135
|
+
node.style.webkitMaskImage = '';
|
|
136
|
+
node.style.maskImage = '';
|
|
137
|
+
node.style.webkitMaskRepeat = '';
|
|
138
|
+
node.style.maskRepeat = '';
|
|
139
|
+
node.style.webkitMaskSize = '';
|
|
140
|
+
node.style.maskSize = '';
|
|
141
|
+
node.style.webkitMaskPosition = '';
|
|
142
|
+
node.style.maskPosition = '';
|
|
143
|
+
node.style.clipPath = 'polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)';
|
|
144
|
+
(node.style as any).webkitClipPath = 'polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
onMounted(() => {
|
|
148
|
+
applyMask();
|
|
149
|
+
ro = new ResizeObserver(() => applyMask());
|
|
150
|
+
if (el.value) {
|
|
151
|
+
ro.observe(el.value);
|
|
152
|
+
}
|
|
153
|
+
// 监听自身与文档根节点的样式/类变化,以捕获 CSS 变量或主题切换
|
|
154
|
+
mo = new MutationObserver(() => scheduleApply());
|
|
155
|
+
if (el.value) {
|
|
156
|
+
mo.observe(el.value, { attributes: true, attributeFilter: ['style', 'class'] });
|
|
157
|
+
}
|
|
158
|
+
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style'], subtree: true });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
onBeforeUnmount(() => {
|
|
162
|
+
if (ro && el.value) {
|
|
163
|
+
ro.unobserve(el.value);
|
|
164
|
+
}
|
|
165
|
+
ro = null;
|
|
166
|
+
if (mo) {
|
|
167
|
+
mo.disconnect();
|
|
168
|
+
}
|
|
169
|
+
mo = null;
|
|
170
|
+
});
|
|
171
|
+
</script>
|
|
172
|
+
|
|
173
|
+
<template>
|
|
174
|
+
<div ref="el" class="prism-box">
|
|
175
|
+
<slot />
|
|
176
|
+
</div>
|
|
177
|
+
</template>
|
|
178
|
+
|
|
179
|
+
<style scoped>
|
|
180
|
+
.prism-box {
|
|
181
|
+
display: inline-flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
box-sizing: border-box;
|
|
185
|
+
overflow: hidden;
|
|
186
|
+
line-height: 1;
|
|
187
|
+
vertical-align: top;
|
|
188
|
+
width: var(--pubinfo-box-size, auto);
|
|
189
|
+
height: var(--pubinfo-box-size, auto);
|
|
190
|
+
border-radius: var(--pubinfo-box-radius) !important;
|
|
191
|
+
background: var(
|
|
192
|
+
--pubinfo-box-full-background,
|
|
193
|
+
linear-gradient(
|
|
194
|
+
var(--pubinfo-box-angle, 65deg),
|
|
195
|
+
var(--pubinfo-box-gradient-from, var(--pubinfo-box-background, #65E54A)),
|
|
196
|
+
var(--pubinfo-box-gradient-to, var(--pubinfo-box-background, #35C724))
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
/* 纯 CSS 六边形裁剪 */
|
|
200
|
+
-webkit-clip-path: polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%);
|
|
201
|
+
clip-path: polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%);
|
|
202
|
+
}
|
|
203
|
+
</style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SquareBox
|
|
4
|
+
* 语义:图标 / 徽记 / 占位的方形或矩形“载体”容器。
|
|
5
|
+
*
|
|
6
|
+
* 设计目标:
|
|
7
|
+
* - 统一图标背景、圆角、渐变、尺寸计算逻辑(与 PrismBox 等保持一致)
|
|
8
|
+
* - 使用 <div> 而不是 <svg>,可直接放置任意 HTML / SVG / 组件
|
|
9
|
+
*
|
|
10
|
+
* 尺寸策略:
|
|
11
|
+
* - width / height / size 三种方式;size 作为宽高同步值
|
|
12
|
+
* - full=true 填充父容器(忽略 width/height/size)
|
|
13
|
+
* - 仅在固定 px 宽高时暴露:--square-inner-size / --box-inner-size = width - padding*2
|
|
14
|
+
*
|
|
15
|
+
* 圆角策略:
|
|
16
|
+
* - radius 支持 数字 | 纯数字字符串 | 其他合法 CSS 长度/百分比
|
|
17
|
+
* - 未传或空串 => 默认 6px;传 0 获得直角
|
|
18
|
+
* - 暴露 CSS 变量:--box-radius 便于主题覆写
|
|
19
|
+
*
|
|
20
|
+
* 背景策略:
|
|
21
|
+
* - 优先使用 props.background
|
|
22
|
+
* - 否则生成线性渐变:gradientFrom -> gradientTo
|
|
23
|
+
*
|
|
24
|
+
* 可访问性:
|
|
25
|
+
* - 不附带 role/aria-*,由使用方按内容语义自行添加
|
|
26
|
+
*
|
|
27
|
+
*/
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="square-box">
|
|
32
|
+
<slot />
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<style scoped>
|
|
37
|
+
.square-box {
|
|
38
|
+
display: inline-flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
box-sizing: border-box;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
line-height: 1;
|
|
44
|
+
vertical-align: top;
|
|
45
|
+
border-radius: var(--pubinfo-box-radius) !important;
|
|
46
|
+
/*
|
|
47
|
+
Allow a complete background override via CSS var when provided,
|
|
48
|
+
otherwise fall back to previous linear gradient behavior.
|
|
49
|
+
*/
|
|
50
|
+
background: var(
|
|
51
|
+
--pubinfo-box-full-background,
|
|
52
|
+
linear-gradient(
|
|
53
|
+
var(--pubinfo-box-angle, 65deg),
|
|
54
|
+
var(--pubinfo-box-gradient-from, var(--pubinfo-box-background, #65E54A)),
|
|
55
|
+
var(--pubinfo-box-gradient-to, var(--pubinfo-box-background, #35C724))
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { PubinfoIconOptions } from './props';
|
|
2
3
|
import * as AntdIcons from '@ant-design/icons-vue';
|
|
3
4
|
import { Icon } from '@iconify/vue';
|
|
4
5
|
import { computedAsync } from '@vueuse/core';
|
|
5
6
|
import { setupIcon } from '@/core';
|
|
6
7
|
import { useProvider } from '../PubinfoProvider';
|
|
8
|
+
import PrismBox from './PrismBox.vue';
|
|
9
|
+
import SquareBox from './SquareBox.vue';
|
|
7
10
|
|
|
8
11
|
defineOptions({
|
|
9
12
|
name: 'PubinfoIcon',
|
|
10
13
|
});
|
|
11
14
|
|
|
12
|
-
const props = defineProps<
|
|
13
|
-
name: string
|
|
14
|
-
async?: boolean
|
|
15
|
-
flip?: 'horizontal' | 'vertical' | 'both'
|
|
16
|
-
rotate?: number
|
|
17
|
-
color?: string
|
|
18
|
-
size?: string | number
|
|
19
|
-
}>();
|
|
15
|
+
const props = defineProps<PubinfoIconOptions>();
|
|
20
16
|
|
|
21
17
|
const { loadIcon } = useProvider();
|
|
22
18
|
|
|
23
|
-
const outputType = computed(() => {
|
|
19
|
+
const outputType = computed<'svg' | 'css' | 'antd' | 'custom'>(() => {
|
|
24
20
|
if (props.name.indexOf('i-') === 0) {
|
|
25
21
|
return props.async ? 'svg' : 'css';
|
|
26
22
|
}
|
|
@@ -51,35 +47,30 @@ const outputName = computed(() => {
|
|
|
51
47
|
return props.name;
|
|
52
48
|
});
|
|
53
49
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
switch (props.flip) {
|
|
58
|
-
case 'horizontal':
|
|
59
|
-
transform.push('rotateY(180deg)');
|
|
60
|
-
break;
|
|
61
|
-
case 'vertical':
|
|
62
|
-
transform.push('rotateX(180deg)');
|
|
63
|
-
break;
|
|
64
|
-
case 'both':
|
|
65
|
-
transform.push('rotateX(180deg)');
|
|
66
|
-
transform.push('rotateY(180deg)');
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
50
|
+
const isBox = computed<'square' | 'prism' | 'null'>(() => {
|
|
51
|
+
if (props.box === undefined) {
|
|
52
|
+
return 'null';
|
|
69
53
|
}
|
|
70
|
-
if (props.
|
|
71
|
-
|
|
54
|
+
else if (['square', 'prism'].includes(props.box)) {
|
|
55
|
+
return props.box;
|
|
72
56
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
57
|
+
else {
|
|
58
|
+
return 'null';
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const element = computed(() => {
|
|
63
|
+
if (isBox.value === 'square') {
|
|
64
|
+
return SquareBox;
|
|
65
|
+
}
|
|
66
|
+
else if (isBox.value === 'prism') {
|
|
67
|
+
return PrismBox;
|
|
68
|
+
}
|
|
69
|
+
return 'i';
|
|
78
70
|
});
|
|
79
71
|
|
|
80
72
|
const icon = computedAsync(async () => {
|
|
81
73
|
const parse = parseString(props.name);
|
|
82
|
-
|
|
83
74
|
return loadIcon?.(props.name) ?? await setupIcon(`${parse.name}${parse.ext || '.svg'}`, parse.prefix);
|
|
84
75
|
});
|
|
85
76
|
|
|
@@ -124,17 +115,117 @@ function parseString(str: string): Parsed {
|
|
|
124
115
|
|
|
125
116
|
return { prefix, name, ext };
|
|
126
117
|
}
|
|
118
|
+
|
|
119
|
+
const style = computed(() => {
|
|
120
|
+
const transform: string[] = [];
|
|
121
|
+
if (props.flip) {
|
|
122
|
+
switch (props.flip) {
|
|
123
|
+
case 'horizontal':
|
|
124
|
+
transform.push('rotateY(180deg)');
|
|
125
|
+
break;
|
|
126
|
+
case 'vertical':
|
|
127
|
+
transform.push('rotateX(180deg)');
|
|
128
|
+
break;
|
|
129
|
+
case 'both':
|
|
130
|
+
transform.push('rotateX(180deg)');
|
|
131
|
+
transform.push('rotateY(180deg)');
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (props.rotate) {
|
|
136
|
+
transform.push(`rotate(${props.rotate % 360}deg)`);
|
|
137
|
+
}
|
|
138
|
+
const result: Record<string, string> = {
|
|
139
|
+
'--pubinfo-icon-color': '',
|
|
140
|
+
'--pubinfo-icon-size': '',
|
|
141
|
+
'--pubinfo-icon-transform': '',
|
|
142
|
+
};
|
|
143
|
+
if (props.color) {
|
|
144
|
+
result['--pubinfo-icon-color'] = props.color;
|
|
145
|
+
}
|
|
146
|
+
if (props.size) {
|
|
147
|
+
result['--pubinfo-icon-size'] = typeof props.size === 'number' ? `${props.size}px` : props.size;
|
|
148
|
+
}
|
|
149
|
+
if (transform.length) {
|
|
150
|
+
result['--pubinfo-icon-transform'] = transform.join(' ');
|
|
151
|
+
}
|
|
152
|
+
// 让内联元素在同一行内进行顶部对齐,避免默认 baseline 导致的视觉不齐
|
|
153
|
+
result.verticalAlign = 'top';
|
|
154
|
+
return result;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const bind = computed(() => {
|
|
158
|
+
if (isBox.value !== 'null') {
|
|
159
|
+
const result: Record<
|
|
160
|
+
'--pubinfo-box-background' | '--pubinfo-box-size' | '--pubinfo-box-radius' | '--pubinfo-box-gradient-from' | '--pubinfo-box-gradient-to' | '--pubinfo-box-angle' | '--pubinfo-box-full-background',
|
|
161
|
+
string> = {
|
|
162
|
+
'--pubinfo-box-background': '',
|
|
163
|
+
'--pubinfo-box-size': '',
|
|
164
|
+
'--pubinfo-box-radius': '',
|
|
165
|
+
'--pubinfo-box-gradient-from': '',
|
|
166
|
+
'--pubinfo-box-gradient-to': '',
|
|
167
|
+
'--pubinfo-box-angle': '',
|
|
168
|
+
'--pubinfo-box-full-background': '',
|
|
169
|
+
};
|
|
170
|
+
if (typeof props.angle === 'string') {
|
|
171
|
+
result['--pubinfo-box-angle'] = props.angle;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
result['--pubinfo-box-angle'] = `${props.angle || 65}deg`;
|
|
175
|
+
}
|
|
176
|
+
if (typeof props.background === 'string') {
|
|
177
|
+
// provide full override for any CSS background value (color or gradient)
|
|
178
|
+
result['--pubinfo-box-full-background'] = props.background;
|
|
179
|
+
// keep legacy variable for solid color fallback use-cases
|
|
180
|
+
result['--pubinfo-box-background'] = props.background;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
result['--pubinfo-box-gradient-from'] = props.background?.from || '#65E54A';
|
|
184
|
+
result['--pubinfo-box-gradient-to'] = props.background?.to || '#35C724';
|
|
185
|
+
}
|
|
186
|
+
result['--pubinfo-box-radius'] = props.radius !== undefined
|
|
187
|
+
? (typeof props.radius === 'number' ? `${props.small ? props.radius / 2 : props.radius}px` : props.small ? `calc(${props.radius} / 2)` : props.radius)
|
|
188
|
+
: (props.box === 'prism' ? `${props.small ? '3px' : '6px'}` : `${props.small ? '7px' : '14px'}`);
|
|
189
|
+
if (props.size) {
|
|
190
|
+
result['--pubinfo-box-size'] = typeof props.size === 'number' ? `${props.size}px` : props.size;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
style: { ...result, ...style.value },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
return {
|
|
198
|
+
style: style.value,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
});
|
|
127
202
|
</script>
|
|
128
203
|
|
|
129
204
|
<template>
|
|
130
|
-
<
|
|
205
|
+
<component
|
|
206
|
+
:is="element"
|
|
207
|
+
:data-icones="props.name"
|
|
208
|
+
:data-box="isBox !== 'null' ? '' : undefined"
|
|
209
|
+
:data-box-type="isBox"
|
|
210
|
+
:data-icon-type="outputType"
|
|
211
|
+
:data-icon-small="props.small ? '' : undefined"
|
|
212
|
+
class="
|
|
213
|
+
data-[box-type=null]:relative data-[box-type=null]:inline-flex
|
|
214
|
+
data-[box-type=null]:items-center data-[box-type=null]:justify-center
|
|
215
|
+
data-[box-type=null]:fill-current data-[box-type=null]:leading-[1]
|
|
216
|
+
data-[box-type=null]:align-top
|
|
217
|
+
data-[box]:size-[var(--pubinfo-box-size)]!
|
|
218
|
+
data-[box]:text-[calc(var(--pubinfo-box-size)_*_0.5)]!
|
|
219
|
+
text-[calc(var(--pubinfo-icon-size))]
|
|
220
|
+
text-[var(--pubinfo-icon-color)]
|
|
221
|
+
[transform:var(--pubinfo-icon-transform)]"
|
|
222
|
+
v-bind="bind"
|
|
223
|
+
>
|
|
131
224
|
<i v-if="outputType === 'css'" :class="outputName" />
|
|
132
225
|
<Icon v-else-if="outputType === 'svg'" :icon="outputName" />
|
|
133
226
|
<span v-else-if="outputType === 'antd'" class="antd-icon" :data-icon="outputName">
|
|
134
|
-
<component
|
|
135
|
-
:is="AntdIcons[outputName as keyof typeof AntdIcons] as any"
|
|
136
|
-
/>
|
|
227
|
+
<component :is="(AntdIcons[outputName as keyof typeof AntdIcons] as any)" />
|
|
137
228
|
</span>
|
|
138
229
|
<img v-else-if="icon" class="size-1em" :src="icon">
|
|
139
|
-
</
|
|
230
|
+
</component>
|
|
140
231
|
</template>
|