labull-framework 0.1.1 → 0.1.2

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.
@@ -1,21 +1,13 @@
1
- /** 🔴 `uuid` 是前端写死的:后端按它找那一行(库里还没有也能先渲染成「未开发」)。`width` / `height` 数字按 rem 处理、字符串原样当 CSS 值。 */
1
+ /** 🔴 `uuid` 是前端写死的:后端按它找那一行(库里还没有也能先渲染成「未开发」)。 */
2
2
  type __VLS_Props = {
3
3
  uuid: string;
4
4
  name?: string;
5
5
  module?: string;
6
- width?: string | number;
7
- height?: string | number;
8
- flexible?: boolean;
9
- fullSize?: boolean;
10
- responsive?: boolean;
11
6
  theme?: boolean;
12
7
  };
13
8
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
14
9
  name: string;
15
10
  module: string;
16
- flexible: boolean;
17
- fullSize: boolean;
18
- responsive: boolean;
19
11
  theme: boolean;
20
12
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
13
  export default _default;
@@ -0,0 +1,12 @@
1
+ type __VLS_Props = {
2
+ uuid: string;
3
+ name?: string;
4
+ module?: string;
5
+ theme?: boolean;
6
+ };
7
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
8
+ name: string;
9
+ module: string;
10
+ theme: boolean;
11
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
12
+ export default _default;
@@ -1,22 +1,26 @@
1
1
  /**
2
- * full-size 的块**不许自己出竖滚动条**(滚的是外层页面,见 `MagicBlockHost.vue` 的 `fullSize`)。
2
+ * 块**不许自己出竖滚动条**(滚的是它所属的那一条标签页,见 `MagicBlockHost.vue`)。
3
3
  *
4
+ * 🔴 这一条对**每一个块**都成立(块一律占满所属标签页,没有"弹性"那一档了)。
4
5
  * 🔴 这条要**在文档里**禁,不是在 iframe 标签上:`scrolling="no"` 与父页给的 `overflow: hidden`
5
6
  * 都管不到文档自己的视口。宿主这一侧看不见块里的 DOM(沙箱是不透明源),所以只能随预览一起注入。
6
7
  * 🔴 为什么不能靠"把宿主那层设得够高"就算了:块一出现滚动条,**内容宽度就少十几像素** ⇒
7
8
  * 文字重新折行 ⇒ 量到的内容高度又变 ⇒ 宿主再改高度 —— 两边来回抖。
8
9
  * `overflow: hidden` 把这个正反馈从源头掐掉(内容高度只由宽度决定,而宽度不再变)。
9
10
  * ⚠️ 代价:块真比宿主给的位置更高时,多出来的部分**是被裁掉的**(不是藏起来还能滚)。
10
- * 所以只在 full-size 这条路上加(`MagicBlockHost.vue` 的 `extras`),弹性那条路块该滚还得滚。
11
11
  */
12
12
  export declare const BLOCK_NO_SCROLL_CSS = "html, body { overflow: hidden !important; }";
13
13
  /** 注入到块 iframe 里的 JS 源码(块没有构建步骤,它必须自己就是能跑的 JS)。
14
14
  * 🔴 这是模板字符串:段内的注释出现反引号会提前截断它(实现也一律用字符串拼接)。 */
