useFloatPanel.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import type { Component, Ref } from 'vue';
  2. import { defineComponent, h, inject, mergeProps, provide, ref, shallowRef } from 'vue';
  3. import __FloatPanel_vue from './FloatPanel.vue';
  4. import type { FloatPanelApi, FloatPanelContentBindings, FloatPanelInstance } from './types';
  5. import { floatPanelContextKey } from './types';
  6. /**
  7. * 在 `useFloatPanel` 提供的内容子树内调用,获取指向 `FloatPanelInstance` 的 ref。
  8. * @throws 若在 provide 范围外调用
  9. */
  10. export function injectFloatPanelContext(): Ref<FloatPanelInstance | undefined> {
  11. const injected = inject(floatPanelContextKey, undefined);
  12. if (injected === undefined) {
  13. throw new Error('injectFloatPanelContext() 须在 useFloatPanel 的 Wrapper 内容树内使用');
  14. }
  15. return injected;
  16. }
  17. /**
  18. * 声明式挂载浮动面板:返回 `[Wrapper, api]`。
  19. *
  20. * - **Wrapper**:放入模板根或任意位置;`open()` 后才会渲染内部 `FloatPanel`。
  21. * - **api**:`open` / `close` 及与 `FloatPanelInstance` 一致的面板操作。
  22. *
  23. * 关闭卸载由 `FloatPanel` 在动画结束后 **`emit('closed')`** 触发,不在此重复定时逻辑。
  24. *
  25. * @param Content - 可选内容组件;不传则用 Wrapper 的默认插槽(作用域参数含 `onComplete` / `onCancel`)
  26. */
  27. export function useFloatPanel<P extends Record<string, any> = Record<string, unknown>, R = P>(
  28. Content?: Component,
  29. ) {
  30. let pending: PromiseWithResolvers<R | void> | undefined;
  31. /** `closeAnimated` 结束时交给 `onClosed` 再 `settle` */
  32. let dismissResult: R | void | undefined;
  33. const show = ref(false);
  34. const title = ref('');
  35. /** 为 true 表示正在等待 FloatPanel `closed`,用于忽略重开后的陈旧 `closed` */
  36. const pendingCloseUnmount = ref(false);
  37. const panelOpenKey = ref(0);
  38. const innerProps = shallowRef<P>();
  39. const floatPanelRef = ref<FloatPanelInstance>();
  40. function settle(result?: R | void) {
  41. pending?.resolve(result);
  42. pending = undefined;
  43. }
  44. function closePanel(result?: R) {
  45. const inst = floatPanelRef.value;
  46. if (inst?.closeAnimated) {
  47. pendingCloseUnmount.value = true;
  48. dismissResult = result;
  49. inst.closeAnimated();
  50. } else {
  51. pendingCloseUnmount.value = false;
  52. dismissResult = undefined;
  53. settle(result);
  54. show.value = false;
  55. }
  56. }
  57. const api: FloatPanelApi<P, R> = {
  58. open(props: P, text = ''): Promise<R | void> {
  59. pending?.resolve(void 0);
  60. pending = Promise.withResolvers<R | void>();
  61. dismissResult = undefined;
  62. pendingCloseUnmount.value = false;
  63. innerProps.value = props;
  64. panelOpenKey.value += 1;
  65. show.value = true;
  66. title.value = text;
  67. return pending.promise;
  68. },
  69. close(result?: R) {
  70. closePanel(result);
  71. },
  72. getAnchors() {
  73. return floatPanelRef.value?.getAnchors() ?? [];
  74. },
  75. setAnchors(value, reset) {
  76. floatPanelRef.value?.setAnchors(value, reset);
  77. },
  78. setHeight(value, updateAnchor) {
  79. floatPanelRef.value?.setHeight(value, updateAnchor);
  80. },
  81. closeAnimated() {
  82. floatPanelRef.value?.closeAnimated();
  83. },
  84. snapMin() {
  85. return floatPanelRef.value?.snapMin() ?? false;
  86. },
  87. snapMax() {
  88. return floatPanelRef.value?.snapMax() ?? false;
  89. },
  90. snapFull() {
  91. return floatPanelRef.value?.snapFull() ?? false;
  92. },
  93. snapToFirstAnchor() {
  94. return floatPanelRef.value?.snapToFirstAnchor() ?? false;
  95. },
  96. snapToLastAnchor() {
  97. return floatPanelRef.value?.snapToLastAnchor() ?? false;
  98. },
  99. snapToMaxContainerHeight() {
  100. return floatPanelRef.value?.snapToMaxContainerHeight() ?? false;
  101. },
  102. };
  103. const Wrapper = defineComponent({
  104. name: 'FloatPanelWrapper',
  105. inheritAttrs: false,
  106. setup(_, { attrs, slots }) {
  107. provide(floatPanelContextKey, floatPanelRef);
  108. return () => {
  109. if (!show.value) return null;
  110. const attrObj = attrs as Record<string, unknown>;
  111. const panelBindings = mergeProps(attrObj, {
  112. 'onUpdate:panelHeight': (value: number) => {
  113. if (value !== 0) {
  114. show.value = true;
  115. return;
  116. }
  117. if (!pendingCloseUnmount.value) {
  118. pendingCloseUnmount.value = true;
  119. }
  120. },
  121. onClosed: () => {
  122. if (!pendingCloseUnmount.value) return;
  123. pendingCloseUnmount.value = false;
  124. const r = dismissResult;
  125. dismissResult = undefined;
  126. settle(r);
  127. show.value = false;
  128. },
  129. });
  130. return h(
  131. __FloatPanel_vue,
  132. { ref: floatPanelRef, key: panelOpenKey.value, closable: true, title: title.value, ...panelBindings },
  133. {
  134. header: slots.header,
  135. content: () => {
  136. const slotProps = innerProps.value ?? ({} as P);
  137. const contentBindings = mergeProps(slotProps as Record<string, unknown>, {
  138. onComplete: (result?: R) => closePanel(result),
  139. onCancel: () => closePanel(),
  140. });
  141. if (Content) return h(Content, contentBindings);
  142. return slots.default?.(contentBindings as FloatPanelContentBindings<P, R>) ?? null;
  143. },
  144. },
  145. );
  146. };
  147. },
  148. });
  149. return [Wrapper, api] as const;
  150. }