소스 검색

⚙️ 升级组件库

快乐的梦鱼 3 주 전
부모
커밋
823c5c0b8b
69개의 변경된 파일1005개의 추가작업 그리고 823개의 파일을 삭제
  1. 25 6
      src/components/basic/BackgroundImageButton.vue
  2. 6 1
      src/components/basic/Button.vue
  3. 1 1
      src/components/basic/IconButton.vue
  4. 3 1
      src/components/basic/ImageButton.vue
  5. 1 16
      src/components/basic/Text.vue
  6. 14 5
      src/components/canvas/MiniRender.ts
  7. 1 1
      src/components/composeabe/loader/LoaderCommon.ts
  8. 4 2
      src/components/composeabe/loader/SimpleDataLoader.ts
  9. 4 2
      src/components/composeabe/loader/SimplePageListLoader.ts
  10. 4 2
      src/components/dialog/Dialog.vue
  11. 4 1
      src/components/dialog/DialogInner.vue
  12. 17 2
      src/components/dialog/Popup.vue
  13. 6 1
      src/components/display/Avatar.vue
  14. 148 0
      src/components/display/block/BackgroundBox.ts
  15. 6 131
      src/components/display/block/BackgroundBox.vue
  16. 21 0
      src/components/display/block/ImageBlock2.vue
  17. 5 0
      src/components/display/block/ImageBlock3.vue
  18. 5 3
      src/components/display/parse/ParseNodeRender.vue
  19. 6 52
      src/components/dynamic/DynamicForm.vue
  20. 0 11
      src/components/dynamic/DynamicFormControl.vue
  21. 16 28
      src/components/dynamic/nest/DynamicFormItemContainer.vue
  22. 19 0
      src/components/dynamic/nest/DynamicFormItemContainerFuckMp.vue
  23. 85 0
      src/components/dynamic/nest/DynamicFormRoot.vue
  24. 1 0
      src/components/dynamic/wrappers/CheckBoxList.vue
  25. 128 43
      src/components/feedback/BubbleBox.vue
  26. 78 0
      src/components/feedback/BubbleTip.vue
  27. 5 0
      src/components/feedback/Touchable.ts
  28. 3 0
      src/components/feedback/Touchable.vue
  29. 2 11
      src/components/form/CalendarField.vue
  30. 0 8
      src/components/form/CascadePicker.vue
  31. 4 8
      src/components/form/CascadePickerField.vue
  32. 8 5
      src/components/form/Cascader.vue
  33. 13 5
      src/components/form/CascaderField.vue
  34. 23 0
      src/components/form/CascaderUtils.ts
  35. 5 8
      src/components/form/DatePicker.vue
  36. 3 4
      src/components/form/DatePickerField.vue
  37. 3 3
      src/components/form/DateTimePicker.vue
  38. 2 2
      src/components/form/DateTimePickerField.vue
  39. 6 24
      src/components/form/Field.vue
  40. 0 2
      src/components/form/Form.vue
  41. 1 11
      src/components/form/FormContext.ts
  42. 0 12
      src/components/form/Picker.vue
  43. 2 7
      src/components/form/PickerField.vue
  44. 36 28
      src/components/form/PickerUtils.ts
  45. 23 71
      src/components/form/Signature.vue
  46. 105 0
      src/components/form/Tags.vue
  47. 2 1
      src/components/form/TimePicker.vue
  48. 3 18
      src/components/form/TimePickerField.vue
  49. 1 10
      src/components/form/Uploader.ts
  50. 46 149
      src/components/form/Uploader.vue
  51. 2 11
      src/components/form/UploaderListItem.vue
  52. 1 1
      src/components/layout/BaseView.ts
  53. 28 23
      src/components/list/SimpleList.vue
  54. 1 1
      src/components/loader/SimplePageContentLoader.vue
  55. 12 2
      src/components/loader/SimplePageListLoader.vue
  56. 1 1
      src/components/nav/Tabs.vue
  57. 18 9
      src/components/theme/Theme.ts
  58. 4 5
      src/components/theme/ThemeDefine.ts
  59. 3 7
      src/components/typography/B.vue
  60. 3 7
      src/components/typography/H1.vue
  61. 3 7
      src/components/typography/H2.vue
  62. 3 7
      src/components/typography/H3.vue
  63. 3 7
      src/components/typography/H4.vue
  64. 3 7
      src/components/typography/H5.vue
  65. 3 7
      src/components/typography/H6.vue
  66. 3 7
      src/components/typography/I.vue
  67. 3 7
      src/components/typography/S.vue
  68. 3 7
      src/components/typography/U.vue
  69. 4 4
      src/components/utils/PageAction.ts

+ 25 - 6
src/components/basic/BackgroundImageButton.vue

@@ -1,21 +1,40 @@
 <template>
-  <Touchable :activeOpacity="activeOpacity" @click="emit('click')">
-    <BackgroundBox v-bind="props">
+  <Touchable 
+    v-bind="props"
+    :innerStyle="style"
+    @click="emit('click')"
+  >
+    <BackgroundBox >
       <slot />
     </BackgroundBox>
   </Touchable>
 </template>
 
 <script setup lang="ts">
-import BackgroundBox, { type BackgroundBoxProps } from '../display/block/BackgroundBox.vue';
 import Touchable from '../feedback/Touchable.vue';
+import { useBackgroundBoxStyleMaker, type BackgroundBoxProps } from '../display/block/BackgroundBox';
+import { computed } from 'vue';
+import type { TouchableFlexProps } from '../feedback/Touchable';
 