15
- export declare const CLIENT_SOURCE = "\n(function () {\n var seq = 0, waiting = {}, queue = [], ready = false, last = -1;\n function call(method, args) {\n return new Promise(function (resolve, reject) {\n function run() {\n var id = ++seq;\n waiting[id] = { resolve: resolve, reject: reject };\n parent.postMessage({ '__mb': 'call', id: id, method: method, args: args }, '*');\n }\n if (ready) run(); else queue.push(run);\n });\n }\n window.addEventListener('message', function (event) {\n var data = event.data, pending;\n if (!data || data['__mb'] === undefined) return;\n if (data['__mb'] === 'host-ready') { ready = true; queue.splice(0).forEach(function (run) { run(); }); return; }\n if (data['__mb'] !== 'reply') return;\n if (!(pending = waiting[data.id])) return;\n delete waiting[data.id];\n if (data.ok) pending.resolve(data.value); else pending.reject(new Error(data.error || '\u8C03\u7528\u5931\u8D25'));\n });\n window.magicBlock = {\n getMe: function () { return call('getMe', []); },\n navigateTab: function (path) { return call('navigateTab', [path]); }\n };\n function make(prefix) {\n return new Proxy(function () {}, {\n get: function (_, name) { return make(prefix ? prefix + '.' + name : name); },\n apply: function (_, __, args) {\n var params = args[0] === undefined || args[0] === null ? {} : args[0];\n if (typeof params !== 'object' || Array.isArray(params)) return Promise.reject(new Error('magicBackendApi.' + prefix + '(params) \u7684\u53C2\u6570\u8981\u4E48\u662F\u4E00\u4E2A\u5BF9\u8C61\uFF08\u62FF\u4E0D\u51C6\u5C31\u4F20 {}\uFF09\uFF0C\u8981\u4E48\u662F\u4E00\u4E2A\u6587\u4EF6\uFF08File / Blob\uFF09'));\n return call('backend', [prefix, params]);\n }\n });\n }\n window.magicBackendApi = make('');\n if (typeof document === 'undefined') return;\n function report() {\n var body = document.body;\n if (!body) return;\n var max = -1, style = window.getComputedStyle(body), bodyTop = body.getBoundingClientRect().top;\n var gap = (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0);\n try {\n for (var i = 0; i < body.children.length; i += 1) {\n var el = body.children[i], cs = window.getComputedStyle(el), rect = el.getBoundingClientRect();\n if (cs.display === 'none' || cs.visibility === 'hidden' || cs.position === 'fixed' || rect.height <= 0) continue;\n if (rect.bottom > max) max = rect.bottom;\n }\n } catch (error) { return; }\n var height = max < 0 ? 0 : Math.ceil(max - bodyTop + gap); /* \u4E00\u6761\u6B63\u5E38\u6D41\u5185\u5BB9\u90FD\u6CA1\u6709 \u21D2 \u62A5 0\uFF08\u5BBF\u4E3B\u6309\u8FD8\u6CA1\u91CF\u5230\u5904\u7406\uFF09 */\n /* \uD83D\uDD34 +1 \u662F\"\u53D6\u6574\u7684\u4F59\u91CF\"\uFF1A\u51E0\u4F55\u4E0A\u7B97\u51FA\u6765\u7684\u5185\u5BB9\u9AD8\u5EA6\u5E38\u5E38\u5DEE\u90A3\u4E48\u96F6\u70B9\u51E0\u50CF\u7D20\uFF08\u5B50\u50CF\u7D20\u6392\u7248\uFF09\uFF0C\n * \u5BBF\u4E3B\u7167\u7740\u5B83\u8BBE\u9AD8\u5EA6\u65F6\u5C31\u4F1A**\u5DEE\u4E00\u50CF\u7D20**\u2014\u2014\u90A3\u4E00\u50CF\u7D20\u6B63\u662F\u4E00\u4E2A\u7AD6\u6EDA\u52A8\u6761\u7684\u5F00\u5173\u3002 */\n if (height > 0) height += 1;\n if (height === last) return; /* \u4E00\u6837\u9AD8\u5C31\u4E0D\u518D\u53D1\uFF1A\u5426\u5219\u300C\u5BBF\u4E3B\u6539\u9AD8\u5EA6 \u2192 \u89E6\u53D1\u89C2\u5BDF \u2192 \u53C8\u53D1\u4E00\u6B21\u300D\u4F1A\u8F6C\u4E2A\u4E0D\u505C */\n last = height;\n parent.postMessage({ '__mb': 'height', height: height }, '*');\n }\n function start() {\n report();\n if (window.ResizeObserver && document.body) new ResizeObserver(report).observe(document.body);\n }\n if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();\n setInterval(report, 500);\n})();\n";
15
+ export declare const CLIENT_SOURCE = "\n(function () {\n var seq = 0, waiting = {}, queue = [], ready = false, last = -1;\n function call(method, args) {\n return new Promise(function (resolve, reject) {\n function run() {\n var id = ++seq;\n waiting[id] = { resolve: resolve, reject: reject };\n parent.postMessage({ '__mb': 'call', id: id, method: method, args: args }, '*');\n }\n if (ready) run(); else queue.push(run);\n });\n }\n window.addEventListener('message', function (event) {\n var data = event.data, pending;\n if (!data || data['__mb'] === undefined) return;\n if (data['__mb'] === 'host-ready') { ready = true; queue.splice(0).forEach(function (run) { run(); }); return; }\n if (data['__mb'] !== 'reply') return;\n if (!(pending = waiting[data.id])) return;\n delete waiting[data.id];\n if (data.ok) pending.resolve(data.value); else pending.reject(new Error(data.error || '\u8C03\u7528\u5931\u8D25'));\n });\n window.magicBlock = {\n getMe: function () { return call('getMe', []); },\n navigateTab: function (path) { return call('navigateTab', [path]); },\n context: function () { return call('context', []); },\n setTabTitle: function (title, icon) { return call('setTabTitle', [title, icon]); }\n };\n function make(prefix) {\n return new Proxy(function () {}, {\n get: function (_, name) { return make(prefix ? prefix + '.' + name : name); },\n apply: function (_, __, args) {\n var params = args[0] === undefined || args[0] === null ? {} : args[0];\n if (typeof params !== 'object' || Array.isArray(params)) return Promise.reject(new Error('magicBackendApi.' + prefix + '(params) \u7684\u53C2\u6570\u8981\u4E48\u662F\u4E00\u4E2A\u5BF9\u8C61\uFF08\u62FF\u4E0D\u51C6\u5C31\u4F20 {}\uFF09\uFF0C\u8981\u4E48\u662F\u4E00\u4E2A\u6587\u4EF6\uFF08File / Blob\uFF09'));\n return call('backend', [prefix, params]);\n }\n });\n }\n window.magicBackendApi = make('');\n if (typeof document === 'undefined') return;\n function report() {\n var body = document.body;\n if (!body) return;\n var max = -1, style = window.getComputedStyle(body), bodyTop = body.getBoundingClientRect().top;\n var gap = (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0);\n try {\n for (var i = 0; i < body.children.length; i += 1) {\n var el = body.children[i], cs = window.getComputedStyle(el), rect = el.getBoundingClientRect();\n if (cs.display === 'none' || cs.visibility === 'hidden' || cs.position === 'fixed' || rect.height <= 0) continue;\n if (rect.bottom > max) max = rect.bottom;\n }\n } catch (error) { return; }\n var height = max < 0 ? 0 : Math.ceil(max - bodyTop + gap); /* \u4E00\u6761\u6B63\u5E38\u6D41\u5185\u5BB9\u90FD\u6CA1\u6709 \u21D2 \u62A5 0\uFF08\u5BBF\u4E3B\u6309\u8FD8\u6CA1\u91CF\u5230\u5904\u7406\uFF09 */\n /* \uD83D\uDD34 +1 \u662F\"\u53D6\u6574\u7684\u4F59\u91CF\"\uFF1A\u51E0\u4F55\u4E0A\u7B97\u51FA\u6765\u7684\u5185\u5BB9\u9AD8\u5EA6\u5E38\u5E38\u5DEE\u90A3\u4E48\u96F6\u70B9\u51E0\u50CF\u7D20\uFF08\u5B50\u50CF\u7D20\u6392\u7248\uFF09\uFF0C\n * \u5BBF\u4E3B\u7167\u7740\u5B83\u8BBE\u9AD8\u5EA6\u65F6\u5C31\u4F1A**\u5DEE\u4E00\u50CF\u7D20**\u2014\u2014\u90A3\u4E00\u50CF\u7D20\u6B63\u662F\u4E00\u4E2A\u7AD6\u6EDA\u52A8\u6761\u7684\u5F00\u5173\u3002 */\n if (height > 0) height += 1;\n if (height === last) return; /* \u4E00\u6837\u9AD8\u5C31\u4E0D\u518D\u53D1\uFF1A\u5426\u5219\u300C\u5BBF\u4E3B\u6539\u9AD8\u5EA6 \u2192 \u89E6\u53D1\u89C2\u5BDF \u2192 \u53C8\u53D1\u4E00\u6B21\u300D\u4F1A\u8F6C\u4E2A\u4E0D\u505C */\n last = height;\n parent.postMessage({ '__mb': 'height', height: height }, '*');\n }\n function start() {\n report();\n if (window.ResizeObserver && document.body) new ResizeObserver(report).observe(document.body);\n }\n if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();\n setInterval(report, 500);\n})();\n";
16
16
  export interface BridgeDeps {
17
17
  uuid: string;
18
18
  onHeight?: (height: number) => void;
19
- /** 🔴 路由白名单在宿主手上(块看不到路由表)。 */
19
+ /** 🔴 路由白名单在宿主手上(块看不到路由表);"跳" = 开那个标签。 */
20
20
  onNavigateTab?: (path: string) => Promise<string> | string;
21
+ /** 这一块所属标签的链接参数(`{ key, path, params, query }`):块自己读不到地址栏,只能由宿主喂。 */
22
+ context?: () => Record<string, unknown>;
23
+ /** 给**所属那条标签**起名 / 换图标("订单 SO-42"这种名字路由表算不出来,只有块自己知道)。 */
24
+ onTabTitle?: (title: string, icon?: string) => void;
21
25
  }
22
26
  export declare function installBridge(iframe: HTMLIFrameElement, deps: BridgeDeps): () => void;
@@ -2,7 +2,7 @@
2
2
  export declare const PAGES_LOCK = "pages.lock.ts";
3
3
  /** 一条页面(从路由表现算出来的那一份:见 `AppSearch` 的 `collect()`,判据与它同一套)。 */
4
4
  export interface PageEntry {
5
- /** 路由路径(`navigateTab()` 收的就是它)。 */
5
+ /** 路由路径(`navigateTab()` 收的就是它:块跳页 = 打开那个页面的标签)。 */
6
6
  path: string;
7
7
  /** 页面的中文名(`meta.title`,没有就退到 `meta.sub` / 路径)。 */
8
8
  title: string;
@@ -1,33 +1,38 @@
1
- /** 亮红框的默认时长(毫秒):够人从一页跳到另一页之后还看得见那一下。 */
1
+ /** 亮红框的默认时长(毫秒):够人从一条标签跳到另一条之后还看得见那一下。 */
2
2
  export declare const HIGHLIGHT_MS = 4000;
3
3
  /** 订阅"高亮变了"(组件用;返回退订函数)。 */
4
4
  export declare function onBlockHighlight(fn: () => void): () => void;
5
5
  /** 现在该亮红框的是哪一块(空串 = 没有)。 */
6
6
  export declare function highlightedUuid(): string;
7
7
  /**
8
- * 记下"这个块挂在这一页",返回退订函数(宿主卸载时调)。
8
+ * 记下"这个块挂在这一条标签上",返回退订函数(宿主卸载时调)。
9
9
  *
10
- * ⚠️ 同一个块挂在两页 ⇒ 两条都在;退订只去掉**这一页**那一条。
11
- * ⚠️ uuid / 路径是空的一律不记(记了会变成"能跳到一页没有路径的地方")。
10
+ * ⚠️ 同一个块挂在两条标签 ⇒ 两条都在;退订只去掉**这一条**。
11
+ * ⚠️ uuid / 标签身份是空的一律不记(记了会变成"能跳到一条没有地址的标签")。
12
12
  */
13
- export declare function mountBlock(uuid: string, path: string): () => void;
14
- /** 这个块还挂在哪一页(**除了 `currentPath`**);没有就回 `null`。 */
15
- export declare function whereElse(uuid: string, currentPath: string): string | null;
16
- /** 这个块记录到的全部页面(按记下来的顺序)。 */
13
+ export declare function mountBlock(uuid: string, tabKey: string): () => void;
14
+ /** 这个块还挂在哪几条标签上(**除了 `currentTab`**);`pick` 用来挑(返回 true 的才算数)。 */
15
+ export declare function whereElse(uuid: string, currentTab: string, pick?: (tabKey: string) => boolean): string | null;
16
+ /** 这个块记录到的全部标签(按记下来的顺序)。 */
17
17
  export declare function mountedAt(uuid: string): string[];
18
18
  /**
19
- * 「定位这一块」该跳到哪一页(`null` = **不用跳**)。
19
+ * 「定位这一块」该跳到哪一条标签(`null` = **不用跳**)。
20
20
  *
21
21
  * 🔴 与 `whereElse()` 是两件事、两套取舍,别混着用:
22
- * · `whereElse()`(块自己那一圈被点)—— 优先去**别的**页面("这个块还挂在别处");
23
- * · `locateTarget()`(控制台里点那一行)—— 本页就有就**留在本页**(就在眼皮底下,
24
- * 把人传送走只会让人找不到刚点的那一块),本页没有才去第一个挂着它的页面。
22
+ * · `whereElse()`(块自己那一圈被点)—— 优先去**别的**标签("这个块还挂在别处");
23
+ * · `locateTarget()`(控制台里点那一行)—— 本条标签就有就**留在本标签**(就在眼皮底下,
24
+ * 把人传送走只会让人找不到刚点的那一块),本标签没有才去第一条挂着它的标签。
25
+ * 🔴 `pick` 是调用方给的"哪条标签能当目标":**实例页(`/orders/42?orderId=7` 这种带参数的)
26
+ * 不该被自动选中** —— 那是"某一单",不是"这个块在哪一页"(判据见 `@labull/ui` 的
27
+ * `isInstanceLocation()`)。不给 `pick` 就是"哪条都行"。
28
+ * 🔴 名单里记着的都是**现在开着**的标签(关掉标签会把这一条一起摘掉,见 `mountBlock` 的退订),
29
+ * 所以按这里的结果去 `openTab()` 只会**在已有标签之间切**,不会新开一条。
25
30
  */
26
- export declare function locateTarget(uuid: string, currentPath: string): string | null;
31
+ export declare function locateTarget(uuid: string, currentTab: string, pick?: (tabKey: string) => boolean): string | null;
27
32
  /**
28
33
  * 让这一块亮几秒红框(点那一圈时调)。
29
34
  *
30
- * 🔴 **先亮、再跳**:目标页那一块一露面就已经是"该亮"的状态,不必等它挂载之后再通知一次。
35
+ * 🔴 **先亮、再跳**:目标标签那一块一露面就已经是"该亮"的状态,不必等它挂载之后再通知一次。
31
36
  */
32
37
  export declare function flashBlock(uuid: string, ms?: number): void;
33
38
  /** 立刻收起高亮(用例、以及"不想再亮"的场合用)。 */
@@ -3,7 +3,7 @@ export declare const BLOCK_BACKEND_FILE = "backend_open.py";
3
3
  export interface PreviewExtras {
4
4
  /** 注入给块的引导脚本(`CLIENT_SOURCE`)。 */
5
5
  bootstrap?: string;
6
- /** 宿主额外加的 CSS:禁掉块自己的滚动条(`fullSize` 用)。 */
6
+ /** 宿主额外加的 CSS(`BLOCK_NO_SCROLL_CSS`:块一律占满所属标签页,块内不出滚动条)。 */
7
7
  css?: string;
8
8
  /** 应用主题 / 控件那一层 CSS(`theme.ts` 的 `collectThemeCss()`;由 `MagicBlockHost` 的 `theme` prop 决定给不给)。 */
9
9
  themeCss?: string;
@@ -1,4 +1,4 @@
1
- export { default as MagicBlockHost } from './magic-block-MagicBlockHost.vue';
1
+ export { default as MagicBlockPage } from './magic-block-MagicBlockPage.vue';
2
2
  export { default as MagicBlockConsole } from './magic-block-MagicBlockConsole.vue';
3
3
  export { loadBlockRender, onBlockRenderRefresh, refreshBlockRender, type BlockRender } from './magic-block-render';
4
4
  export { BLOCK_BACKEND_FILE, BLOCK_ENTRY, buildPreview } from './magic-block-preview';
@@ -4,7 +4,7 @@ type __VLS_Props = {
4
4
  side?: SideItem[];
5
5
  logo?: string;
6
6
  caption?: string;
7
- /** 左侧图标栏最上面那个"首页"按钮的路径(默认 `/`)。配合 `side` 用。 */
7
+ /** 左侧图标栏最上面那个"首页"按钮的路径(默认 `/`)。配合 `side` 用;标签全关掉时也回这里。 */
8
8
  home?: string;
9
9
  /** 底部竖排的产品名(不给就不画)。 */
10
10
  brand?: string;
@@ -18,9 +18,9 @@ type __VLS_Slots = {} & {
18
18
  actions?: (props: typeof __VLS_5) => any;
19
19
  };
20
20
  declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
21
+ caption: string;
21
22
  home: string;
22
23
  actions: boolean;
23
- caption: string;
24
24
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
25
  declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
26
26
  export default _default;
@@ -3,7 +3,7 @@ type __VLS_Props = {
3
3
  items: SideItem[];
4
4
  /** 底部竖排的产品名(不给就不画)。 */
5
5
  brand?: string;
6
- /** 图标栏最上面那一个"首页"按钮:点它跳这个路径(**不开新标签的例外**,它由路由自己记账)。 */
6
+ /** 图标栏最上面那一个"首页"按钮:点它开这个地址的标签(默认动作,不走 `onClick`)。 */
7
7
  home?: string;
8
8
  /** 首页按钮的名字。 */
9
9
  homeTitle?: string;
@@ -1,2 +1,7 @@
1
- declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
1
+ type __VLS_Props = {
2
+ home?: string;
3
+ };
4
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
5
+ home: string;
6
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
2
7
  export default _default;
@@ -0,0 +1,6 @@
1
+ import type { TabItem } from './ui-tabs';
2
+ type __VLS_Props = {
3
+ tab: TabItem;
4
+ };
5
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ export default _default;
@@ -3,10 +3,13 @@ import AppSearch from './ui-AppSearch.vue';
3
3
  import ProductShell from './ui-ProductShell.vue';
4
4
  import ProductSide from './ui-ProductSide.vue';
5
5
  import ProductTabs from './ui-ProductTabs.vue';
6
+ import TabPage from './ui-TabPage.vue';
6
7
  import UserMenu from './ui-UserMenu.vue';
7
- export { AppSearch, ProductShell, ProductSide, ProductTabs, UserMenu };
8
- export { openTab, closeTab, registerTabLabel, useTabs, TABS_MAX } from './ui-tabs';
9
- export type { TabItem } from './ui-tabs';
8
+ export { AppSearch, ProductShell, ProductSide, ProductTabs, TabPage, UserMenu };
9
+ export { activateTab, closeTab, followUrl, installTabNavigator, openTab, setTabTitle, useTabs } from './ui-tabs';
10
+ export type { TabItem, TabMeta, TabNavigator } from './ui-tabs';
11
+ export { TAB_CONTEXT, isInstanceLocation, provideTabContext, useTabContext } from './ui-tab-context';
12
+ export type { LocationResolver, TabContext } from './ui-tab-context';
10
13
  export type { SideItem } from './ui-types';
11
14
  export interface LabullOptions {
12
15
  extra?: Plugin[];
@@ -0,0 +1,46 @@
1
+ import type { TabItem } from './ui-tabs';
2
+ /** 一条标签给它子树的那一份。 */
3
+ export interface TabContext {
4
+ /** 这一条标签的身份 = 完整地址(路径 + query)。 */
5
+ key: string;
6
+ /** 路由路径(不带 query)。 */
7
+ path: string;
8
+ /** 路由参数:`/orders/:id` 上的 `id`。 */
9
+ params: Readonly<Record<string, string | string[]>>;
10
+ /** 查询串参数:`?orderId=7` 上的 `orderId`。分享出去的链接带的就是它。 */
11
+ query: Readonly<Record<string, string | string[]>>;
12
+ /** 这一条标签现在是不是**显示中的**那一条。隐藏的那些拿它决定"先别量高度"。 */
13
+ active: {
14
+ readonly value: boolean;
15
+ };
16
+ /** 给自己这一条标签起名(详情页那种名字路由表算不出来)。 */
17
+ setTitle(title: string, icon?: string): void;
18
+ }
19
+ export declare const TAB_CONTEXT = "labull:tab";
20
+ /** 由标签页调(`TabPage` 的 setup 里):把这一条标签的身份提供下去。 */
21
+ export declare function provideTabContext(tab: TabItem): TabContext;
22
+ /**
23
+ * 读"我在哪一条标签里"(页面、以及页面里的 magic block 用)。
24
+ *
25
+ * 🔴 不在标签页里就**直接报错**:magic block 与标签页是绑死的(块要占满标签页、要挂在那条标签上、
26
+ * 要读那条标签的链接参数),把它放到标签页外面是配置错了 —— 安静降级只会让人对着一个
27
+ * 高度不对、参数为空的块查半天。
28
+ */
29
+ export declare function useTabContext(): TabContext;
30
+ /** 判断"这条地址是不是实例页"要看的那一点点路由能力(拿不到就当"不是")。 */
31
+ export interface LocationResolver {
32
+ resolve?: (target: string) => {
33
+ params?: unknown;
34
+ query?: unknown;
35
+ };
36
+ }
37
+ /**
38
+ * 这条标签地址是不是**实例页**(带路由参数或查询串的,例如 `/orders/42?orderId=7`)。
39
+ *
40
+ * 🔴 用来回答"这个块在哪一页":实例页是**一次一条**的(同一个路由不同参数 = 不同标签),
41
+ * 拿它当定位/跳转目标会把人送到随机某一单去;固定页 / 列表页(`/orders`、`/about`)
42
+ * 才是"这个块属于哪一页"那个答案。
43
+ * ⚠️ 判据是**路由表说了算**(`params` / `query` 非空),不是"路径里斜杠多就是详情页"那种猜法。
44
+ * ⚠️ 拿不到 router(这个应用没装 vue-router)就当作"不是实例页":宁可多给一个目标,也别在这里抛。
45
+ */
46
+ export declare function isInstanceLocation(key: string, router?: LocationResolver): boolean;
@@ -1,31 +1,79 @@
1
- /** 标签上限:点开太多页时丢最早那个(界面上的东西,不设上限会越攒越长)。 */
2
- export declare const TABS_MAX = 12;
1
+ /** 一条标签。 */
3
2
  export interface TabItem {
4
- /** 标签的键,就是路径。 */
3
+ /** 身份 = 完整地址(路径 + query)。 */
5
4
  key: string;
5
+ /** 路由路径(不带 query):导航高亮、块跳页白名单用它。 */
6
6
  path: string;
7
+ /** 标签栏上显示的名字。 */
7
8
  title: string;
8
9
  /** PrimeIcons 的类名,例如 `pi pi-home`。 */
9
10
  icon: string;
10
11
  }
12
+ /** 开标签时可以顺手指定的名字 / 图标(不给就按路径推)。 */
13
+ export interface TabMeta {
14
+ title?: unknown;
15
+ icon?: unknown;
16
+ }
17
+ /** 地址解析与跳转:外壳(`ProductShell`)在 setup 里装上,卸载时摘掉。 */
18
+ export interface TabNavigator {
19
+ /** 用户写的目标(`/orders/42?orderId=7`)→ 规范地址。`matched = false` 表示这个应用里没有这个页面。 */
20
+ resolve(target: string): {
21
+ fullPath: string;
22
+ path: string;
23
+ matched: boolean;
24
+ };
25
+ /** 现在地址栏上是哪个地址。 */
26
+ current(): string;
27
+ /** 把地址栏改成这个地址(外壳用 router.push)。 */
28
+ go(fullPath: string): void;
29
+ }
30
+ /** 装 / 摘地址能力(外壳用)。传 `null` = 这个应用里暂时没有 router(纯状态照样能建标签)。 */
31
+ export declare function installTabNavigator(next: TabNavigator | null): void;
11
32
  /**
12
- * 页面自报名字(可选)。
33
+ * 开一个标签并跳过去 —— **建标签的唯一入口**。
13
34
  *
14
- * 🔴 为什么需要它:`meta.title` 只对**写死在路由表里**的页面有效;带参数的页面(`/orders/42`)
15
- * 要显示"订单 SO-1"这种名字,只有页面自己知道。页面在 `onMounted` 里调一次即可。
35
+ * 🔴 导航项的 `onClick`、搜索命中、块里的 `magicBlock.navigateTab()` 都走它。
36
+ * 🔴 这个应用里没有这个页面(`matched = false`)就**什么都不做**:开一条空白标签只是把
37
+ * "地址写错了"变成一个看不出原因的空白页 —— 当场警告一句更有用。
38
+ * 🔴 地址已经是它了就不重复 push(push 同一个地址会往历史里塞一条没用的记录)。
16
39
  */
17
- export declare function registerTabLabel(path: string, title: string, icon?: string): void;
18
- /** 开一个标签(已经开着就切过去)并把当前页指到它 —— **这是"跳页"的唯一入口**。 */
19
- export declare function openTab(path: string, meta?: {
20
- title?: unknown;
21
- icon?: unknown;
22
- }): void;
23
- /** 关一个标签,回"下一个该去哪"的路径(关掉的不是当前那个就回空串)。 */
24
- export declare function closeTab(path: string): string;
25
- /** 标签那一栏要读的东西。 */
40
+ export declare function openTab(target: string, meta?: TabMeta): string;
41
+ /**
42
+ * 切到已经开着的那一条标签(并把地址栏改成它的地址)。
43
+ *
44
+ * ⚠️ 没这条标签就什么都不做:标签栏上点得到的,一定是已经开着的。
45
+ */
46
+ export declare function activateTab(key: string): void;
47
+ /**
48
+ * 地址栏自己变了:把标签栏对齐过去(**不改地址栏**)。
49
+ *
50
+ * 🔴 只有外壳调它(`router.afterEach` + 首次挂载那一发):刷新、分享链接、前进后退、
51
+ * 以及别处直接 `router.push` 的页面,都靠这一句得到自己的标签。
52
+ */
53
+ export declare function followUrl(fullPath: string, path: string, meta?: TabMeta): string;
54
+ /**
55
+ * 关一条标签,返回"接下来该显示哪一条"的 key(空串 = 没有下一条,或者关的根本不是当前那条)。
56
+ *
57
+ * 🔴 关掉当前那条时**顺手把邻居接上**(右边的优先,没有右边就左边):这是"关掉当前页"时
58
+ * 唯一说得通的落点。关掉的不是当前那条就只把它去掉,当前页一秒都不动。
59
+ * ⚠️ 邻居的地址也要推过去,否则标签栏与地址栏立刻说两句话。
60
+ * ⚠️ 最后一条被关掉时这里只清空(`active = ''`):**"标签全关掉之后去哪"是外壳的事**
61
+ * (它知道 `home`),见 `ProductTabs`。
62
+ */
63
+ export declare function closeTab(key: string): string;
64
+ /**
65
+ * 给某一条标签改名字 / 图标(**详情页自报名字**用的:`/orders/42` 要显示"订单 SO-1")。
66
+ *
67
+ * 🔴 为什么必须有:路由表里那条 `/orders/:id` 只有一个名字,而具体是哪一单只有页面自己知道。
68
+ * 页面在自己的 `setup` 里调一次即可(`useTabContext().setTitle(...)` 就是这一条)。
69
+ * ⚠️ 这条标签已经关掉了就什么都不做:关掉之后回来的那一发不该把它变回标签栏上的一条幽灵。
70
+ */
71
+ export declare function setTabTitle(key: string, title: string, icon?: string): void;
72
+ /** 标签栏 / 外壳要读的东西。 */
26
73
  export declare function useTabs(): {
27
74
  tabs: import("vue").ComputedRef<TabItem[]>;
28
75
  active: import("vue").ComputedRef<string>;
29
76
  openTab: typeof openTab;
77
+ activateTab: typeof activateTab;
30
78
  closeTab: typeof closeTab;
31
79
  };
@@ -1,20 +1,28 @@
1
- /** 侧栏菜单的形状。
1
+ /** 左侧导航菜单的形状。
2
2
  *
3
- * 🔴 **只有两级**(与老项目一致):
4
- * · 一级 = **分组**:只负责展开 / 收起,**本身不是页面、点它不跳转、也不开标签**;
5
- * · 二级 = **页面**:点它才跳转并**开一个标签**。
6
- * 所以下面 `children` 里的每一项都必须有 `path`(一级那个 `path` 只是这一组的标识,页面用不到它)。
3
+ * 🔴 **只有两级**:
4
+ * · 一级 = **分组**:画在图标栏里,**本身不是页面**;鼠标移上去/点一下会滑出它的二级菜单;
5
+ * · 二级 = **页面**:在滑出的菜单里,点它才执行动作。
7
6
  *
8
- * ⚠️ 想从路由表现算这份名单也行(老项目的 `pages.lock.ts` 就是那么做的:按 `meta.group` 分组、
9
- * 二级取 `meta.title` / `meta.sub`),但**那是使用者的事** —— 库只认这张表。
7
+ * 🔴 **导航与标签解耦**:点一项要干什么,由 `onClick` 自己写 —— 标签栏开不开新标签是**你的决定**,
8
+ * 不是"点了导航就自动开一条"(老实现里二级画成 `RouterLink`,点它必然跳路由、路由落定又必然开标签,
9
+ * 于是"点了开个抽屉""点了跳但不进标签栏"这类需求根本表达不出来)。
10
+ * · 不写 `onClick` 时的默认动作:**有 `path` 就 `openTab(path)`**(最常用的那一种,省得每项都抄一遍);
11
+ * 一级分组本来就没有默认动作(它只负责展开菜单)。
12
+ * · 写成 `onClick: () => openDrawer('settings')` 就不会开标签 —— 这就是"有些不是 tab"。
13
+ *
14
+ * ⚠️ `path` 只在"这一项对应一个真页面"时才需要写:默认动作与"当前在哪一项"的高亮都靠它。
15
+ * 一级分组的 `path` 只是这一组的标识(不写就用 `title` 认)。
10
16
  */
11
17
  export interface SideItem {
12
- /** 一级分组的标识(**不跳转**,只用来认这一组)。 */
13
- path: string;
14
- /** 分组名(一级)。 */
18
+ /** 分组名 / 页面名。 */
15
19
  title: string;
16
- /** 分组图标(PrimeIcons 类名,例如 `pi pi-home`)。 */
20
+ /** 图标(PrimeIcons 类名,例如 `pi pi-home`)。 */
17
21
  icon?: string;
18
- /** 这一组下面的页面(二级)—— 点它才跳转并开标签。 */
22
+ /** 这一项对应的页面路径(二级页面要写:默认动作与高亮都靠它)。 */
23
+ path?: string;
24
+ /** 点它干什么。不写时:有 `path` 就开那个标签;一级分组只展开菜单。 */
25
+ onClick?: (item: SideItem) => void;
26
+ /** 二级页面(一级分组的成员)。 */
19
27
  children?: SideItem[];
20
28
  }
package/dist/ui.js CHANGED
@@ -1,15 +1,23 @@
1
- import { A as e, P as l, a as r, b, T as o, U as u, c as T, i as c, l as p, o as t, r as A, u as d } from "./chunk-index-_jasBCim.js";
1
+ import { A as e, P as o, a as t, b as l, _ as T, U as b, i as r, l as i } from "./chunk-index-DfIoolMt.js";
2
+ import { T as n, a as p, c as u, f as d, i as P, b as f, o as x, p as A, s as v, u as C, d as S } from "./chunk-ProductShell.vue_vue_type_style_index_0_scoped_f78521a6_lang-qtJ-k8Ss.js";
2
3
  export {
3
4
  e as AppSearch,
4
- l as ProductShell,
5
- r as ProductSide,
6
- b as ProductTabs,
7
- o as TABS_MAX,
8
- u as UserMenu,
9
- T as closeTab,
10
- c as installApp,
11
- p as labull,
12
- t as openTab,
13
- A as registerTabLabel,
14
- d as useTabs
5
+ o as ProductShell,
6
+ t as ProductSide,
7
+ l as ProductTabs,
8
+ n as TAB_CONTEXT,
9
+ T as TabPage,
10
+ b as UserMenu,
11
+ p as activateTab,
12
+ u as closeTab,
13
+ d as followUrl,
14
+ r as installApp,
15
+ P as installTabNavigator,
16
+ f as isInstanceLocation,
17
+ i as labull,
18
+ x as openTab,
19
+ A as provideTabContext,
20
+ v as setTabTitle,
21
+ C as useTabContext,
22
+ S as useTabs
15
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "labull-framework",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Labull 前端 SDK:主题 CSS + 登录态 / 外壳组件 / magic block(四块合一,一个包)",
@@ -1,9 +0,0 @@
1
- const s = (t, r) => {
2
- const o = t.__vccOpts || t;
3
- for (const [c, e] of r)
4
- o[c] = e;
5
- return o;
6
- };
7
- export {
8
- s as _
9
- };