-const props = withDefaults(defineProps<BackgroundBoxProps & {
-  activeOpacity?: number;
-}>(), {
+const props = withDefaults(defineProps<BackgroundBoxProps & TouchableFlexProps>(), {
   activeOpacity: 0.7,
+  touchable: true,
+});
+
+const { makeStyle } = useBackgroundBoxStyleMaker(props);
+const style = computed(() => {
+  return {
+    ...props.innerStyle,
+    ...makeStyle(),
+  }
 });
 
 const emit = defineEmits(['click']);
 
+
+defineOptions({
+  options: {
+    virtualHost: true,
+  },
+})
 </script>

+ 6 - 1
src/components/basic/Button.vue

@@ -30,6 +30,7 @@
     </slot>
     <slot>
       <Text 
+        v-bind="textProps"
         :color="textColorFinal" 
         :fontSize="selectStyleType(size, 'medium', FonstSizes)" 
         :fontWeight="type === 'text' ? 'bold' : undefined"
@@ -55,7 +56,7 @@ import { propGetThemeVar, useTheme, type ViewStyle } from '../theme/ThemeDefine'
 import { configPadding, DynamicColor, DynamicSize, selectStyleType } from '../theme/ThemeTools';
 import type { IconProps } from './Icon.vue';
 import type { FlexProps } from '../layout/FlexView.vue';
-import Text from './Text.vue';
+import Text, { type TextProps } from './Text.vue';
 import ActivityIndicator from './ActivityIndicator.vue';
 import Icon from './Icon.vue';
 import Touchable from '../feedback/Touchable.vue';
@@ -71,6 +72,10 @@ export interface ButtonProp {
    */
   text?: string,
   /**
+   * 按钮文字的额外属性
+   */
+  textProps?: TextProps,
+  /**
    * 按钮支持 default、primary、success、warning、danger、custom 自定义 六种类型
    * @default 'default'
    */

+ 1 - 1
src/components/basic/IconButton.vue

@@ -9,7 +9,7 @@
   >
     <Icon v-if="icon" v-bind="props" />
     <slot>
-      <Text :text="text" />
+      <Text v-if="text" :text="text" />
     </slot>
   </Touchable>
 </template>

+ 3 - 1
src/components/basic/ImageButton.vue

@@ -2,11 +2,12 @@
   <Touchable 
     :activeOpacity="activeOpacity" 
     :touchable="touchable"
+    :innerStyle="innerStyle"
     position="relative" 
     center
     @click="emit('click')"
   >
-    <Image v-bind="props" />
+    <Image v-bind="props" :innerStyle="imageStyle" />
     <slot>
       <FlexCol center position="absolute" inset="0">
         <Text v-if="text" :text="text" v-bind="textProps" />
@@ -27,6 +28,7 @@ const props = withDefaults(defineProps<ImageProps & {
   touchable?: boolean;
   text?: string;
   textProps?: TextProps;
+  imageStyle?: object;
 }>(), {
   activeOpacity: 0.7,
   touchable: true,

+ 1 - 16
src/components/basic/Text.vue

@@ -1,8 +1,5 @@
 <template>
-  <view v-if="allowChildNode" :id="id" :class="innerClass" :style="style"  @click="onClick">
-    <slot>{{ text }}</slot>
-  </view>
-  <text v-else :id="id" :class="innerClass" :style="style" @click="onClick">
+  <text :id="id" :class="innerClass" :style="style" @click="onClick">
     <!-- #ifdef APP-NVUE -->
     {{ text }}
     <!-- #endif -->
@@ -23,11 +20,6 @@ export interface TextProps {
    */
   color?: string,
   /**
-   * 是否允许子节点。如果为 true,则使用view包裹。
-   * @default false
-   */
-  allowChildNode?: boolean,
-  /**
    * 文字阴影颜色。可以是颜色字符串或者在主题中配置的预设名称。
    */
   shadowColor?: string,
@@ -89,11 +81,6 @@ export interface TextProps {
    */
   wrap?: boolean,
   /**
-   * 单词换行方式
-   * @default 'normal'
-   */
-  wordBreak?: 'normal'|'break-all'|'break-word',
-  /**
    * 行数限制
    */
   lines?: number,
@@ -230,8 +217,6 @@ const style = computed(() => {
     if (s)
       o.fontSize = s + 'rpx';
   }
-  if (props.wordBreak)
-    o.wordBreak = props.wordBreak;
 
   const rs = {
     ...o,

+ 14 - 5
src/components/canvas/MiniRender.ts

@@ -66,6 +66,8 @@ export namespace MiniRender {
 
     public parent: Container | null = null;
 
+    public data: any = null;
+
     private listeners = new Map<RenderObjectEventName, Set<RenderObjectEventHandler>>();
 
     constructor(id?: RenderObjectId) {
@@ -493,7 +495,7 @@ export namespace MiniRender {
       super();
       if (config) Object.assign(this, config);
       this._resolvedImages = new Array(this.images.length).fill(null);
-      if (this.currentAnimation && !this.animations[this.currentAnimation]) {
+      if (this.currentAnimation && this.animations && !this.animations?.[this.currentAnimation]) {
         // 若未配置 animations,则允许把 currentAnimation 当成 “默认帧序列” 的别名
         this.animations[this.currentAnimation] = { frames: [0] };
       }
@@ -709,8 +711,8 @@ export namespace MiniRender {
         height: number,
         backgroundColor: string,
       },
-      public onInit: () => void,
-      public updateFn: (dtMs: number) => void,
+      public onInit: (this: Scene) => void,
+      public updateFn: (this: Scene, dtMs: number) => void,
     ) {
       this.canvas = options.canvas;
       this.width = options.width;
@@ -723,7 +725,7 @@ export namespace MiniRender {
       console.log(`[Canvas] init canvas ${this.width}x${this.height}`);
       
       await this.canvas.initCanvas(this.width, this.height);
-      this.onInit();
+      this.onInit.call(this);
     }
 
     public clear(): void {
@@ -745,7 +747,7 @@ export namespace MiniRender {
     public tick(ts: number): void {
       const dt = this.lastTs === null ? 16 : Math.max(0, ts - this.lastTs);
       this.lastTs = ts;
-      this.updateFn(dt);
+      this.updateFn.call(this, dt);
       this.root.update(dt);
       this.renderOnce();
     }
@@ -772,6 +774,13 @@ export namespace MiniRender {
       this.root.removeAll();
     }
 
+    public precentXToPixel(precent: number): number {
+      return this.width * precent;
+    }
+    public precentYToPixel(precent: number): number {
+      return this.height * precent;
+    }
+
     private getEventPosition(e: any): { x: number; y: number } {
       if (e?.detail?.x !== undefined) return { x: e.detail.x, y: e.detail.y };
       if (e?.offsetX !== undefined) return { x: e.offsetX, y: e.offsetY };

+ 1 - 1
src/components/composeabe/loader/LoaderCommon.ts

@@ -10,7 +10,7 @@ export type LoaderLoadType = 'loading' | 'finished' | 'nomore' | 'error' | 'empt
 export interface ILoaderCommon<P> {
   error: Ref<string>;
   status: Ref<LoaderLoadType>;
-  isFinished: ComputedRef<boolean>;
+  isLoaded: ComputedRef<boolean>;
   load: (refresh?: boolean, params?: P) => Promise<void>;
   reload: () => Promise<void>;
 }

+ 4 - 2
src/components/composeabe/loader/SimpleDataLoader.ts

@@ -62,13 +62,15 @@ export function useSimpleDataLoader<T, P = any>(
     }
   })
 
-  const isFinished = computed(() => status.value === 'finished' || status.value === 'nomore');
+  const isLoaded = computed(() => {
+    return status.value === 'finished' || status.value === 'nomore';
+  });
 
   return {
     content,
     status,
-    isFinished,
     error,
+    isLoaded,
     load,
     reload: () => load(true),
     getLastParams: () => lastParams,

+ 4 - 2
src/components/composeabe/loader/SimplePageListLoader.ts

@@ -83,16 +83,18 @@ export function useSimplePageListLoader<T, P = any>(
       load(false, lastParams);
   })
 
-  const isFinished = computed(() => status.value === 'finished' || status.value === 'nomore');
+  const isLoaded = computed(() => {
+    return status.value === 'finished' || status.value === 'nomore';
+  });
 
   return {
     list,
     total,
     page,
     status,
-    isFinished,
     error,
     load,
     reload: () => load(true),
+    isLoaded,
   }
 }

+ 4 - 2
src/components/dialog/Dialog.vue

@@ -2,12 +2,13 @@
   <Popup
     v-bind="props"
     round
-    position="center"
     @close="onClose"
   >
     <DialogInner 
       ref="dialogInner"
       v-bind="props"
+      :contentScroll="props.contentScroll"
+      :contentScrollMaxHeight="props.contentScrollMaxHeight"
       :onConfirm="$props.onConfirm"
       :onCancel="$props.onCancel"
       :confirmCountDownTime="props.confirmCountDown"
@@ -49,7 +50,7 @@ import DialogInner from './DialogInner.vue';
 import Popup from './Popup.vue';
 import type { PopupProps } from './Popup.vue';
 
-export interface DialogProps extends Omit<PopupProps, 'onClose'|'position'|'renderContent'> {
+export interface DialogProps extends Omit<PopupProps, 'onClose'|'renderContent'> {
   /**
    * 对话框的标题
    */
@@ -157,6 +158,7 @@ const props = withDefaults(defineProps<DialogProps>(), {
   mask: true,
   showConfirm: true,
   contentScroll: true,
+  position: 'center',
 });
 const emit = defineEmits([ 'close', 'update:show' ]);
 

+ 4 - 1
src/components/dialog/DialogInner.vue

@@ -1,6 +1,9 @@
 <template>
   <!--TODO: 在uniapp插槽问题修复后,此处可修改为插槽默认值-->
-  <FlexCol :innerStyle="{ ...themeStyles.dialog.value, width: themeContext.resolveThemeSize(width) }">
+  <FlexCol :innerStyle="{ 
+    ...themeStyles.dialog.value, 
+    width: themeContext.resolveThemeSize(width) 
+  }">
     <slot v-if="topSlots?.default" />
     <FlexCol v-else :padding="contentPadding" align="center">
       <!-- 图标 -->

+ 17 - 2
src/components/dialog/Popup.vue

@@ -29,6 +29,10 @@
           alignItems: 'flex-end',
           justifyContent: 'center',
         },
+        fill: {
+          alignItems: 'stretch',
+          justifyContent: 'stretch',
+        },
       }),
       top: inset[0] ? `${themeContext.resolveThemeSize(inset[0])}` : undefined,
       right: inset[1] ? `${themeContext.resolveThemeSize(inset[1])}` : undefined,
@@ -86,6 +90,10 @@
             height: '100%',
             width: themeContext.resolveThemeSize(props.size),
           },
+          fill: {
+            height: '100%',
+            width: '100%',
+          },
         }),
         zIndex: popupZIndex + 2,
         backgroundColor: themeContext.resolveThemeColor(backgroundColor),
@@ -107,7 +115,7 @@
           :top="true"
           @close="doClose"
         />
-        <slot :show="show" />
+        <slot />
         <PopupTitle
           v-if="position === 'top'"
           :closeable="closeable"
@@ -132,8 +140,15 @@ import { getCurrentZIndex } from './CommonRoot';
 
 /**
  * Popup 的显示位置
+ * 
+ * * center: 居中弹出
+ * * top: 从上弹出
+ * * bottom: 从下弹出
+ * * left: 从左弹出
+ * * right: 从右弹出
+ * * fill: 充满屏幕
  */
-export type PopupPosition = 'center'|'top'|'bottom'|'left'|'right';
+export type PopupPosition = 'center'|'top'|'bottom'|'left'|'right'|'fill';
 /**
  * Popup 关闭按钮显示位置
  */

+ 6 - 1
src/components/display/Avatar.vue

@@ -8,7 +8,8 @@
     @click="$emit('click')"
   >
     <image 
-      v-if="url || defaultAvatar" :src="url || defaultAvatar"
+      v-if="url || defaultAvatar || src" 
+      :src="url || defaultAvatar || src"
       :style="{
         width: themeContext.resolveThemeSize(size),
         height: themeContext.resolveThemeSize(size),
@@ -50,6 +51,10 @@ export interface AvatarProps {
    */
   url?: string,
   /**
+   * 头像的图片URL
+   */
+  src?: string,
+  /**
    * 背景颜色
    */
   background?: string,

+ 148 - 0
src/components/display/block/BackgroundBox.ts

@@ -0,0 +1,148 @@
+import { resolveSize, useTheme, type ViewStyle } from "@/components/theme/ThemeDefine";
+import { solveUrl } from "@/components/theme/ThemeTools";
+
+export interface BackgroundBoxProps {
+  /**
+   * 背景颜色(1)。
+   * 
+   * 格式:字符串格式或主题中定义的颜色预设。
+   */
+  color1?: string;
+  /**
+   * 背景颜色(2)。
+   * 
+   * 格式:字符串格式或主题中定义的颜色预设。
+   */
+  color2?: string;
+  /**
+   * 背景颜色(3)。
+   * 
+   * 格式:字符串格式或主题中定义的颜色预设。
+   */
+  color3?: string;
+  /**
+   * 背景颜色(2)位置。
+   * 
+   * 格式:50% 表示颜色(2)位置为50%,即颜色(2)在背景中间。
+   */
+  color2Position?: string;
+  /**
+   * 圆角。
+   */
+  radius?: string | number;
+  /**
+   * 背景渐变角度。
+   * 只有 color1 和 color2 都定义时有效。
+   *
+   * 格式:角度(0-360)。
+   */
+  gradientAngle?: number;
+  /**
+   * 背景图片。
+   */
+  backgroundImage?: string;
+  /**
+   * 背景填充方式。
+   *
+   * 格式:
+   * - fillW:横向填充,高度变化。
+   * - fillH:纵向填充,宽度变化。
+   * - none:不填充。
+   */
+  backgroundFillType?: 'none'|'fillH'|'fillW';
+  /**
+   * 背景填充大小。
+   */
+  backgroundSize?: string;
+  /**
+   * 背景填充位置。
+   */
+  backgroundPosition?: string;
+  /**
+   * 背景图片九宫格裁剪大小。
+   *
+   * 格式:
+   * - 数组:[ top, right, bottom, left ]
+   */
+  backgroundCutBorder?: Array<number|string> | string | number;
+  /**
+   * 背景图片九宫格渲染大小。
+   *
+   * 格式:
+   * - 数组:[ top, right, bottom, left ]
+   */
+  backgroundCutBorderSize?: Array<number|string> | string | number;
+}
+
+export function useBackgroundBoxStyleMaker(props: BackgroundBoxProps) {
+
+  const theme = useTheme();
+  
+  function makeStyle() {
+    const o : ViewStyle = {}
+    if (props.radius) {
+      o.borderRadius = theme.resolveThemeSize(props.radius);
+    }
+    if (props.color1 !== undefined && (props.color2 !== undefined || props.color3 !== undefined)) {
+      // 支持 color2Position, color3
+      if (props.color3 !== undefined) {
+        // 当有 color3 时,支持三色渐变
+        const colorStops = [
+          `${theme.resolveThemeColor(props.color1)} 0%`,
+          props.color2Position !== undefined
+            ? `${theme.resolveThemeColor(props.color2)} ${props.color2Position}`
+            : `${theme.resolveThemeColor(props.color2)} 50%`,
+          `${theme.resolveThemeColor(props.color3)} 100%`
+        ];
+        o.background = `linear-gradient(${props.gradientAngle || 180}deg, ${colorStops.join(', ')})`;
+      } else {
+        // 仅 color1/color2 时,支持 color2Position
+        if (props.color2Position !== undefined) {
+          o.background = `linear-gradient(${props.gradientAngle || 180}deg, ${theme.resolveThemeColor(props.color1)} 0%, ${theme.resolveThemeColor(props.color2)} ${props.color2Position})`;
+        } else {
+          o.background = `linear-gradient(${props.gradientAngle || 180}deg, ${theme.resolveThemeColor(props.color1)}, ${theme.resolveThemeColor(props.color2)})`;
+        }
+      }
+  
+    } else if (props.backgroundImage) {
+      const b = props.backgroundCutBorder;
+      let s = props.backgroundCutBorderSize;
+      if (!s) {
+        if (Array.isArray(b)) {
+          s = b.map((i) => resolveSize(i) as any);
+        } else {
+          s = resolveSize(b) as any;
+        }
+      }
+      if (b) {
+        o.borderImageSource = solveUrl(props.backgroundImage);
+        o.borderImageSlice =  Array.isArray(b) ? `${b[0]} ${b[1]} ${b[2]} ${b[3]} fill` : `${b} fill`;
+        o.borderImageWidth = Array.isArray(s) ? `${theme.resolveSize(s[0])} ${theme.resolveSize(s[1])} ${theme.resolveSize(s[2])} ${theme.resolveSize(s[3])}` : `${theme.resolveSize(s)}`;
+        o.borderImageRepeat = 'stretch';
+      } else {
+        o.backgroundImage = solveUrl(props.backgroundImage);
+        o.backgroundPosition = props.backgroundPosition;
+        o.backgroundRepeat = "no-repeat";
+        switch (props.backgroundFillType) {
+          case 'fillW':
+            o.backgroundSize = `${props.backgroundSize} auto`;
+            break;
+          case 'fillH':
+            o.backgroundSize = `auto ${props.backgroundSize}`;
+            break;
+          case 'none':
+            o.backgroundSize = `${props.backgroundSize}`;
+            break;
+        }
+      }
+    } else if (props.color1) {
+      o.backgroundColor = theme.resolveThemeColor(props.color1);
+    } else if (props.color2) {
+      o.backgroundColor = theme.resolveThemeColor(props.color2);
+    }
+    return o;
+  }
+  return {
+    makeStyle,
+  }
+}

+ 6 - 131
src/components/display/block/BackgroundBox.vue

@@ -38,82 +38,9 @@ export default {}
 
 <script setup lang="ts">
 import FlexView, { type FlexProps } from '@/components/layout/FlexView.vue';
-import { useTheme, type ViewStyle } from '@/components/theme/ThemeDefine';
-import { solveUrl } from '@/components/theme/ThemeTools';
-import { computed, type PropType } from 'vue';
-
-export interface BackgroundBoxProps extends FlexProps {
-  /**
-   * 背景颜色(1)。
-   * 
-   * 格式:字符串格式或主题中定义的颜色预设。
-   */
-  color1?: string;
-  /**
-   * 背景颜色(2)。
-   * 
-   * 格式:字符串格式或主题中定义的颜色预设。
-   */
-  color2?: string;
-  /**
-   * 背景颜色(3)。
-   * 
-   * 格式:字符串格式或主题中定义的颜色预设。
-   */
-  color3?: string;
-  /**
-   * 背景颜色(2)位置。
-   * 
-   * 格式:50% 表示颜色(2)位置为50%,即颜色(2)在背景中间。
-   */
-  color2Position?: string;
-  /**
-   * 圆角。
-   */
-  radius?: string | number;
-  /**
-   * 背景渐变角度。
-   * 只有 color1 和 color2 都定义时有效。
-   *
-   * 格式:角度(0-360)。
-   */
-  gradientAngle?: number;
-  /**
-   * 背景图片。
-   */
-  backgroundImage?: string;
-  /**
-   * 背景填充方式。
-   *
-   * 格式:
-   * - fillW:横向填充,高度变化。
-   * - fillH:纵向填充,宽度变化。
-   * - none:不填充。
-   */
-  backgroundFillType?: 'none'|'fillH'|'fillW';
-  /**
-   * 背景填充大小。
-   */
-  backgroundSize?: string;
-  /**
-   * 背景填充位置。
-   */
-  backgroundPosition?: string;
-  /**
-   * 背景图片九宫格裁剪大小。
-   *
-   * 格式:
-   * - 数组:[ top, right, bottom, left ]
-   */
-  backgroundCutBorder?: Array<number|string>;
-  /**
-   * 背景图片九宫格渲染大小。
-   *
-   * 格式:
-   * - 数组:[ top, right, bottom, left ]
-   */
-  backgroundCutBorderSize?: Array<number|string>;
-}
+import { type ViewStyle } from '@/components/theme/ThemeDefine';
+import { computed } from 'vue';
+import { useBackgroundBoxStyleMaker, type BackgroundBoxProps } from './BackgroundBox';
 
 defineOptions({
   options: {
@@ -125,7 +52,7 @@ defineOptions({
 /**
  * 内容积木组件:背景盒子,
  */
-const props = withDefaults(defineProps<BackgroundBoxProps>(), {
+const props = withDefaults(defineProps<BackgroundBoxProps & FlexProps>(), {
   color1: undefined,
   color2: undefined,
   radius: undefined,
@@ -138,64 +65,12 @@ const props = withDefaults(defineProps<BackgroundBoxProps>(), {
   backgroundCutBorderSize: () => ([ 'auto' ]),
 });
 
-const theme = useTheme();
+const { makeStyle } = useBackgroundBoxStyleMaker(props);
 
 const style = computed(() => {
   const o : ViewStyle = {
     ...props.innerStyle,
-  }
-  if (props.radius) {
-    o.borderRadius = theme.resolveThemeSize(props.radius);
-  }
-  if (props.color1 !== undefined && (props.color2 !== undefined || props.color3 !== undefined)) {
-    // 支持 color2Position, color3
-    if (props.color3 !== undefined) {
-      // 当有 color3 时,支持三色渐变
-      const colorStops = [
-        `${theme.resolveThemeColor(props.color1)} 0%`,
-        props.color2Position !== undefined
-          ? `${theme.resolveThemeColor(props.color2)} ${props.color2Position}`
-          : `${theme.resolveThemeColor(props.color2)} 50%`,
-        `${theme.resolveThemeColor(props.color3)} 100%`
-      ];
-      o.background = `linear-gradient(${props.gradientAngle || 180}deg, ${colorStops.join(', ')})`;
-    } else {
-      // 仅 color1/color2 时,支持 color2Position
-      if (props.color2Position !== undefined) {
-        o.background = `linear-gradient(${props.gradientAngle || 180}deg, ${theme.resolveThemeColor(props.color1)} 0%, ${theme.resolveThemeColor(props.color2)} ${props.color2Position})`;
-      } else {
-        o.background = `linear-gradient(${props.gradientAngle || 180}deg, ${theme.resolveThemeColor(props.color1)}, ${theme.resolveThemeColor(props.color2)})`;
-      }
-    }
-
-  } else if (props.backgroundImage) {
-    const b = props.backgroundCutBorder;
-    const s = props.backgroundCutBorderSize;
-    if (b) {
-      o.borderImageSource = solveUrl(props.backgroundImage);
-      o.borderImageSlice = `${b[0]} ${b[1]} ${b[2]} ${b[3]} fill`;
-      o.borderImageWidth = `${theme.resolveSize(s[0])} ${theme.resolveSize(s[1])} ${theme.resolveSize(s[2])} ${theme.resolveSize(s[3])}`;
-      o.borderImageRepeat = 'stretch';
-    } else {
-      o.backgroundImage = solveUrl(props.backgroundImage);
-      o.backgroundPosition = props.backgroundPosition;
-      o.backgroundRepeat = "no-repeat";
-      switch (props.backgroundFillType) {
-        case 'fillW':
-          o.backgroundSize = `${props.backgroundSize} auto`;
-          break;
-        case 'fillH':
-          o.backgroundSize = `auto ${props.backgroundSize}`;
-          break;
-        case 'none':
-          o.backgroundSize = `${props.backgroundSize}`;
-          break;
-      }
-    }
-  } else if (props.color1) {
-    o.backgroundColor = theme.resolveThemeColor(props.color1);
-  } else if (props.color2) {
-    o.backgroundColor = theme.resolveThemeColor(props.color2);
+    ...makeStyle(),
   }
   return o;
 })

+ 21 - 0
src/components/display/block/ImageBlock2.vue

@@ -13,6 +13,7 @@
       :height="imageHeight"
       :radius="imageRadius"
       :mode="imageHeight ? 'aspectFill' : 'widthFix'"
+      :defaultImage="defaultImage"
     />
     <slot name="desc">
       <FlexCol :padding="15">
@@ -20,6 +21,9 @@
           :title="title"
           :desc="desc"
           :extra="extra"
+          :titleProps="titleProps"
+          :descProps="descProps"
+          :extraProps="extraProps"
           :extraMpSlotState="Boolean($slots.extra)"
         >
           <template v-if="$slots.extra" #extra>
@@ -50,6 +54,7 @@ import Touchable from '@/components/feedback/Touchable.vue';
 import type { FlexProps } from '../../layout/FlexView.vue';
 import IconTextBlock from './IconTextBlock.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
+import type { TextProps } from '@/components/basic/Text.vue';
 
 export interface ImageBlock2Props extends Partial<FlexProps> {
   /**
@@ -73,17 +78,33 @@ export interface ImageBlock2Props extends Partial<FlexProps> {
    */
   imageRadius?: string | number;
   /**
+   * 图片的默认路径。
+   */
+  defaultImage?: string;
+  /**
    * 图片下方显示标题。
    */
   title?: string;
   /**
+   * 图片下方显示标题属性。
+   */
+  titleProps?: TextProps;
+  /**
    * 图片下方显示描述。
    */
   desc?: string;
   /**
+   * 图片下方显示描述属性。
+   */
+  descProps?: TextProps;
+  /**
    * 图片下方显示额外信息。
    */
   extra?: string;
+  /**
+   * 图片下方显示额外信息属性。
+   */
+  extraProps?: TextProps;
 }
 
 const theme = useTheme();

+ 5 - 0
src/components/display/block/ImageBlock3.vue

@@ -11,6 +11,7 @@
       :width="imageWidth"
       :height="imageHeight"
       :radius="imageRadius"
+      :defaultImage="defaultImage"
       mode="aspectFill"
     />
     <FlexView direction="column">
@@ -63,6 +64,10 @@ export interface ImageBlock3Props extends Partial<FlexProps> {
    */
   imageHeight?: string | number;
   /**
+   * 默认图片。
+   */
+  defaultImage?: string;
+  /**
    * 图片的路径。
    */
   src?: string;

+ 5 - 3
src/components/display/parse/ParseNodeRender.vue

@@ -2,13 +2,14 @@
   <!-- 节点渲染 -->
 
   <!-- 图片 -->
-  <image 
+  <Image
     v-if="node.tag === 'img'" 
     :id="node.attrs?.id" 
+    mode="widthFix" 
     :class="'_img ' + (node.attrs?.class || '')" 
     :style="node.attrs?.style || {}" 
-    :src="node.attrs?.src || ''" 
-    mode="widthFix" 
+    :src="node.attrs?.src as string || ''" 
+    :touchable="true"
     @click="preview(node.attrs?.src as string)"
   />
   
@@ -177,6 +178,7 @@ import { computed, inject, ref, type Ref } from 'vue';
 import ParseNodeRender from './ParseNodeRender.vue';
 import type { ParseNode } from './Parse';
 import InjectMPRender from '@/common/components/rich/InjectMPRender.vue';
+import Image from '@/components/basic/Image.vue';
 
 const props = withDefaults(defineProps<{
   node: ParseNode;

+ 6 - 52
src/components/dynamic/DynamicForm.vue

@@ -10,56 +10,16 @@
     @submit="(e) => emit('submit', e)"
     @submitFailed="() => emit('finishFailed')"
   >
-    <!--空显示-->
-    <slot name="empty" v-if="options.formItems?.length == 0 || !model">
-      <div v-if="options.emptyText" class="dynamic-form-item-empty">{{ options.emptyText }}</div>
-    </slot>
-    <Alert
-      v-else-if="(typeof model !== 'object' && !options.suppressRootError)"
-      type="warning"
-      message="DynamicForm: model is not a object!"
-      :description="`At form ${name || 'unnamed'} Root`"
+    <DynamicFormRoot 
+      :options="finalOptions"
+      :model="model"
+      :name="name"
     />
-    <template v-else>
-      <template v-for="(item, index) in options.formItems" :key="item.name">
-        <template v-if="item.type === 'insertion'">
-          <slot name="insertion" :data="item" />
-        </template>
-        <!--表单条目渲染核心-->
-        <DynamicFormItemContainer 
-          v-else
-          :item="item"
-          :name="item.name"
-          :rawModel="finalModel"
-          :model="finalModel[item.name]"
-          :parentModel="finalModel"
-          :isFirst="index === 0"
-          :isLast="index === options.formItems.length - 1"
-          @update:model="(v: unknown) => finalModel[item.name] = v"
-          :disabled="options.disabled"
-        >
-          <template #arrayButtonAdd="props">
-            <slot name="formArrayButtonAdd" :onClick="props.onClick" />
-          </template>
-          <template #arrayButtons="props">
-            <slot name="formArrayButtons"
-              :onDeleteClick="props.onDeleteClick"
-              :onUpClick="props.onUpClick"
-              :onDownClick="props.onDownClick" 
-            />
-          </template>
-          <template #formCeil="values">
-            <slot name="formCeil" :data="values.data" />
-          </template>
-        </DynamicFormItemContainer>
-      </template>
-      <slot name="endButton" />
-    </template>
   </Form>
 </template>
 
 <script setup lang="ts">
-import { computed, onMounted, provide, ref, toRef, toRefs, type PropType } from 'vue';
+import { computed, onMounted, provide, ref, shallowRef, toRef, toRefs, type PropType } from 'vue';
 import Form, { type FormInstance } from '../form/Form.vue';
 import { 
   type IDynamicFormOptions, type IDynamicFormItem, type IDynamicFormRef, 
@@ -69,8 +29,7 @@ import {
   MESSAGE_RELOAD,
   type IDynamicFormWidgetRef
 } from '.';
-import DynamicFormItemContainer from './nest/DynamicFormItemContainer.vue';
-import Alert from '@/components/feedback/Alert.vue';
+import DynamicFormRoot from './nest/DynamicFormRoot.vue';
 
 const props = defineProps({	
   /**
@@ -109,11 +68,6 @@ const finalOptions = computed<IDynamicFormOptions>(() => ({
   ...defaultDynamicFormOptions,
   ...options.value,
 }));
-const finalModel = computed(() => {
-  if (typeof props.model !== 'object')
-    return {};
-  return props.model;
-});
 
 provide('rawModel', model);
 provide('globalParams', toRef(props, 'globalParams'));

+ 0 - 11
src/components/dynamic/DynamicFormControl.vue

@@ -222,16 +222,6 @@
           />
         </view>
       </template>
-      <template v-else-if="item.type === 'sign'">
-        <view>
-          <SignatureField
-            ref="itemRef"
-            :modelValue="model"
-            v-bind="params"
-            @update:modelValue="(e: any) => onValueChanged(e)"
-          />
-        </view>
-      </template>
       <template v-else-if="item.type === 'button'">
         <Button
           ref="itemRef"
@@ -298,7 +288,6 @@ import Alert from '../feedback/Alert.vue';
 import Image from '../basic/Image.vue';
 import CheckBoxTreeList, { type CheckBoxTreeListProps } from './wrappers/CheckBoxTreeList.vue';
 import { useInjectFormContext, useInjectFormItemContext } from '../form/FormContext';
-import SignatureField from '../form/SignatureField.vue';
 
 export interface FormCeilProps {
   model: unknown,

+ 16 - 28
src/components/dynamic/nest/DynamicFormItemContainer.vue

@@ -38,9 +38,9 @@
         </template>
       </DynamicFormItemNormal>
       <!--循环子条目-->
-      <DynamicFormItemContainer 
+      <DynamicFormItemContainerFuckMp 
         v-for="(child, k) in item.children"
-        :key="child.name"
+        :key="k"
         :item="child"
         :name="name+'.'+child.name"
         :rawModel="rawModel"
@@ -52,11 +52,7 @@
         :isLast="k === (item.children?.length || 0) - 1"
         @update:model="(v: unknown) => (model as IDynamicFormObject)[child.name] = v"
         :disabled="disabled || evaluateCallback(item.disabled)"
-      >
-        <template #formCeil="values">
-          <slot name="formCeil" :data="values.data" />
-        </template>
-      </DynamicFormItemContainer>
+      />
     </DynamicFormCheckEmpty>
     <!--对象组-->
     <DynamicFormCheckEmpty 
@@ -70,9 +66,9 @@
       <FormGroup :title="evaluateCallback(item.label)" v-bind="(item.additionalProps as object)">
         <Row v-bind="item.rowProps">
           <!--循环子条目-->
-          <DynamicFormItemContainer 
+          <DynamicFormItemContainerFuckMp 
             v-for="(child, k) in item.children" 
-            :key="child.name"
+            :key="k"
             :item="child"
             :colProps="{ ...item.childrenColProps, ...child.colProps }"
             :name="name+'.'+child.name"
@@ -85,11 +81,7 @@
             :isLast="k === (item.children?.length || 0) - 1"
             @update:model="(v: unknown) => (model as IDynamicFormObject)[child.name] = v"
             :disabled="disabled || evaluateCallback(item.disabled)"
-          >
-            <template #formCeil="values">
-              <slot name="formCeil" :data="values.data" />
-            </template>
-          </DynamicFormItemContainer>
+          />
         </Row>
       </FormGroup>
     </DynamicFormCheckEmpty>
@@ -97,10 +89,10 @@
     <FormGroup v-else-if="item.type === 'flat-group'" :title="evaluateCallback(item.label)" v-bind="(item.additionalProps as object)">
       <Row v-bind="item.rowProps">
         <!--循环子条目-->
-        <DynamicFormItemContainer 
+        <DynamicFormItemContainerFuckMp 
           v-for="(child, k) in item.children" 
           :colProps="{ ...item.childrenColProps, ...child.colProps }"
-          :key="child.name"
+          :key="k"
           :item="child"
           :name="parentName ? `${parentName}.${child.name}` : child.name"
           :rawModel="rawModel"
@@ -116,7 +108,7 @@
           <template #formCeil="values">
             <slot name="formCeil" :data="values.data" />
           </template>
-        </DynamicFormItemContainer>
+        </DynamicFormItemContainerFuckMp>
       </Row>
     </FormGroup>
     <!--扁平普通-->
@@ -133,9 +125,9 @@
       <template #insertion>
         <Row v-bind="item.rowProps">
           <!--循环子条目-->
-          <DynamicFormItemContainer 
+          <DynamicFormItemContainerFuckMp 
             v-for="(child, k) in item.children" 
-            :key="child.name"
+            :key="k"
             :item="child"
             :colProps="{ ...item.childrenColProps, ...child.colProps }"
             :name="parentName ? `${parentName}.${child.name}` : child.name"
@@ -152,7 +144,7 @@
             <template #formCeil="values">
               <slot name="formCeil" :data="values.data" />
             </template>
-          </DynamicFormItemContainer>
+          </DynamicFormItemContainerFuckMp>
         </Row>
       </template>
     </DynamicFormItemNormal>
@@ -198,7 +190,7 @@
               />
             </template>
             <template #child="{ item, pitem, kname, model: child, onUpdateValue, isFirst, isLast }">
-              <DynamicFormItemContainer
+              <DynamicFormItemContainerFuckMp
                 :item="item"
                 :name="kname"
                 :rawModel="rawModel"
@@ -260,7 +252,7 @@
               />
             </template>
             <template #child="{ item, pitem, kname, model: child, onUpdateValue, isFirst, isLast }">
-              <DynamicFormItemContainer
+              <DynamicFormItemContainerFuckMp
                 :item="item"
                 :name="kname"
                 :rawModel="rawModel"
@@ -272,11 +264,7 @@
                 :isLast="isLast"
                 :disabled="disabled || evaluateCallback(item.disabled)"
                 @update:model="(v: unknown) => onUpdateValue(v)"
-              >
-                <template #formCeil="values">
-                  <slot name="formCeil" :data="values.data" />
-                </template>
-              </DynamicFormItemContainer>
+              />
             </template>
           </FormArrayGroup>
         </template>
@@ -313,7 +301,7 @@ import FormArrayGroup from '../group/FormArrayGroup.vue';;
 import Col, { type ColProps } from '@/components/layout/grid/Col.vue';
 import Row from '@/components/layout/grid/Row.vue';
 import DynamicFormCheckEmpty from './DynamicFormCheckEmpty.vue';
-import DynamicFormItemContainer from './DynamicFormItemContainer.vue';
+import DynamicFormItemContainerFuckMp from './DynamicFormItemContainerFuckMp.vue';
 
 /**
  * 动态表单条目包装组件,处理基础类型分支、数据传入、回调处理、事件传递。

+ 19 - 0
src/components/dynamic/nest/DynamicFormItemContainerFuckMp.vue

@@ -0,0 +1,19 @@
+<script setup lang="ts">
+import DynamicFormItemContainer, { type DynamicFormItemContainerProps } from './DynamicFormItemContainer.vue';
+
+defineProps<DynamicFormItemContainerProps>()
+defineEmits(['update:model'])
+defineOptions({
+  options: {
+    virtualHost: true,
+  }
+})
+</script>
+
+<template>
+  <DynamicFormItemContainer v-bind="($props as any)" @update:model="(val) => $emit('update:model', val)">
+    <template #formCeil="values">
+      <slot name="formCeil" :data="values.data" />
+    </template>
+  </DynamicFormItemContainer>
+</template>

+ 85 - 0
src/components/dynamic/nest/DynamicFormRoot.vue

@@ -0,0 +1,85 @@
+<!-- eslint-disable vue/no-mutating-props -->
+<template>
+  <!--空显示-->
+  <slot name="empty" v-if="options.formItems?.length == 0 || !model">
+    <div v-if="options.emptyText" class="dynamic-form-item-empty">{{ options.emptyText }}</div>
+  </slot>
+  <Alert
+    v-else-if="(typeof model !== 'object' && !options.suppressRootError)"
+    type="warning"
+    message="DynamicForm: model is not a object!"
+    :description="`At form ${name || 'unnamed'} Root`"
+  />
+  <template v-else>
+    <!--表单条目渲染核心-->
+    <DynamicFormItemContainer 
+      v-for="(item, index) in options.formItems"
+      :key="index"
+      :item="item"
+      :name="item.name"
+      :rawModel="finalModel"
+      :model="finalModel[item.name]"
+      :parentModel="finalModel"
+      :isFirst="index === 0"
+      :isLast="index === options.formItems.length - 1"
+      @update:model="(v: unknown) => finalModel[item.name] = v"
+      :disabled="options.disabled"
+    >
+      <template #arrayButtonAdd="props">
+        <slot name="formArrayButtonAdd" :onClick="props.onClick" />
+      </template>
+      <template #arrayButtons="props">
+        <slot name="formArrayButtons"
+          :onDeleteClick="props.onDeleteClick"
+          :onUpClick="props.onUpClick"
+          :onDownClick="props.onDownClick" 
+        />
+      </template>
+      <template #formCeil="{ data }">
+        <slot name="formCeil"
+          :name="data.name"
+          :item="data.item"
+          :model="data.model"
+          :onModelUpdate="data.onModelUpdate"
+          :rawModel="data.rawModel"
+          :parentModel="data.parentModel"
+          :parent="data.parent"
+          :rules="data.item.rules"
+          :disabled="data.disabled"
+          :additionalProps="data.additionalProps"
+        />
+      </template>
+    </DynamicFormItemContainer>
+    <slot name="endButton" />
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { computed, inject, type PropType } from 'vue';
+import DynamicFormItemContainer from './DynamicFormItemContainer.vue';
+import type { IDynamicFormObject, IDynamicFormOptions } from '..';
+import Alert from '@/components/feedback/Alert.vue';
+
+/**
+ * 动态表单组件。
+ */
+const props = defineProps({
+  model: {
+    type: Object as PropType<IDynamicFormObject>,
+    default: null
+  },
+  options: {
+    type: Object as PropType<IDynamicFormOptions>,
+    default: null
+  },
+  name: {
+    type: String,
+    default: ''
+  }
+});
+const finalModel = computed(() => {
+  if (typeof props.model !== 'object')
+    return {};
+  return props.model;
+});
+</script>

+ 1 - 0
src/components/dynamic/wrappers/CheckBoxList.vue

@@ -30,6 +30,7 @@
           :key="value.value"
           :name="value.value"
           :disabled="value.disable"
+          :padding="0"
         >
           <CheckBox
             checkPosition="right" 

+ 128 - 43
src/components/feedback/BubbleBox.vue

@@ -12,8 +12,8 @@
     <SimpleTransition name="bubble-box" :show="showState" :duration="200">
       <template #show="{ classNames }">
         <view class="nana-bubble-box-popup-mask" @click="hide" />
+        <view class="nana-bubble-box-holder-position" @click="hideAndEmitClickOnHolder" />
         <FlexView
-          v-if="items?.length"
           position="absolute"
           :direction="direction"
           :backgroundColor="backgroundColor"
@@ -35,6 +35,8 @@
           <view 
             class="nana-bubble-box-arrow" 
             :style="{ 
+              marginTop: theme.resolveThemeSize(arrowOffsetY),
+              marginLeft: theme.resolveThemeSize(arrowOffsetX),
               borderWidth: theme.resolveThemeSize(arrowWidth),
               borderColor: backgroundColor,
               borderRightColor: 'transparent',
@@ -42,29 +44,31 @@
               borderLeftColor: 'transparent',
             }"
           />
-          <Touchable
-            v-for="item in items"
-            :key="item.text"
-            direction="row"
-            align-items="center"
-            :gap="10"
-            :padding="[5, 20]"
-            v-bind="itemProps"
-            @click="handleItemClick(item)"
-          >
-            <Icon
-              :name="item.icon"
-              :size="44"
-              :color="item.textColor || itemTextColor"
-              v-bind="{ ...itemIconProps, ...item.iconProps }"
-            />
-            <Text 
-              :wrap="false" 
-              v-bind="itemTextProps" 
-              :color="item.textColor || itemTextColor"
-              :text="item.text" 
-            />
-          </Touchable>
+          <slot name="content">
+            <Touchable
+              v-for="item in items"
+              :key="item.text"
+              direction="row"
+              align-items="center"
+              :gap="10"
+              :padding="[5, 20]"
+              v-bind="itemProps"
+              @click="handleItemClick(item)"
+            >
+              <Icon
+                :name="item.icon"
+                :size="44"
+                :color="item.textColor || itemTextColor"
+                v-bind="{ ...itemIconProps, ...item.iconProps }"
+              />
+              <Text 
+                :wrap="false" 
+                v-bind="itemTextProps" 
+                :color="item.textColor || itemTextColor"
+                :text="item.text" 
+              />
+            </Touchable>
+          </slot>
         </FlexView>
       </template>
     </SimpleTransition>
@@ -74,7 +78,7 @@
 <script setup lang="ts">
 import { computed, ref } from 'vue';
 import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
-import { selectObjectByType } from '../theme/ThemeTools';
+import { selectObjectByType, selectStyleType } from '../theme/ThemeTools';
 import type { FlexProps } from '../layout/FlexView.vue';
 import type { TextProps } from '../basic/Text.vue';
 import Icon, { type IconProps } from '../basic/Icon.vue';
@@ -97,6 +101,11 @@ export interface BubbleBoxProps {
    */
   position?: 'left' | 'right' | 'top' | 'bottom',
   /**
+   * 气泡在横轴上的对齐位置,默认居中
+   * @default center
+   */
+  crossPosition?: 'left' | 'center' | 'right',
+  /**
    * 触发点击事件模式
    * @default click
    */
@@ -147,6 +156,16 @@ export interface BubbleBoxProps {
    */
   arrowWidth?: number,
   /**
+   * 气泡框箭头X轴偏移
+   * @default 0
+   */
+  arrowOffsetX?: number|string,
+  /**
+   * 气泡框箭头X轴偏移
+   * @default 0
+   */
+  arrowOffsetY?: number|string,
+  /**
    * 气泡框圆角半径
    * @default 12
    */
@@ -176,6 +195,8 @@ const props = withDefaults(defineProps<BubbleBoxProps>(), {
   radius: () => propGetThemeVar('BubbleBoxRadius', 12),
 });
 
+const emit = defineEmits(['show', 'hide', 'clickOnHolder']);
+
 const backgroundColor = computed(() => theme.resolveThemeColor(props.backgroundColor));
 const showState = ref(false);
 const lock = ref(false);
@@ -187,16 +208,20 @@ function handleItemClick(item: BubbleBoxItem) {
 function handleClick() {
   if (lock.value) return;
   if (props.trigger === 'click' && !props.disabled)  {
-     enterLock();
+    enterLock();
     showState.value = !showState.value;
+    emit(showState.value ? 'show' : 'hide');
   }
 }
 function handleHover(show: boolean) {
   if (lock.value) return;
-  if (props.trigger === 'hover' && !props.disabled)
+  if (props.trigger === 'hover' && !props.disabled) {
     showState.value = show;
+    emit(show ? 'show' : 'hide');
+  }
 }
 
+
 function enterLock() {
   lock.value = true;
   setTimeout(() => {
@@ -205,12 +230,80 @@ function enterLock() {
 }
 function show() { 
   showState.value = true;
+  emit('show');
   enterLock();
 }
 function hide() { 
   showState.value = false; 
+  emit('hide');
   enterLock();
 }
+function hideAndEmitClickOnHolder() {
+  hide();
+  emit('clickOnHolder');
+}
+
+const innerStyle = computed(() => {
+  const horzLayout = selectStyleType(props.crossPosition, 'center', {
+    left: {
+      k: 'top',
+      y: '0%',
+      t: 'translateY(0%)',
+    },
+    center: {
+      k: 'top',
+      y: '50%',
+      t: 'translateY(-50%)',
+    },
+    right: {
+      k: 'bottom',
+      y: '0',
+      t: 'translateY(0%)',
+    }
+  });
+  const vertLayout = selectStyleType(props.crossPosition, 'center', {
+    left: {
+      k: 'left',
+      x: '0%',
+      t: 'translateX(0%)',
+    },
+    center: {
+      k: 'left',
+      x: '50%',
+      t: 'translateX(-50%)',
+    },
+    right: {
+      k: 'right',
+      x: '0%',
+      t: 'translateX(0%)',
+    }
+  });
+  return {
+    ...props.innerStyle,
+    ...selectStyleType(props.position, 'top', {
+      left: {
+        [horzLayout.k]: horzLayout.y,
+        right: '100%',
+        transform: horzLayout.t + ' translateX(0)',
+      },
+      right: {
+        [horzLayout.k]: horzLayout.y,
+        left: '100%',
+        transform: horzLayout.t + '',
+      },
+      top: {
+        top: '0%',
+        [vertLayout.k]: vertLayout.x,
+        transform: vertLayout.t + ' translateY(-100%)',
+      },
+      bottom: {
+        bottom: '0%',
+        [vertLayout.k]: vertLayout.x,
+        transform: vertLayout.t + ' translateY(100%)',
+      }
+    })
+  }
+})
 
 defineExpose<BubbleBoxExpose>({
   show,
@@ -235,10 +328,6 @@ defineOptions({
     transition: opacity ease-in-out 0.2s;
 
     &.left {
-      top: 50%;
-      right: 100%;
-      transform: translateY(-50%) translateX(0);
-
       .nana-bubble-box-arrow {
         top: 50%;
         left: 100%;
@@ -246,10 +335,6 @@ defineOptions({
       }
     }
     &.right {
-      top: 50%;
-      left: 100%;
-      transform: translateY(-50%);
-
       .nana-bubble-box-arrow {
         top: 50%;
         left: 0;
@@ -257,10 +342,6 @@ defineOptions({
       }
     }
     &.top {
-      bottom: -100%;
-      left: 50%;
-      transform: translateX(-50%) translateY(-100%);
-
       .nana-bubble-box-arrow {
         top: 100%;
         left: 50%;
@@ -268,10 +349,6 @@ defineOptions({
       }
     }
     &.bottom {
-      top: 100%;
-      left: 50%;
-      transform: translateX(-50%);
-
       .nana-bubble-box-arrow {
         top: 0;
         left: 50%;
@@ -288,6 +365,14 @@ defineOptions({
       opacity: 0;
     }
   }
+  .nana-bubble-box-holder-position {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 1001;
+  }
   .nana-bubble-box-popup-mask {
     position: fixed;
     top: 0;

+ 78 - 0
src/components/feedback/BubbleTip.vue

@@ -0,0 +1,78 @@
+<template>
+  <BubbleBox 
+    ref="followBubbleBoxRef" 
+    v-bind="props"
+    @clickOnHolder="emit('contentClick');hideTip()"
+  >
+    <template #content>
+      <FlexRow justify="space-between" align="center">
+        <Text :text="content" v-bind="contentTextProps" :wrap="false" touchable @click="emit('contentClick');hideTip()" />
+        <IconButton v-if="closeButton" v-bind="closeButtonProps" :icon="closeButton" @click="emit('close');hideTip()" />
+      </FlexRow>
+    </template>
+    <slot />
+  </BubbleBox>
+</template>
+
+<script setup lang="ts">
+import Text, { type TextProps } from '../basic/Text.vue';
+import BubbleBox, { type BubbleBoxProps } from './BubbleBox.vue';
+import FlexRow from '../layout/FlexRow.vue';
+import IconButton, { type IconButtonProps } from '../basic/IconButton.vue';
+import { onMounted, ref, watch } from 'vue';
+
+export interface BubbleTiProps extends BubbleBoxProps {
+  show?: boolean;
+  content?: string;
+  contentTextProps?: TextProps;
+  closeButton?: string;
+  closeButtonProps?: IconButtonProps;
+}
+
+const props = withDefaults(defineProps<BubbleTiProps>(), {
+  content: '',
+  contentTextProps: () => ({
+    color: 'white',
+    fontConfig: 'contentText',
+  }),
+  closeButton: 'close',
+  closeButtonProps: () => ({
+    size: 26,
+    color: 'white',
+  }),
+  backgroundColor: 'rgba(0,0,0,0.9)',
+  radius: 10,
+});
+
+const emit = defineEmits(['contentClick', 'close', 'update:show']);
+
+const followBubbleBoxRef = ref<InstanceType<typeof BubbleBox>>();
+
+watch(() => props.show, (newVal) => {
+  if (newVal)
+    followBubbleBoxRef.value?.show();
+  else 
+    followBubbleBoxRef.value?.hide();
+});
+
+function hideTip() {
+  emit('update:show', false);
+}
+
+onMounted(() => {
+  setTimeout(() => {
+    if (props.show)
+      followBubbleBoxRef.value?.show();
+  }, 300);
+});
+
+defineOptions({
+  options: {
+    virtualHost: true,
+    styleIsolation: "shared",
+  }
+})
+</script>
+
+<style lang="scss">
+</style>

+ 5 - 0
src/components/feedback/Touchable.ts

@@ -17,6 +17,11 @@ export interface TouchableFlexProps extends FlexProps {
    */
   activeOpacity?: number,
   /**
+   * 禁用时的透明度
+   * @default 0.65
+   */
+  disabledOpacity?: number,
+  /**
    * 是否设置鼠标指针为指针
    * @default true
    */

+ 3 - 0
src/components/feedback/Touchable.vue

@@ -31,6 +31,7 @@ import { TouchableClickEventInceptorKey, type TouchableFlexProps } from './Touch
 
 const props = withDefaults(defineProps<TouchableFlexProps>(), {
   activeOpacity: 0.7,
+  disabledOpacity: 0.65,
   touchable: true,
   setCursor: true,
 });
@@ -45,6 +46,8 @@ const finalStyle = computed(() => {
       obj.backgroundColor = themeContext.resolveThemeColor(props.pressedColor);
   } else if (props.activeOpacity != undefined) 
     obj.opacity = isPressed.value ? props.activeOpacity : 1;
+  if (props.disabledOpacity != undefined && props.touchable === false)
+    obj.opacity = props.disabledOpacity;
   const o : Record<string, any> = {
     ...commonStyle.value,
     ...obj,

+ 2 - 11
src/components/form/CalendarField.vue

@@ -19,6 +19,7 @@
       v-if="popupShow"
       v-bind="props"
       v-model="tempValue"
+      @selectTextChange="onSelectTextChange"
     />
     <slot name="footer">
       <Height :size="20" />
@@ -131,6 +132,7 @@ const {
 );
 
 const {
+  onSelectTextChange,
   onCancel,
   onConfirm,
   selectText,
@@ -142,17 +144,6 @@ const {
   emit as any,
   [],
   props.shouldUpdateValueImmediately,
-  (v) => {
-    if (!v)
-      return '';
-    if (typeof v === 'string')
-      return v;
-    if (v.length === 0)
-      return '';
-    if (v.length === 1)
-      return v[0];
-    return v.join(',');
-  },
   props.beforeConfirm,
   popupShow,
 );

+ 0 - 8
src/components/form/CascadePicker.vue

@@ -62,7 +62,6 @@ const props = withDefaults(defineProps<CascadePickerProps>(), {
 
 const pickerVisibleCols = ref<CascadePickerItem[][]>([]);
 const pickerSelectIndex = ref<number[]>([]);
-const currentText = ref<string>('');
 
 function bindChange(e: any) {
   const val = e.detail.value as number[];
@@ -95,7 +94,6 @@ function bindChange(e: any) {
 
   emit('update:value', resultValue);
   emit('selectTextChange', selectText.join(' '));
-  currentText.value = selectText.join(' ');
 }
 function loadCols() {
   const selectText : string[] = [];
@@ -113,7 +111,6 @@ function loadCols() {
     } 
   }
   emit('selectTextChange', selectText.join(' '), true);
-  currentText.value = selectText.join(' ');
 }
 
 watch(() => props.value, (v) => {
@@ -123,11 +120,6 @@ onMounted(() => {
   loadCols();
 })
 
-defineExpose({
-  getSelectedText: () => {
-    return currentText.value;
-  },
-});
 defineOptions({
   options: {
     styleIsolation: "shared",

+ 4 - 8
src/components/form/CascadePickerField.vue

@@ -13,9 +13,9 @@
       @confirm="onConfirm"
     />
     <CascadePicker 
-      ref="pickerRef"
       v-bind="props"
-      v-model:value="tempValue"
+      v-model:value="tempValue" 
+      @selectTextChange="onSelectTextChange"
     />
   </Popup>
   <Text
@@ -90,7 +90,7 @@ const props = withDefaults(defineProps<CascadePickerFieldProps>(), {
 });
 
 const popupShow = ref(false);
-const pickerRef = ref();
+
 const {
   value,
   updateValue,
@@ -105,6 +105,7 @@ const {
 );
 
 const {
+  onSelectTextChange,
   onCancel,
   onConfirm,
   selectText,
@@ -116,11 +117,6 @@ const {
   emit as any,
   [],
   props.shouldUpdateValueImmediately,
-  (v) => {
-    if (!v || v.length === 0)
-      return '';
-    return pickerRef.value?.getSelectedText() ?? '';
-  },
   undefined,
   popupShow,
 );

+ 8 - 5
src/components/form/Cascader.vue

@@ -11,7 +11,6 @@
     <slot name="header" :currentIndex="headerTabCurrent" />
     <SimpleList 
       mode="single-check"
-      virtual
       :data="currentDataDynamicLoading ? [] : currentData"
       :dataDisplayProp="textKey"
       colorProp="color"
@@ -34,12 +33,12 @@
 <script setup lang="ts">
 import { computed, onMounted, ref, watch } from 'vue';
 import { useTheme } from '../theme/ThemeDefine';
+import { DynamicSize } from '../theme/ThemeTools';
+import { getCascaderText, getCascaderItemByValue } from './CascaderUtils';
 import Tabs, { type TabsItemData, type TabsProps } from '../nav/Tabs.vue';
 import SimpleList from '../list/SimpleList.vue';
-import { DynamicSize } from '../theme/ThemeTools';
 import LoadingPage from '../display/loading/LoadingPage.vue';
 import Empty from '../feedback/Empty.vue';
-import { getCascaderText } from './CascaderUtils';
 
 const themeContext = useTheme();
 
@@ -69,6 +68,10 @@ export interface CascaderItem extends Record<string, any> {
    * 子选项
    */
   children?: CascaderItem[];
+  /**
+   * 自定义数据
+   */
+  data?: any;
 }
 
 export interface CascaderProps {
@@ -128,6 +131,7 @@ const props = withDefaults(defineProps<CascaderProps>(), {
   listHeight: 700,
   tabProps: () => ({
     width: 750 - 70,
+    
   }),
   textKey: 'text',
   valueKey: 'value',
@@ -202,7 +206,6 @@ async function loadAsyncData(group: CascaderItem) {
         group[props.childrenKey] = data;
         currentDataDynamicLoad.value = data;
         currentDataDynamicLoadErrorText.value = '';
-        console.log(group);
       }).catch((e) => {
         console.error('asyncLoadData failed', e);
         currentDataDynamicLoadErrorText.value = '' + e;
@@ -227,7 +230,7 @@ function handleItemClick(item: CascaderItem) {
     (!nextGroup || nextGroup.length === 0)
     && !canLoadLevel(headerTabCurrent.value + 1)
   )
-    emit('pickEnd');
+    emit('pickEnd', getCascaderItemByValue(values, props.valueKey, props.childrenKey, props.data));
 }
 function handleTabChange(v: number) {
   headerTabCurrent.value = v;

+ 13 - 5
src/components/form/CascaderField.vue

@@ -16,6 +16,7 @@
       v-bind="props"
       :modelValue="tempValue"
       @update:modelValue="(v:any) => tempValue = v"
+      @selectTextChange="onSelectTextChange"
       @pickEnd="onPickEnd"
     />
     <slot name="footer">
@@ -123,6 +124,7 @@ const {
 );
 
 const {
+  onSelectTextChange,
   onCancel,
   onConfirm,
   selectText,
@@ -134,11 +136,6 @@ const {
   emit as any,
   [],
   props.shouldUpdateValueImmediately,
-  (v) => {
-    if (!v || v.length === 0)
-      return '';
-    return getCascaderText(v, props.valueKey, props.textKey, props.childrenKey, props.data);
-  },
   props.beforeConfirm,
   popupShow,
 );
@@ -148,6 +145,17 @@ function onPickEnd() {
     onConfirm();
 }
 
+watch(tempValue, (v) => {
+  if (!popupShow.value)
+    onSelectTextChange(getCascaderText(
+      v, 
+      props.valueKey, 
+      props.textKey, 
+      props.childrenKey, 
+      props.data
+    ), true);
+}, { immediate: true })
+
 defineExpose({
   confirm: onConfirm,
   cancel: onCancel,

+ 23 - 0
src/components/form/CascaderUtils.ts

@@ -23,4 +23,27 @@ export function getCascaderText(
       selectTexts.push(item[textKey]);
   }
   return selectTexts.join(' / ');
+}
+export function getCascaderItemByValue(
+  value: (number|string)[], 
+  valueKey: string, 
+  childrenKey: string,
+  data: CascaderItem[],
+) {
+  const selectItems : CascaderItem[] = [];
+  let currentGroup : CascaderItem[]|undefined = data;
+  let i = 0;
+  for (; i < value.length; i++) {
+    const item : CascaderItem|undefined = currentGroup?.find(item => item[valueKey] === value[i]);
+    if (item) {
+      selectItems.push(item);
+      currentGroup = item[childrenKey];
+    }
+  }
+  if (currentGroup !== undefined) {
+    const item : CascaderItem|undefined = currentGroup?.find(item => item[valueKey] === value[i]);
+    if (item)
+      selectItems.push(item);
+  }
+  return selectItems;
 }

+ 5 - 8
src/components/form/DatePicker.vue

@@ -11,9 +11,9 @@
 <script setup lang="ts">
 import { computed, onMounted } from 'vue';
 import type { PickerProps } from './Picker.vue';
+import type { PickerItem } from './Picker';
 import Picker from './Picker.vue';
 import { DateUtils } from '@imengyu/imengyu-utils';
-import type { PickerItem } from './Picker';
 
 // 更新 DatePickerProps 接口
 export interface DatePickerProps extends Omit<PickerProps, 'columns'|'value'> {
@@ -39,8 +39,8 @@ const props = withDefaults(defineProps<DatePickerProps>(), {
   showYears: true,
   showMonths: true,
   showDays: true,
-  startYear: () => new Date().getFullYear() - 10,
-  endYear: () => new Date().getFullYear() + 10,
+  startYear: () => new Date().getFullYear() - 50,
+  endYear: () => new Date().getFullYear() + 20,
   startMonth: 1,
   endMonth: 12,
   startDay: 1,
@@ -51,8 +51,6 @@ const props = withDefaults(defineProps<DatePickerProps>(), {
   dayText: '日',
 });
 
-let forceUpdate = true;
-
 // 计算当前选中的值
 const value = computed(() => {
   const value : number[] = [];
@@ -132,11 +130,10 @@ function updateValue(v: number[]) {
       }
       emit('update:modelValue', date);
     }
-    emit('selectTextChange', DateUtils.formatDate(date, 'yyyy-MM-dd'), forceUpdate);
+    emit('selectTextChange', DateUtils.formatDate(date, 'yyyy-MM-dd'));
   } else {
-    emit('selectTextChange', '', forceUpdate);
+    emit('selectTextChange', '');
   }
-  forceUpdate = false;
 }
 onMounted(() => updateValue([]));
 

+ 3 - 4
src/components/form/DatePickerField.vue

@@ -15,6 +15,7 @@
     <DatePicker 
       v-bind="props"
       v-model="tempValue"
+      @selectTextChange="onSelectTextChange"
     />
   </Popup>
   <Text
@@ -28,7 +29,7 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref, toRef } from 'vue';
+import { ref, toRef } from 'vue';
 import { useFieldChildValueInjector } from './FormContext';
 import type { DatePickerProps } from './DatePicker.vue';
 import Popup from '../dialog/Popup.vue';
@@ -37,7 +38,6 @@ import DatePicker from './DatePicker.vue';
 import { usePickerFieldTempStorageData } from './PickerUtils';
 import Text, { type TextProps } from '../basic/Text.vue';
 import { usePickerFieldInstance, type PickerFieldInstance } from './Picker';
-import { DateUtils } from '@imengyu/imengyu-utils';
 
 export interface DatePickerFieldProps extends Omit<DatePickerProps, 'modelValue'> {
   modelValue?: Date;
@@ -105,6 +105,7 @@ const {
 );
 
 const {
+  onSelectTextChange,
   onCancel,
   onConfirm,
   selectText,
@@ -116,12 +117,10 @@ const {
   emit as any,
   new Date(),
   props.shouldUpdateValueImmediately,
-  (v) => DateUtils.formatDate(v, 'yyyy-MM-dd'),
   undefined,
   popupShow,
 );
 
-
 defineExpose<PickerFieldInstance>(usePickerFieldInstance(popupShow));
 defineOptions({
   options: {

+ 3 - 3
src/components/form/DateTimePicker.vue

@@ -11,9 +11,9 @@
 <script setup lang="ts">
 import { computed, onMounted } from 'vue';
 import type { PickerProps } from './Picker.vue';
+import type { PickerItem } from './Picker';
 import Picker from './Picker.vue';
 import { DateUtils } from '@imengyu/imengyu-utils';
-import type { PickerItem } from './Picker';
 
 // 更新 DateTimePickerProps 接口,添加日期相关属性
 export interface DateTimePickerProps extends Omit<PickerProps, 'columns'|'value'> {
@@ -48,8 +48,8 @@ const props = withDefaults(defineProps<DateTimePickerProps>(), {
   showHours: true,
   showMinute: true,
   showSecond: true,
-  startYear: () => new Date().getFullYear() - 10,
-  endYear: () => new Date().getFullYear() + 10,
+  startYear: () => new Date().getFullYear() - 50,
+  endYear: () => new Date().getFullYear() + 20,
   startMonth: 1,
   endMonth: 12,
   startDay: 1,

+ 2 - 2
src/components/form/DateTimePickerField.vue

@@ -15,6 +15,7 @@
     <DateTimePicker 
       v-bind="props"
       v-model="tempValue"
+      @selectTextChange="onSelectTextChange"
     />
   </Popup>
   <Text
@@ -37,7 +38,6 @@ import DateTimePicker from './DateTimePicker.vue';
 import { usePickerFieldTempStorageData } from './PickerUtils';
 import Text, { type TextProps } from '../basic/Text.vue';
 import { usePickerFieldInstance, type PickerFieldInstance } from './Picker';
-import { DateUtils } from '@imengyu/imengyu-utils';
 
 export interface DateTimePickerFieldProps extends Omit<DateTimePickerProps, 'modelValue'> {
   modelValue?: Date;
@@ -108,6 +108,7 @@ const {
 );
 
 const {
+  onSelectTextChange,
   onCancel,
   onConfirm,
   selectText,
@@ -119,7 +120,6 @@ const {
   emit as any,
   new Date(),
   props.shouldUpdateValueImmediately,
-  (v) => DateUtils.formatDate(v, 'yyyy-MM-dd HH:mm:ss'),
   undefined,
   popupShow,
 );

+ 6 - 24
src/components/form/Field.vue

@@ -11,6 +11,7 @@
       ...(error || finalErrorMessage ? errorFieldStyle : {})
     }"
     :setCursor="false"
+    :disabledOpacity="1"
     :direction="labelPosition === 'top' ? 'column' : 'row'"
     :justify="labelPosition === 'top' ? 'flex-start' : 'center'"
     @click="onClick"
@@ -99,7 +100,7 @@
             :password="type==='password'"
             :placeholder="placeholder"
             :placeholder-style="`color: ${themeContext.resolveThemeColor(error ? errorTextColor : placeholderTextColor)}`"
-            :confirm-type="confirmType"
+            confirm-type="done"
             :rows="rows"
             :maxlength="maxLength"
             :disabled="disabled || readonly"
@@ -123,7 +124,7 @@
             :password="type==='password'"
             :placeholder="placeholder"
             :placeholder-style="`color: ${themeContext.resolveThemeColor(error ? errorTextColor : placeholderTextColor)}`"
-            :confirm-type="confirmType"
+            confirm-type="done"
             :type="selectStyleType(type, 'text', {
               text: 'text',
               password: 'text',
@@ -192,7 +193,6 @@
       @click="onClear"
     />
   </Touchable>
-  <slot name="extra" />
 </template>
 
 <script setup lang="ts">
@@ -404,15 +404,7 @@ export interface FieldProps {
    */
   extraIcon?: string;
   extraIconProps?: IconProps;
-  /**
-   * send	右下角按钮为“发送”
-      search	右下角按钮为“搜索”
-      next	右下角按钮为“下一个”
-      go	右下角按钮为“前往”
-      done	右下角按钮为“完成”
-      return	右下角按钮为“换行”
-   */
-  confirmType?: 'send'|'search'|'next'|'go'|'done'|'return';
+
   /**
    * 文本框水印文字颜色
    * @default Color.grey
@@ -496,11 +488,6 @@ export interface FieldProps {
    * @default ','
    */
   tagJoinType?: string,
-  /**
-   * 是否自动更新表单值
-   * @default true
-   */
-  autoUpdateFormValue?: boolean;
 
   requireChildRef?: () => any,
 }
@@ -552,7 +539,6 @@ const props = withDefaults(defineProps<FieldProps>(), {
   autoHeight: false,
   maxLength: 100,
   modelValue: undefined,
-  autoUpdateFormValue: true,
   tags: false,
   tagJoinType: ',',
   errorIcon: () => propGetThemeVar('FieldErrorIcon', 'prompt'),
@@ -604,10 +590,7 @@ const formItemContext : FormItemContext = {
     emit('blur');
   },
   getFormModelValue: () => formContextProps?.getItemValue(formItemInternalContext),
-  onFieldChange: (newValue: unknown) => { 
-    if (props.autoUpdateFormValue)
-      formContextProps?.onFieldChange(formItemInternalContext, newValue); 
-  },
+  onFieldChange: (newValue: unknown) => { formContextProps?.onFieldChange(formItemInternalContext, newValue); },
   clearValidate: () => { formContextProps?.clearValidate(formItemInternalContext); },
   setOnClickListener(listener: (() => void)|undefined) {
     childOnClickListener.value = listener;
@@ -726,8 +709,7 @@ onMounted(() => {
 function emitChangeText(text: string) {
   emit('update:modelValue', text);
   inputValue.value = text;
-  if (props.autoUpdateFormValue)
-    formItemContext.onFieldChange(text);
+  formItemContext.onFieldChange(text);
 }
 function doFormatter(text: string) {
   switch (props.type) {

+ 0 - 2
src/components/form/Form.vue

@@ -315,8 +315,6 @@ function validate(name?: string|string[]) {
   //开始验证
   return new Promise<void>((resolve, reject) => {
     const validator = new Schema(filteredRules as Rules);
-    console.log(validator, nowValues);
-    
     validator.validate(nowValues, {}, (errors) => {
       if (errors) {
         //验证失败,把错误字段显示

+ 1 - 11
src/components/form/FormContext.ts

@@ -158,17 +158,7 @@ export function useFieldChildValueInjector<T>(
   const cellContext = useCellContext();
   const context = useInjectFormItemContext();
   const formContext = useInjectFormContext();
-  const shadowRefValue = ref(getInitialValue()) as Ref<T>;
-
-  function getInitialValue() {
-    if (typeof propsModelValue.value !== 'undefined') {
-      return propsModelValue.value;
-    }
-    if (typeof context?.getFormModelValue() !== 'undefined') {
-      return context.getFormModelValue();
-    };
-    return initialValue;
-  }
+  const shadowRefValue = ref(propsModelValue.value ?? context.getFormModelValue() ?? initialValue) as Ref<T>;
 
   const value = computed(() => {
     if (secondParentContext)

+ 0 - 12
src/components/form/Picker.vue

@@ -59,7 +59,6 @@ const emit = defineEmits([ 'update:value', 'selectTextChange' ]);
 const props = withDefaults(defineProps<PickerProps>(), {
   pickerHeight: 300,
   pickerWidth: 750,
-  columns: () => [],
 });
 
 const loaded = ref(false);
@@ -118,17 +117,6 @@ onMounted(() => {
 const themeStyles = themeContext.useThemeStyles({
 });
 
-defineExpose({
-  refresh: loadValues,
-  getSelectedText: () => {
-    return pickerSelectIndex.value.map((p, i) => {
-      const cols = props.columns[i];
-      if (!cols || cols.length === 0) 
-        return null;
-      return cols[p]?.text ?? cols[0]?.text ?? null;
-    }).join(' ');
-  },
-});
 defineOptions({
   options: {
     styleIsolation: "shared",

+ 2 - 7
src/components/form/PickerField.vue

@@ -13,9 +13,9 @@
       @confirm="onConfirm"
     />
     <Picker 
-      ref="pickerRef"
       v-bind="props"
       v-model:value="tempValue"
+      @selectTextChange="onSelectTextChange"
     />
   </Popup>
   <Text
@@ -93,7 +93,6 @@ const props = withDefaults(defineProps<PickerFieldProps>(), {
 });
 
 const popupShow = ref(false);
-const pickerRef = ref();
 
 const {
   value,
@@ -109,6 +108,7 @@ const {
 );
 
 const {
+  onSelectTextChange,
   onCancel,
   onConfirm,
   selectText,
@@ -120,11 +120,6 @@ const {
   emit as any,
   [],
   props.shouldUpdateValueImmediately,
-  (v) => {
-    if (!v || v.length === 0)
-      return '';
-    return pickerRef.value?.getSelectedText() ?? '';
-  },
   undefined,
   popupShow,
 );

+ 36 - 28
src/components/form/PickerUtils.ts

@@ -1,18 +1,18 @@
-import { nextTick, onMounted, ref, watch, type Ref } from "vue";
+import { nextTick, ref, watch, type Ref } from "vue";
 
 /**
  * 选择器字段临时存储数据的组合式函数
  * 用于管理选择器组件的临时值、选择文本和交互逻辑
  * 
  * @template T - 值的类型
- * @param value - 当前值的响应式引用
- * @param updateValue - 更新值的回调函数
- * @param closePopup - 关闭弹窗的回调函数
- * @param emit - 事件发射器函数
- * @param defaultNewValue - 默认的新值
- * @param shouldUpdateValueImmediately - 是否立即更新值至绑定值
- * @param beforeConfirm - 确认前的回调函数,返回true时取消确认
- * @param popupShow - 弹窗显示状态的响应式引用
+ * @param 当前值的响应式引用
+ * @param 更新值的回调函数
+ * @param 关闭弹窗的回调函数
+ * @param 事件发射器函数
+ * @param 默认的新值
+ * @param 是否立即更新值
+ * @param 确认前的回调函数,返回true时取消确认
+ * @param 弹窗显示状态的响应式引用
  */
 export function usePickerFieldTempStorageData<T>(
   value: Ref<T>, 
@@ -21,25 +21,36 @@ export function usePickerFieldTempStorageData<T>(
   emit: (name: string, d?: any) => void,
   defaultNewValue: T,
   shouldUpdateValueImmediately: boolean,
-  requireFormat: (value: T) => string,
   beforeConfirm?: ((value: T) => Promise<boolean>) | undefined,
   popupShow?: Ref<boolean>,
 ) {
 
+  let tempSelectText = '';
+  let tempLastSelectText = '';
   // 临时值的响应式引用,初始值为当前值或默认新值
   const tempValue = ref(value.value ?? defaultNewValue) as Ref<T>;
-  const beforeConfirmValue = ref(value.value ?? defaultNewValue) as Ref<T>;
   // 显示的文本的响应式引用
   const selectText = ref('');
 
   /**
-   * 取消选择。恢复临时值为当前值或默认新值
+   * 当选择文本变化时的处理函数
+   * @param 新的选择文本
+   * @param 是否强制更新显示的选择文本
+   */
+  function onSelectTextChange(t: string, forceUpdate = false) {
+    tempSelectText = t;
+    emit('selectTextChange', t);
+    if (forceUpdate)
+      selectText.value = t;
+  }
+
+  /**
+   * 取消选择的处理函数
    */
   function onCancel() {
     closePopup();
-    tempValue.value = beforeConfirmValue.value;
-    updateValue(tempValue.value);
     emit('cancel');
+    selectText.value = tempLastSelectText;
   }
 
   /**
@@ -50,13 +61,13 @@ export function usePickerFieldTempStorageData<T>(
     // 如果有确认前回调且返回true,则取消确认
     if (beforeConfirm && await beforeConfirm(tempValue.value))
       return;
+    selectText.value = tempSelectText;
     updateValue(tempValue.value);
     emit('confirm', value.value);
   }
 
   watch(value, (v) => {
     tempValue.value = v;
-    selectText.value = requireFormat(v);
   });
   watch(tempValue, (v) => {
     emit('tempValueChange', tempValue.value);
@@ -68,22 +79,19 @@ export function usePickerFieldTempStorageData<T>(
   if (popupShow) {
     watch(popupShow, (v) => {
       if (v) {
-        beforeConfirmValue.value = tempValue.value;
+        // 弹窗显示时,记录当前的选择文本作为上一次的选择文本
+        nextTick(() => {
+          tempLastSelectText = tempSelectText;
+        });
       }
-    });
+    })
   }
 
-  onMounted(() => {
-    if (value.value)
-      selectText.value = requireFormat(value.value);
-    else
-      selectText.value = '';
-  });
-
   return {
-    onCancel,
-    onConfirm,
-    selectText,
-    tempValue,
+    onSelectTextChange, // 选择文本变化处理函数
+    onCancel, // 取消处理函数
+    onConfirm, // 确认处理函数
+    selectText, // 显示的选择文本
+    tempValue, // 临时值
   }
 }

+ 23 - 71
src/components/form/Signature.vue

@@ -1,53 +1,42 @@
 <template>
   <view 
-    :id="containerId"
     class="signature-container"
     :style="containerStyle"
   >
-    <text v-if="placeholder" :style="placeholderStyle">{{ placeholder }}</text>
     <canvas
       class="signature-canvas"
       disable-scroll
       :id="id"
       :canvas-id="id"
       :style="canvasStyle"
-      :width="canvasWidth"
-      :height="canvasHeight"
-      @touchstart.stop="handleTouchStart"
-      @touchmove.stop="handleTouchMove"
-      @touchend.stop="handleTouchEnd"
-      @touchcancel.stop="handleTouchEnd"
-      @mousedown.stop="handleMouseDown"
-      @mousemove.stop="handleMouseMove"
-      @mouseup.stop ="handleMouseUp"
+      @touchstart="handleTouchStart"
+      @touchmove="handleTouchMove"
+      @touchend="handleTouchEnd"
+      @touchcancel="handleTouchEnd"
+      @mousedown="handleMouseDown"
+      @mousemove="handleMouseMove"
+      @mouseup="handleMouseUp"
     ></canvas>
   </view>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted, type Ref, computed, getCurrentInstance, onBeforeUnmount, nextTick } from 'vue';
+import { ref, onMounted, type Ref, computed, getCurrentInstance, onBeforeUnmount } from 'vue';
 import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
-import { RandomUtils, waitTimeOut } from '@imengyu/imengyu-utils';
+import { RandomUtils } from '@imengyu/imengyu-utils';
 
 const id = `signatureCanvas${RandomUtils.genNonDuplicateID(10)}`;
-const containerId = `signatureContainer${RandomUtils.genNonDuplicateID(10)}`;
 
 // 定义绘图相关类型
 interface Point { x: number; y: number; }
 interface Line { points: Point[]; color: string; width: number; }
 
 export interface SignatureProps {
-  innerStyle?: object;
-  placeholderStyle?: object;
   backgroundColor?: string;
   lineColor?: string;
   lineWidth?: number;
   round?: boolean;
   border?: boolean;
-  borderStyle?: string;
-  borderWidth?: number;
-  borderColor?: string;
-  placeholder?: string;
 }
 export interface SignatureInstance {
   /** 
@@ -55,10 +44,6 @@ export interface SignatureInstance {
    */
   clear: () => void;
   /** 
-   * 设置签名图片 
-   */
-  setImage: (image: string) => void;
-  /** 
    * 导出签名为图片 
    */
   export: () => Promise<string>;
@@ -70,10 +55,8 @@ const props = withDefaults(defineProps<SignatureProps>(), {
   lineWidth: () => propGetThemeVar('SignatureLineWidth', 3),
   round: () => propGetThemeVar('SignatureRound', true),
   border: () => propGetThemeVar('SignatureBorder', true),
-  borderWidth: () => propGetThemeVar('SignatureBorderWidth', 3),
-  borderStyle: () => propGetThemeVar('SignatureBorderStyle', 'dashed'),
-  borderColor: () => propGetThemeVar('SignatureBorderColor', 'border.signature'),
-  placeholder: () => propGetThemeVar('SignaturePlaceholder', '请在虚线框内签名'),
+  borderWidth: () => propGetThemeVar('SignatureBorderWidth', 2),
+  borderColor: () => propGetThemeVar('SignatureBorderColor', 'border.cell'),
 });
 
 const theme = useTheme();
@@ -83,14 +66,8 @@ const containerStyle = computed(() => ({
   backgroundColor: theme.resolveThemeColor(props.backgroundColor),
   borderRadius: props.round ? theme.resolveThemeSize('SignatureBorderRadius', 12) : '0',
   border: props.border ? 
-    `${theme.resolveThemeSize(props.borderWidth)} ${props.borderStyle} ${theme.resolveThemeColor(props.borderColor)}`
-    : 'none',
-  ...props.innerStyle
-}));
-const placeholderStyle = computed(() => ({
-  fontSize: theme.resolveThemeSize('SignaturePlaceholderFontSize', 26),
-  color: theme.resolveThemeColor('SignaturePlaceholderColor', 'text.second'),
-  ...props.placeholderStyle
+    `${theme.resolveThemeSize(props.borderWidth)}px solid ${theme.resolveThemeColor(props.borderColor)}`
+    : 'none'
 }));
 const canvasStyle = computed(() => ({
   width: `${canvasWidth.value}px`,
@@ -108,38 +85,27 @@ let timer = 0;
 let isDrawing = false;
 let absPos = [0,0];
 let isDirty = true;
-let isCreating = false;
 
 async function initCanvas() {
-  if (isCreating)
-    return;
-  isCreating = true;
-  await waitTimeOut(100);
   // 获取容器尺寸信息
   containerRect.value = await new Promise<UniApp.NodeInfo>((resolve) => {
     uni.createSelectorQuery()
       .in(instance)
-      .select(`#${containerId}`)
+      .select('.signature-container')
       .boundingClientRect(resolve as any)
       .exec();
   });
 
   if (containerRect.value) {
-    canvasWidth.value = containerRect.value.width! - 2;
-    canvasHeight.value = containerRect.value.height! - 2;
+    canvasWidth.value = containerRect.value.width!;
+    canvasHeight.value = containerRect.value.height!;
   }
-  if (canvasWidth.value > 0 && canvasHeight.value > 0) {
-    if (!canvasContext.value)
-      // 创建Canvas上下文
-      canvasContext.value = uni.createCanvasContext(id, instance);
-  }
-  isCreating = false;
+
+  // 创建Canvas上下文
+  canvasContext.value = uni.createCanvasContext(id, instance);
+  timer = setInterval(render, 50) as any;
 }
 function render() {
-  if (canvasWidth.value <= 0 || canvasHeight.value <= 0) {
-    initCanvas();
-    return;
-  }
   if (!canvasContext.value)
     return;
   if (!isDirty)
@@ -176,7 +142,7 @@ function endDrag() {
 }
 
 function handleTouchStart(e: any) {
-  if (!canvasContext.value) return;   
+  if (!canvasContext.value) return;
   const { x, y } = e.touches[0];
   startDrag(x, y);
 }
@@ -267,26 +233,15 @@ async function exportImage(): Promise<string> {
     }, instance);
   });
 }
-function setImage(image: string) {
-  if (!canvasContext.value) return;
-  canvasContext.value.drawImage(image, 0, 0, canvasWidth.value, canvasHeight.value);
-  isDirty = true;
-}
 
-onMounted(async () => {
-  await nextTick();
-  await initCanvas();
-  timer = setInterval(render, 100) as any;
-});
+onMounted(initCanvas);
 onBeforeUnmount(() => {
   clearInterval(timer);
-  timer = 0;
 });
 
 defineExpose<SignatureInstance>({
   clear,
-  export: exportImage,
-  setImage
+  export: exportImage
 });
 </script>
 
@@ -295,9 +250,6 @@ defineExpose<SignatureInstance>({
   width: 100%;
   height: 200px;
   position: relative;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
 }
 .signature-canvas {
   width: 100%;

+ 105 - 0
src/components/form/Tags.vue

@@ -0,0 +1,105 @@
+<template>
+  <scroll-view :scroll-x="!wrap" class="nana-tags">
+    <FlexRow :wrap="wrap" align="center" gap="gap.md">
+      <Tag 
+        v-for="(tag, index) in tags"
+        :key="tag.value"
+        v-bind="props"
+        :text="tag.text"
+        :type="value == tag.value || (Array.isArray(value) && value.includes(tag.value)) ? activeType : unActiveType"
+        :touchable="!disabled"
+        @click="onToggleTag(tag.value)"
+        @close="onTagClose(tag.value)"
+      />
+    </FlexRow>
+  </scroll-view>
+</template>
+
+<script setup lang="ts">
+import { computed, toRef } from 'vue';
+import Tag, { type TagProps } from '../display/Tag.vue';
+import FlexRow from '../layout/FlexRow.vue';
+import { useFieldChildValueInjector } from './FormContext';
+
+export type TagsAcceptKey = string|number;
+export interface TagsProps extends TagProps {
+  /**
+   * 当前选中的标签值,传入数组时为多选模式
+   */
+  modelValue?: TagsAcceptKey|TagsAcceptKey[];
+  /**
+   * 标签数据列表
+   */
+  tags?: {
+    value: TagsAcceptKey,
+    text: string,
+  }[],
+  /**
+   * 是否禁用
+   * @default false
+   */
+  disabled?: boolean;
+  /**
+   * 激活时的标签颜色类型
+   * @default 'primary'
+   */
+  activeType?: TagProps['type'];
+  /**
+   * 非激活时的标签颜色类型
+   * @default 'default'
+   */
+  unActiveType?: TagProps['type'];
+  /**
+   * 是否换行显示,为 false 时横向滚动
+   * @default false
+   */
+  wrap?: boolean;
+}
+
+const props = withDefaults(defineProps<TagsProps>(), {
+  modelValue: 50,
+  disabled: false,
+  size: "small",
+  activeType: "primary",
+  unActiveType: "default",
+  wrap: false,
+  tags: () => [],
+});
+
+const emit = defineEmits([ 'update:modelValue', 'tagClose' ])
+
+const {
+  value,
+  updateValue,
+  context,
+} = useFieldChildValueInjector(
+  toRef(props, 'modelValue'), 
+  (v) => emit('update:modelValue', v)
+);
+
+const isMulitselect = computed(() => Array.isArray(value.value));
+
+function onTagClose(tag: TagsAcceptKey) {
+  emit('tagClose', tag);
+}
+function onToggleTag(tag: TagsAcceptKey) {
+  if (isMulitselect.value) {
+    const arr = value.value as TagsAcceptKey[];
+    if (arr.includes(tag)) {
+      arr.splice(arr.indexOf(tag), 1);
+    } else {
+      arr.push(tag);
+    }
+    updateValue(arr);
+  } else {
+    updateValue(tag);
+  } 
+}
+
+defineOptions({
+  options: {
+    styleIsolation: "shared",
+    virtualHost: true,
+  }
+})
+</script>

+ 2 - 1
src/components/form/TimePicker.vue

@@ -10,9 +10,10 @@
 
 <script setup lang="ts">
 import { computed, onMounted } from 'vue';
-import type { PickerItem, PickerProps } from './Picker.vue';
+import type { PickerProps } from './Picker.vue';
 import Picker from './Picker.vue';
 import { DateUtils } from '@imengyu/imengyu-utils';
+import type { PickerItem } from './Picker';
 
 export interface TimePickerProps extends Omit<PickerProps, 'columns'|'value'> {
   modelValue?: Date,

+ 3 - 18
src/components/form/TimePickerField.vue

@@ -15,6 +15,7 @@
     <TimePicker 
       v-bind="props"
       v-model="tempValue"
+      @selectTextChange="onSelectTextChange"
     />
   </Popup>
   <Text
@@ -28,7 +29,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref, toRef } from 'vue';
+import { ref, toRef } from 'vue';
 import { useFieldChildValueInjector } from './FormContext';
 import type { TimePickerProps } from './TimePicker.vue';
 import Popup from '../dialog/Popup.vue';
@@ -37,7 +38,6 @@ import TimePicker from './TimePicker.vue';
 import { usePickerFieldTempStorageData } from './PickerUtils';
 import Text, { type TextProps } from '../basic/Text.vue';
 import { usePickerFieldInstance, type PickerFieldInstance } from './Picker';
-import { DateUtils } from '@imengyu/imengyu-utils';
 
 export interface TimePickerFieldProps extends Omit<TimePickerProps, 'modelValue'> {
   modelValue?: Date;
@@ -100,18 +100,8 @@ const {
   props.initalValue,
 );
 
-const format = computed(() => {
-  const formats = []
-  if (props.showHours)
-    formats.push('HH');
-  if (props.showMinute)
-    formats.push('mm');
-  if (props.showSecond)
-    formats.push('ss');
-  return formats.join(':');
-});
-
 const {
+  onSelectTextChange,
   onCancel,
   onConfirm,
   selectText,
@@ -123,11 +113,6 @@ const {
   emit as any,
   new Date(),
   props.shouldUpdateValueImmediately,
-  (v) => {
-    if (!v)
-      return '';
-    return DateUtils.formatDate(v, format.value);
-  },
   undefined,
   popupShow,
 );

+ 1 - 10
src/components/form/Uploader.ts

@@ -1,11 +1,5 @@
-import { StringUtils } from "@imengyu/imengyu-utils";
-
 export interface UploaderItem {
   /**
-   * 文件名称
-   */
-  name: string;
-  /**
    * 上传文件源路径
    */
   filePath: string;
@@ -37,8 +31,6 @@ export interface UploaderItem {
    * 当前上传进度,0-100
    */
   progress?: number;
-
-  isTitle?: boolean;
   /**
    * 取消上传回调
    */
@@ -80,9 +72,8 @@ export interface UploaderAction {
   }, message?: string) => void;
 }
 
-export function stringUrlToUploaderItem(url: string, name?: string): UploaderItem {
+export function stringUrlToUploaderItem(url: string): UploaderItem {
   return {
-    name: name || StringUtils.path.getFileName(url) || '',
     filePath: url,
     uploadedPath: url,
     state: 'success',

+ 46 - 149
src/components/form/Uploader.vue

@@ -6,7 +6,7 @@
     <slot 
       name="uploader" 
       :onClick="onUploadPress"
-      :items="finalUploadList"
+      :items="currentUpladList"
     >
     <!-- #endif -->
       <FlexView
@@ -16,45 +16,41 @@
         :innerStyle="(props.itemListStyle as ViewStyle)"
       >
         <template
-          v-for="(item, index) in finalUploadList"
-          :key="item.isTitle ? `title-${item.filePath}-${index}` : `${item.filePath}-${index}`"
+          v-for="(item, index) in currentUpladList"
+          :key="index"
         >
-          <Text v-if="item.isTitle" bold :text="item.filePath" />
-          <template v-else>
-            <!-- #ifndef MP -->
-            <slot 
-              name="uploadItem"
-              :index="index"
+          <!-- #ifndef MP -->
+          <slot 
+            name="uploadItem"
+            :index="index"
+            :item="item"
+            :onClick="() => onItemPress(item)"
+            :onDeleteClick="() => onItemDeletePress(item)"
+            :style="props.itemStyle"
+            :imageStyle="props.itemImageStyle"
+            :itemMaskStyle="props.itemMaskStyle"
+            :itemMaskTextStyle="props.itemMaskTextStyle"
+            :itemSize="itemSize"
+            :showDelete="showDelete"
+            :defaultSource="props.itemDefaultSource"
+          >
+          <!-- #endif -->
+            <UploaderListItem
               :item="item"
-              :onClick="() => onItemPress(item)"
-              :onDeleteClick="() => onItemDeletePress(item)"
-              :style="props.itemStyle"
-              :imageStyle="props.itemImageStyle"
-              :itemMaskStyle="props.itemMaskStyle"
-              :itemMaskTextStyle="props.itemMaskTextStyle"
+              :showDelete="showDelete && !disabled && !readonly"
+              :isListStyle="props.listType === 'list'"
+              :style="itemStyle"
+              :imageStyle="itemImageStyle"
+              :itemMaskStyle="itemMaskStyle"
+              :itemMaskTextStyle="itemMaskTextStyle"
+              :defaultSource="itemDefaultSource"
               :itemSize="itemSize"
-              :showDelete="showDelete"
-              :defaultSource="props.itemDefaultSource"
-            >
-            <!-- #endif -->
-              <UploaderListItem
-                :item="item"
-                :showDelete="showDelete && !disabled && !readonly"
-                :isListStyle="props.listType === 'list'"
-                :style="itemStyle"
-                :imageStyle="itemImageStyle"
-                :itemMaskStyle="itemMaskStyle"
-                :itemMaskTextStyle="itemMaskTextStyle"
-                :defaultSource="itemDefaultSource"
-                :itemSize="itemSize"
-                :itemExtraButtons="itemExtraButtons"
-                @click="() => onItemPress(item)"
-                @delete="() => onItemDeletePress(item)"
-              />
-            <!-- #ifndef MP -->
-            </slot>
-            <!-- #endif -->
-          </template>
+              @click="() => onItemPress(item)"
+              @delete="() => onItemDeletePress(item)"
+            />
+          <!-- #ifndef MP -->
+          </slot>
+          <!-- #endif -->
         </template>
         <slot v-if="currentUpladList.length < maxUploadCount && !disabled && !readonly" name="addButton" :onUploadPress="onUploadPress" :itemSize="itemSize">
           <UploaderListAddItem :itemSize="itemSize" :style="itemStyle" @click="onUploadPress" :isListStyle="props.listType === 'list'" />
@@ -74,19 +70,19 @@
 </template>
 
 <script setup lang="ts">
-import { computed, reactive, ref } from 'vue';
+import { reactive, ref, watch } from 'vue';
 import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
-import { Debounce, LogUtils, StringUtils } from '@imengyu/imengyu-utils';
-import { actionSheet } from '../dialog/CommonRoot';
 import type { ToastInstance } from '../feedback/Toast.vue';
-import type { UploaderAction, UploaderItem } from './Uploader';
 import Toast from '../feedback/Toast.vue';
-import Text from '../basic/Text.vue';
 import DialogRoot, { type DialogAlertRoot } from '../dialog/DialogRoot.vue';
 import UploaderListAddItem from './UploaderListAddItem.vue';
 import UploaderListItem from './UploaderListItem.vue';
 import FlexView from '../layout/FlexView.vue';
 import FlexCol from '../layout/FlexCol.vue';
+import type { UploaderAction, UploaderItem } from './Uploader';
+import { Debounce, LogUtils } from '@imengyu/imengyu-utils';
+import Text from '../basic/Text.vue';
+import { actionSheet } from '../dialog/CommonRoot';
 
 const themeContext = useTheme();
 const TAG = 'Uploader';
@@ -171,14 +167,6 @@ export interface UploaderProps {
    */
   itemMaskStyle?: ViewStyle;
   /**
-   * 条目的自定义额外按钮,仅当listType为list时有效
-   * @default []
-   */
-  itemExtraButtons?: {
-    icon: string;
-    onClick: (item: UploaderItem) => void;
-  }[];
-  /**
    * 初始列表中的条目
    * @default []
    */
@@ -195,11 +183,6 @@ export interface UploaderProps {
    * @default true
    */
   formMessage?: boolean;
-  /**
-   * 列表是否按类型分组,仅当listType为list时有效
-   * @default false
-   */
-  groupType?: boolean;
 
   /**
    * 上传处理。不提供则无法上传
@@ -302,79 +285,11 @@ const props = withDefaults(defineProps<UploaderProps>(), {
   uploadQueueMode: 'all',
   listType: 'grid',
   chooseType: 'image',
-  groupType: false,
   itemSize: () => propGetThemeVar('UploaderItemSize', { width: 750 / 4 - 15, height: 750 / 4 - 15 }),
 });
 
 const currentUpladList = ref<UploaderItem[]>(props.intitalItems?.concat() || []);
 
-type UploadDisplayKind = 'image' | 'video' | 'audio' | 'document' | 'other';
-
-const UPLOAD_GROUP_META: { kind: UploadDisplayKind; label: string }[] = [
-  { kind: 'image', label: '图片' },
-  { kind: 'video', label: '视频' },
-  { kind: 'audio', label: '音频' },
-  { kind: 'document', label: '文档' },
-  { kind: 'other', label: '其他' },
-];
-
-function isImagePath(path: string) {
-  return path.match(/\.(jpg|jpeg|png|gif|webp)$/i) !== null;
-}
-
-function getUploadItemDisplayKind(item: UploaderItem): UploadDisplayKind {
-  if (item.isTitle)
-    return 'other';
-  const path = item.previewPath || item.uploadedPath || item.filePath;
-  if (item.isImage || isImagePath(path))
-    return 'image';
-  if (/\.(mp4|m4v|mov|webm|avi|wmv|flv|mkv|3gp|3g2|ts)$/i.test(path))
-    return 'video';
-  if (/\.(mp3|wav|m4a|aac|ogg|opus|flac|wma|amr|aiff|aif)$/i.test(path))
-    return 'audio';
-  if (/\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|rtf|csv|md)$/i.test(path))
-    return 'document';
-  return 'other';
-}
-
-function makeUploadTitleItem(label: string): UploaderItem {
-  return {
-    name: label,
-    filePath: label,
-    isTitle: true,
-    state: 'success',
-  };
-}
-
-/** 列表模式 + groupType:按文件类型插入分组标题行(isTitle) */
-function buildGroupedUploadList(items: UploaderItem[]): UploaderItem[] {
-  const files = items.filter((i) => !i.isTitle);
-  if (files.length === 0)
-    return [];
-  const buckets = new Map<UploadDisplayKind, UploaderItem[]>();
-  for (const { kind } of UPLOAD_GROUP_META)
-    buckets.set(kind, []);
-  for (const item of files) {
-    const kind = getUploadItemDisplayKind(item);
-    buckets.get(kind)!.push(item);
-  }
-  const out: UploaderItem[] = [];
-  for (const { kind, label } of UPLOAD_GROUP_META) {
-    const group = buckets.get(kind)!;
-    if (group.length === 0)
-      continue;
-    out.push(makeUploadTitleItem(label));
-    out.push(...group);
-  }
-  return out;
-}
-
-const finalUploadList = computed(() => {
-  if (props.groupType && props.listType === 'list')
-    return buildGroupedUploadList(currentUpladList.value);
-  return currentUpladList.value;
-});
-
 //上传按钮点击
 function onUploadPress() {
   if (props.disabled || props.readonly)
@@ -391,7 +306,6 @@ function onUploadPress() {
       function handleFiles(res: {
         path: string;
         size: number;
-        name: string;
       }[]) {
         resolve(res.map((item) => {
           let isImage = typeof (item as any).type === 'string' ? (item as any).type.startsWith('image/') : false;
@@ -399,7 +313,6 @@ function onUploadPress() {
             isImage = isImagePath(item.path);
           }
           return {
-            name: item.name,
             filePath: item.path,
             previewPath: item.path,
             size: item.size,
@@ -432,11 +345,11 @@ function onUploadPress() {
             chooseLocal();
           } else if (index === 1) {
             uni.chooseMessageFile({
-              type: props.chooseType === 'file' ? 'all' :(props.chooseType || 'all'),
+              type: props.chooseType || 'all',
               count: props.maxUploadCount - currentUpladList.value.length,
               success: (res) => {
                 LogUtils.printLog(TAG, 'info', 'chooseMessageFile', res);
-                handleFiles(res.tempFiles as { path: string; name: string; size: number; }[])
+                handleFiles(res.tempFiles as { path: string; size: number; }[])
               },
               fail: (e) => {
                 LogUtils.printLog(TAG, 'error', 'chooseMessageFile', e);
@@ -459,29 +372,18 @@ function onUploadPress() {
             uni.chooseVideo().then((res) => handleFiles([
               {
                 path: res.tempFilePath,
-                name: res.name || StringUtils.path.getFileName(res.tempFilePath) || '',
                 size: res.size,
               }
             ])).catch(reject);
             break;
-          //#ifdef H5
           case 'file':
-            uni.chooseFile().then((res) => handleFiles((res.tempFiles as any []).map((item) => ({
-              path: item.path,
-              name: item.name || StringUtils.path.getFileName(item.path) || '',
-              size: item.size,
-            })))).catch(reject);
+            uni.chooseFile().then((res) => handleFiles(res.tempFiles as { path: string; size: number; }[])).catch(reject);
             break;
-          //#endif
           default:
           case 'image':
             uni.chooseImage({
               count: props.maxUploadCount - currentUpladList.value.length,
-            }).then((res) => handleFiles((res.tempFiles as any[]).map((item) => ({
-              path: item.path,
-              name: item.name || StringUtils.path.getFileName(item.path) || '',
-              size: item.size,
-            })))).catch(reject);
+            }).then((res) => handleFiles(res.tempFiles as { path: string; size: number; }[])).catch(reject);
             break;
         }
       }
@@ -585,16 +487,11 @@ function deleteListItem(item: UploaderItem) {
   }
 }
 
-function formatError(error: unknown) {
-  if (error instanceof Error)
-    return error.message;
-  if (typeof error === 'string')
-    return error;
-  if (typeof error === 'object')
-    return JSON.stringify(error);
-  return '' + error;
+function isImagePath(path: string) {
+  return path.match(/\.(jpg|jpeg|png|gif|webp)$/) !== null;
 }
 
+
 //开始上传条目
 function startUploadItem(item: UploaderItem) {
   if (item.state === 'uploading')
@@ -617,7 +514,7 @@ function startUploadItem(item: UploaderItem) {
       item,
       onError(error) {
         item.state = 'fail';
-        item.message = formatError(error) || '上传失败';
+        item.message = ('' + error) || '上传失败';
         updateListItem(item);
         reject(error);
         LogUtils.printLog(TAG, 'error', `上传文件 ${item.filePath} 失败,错误信息:${error}`);

+ 2 - 11
src/components/form/UploaderListItem.vue

@@ -59,8 +59,8 @@
     <FlexRow v-if="isListStyle" :flex="1" align="center">
       <Width :size="20" />
       <FlexCol :flex="1">
-        <Text :fontSize="26" :text="item.name" wrap wordBreak="break-all" />
-        <Text :fontSize="22" :text="item.message" wrap wordBreak="break-all" />
+        <Text :fontSize="26" :text="StringUtils.path.getFileName(item.filePath)" />
+        <Text :fontSize="22" :text="item.message" />
         <Height :size="10" /> 
         <Progress :progressColor="selectStyleType(item.state, 'notstart', {
           notstart: 'primary',
@@ -74,11 +74,6 @@
       <ActivityIndicator v-else-if="item.state === 'uploading'" :size="45" />
       <Width :size="20" />
       <IconButton v-if="showDelete" icon="trash" @click.stop="emit('delete')" />
-      <IconButton v-for="button in itemExtraButtons" 
-        :key="button.icon" 
-        :icon="button.icon" 
-        @click.stop="button.onClick(item)" 
-      />
     </FlexRow>
   </Touchable>
 </template>
@@ -122,10 +117,6 @@ export interface UploaderListItemProps {
   showDelete: boolean;
   defaultSource: string | undefined;
   isListStyle: boolean;
-  itemExtraButtons?: {
-    icon: string;
-    onClick: (item: UploaderItem) => void;
-  }[];
 }
 
 const props = defineProps<UploaderListItemProps>();

+ 1 - 1
src/components/layout/BaseView.ts

@@ -69,7 +69,7 @@ export function useBaseViewStyleBuilder(props: FlexProps) {
       delete obj.marginHorizontal;
     }
 
-    if (props.inset) {
+    if (props.inset != undefined) {
       if (Array.isArray(props.inset)) {
         obj.top = themeContext.resolveThemeSize(props.inset[0]);
         obj.right = themeContext.resolveThemeSize(props.inset[1]);

+ 28 - 23
src/components/list/SimpleList.vue

@@ -34,29 +34,34 @@
       <slot name="inner" />
     </template>
   </FixedVirtualList>
-  <FlexCol v-else :innerStyle="innerStyle">
-    <SimpleListItem
-      v-for="(item, index) in data"
-      :key="dataKey ? (item as Record<string, any>)[dataKey] : index"
-      :item="item"
-      :index="index"
-      :dataDisplayProp="dataDisplayProp"
-      :colorProp="colorProp"
-      :disabledProp="disabledProp"
-      :showCheck="mode !== 'select'"
-      :checked="checkedList.indexOf(item) >= 0"
-      :overrideItem="Boolean($slots.itemContent)"
-      @click="onItemPress(item, index)"
-    >
-      <template v-if="$slots.itemContent" #itemContent>
-        <slot name="itemContent" :item="item" :index="index" />
-      </template>
-    </SimpleListItem>
-    <slot v-if="data.length == 0" name="empty">
-      <Empty :description="emptyText" />
-    </slot>
-    <slot name="inner" />
-  </FlexCol>
+  <scroll-view v-else :scroll-y="true" :scroll-with-animation="true" :style="{ 
+    width: innerStyle?.width,
+    height: innerStyle?.height, 
+  }">
+    <FlexCol :innerStyle="innerStyle">
+      <SimpleListItem
+        v-for="(item, index) in data"
+        :key="dataKey ? (item as Record<string, any>)[dataKey] : index"
+        :item="item"
+        :index="index"
+        :dataDisplayProp="dataDisplayProp"
+        :colorProp="colorProp"
+        :disabledProp="disabledProp"
+        :showCheck="mode !== 'select'"
+        :checked="checkedList.indexOf(item) >= 0"
+        :overrideItem="Boolean($slots.itemContent)"
+        @click="onItemPress(item, index)"
+      >
+        <template v-if="$slots.itemContent" #itemContent>
+          <slot name="itemContent" :item="item" :index="index" />
+        </template>
+      </SimpleListItem>
+      <slot v-if="data.length == 0" name="empty">
+        <Empty :description="emptyText" />
+      </slot>
+      <slot name="inner" />
+    </FlexCol>
+  </scroll-view>
 </template>
 
 <script setup lang="ts" generic="T">

+ 1 - 1
src/components/loader/SimplePageContentLoader.vue

@@ -18,7 +18,7 @@
     </Empty>
   </view> 
   <view
-    v-if="showEmpty || loader?.status.value == 'nomore' || loader?.status.value == 'empty'"
+    v-if="showEmpty || loader?.status.value == 'nomore'"
     class="loader-view"
   >
     <Empty

+ 12 - 2
src/components/loader/SimplePageListLoader.vue

@@ -2,11 +2,11 @@
   <slot />
   <Loadmore
     v-if="loader.status.value == 'loading' 
-      || (loader.status.value == 'nomore' && !$slots.empty)" 
+      || (loader.status.value == 'nomore' && !$slots.empty && showNomore)" 
     :status="loader.status.value" 
   />
   <slot v-else-if="loader.status.value == 'empty'" name="empty">
-    <Empty description="暂无数据" />
+    <Empty :description="emptyView?.text || '暂无数据'" />
   </slot>
   <Loadmore 
     v-else-if="loader.status.value == 'error'"
@@ -27,6 +27,16 @@ const props = defineProps({
     type: Object as PropType<ISimplePageListLoader<any, any>>,
     default: null,
   },
+  showNomore: {
+    type: Boolean,
+    default: true,
+  },
+  emptyView: {
+    type: Object as PropType<{
+      text: string;
+    }>,
+    default: null,
+  },
 })
 
 function handleRetry() {

+ 1 - 1
src/components/nav/Tabs.vue

@@ -235,7 +235,7 @@ const props = withDefaults(defineProps<TabsProps>(), {
 
 const theme = useTheme();
 
-const tabPaddingHorizontal = computed(() => theme.getVar('TabsItemPaddingHorizontal', 0));
+const tabPaddingHorizontal = computed(() => theme.getVar('TabsItemPaddingHorizontal', 10));
 const themedUnderlayColor = computed(() => theme.resolveThemeColor(props.underlayColor));
 const themedActiveTextColor = computed(() => theme.resolveThemeColor(props.activeTextColor));
 const themedTextColor = computed(() => theme.resolveThemeColor(props.textColor));

+ 18 - 9
src/components/theme/Theme.ts

@@ -14,6 +14,24 @@ export const DefaultTheme : ThemeConfig = {
       '4xl': '70rpx',
       '5xl': '80rpx',
     },
+    padding: {
+      xs: '10rpx',
+      sm: '15rpx',
+      md: '20rpx',
+      lg: '30rpx',
+      xl: '40rpx',
+      '2xl': '50rpx',
+      '3xl': '60rpx',
+    },
+    margin: {
+      xs: '10rpx',
+      sm: '15rpx',
+      md: '20rpx',
+      lg: '30rpx',
+      xl: '40rpx',
+      '2xl': '50rpx',
+      '3xl': '60rpx',
+    },
     fontSize: {
       xs: 22,
       sm: 26,
@@ -123,7 +141,6 @@ export const DefaultTheme : ThemeConfig = {
     border: {
       input: '#dadada',
       default: '#dddddd',
-      signature: '#333',
       cell: '#efefef',
       light: '#eeeeee',
     },
@@ -159,14 +176,6 @@ export const DefaultTheme : ThemeConfig = {
       fontSize: '26rpx',
       fontWeight: 'bold',
     },
-    p: {
-      color: 'text.content',
-      innerStyle: {
-        lineHeight: '1.5',
-        marginBottom: '10rpx',
-      },
-      fontSize: '24rpx',
-    },
     content: {
       color: 'text.content',
       fontSize: '24rpx',

+ 4 - 5
src/components/theme/ThemeDefine.ts

@@ -270,16 +270,15 @@ function isRealSize(inValue: ThemeSizeType|undefined) {
 }
 /**
  * 数字单位的处理
- * @param inValue 
- * @param theme 
- * @param themeType 
+ * @param inValue 输入值
+ * @param forceUnit 强制单位,默认为defaultSizeUnit即系统配置单位
  * @returns 
  */
-export function resolveSize(inValue: ThemeSizeType|undefined) : string|undefined {
+export function resolveSize(inValue: ThemeSizeType|undefined, forceUnit = defaultSizeUnit) : string|undefined {
   if (inValue == undefined)
     return undefined;
   if (isNumbrSize(inValue))
-    return `${inValue}${defaultSizeUnit}`;
+    return `${inValue}${forceUnit}`;
   if (isRealSize(inValue as string))
     return inValue as string;
   if (inValue == 'fill')

+ 3 - 7
src/components/typography/B.vue

@@ -1,13 +1,9 @@
 <template>
-  <Text v-bind="props" bold>
-    <slot v-if="$slots.default" />
+  <Text bold v-bind="$props">
+    <slot />
   </Text>
 </template>
 
 <script setup lang="ts">
-import Text, { type TextProps } from '../basic/Text.vue';
-
-const props = withDefaults(defineProps<Partial<TextProps>>(), {
-  wrap: true,
-});
+import Text from '../basic/Text.vue';
 </script>

+ 3 - 7
src/components/typography/H1.vue

@@ -1,13 +1,9 @@
 <template>
-  <Text v-bind="props" fontConfig="h1">
-    <slot v-if="$slots.default" />
+  <Text fontConfig="h1" v-bind="$props">
+    <slot />
   </Text>
 </template>
 
 <script setup lang="ts">
-import Text, { type TextProps } from '../basic/Text.vue';
-
-const props = withDefaults(defineProps<Partial<TextProps>>(), {
-  wrap: true,
-});
+import Text from '../basic/Text.vue';
 </script>

+ 3 - 7
src/components/typography/H2.vue

@@ -1,13 +1,9 @@
 <template>
-  <Text v-bind="props" fontConfig="h2">
-    <slot v-if="$slots.default" />
+  <Text fontConfig="h2" v-bind="$props">
+    <slot />
   </Text>
 </template>
 
 <script setup lang="ts">
-import Text, { type TextProps } from '../basic/Text.vue';
-
-const props = withDefaults(defineProps<Partial<TextProps>>(), {
-  wrap: true,
-});
+import Text from '../basic/Text.vue';
 </script>

+ 3 - 7
src/components/typography/H3.vue

@@ -1,13 +1,9 @@
 <template>
-  <Text v-bind="props" fontConfig="h3">
-    <slot v-if="$slots.default" />
+  <Text fontConfig="h3" v-bind="$props">
+    <slot />
   </Text>
 </template>
 
 <script setup lang="ts">
-import Text, { type TextProps } from '../basic/Text.vue';
-
-const props = withDefaults(defineProps<Partial<TextProps>>(), {
-  wrap: true,
-});
+import Text from '../basic/Text.vue';
 </script>

+ 3 - 7
src/components/typography/H4.vue

@@ -1,13 +1,9 @@
 <template>
-  <Text v-bind="props" fontConfig="h4">
-    <slot v-if="$slots.default" />
+  <Text fontConfig="h4" v-bind="$props">
+    <slot />
   </Text>
 </template>
 
 <script setup lang="ts">
-import Text, { type TextProps } from '../basic/Text.vue';
-
-const props = withDefaults(defineProps<Partial<TextProps>>(), {
-  wrap: true,
-});
+import Text from '../basic/Text.vue';
 </script>

+ 3 - 7
src/components/typography/H5.vue

@@ -1,13 +1,9 @@
 <template>
-  <Text v-bind="props" fontConfig="h5">
-    <slot v-if="$slots.default" />
+  <Text fontConfig="h5" v-bind="$props">
+    <slot />
   </Text>
 </template>
 
 <script setup lang="ts">
-import Text, { type TextProps } from '../basic/Text.vue';
-
-const props = withDefaults(defineProps<Partial<TextProps>>(), {
-  wrap: true,
-});
+import Text from '../basic/Text.vue';
 </script>

+ 3 - 7
src/components/typography/H6.vue

@@ -1,13 +1,9 @@
 <template>
-  <Text v-bind="props" fontConfig="h6">
-    <slot v-if="$slots.default" />
+  <Text fontConfig="h6" v-bind="$props">
+    <slot />
   </Text>
 </template>
 
 <script setup lang="ts">
-import Text, { type TextProps } from '../basic/Text.vue';
-
-const props = withDefaults(defineProps<Partial<TextProps>>(), {
-  wrap: true,
-});
+import Text from '../basic/Text.vue';
 </script>

+ 3 - 7
src/components/typography/I.vue

@@ -1,13 +1,9 @@
 <template>
-  <Text v-bind="props" italic>
-    <slot v-if="$slots.default" />
+  <Text italic v-bind="$props">
+    <slot />
   </Text>
 </template>
 
 <script setup lang="ts">
-import Text, { type TextProps } from '../basic/Text.vue';
-
-const props = withDefaults(defineProps<Partial<TextProps>>(), {
-  wrap: true,
-});
+import Text from '../basic/Text.vue';
 </script>

+ 3 - 7
src/components/typography/S.vue

@@ -1,13 +1,9 @@
 <template>
-  <Text v-bind="props" lineThrough>
-    <slot v-if="$slots.default" />
+  <Text lineThrough v-bind="$props">
+    <slot />
   </Text>
 </template>
 
 <script setup lang="ts">
-import Text, { type TextProps } from '../basic/Text.vue';
-
-const props = withDefaults(defineProps<Partial<TextProps>>(), {
-  wrap: true,
-});
+import Text from '../basic/Text.vue';
 </script>

+ 3 - 7
src/components/typography/U.vue

@@ -1,13 +1,9 @@
 <template>
-  <Text v-bind="props" underline>
-    <slot v-if="$slots.default" />
+  <Text underline v-bind="$props">
+    <slot />
   </Text>
 </template>
 
 <script setup lang="ts">
-import Text, { type TextProps } from '../basic/Text.vue';
-
-const props = withDefaults(defineProps<Partial<TextProps>>(), {
-  wrap: true,
-});
+import Text from '../basic/Text.vue';
 </script>

+ 4 - 4
src/components/utils/PageAction.ts

@@ -76,11 +76,11 @@ function navTo(url: string, data: Record<string, unknown> = {}) {
  * @param name 方法名
  * @param data 要传递的数据
  */
-function callPrevOnPageBack(name: string, data: Record<string, unknown>) {
+function callPrevOnPageBack(name: string, data: Record<string, unknown>, index: number = 0) {
   var pages = getCurrentPages(); // 获取页面栈
-  var prevPage = pages[pages.length - 2] as { $vm: Record<string, unknown> }; // 上一个页面
+  var prevPage = pages[pages.length - 2 - index] as { $vm: Record<string, unknown> }; // 上一个页面
 
-  if (typeof prevPage.$vm.onPageBack === 'function')
+  if (prevPage && typeof prevPage.$vm.onPageBack === 'function')
     prevPage.$vm.onPageBack(name, data);
 }
 /**
@@ -92,7 +92,7 @@ function backAndCallOnPageBack(name: string, data: Record<string, unknown>) {
   var pages = getCurrentPages(); // 获取页面栈
   var prevPage = pages[pages.length - 2] as { $vm: Record<string, unknown> }; // 上一个页面
 
-  if (typeof prevPage.$vm.onPageBack === 'function')
+  if (prevPage && typeof prevPage.$vm.onPageBack === 'function')
     prevPage.$vm.onPageBack(name, data);
 
   uni.navigateBack({ delta: 1 });