快乐的梦鱼 3 mesiacov pred
rodič
commit
ae31b103fb
54 zmenil súbory, kde vykonal 694 pridanie a 459 odobranie
  1. 1 0
      src/components/basic/ActivityIndicator.vue
  2. 2 3
      src/components/basic/Icon.vue
  3. 6 0
      src/components/basic/IconButton.vue
  4. 23 5
      src/components/basic/Text.vue
  5. 1 0
      src/components/dialog/ActionSheet.vue
  6. 12 1
      src/components/dialog/Dialog.vue
  7. 5 1
      src/components/dialog/DialogButton.vue
  8. 62 63
      src/components/dialog/DialogInner.vue
  9. 6 0
      src/components/dialog/Overlay.vue
  10. 1 0
      src/components/dialog/Popup.vue
  11. 22 9
      src/components/display/CollapseBox.vue
  12. 8 5
      src/components/display/CollapseItem.vue
  13. 1 0
      src/components/display/Divider.vue
  14. 1 1
      src/components/display/NoticeBar.vue
  15. 24 7
      src/components/display/TextEllipsis.vue
  16. 39 23
      src/components/display/Watermark.vue
  17. 1 1
      src/components/display/countdown/CountDownButton.vue
  18. 2 2
      src/components/feedback/BackToTop.vue
  19. 19 6
      src/components/feedback/DropdownMenu.vue
  20. 27 11
      src/components/feedback/DropdownMenuItem.vue
  21. 7 2
      src/components/feedback/DropdownMenuProvide.vue
  22. 28 33
      src/components/feedback/ShareSheet.vue
  23. 25 18
      src/components/feedback/ShareSheetButtons.vue
  24. 5 0
      src/components/form/Calendar.vue
  25. 5 1
      src/components/form/CalendarItem.vue
  26. 3 3
      src/components/form/CheckBox.vue
  27. 2 0
      src/components/form/DateTimePicker.vue
  28. 3 3
      src/components/form/Radio.vue
  29. 6 2
      src/components/form/Rate.vue
  30. 2 2
      src/components/form/Signature.vue
  31. 12 21
      src/components/form/Slider.vue
  32. 2 0
      src/components/form/TimePicker.vue
  33. 12 13
      src/components/form/Uploader.vue
  34. 3 3
      src/components/form/UploaderListItem.vue
  35. 14 2
      src/components/index.scss
  36. 6 0
      src/components/keyboard/PlateKeyBoardInputBox.vue
  37. 5 1
      src/components/keyboard/PlateKeyBoardKey.vue
  38. 19 2
      src/components/layout/FlexView.vue
  39. 1 1
      src/components/layout/space/SafeAreaMargin.vue
  40. 4 2
      src/components/layout/space/SafeAreaPadding.vue
  41. 2 2
      src/components/layout/space/XBarSpace.vue
  42. 20 9
      src/components/list/FixedVirtualList.vue
  43. 46 33
      src/components/list/IndexList.vue
  44. 1 2
      src/components/list/SimpleList.vue
  45. 15 13
      src/components/list/SimpleListItem.vue
  46. 12 6
      src/components/nav/IndexBar.vue
  47. 16 16
      src/components/nav/Pagination.vue
  48. 9 2
      src/components/nav/SegmentedControl.vue
  49. 6 0
      src/components/nav/SegmentedControlItem.vue
  50. 8 4
      src/components/nav/TabBarItem.vue
  51. 48 39
      src/components/nav/Tabs.vue
  52. 7 3
      src/components/theme/ThemeTools.ts
  53. 30 18
      src/components/typography/HorizontalScrollText.vue
  54. 47 65
      src/components/typography/VerticalScrollOneText.vue

+ 1 - 0
src/components/basic/ActivityIndicator.vue

@@ -59,5 +59,6 @@ const style = computed(() => {
   border-top: 5px solid #3498db;
   border-radius: 50%;
   animation: spin 1s linear infinite;
+  box-sizing: border-box;
 }
 </style>

+ 2 - 3
src/components/basic/Icon.vue

@@ -28,12 +28,11 @@
     :style="style"
     :src="IconUtils.getColoredSvg(iconData.rawSvg, style.color)"
   />
-  <view
+  <image
     v-else-if="iconData.type == 'svg'"
     :style="style"
     :src="iconData.value"
-  >
-  </view>
+  />
   <!-- #endif -->
 
   <image

+ 6 - 0
src/components/basic/IconButton.vue

@@ -74,4 +74,10 @@ const style = computed(() => {
   };
 });
 
+defineOptions({
+  options: {
+    inheritAttrs: false,
+    virtualHost: true,
+  }
+})
 </script>

+ 23 - 5
src/components/basic/Text.vue

@@ -1,5 +1,5 @@
 <template>
-  <text :class="innerClass" :style="style" @click="onClick">
+  <text :id="id" :class="innerClass" :style="style" @click="onClick">
     <!-- #ifdef APP-NVUE -->
     {{ text }}
     <!-- #endif -->
@@ -10,8 +10,9 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, useSlots } from 'vue';
+import { computed, getCurrentInstance } from 'vue';
 import { useTheme, type ThemePaddingMargin } from '../theme/ThemeDefine';
+import { RandomUtils } from '@imengyu/imengyu-utils';
 
 export interface TextProps {
   /**
@@ -138,10 +139,10 @@ const props = withDefaults(defineProps<TextProps>(), {
 });
 const emit = defineEmits([ 'click' ])
 
+const id = `text-${RandomUtils.genNonDuplicateID(12)}`;
+const instance = getCurrentInstance();
 const { resolveThemeColor, resolveThemeSize, getText } = useTheme();
 
-const slots = useSlots();
-
 function getAutoSize() {
   //自动缩放大小,文字越长,字号越小
   const autoSizeOption = props.autoSize;
@@ -159,7 +160,6 @@ function onClick(e: any) {
   }
 }
 
-
 const style = computed(() => {
   const o : Record<string, any> = {
   }
@@ -227,12 +227,30 @@ const style = computed(() => {
   return rs;
 })
 
+async function measureTextWidth() {
+  return await new Promise<number>((resolve) => {
+    uni.createSelectorQuery()
+      // #ifdef MP
+      .in(instance)
+      // #endif
+      .select(`#${id}`)
+      .boundingClientRect((data) => {
+        resolve((data as UniApp.NodeInfo)?.width ?? 0)
+      })
+      .exec();
+  })
+}
+
+defineExpose({
+  measureTextWidth,
+})
 defineOptions({
   options: {
     virtualHost: true,
     styleIsolation: "shared",
   },
 });
+
 </script>
 
 <style lang="scss">

+ 1 - 0
src/components/dialog/ActionSheet.vue

@@ -128,6 +128,7 @@ export interface ActionSheetItem {
 const emit = defineEmits([ 'close', 'select' ]);
 const props = withDefaults(defineProps<ActionSheetProps>(), {
   mask: true,
+  safeArea: true,
   centerWidth: () => propGetThemeVar('ActionSheetCenterWidth', '600rpx'),
 });
 const themeContext = useTheme();

+ 12 - 1
src/components/dialog/Dialog.vue

@@ -7,6 +7,16 @@
   >
     <DialogInner 
       v-bind="$props"
+      :onConfirm="$props.onConfirm"
+      :onCancel="$props.onCancel"
+      :topSlots="{
+        default: Boolean($slots?.default),
+        bottomContent: Boolean($slots?.bottomContent),
+        title: Boolean($slots?.title),
+        content: Boolean($slots?.content),
+        icon: Boolean($slots?.icon),
+      }"
+      @close="onClose"
     >
       <template #default>
         <slot />
@@ -143,10 +153,11 @@ const props = withDefaults(defineProps<DialogProps>(), {
   showConfirm: true,
   contentScroll: true,
 });
-const emit = defineEmits([ 'close' ]);
+const emit = defineEmits([ 'close', 'update:show' ]);
 
 function onClose() {
   props.onClose?.();
   emit('close');
+  emit('update:show', false);
 }
 </script>

+ 5 - 1
src/components/dialog/DialogButton.vue

@@ -96,5 +96,9 @@ const props = withDefaults(defineProps<DialogButtonProps>(), {
   pressedColor: () => propGetThemeVar('DialogButtonPressedColor', 'pressed.white'),
 });
 
-
+defineOptions({
+  options: {
+    virtualHost: true,
+  }
+})
 </script>

+ 62 - 63
src/components/dialog/DialogInner.vue

@@ -1,72 +1,71 @@
 <template>
+  <!--TODO: 在uniapp插槽问题修复后,此处可修改为插槽默认值-->
   <FlexCol :innerStyle="{ ...themeStyles.dialog.value, width: width }">
-    <slot>
-      <FlexCol :padding="contentPadding" align="center">
-        <!-- 图标 -->
-        <FlexCol v-if="icon" :innerStyle="themeStyles.icon.value">
-          <slot name="icon" :icon="icon">
-            <Icon :icon="icon" :color="iconColor" :size="iconSize || 40" />
-          </slot>
-        </FlexCol>
-        <!-- 标题 -->
-        <slot name="title" :title="title">
-          <text v-if="title" :style="themeStyles.title.value">{{ title }}</text>
-        </slot>
-        <!-- 内容 -->
-        <scroll-view 
-          v-if="contentScroll" 
-          scroll-y
-          scroll-x
-          :style="{
-            position: 'relative',
-            maxHeight: contentScrollMaxHeight
-          }"
-        >
-          <slot name="content">
-            <text :style="themeStyles.contentText.value">{{ content }}</text>
-          </slot>
-        </scroll-view>
-        <slot v-else name="content">
-          <text :style="themeStyles.contentText.value">{{ content }}</text>
-        </slot>
+    <slot v-if="topSlots?.default" />
+    <FlexCol v-else :padding="contentPadding" align="center">
+      <!-- 图标 -->
+      <FlexCol v-if="icon" :innerStyle="themeStyles.icon.value">
+        <slot name="icon" :icon="icon" />
+        <Icon :icon="icon" :color="iconColor" :size="iconSize || 40" />
       </FlexCol>
-    </slot>
+      <!-- 标题 -->
+      <slot v-if="topSlots?.title" name="title" :title="title" />
+      <text v-else-if="title" :style="themeStyles.title.value">{{ title }}</text>
+
+      <!-- 内容 -->
+      <scroll-view 
+        v-if="contentScroll" 
+        scroll-y
+        scroll-x
+        :style="{
+          position: 'relative',
+          maxHeight: contentScrollMaxHeight
+        }"
+      >
+        <slot v-if="topSlots?.content" name="content" />
+        <text :style="themeStyles.contentText.value">{{ content }}</text>
+      </scroll-view>
+      <template v-else>
+        <slot v-if="topSlots?.content" name="content" />
+        <text v-else :style="themeStyles.contentText.value">{{ content }}</text>
+      </template>
+    </FlexCol>
     <!-- 底部按钮 -->
     <slot 
+      v-if="topSlots?.bottomContent"
       name="bottomContent" 
       :onConfirmClick="(name?: string) => onConfirmClick(name || 'confirm')" 
       :onCancelClick="onCancelClick"
-    >
-      <FlexView :direction="bottomVertical ? 'column' : 'row'" :innerStyle="themeStyles.bottomView.value">
-        <DialogButton
-          v-if="showCancel"
-          key="cancel"
-          :vertical="bottomVertical"
-          :text="cancelText"
-          :loading="buttomLoadingState.cancel"
-          :buttonColor="cancelColor"
-          @click="onCancelClick"
-        />
-        <DialogButton
-          v-for="(button, key) in customButtons"
-          :vertical="bottomVertical"
-          :key="key"
-          :text="button.text"
-          :loading="buttomLoadingState[button.name]"
-          :buttonColor="button.color || 'text.content'"
-          @click="onConfirmClick(button.name)"
-        />
-        <DialogButton
-          v-if="showConfirm"
-          key="confirm"
-          :vertical="bottomVertical"
-          :text="confirmText"
-          :loading="buttomLoadingState.confirm"
-          :buttonColor="confirmColor"
-          @click="onConfirmClick('confirm')"
-        />
-      </FlexView>
-    </slot>
+    />
+    <FlexView v-else :direction="bottomVertical ? 'column' : 'row'" :innerStyle="themeStyles.bottomView.value">
+      <DialogButton
+        v-if="showCancel"
+        key="cancel"
+        :vertical="bottomVertical"
+        :text="cancelText"
+        :loading="buttomLoadingState.cancel"
+        :buttonColor="cancelColor"
+        @click="onCancelClick"
+      />
+      <DialogButton
+        v-for="(button, key) in customButtons"
+        :vertical="bottomVertical"
+        :key="key"
+        :text="button.text"
+        :loading="buttomLoadingState[button.name]"
+        :buttonColor="button.color || 'text.content'"
+        @click="onConfirmClick(button.name)"
+      />
+      <DialogButton
+        v-if="showConfirm"
+        key="confirm"
+        :vertical="bottomVertical"
+        :text="confirmText"
+        :loading="buttomLoadingState.confirm"
+        :buttonColor="confirmColor"
+        @click="onConfirmClick('confirm')"
+      />
+    </FlexView>
   </FlexCol>
 </template>
 
@@ -112,7 +111,7 @@ const themeStyles = themeContext.useThemeStyles({
 });
 
 export interface DialogInnerProps extends Omit<DialogProps, 'show'> {
-
+  topSlots?: Record<string, boolean>,
 }
 
 const emit = defineEmits([ 'close' ]);
@@ -146,7 +145,7 @@ function checkAnyButtonLoading() {
 function onPopupClose() {
   emit('close');
 }
-function onCancelClick() {
+function onCancelClick() {  
   if (checkAnyButtonLoading())
     return;
   if (!props.onCancel) {

+ 6 - 0
src/components/dialog/Overlay.vue

@@ -2,7 +2,11 @@
   <Popup 
     v-bind="$props"
     closeIcon=""
+    closeable
     position="center"
+    :show="show"
+    @close="$emit('close')"
+    @update:show="$emit('update:show', $event)"
   >
     <slot />
   </Popup>
@@ -15,6 +19,8 @@ import Popup from './Popup.vue';
 export interface OverlayProps extends Omit<PopupProps, 'closeIcon'|'position'> {
 }
 
+defineEmits(['close','update:show'])
+
 withDefaults(defineProps<OverlayProps>(), {
   maskColor: 'background.mask',
   mask: true,

+ 1 - 0
src/components/dialog/Popup.vue

@@ -232,6 +232,7 @@ const props = withDefaults(defineProps<PopupProps>(), {
 });
 
 function handleClose(e: Event) {
+  
   e.stopPropagation();
   if (props.closeable)
     doClose();

+ 22 - 9
src/components/display/CollapseBox.vue

@@ -1,16 +1,20 @@
 <template>
-  <view :id="`e${id}`" class="nana-collapse-box" :style="{
-    display: realOpenState ? '' : 'none',
-    height: animDuration > 0 && targetHeight >= 0 ? `${targetHeight}px` : undefined,
-    transition: animDuration > 0 ? `height ${animDuration}ms ease-in-out` : undefined,
-  }">
+  <view 
+    :id="id" 
+    class="nana-collapse-box" 
+    :style="{
+      display: realOpenState ? '' : 'none',
+      height: animDuration > 0 && targetHeight >= 0 ? `${targetHeight}px` : undefined,
+      transition: animDuration > 0 ? `height ${animDuration}ms ease-in-out` : undefined,
+    }"
+  >
     <slot />
   </view>
 </template>
 
 <script setup lang="ts">
 import { RandomUtils } from '@imengyu/imengyu-utils';
-import { nextTick, ref, watch } from 'vue';
+import { computed, getCurrentInstance, nextTick, ref, watch } from 'vue';
 
 export interface CollapseBoxProps {
   /**
@@ -22,15 +26,20 @@ export interface CollapseBoxProps {
    * @default 300
    */
   animDuration?: number;
+  /**
+   * 名称,用于唯一标识
+   */
+  name?: string;
 }
 
-const id = RandomUtils.genNonDuplicateIDHEX(12);
+const id = computed(() => `nana-collapse-box-${props.name}-${RandomUtils.genNonDuplicateIDHEX(16)}`);
 const props = withDefaults(defineProps<CollapseBoxProps>(), {
   animDuration: 300,
 });
 
 const realOpenState = ref(false);
 const targetHeight = ref(0);
+const instance = getCurrentInstance();
 let isAnimWorking = false;
 
 watch(() => props.open, (newVal) => {
@@ -46,9 +55,13 @@ watch(() => props.open, (newVal) => {
     isAnimWorking = true;
     nextTick(() => {
       uni.createSelectorQuery()
-        .select(`#e${id}`)
+        // #ifdef MP
+        .in(instance)
+        // #endif
+        .select(`#${id.value}`)
         .boundingClientRect(rect => {
-          const height = (rect as UniApp.NodeInfo).height || 500;
+          const ref = rect instanceof Array ? rect[0] : rect;
+          const height = ref?.height || 500;
           targetHeight.value = 0;
           setTimeout(() => {
             targetHeight.value = height;

+ 8 - 5
src/components/display/CollapseItem.vue

@@ -21,19 +21,22 @@
         }"
         :topBorder="props.border"
         @click="context.itemClick(id)"
-      >
+      > 
+        <!-- TODO: Fix -->
+        <!-- #ifndef MP -->
         <template v-if="$slots.icon" #leftIcon>
           <slot name="icon" />
         </template>
         <template v-if="$slots.label" #label>
           <slot name="label" />
         </template>
-        <template v-if="$slots.title" #title>
-          <slot name="title" />
-        </template>
         <template v-if="$slots.value" #value>
           <slot name="value" />
         </template>
+        <template v-if="$slots.title" #title>
+          <slot name="title" />
+        </template>
+        <!-- #endif -->
         <template #rightIcon>
           <Icon 
             icon="arrow-down" 
@@ -42,7 +45,7 @@
         </template>
       </Cell>
     </slot>
-    <CollapseBox :open="state" :anim-duration="context.animDuration.value">
+    <CollapseBox :open="state" :anim-duration="context.animDuration.value" :name="name || position.value">
       <slot />
     </CollapseBox>
   </FlexView>

+ 1 - 0
src/components/display/Divider.vue

@@ -152,6 +152,7 @@ const barStyle = computed(() => {
 
   &.vertical {
     flex-direction: column;
+    height: 100%;
   }
   &.horizontal {
     flex-direction: row;

+ 1 - 1
src/components/display/NoticeBar.vue

@@ -19,7 +19,7 @@
     </slot>
 
     <view v-if="scroll" :style="themeStyles.contentView.value">
-      <HorizontalScrollText :innerStyle="textStyleFinal" :scrollDuration="scrollDuration">{{ content }}</HorizontalScrollText>
+      <HorizontalScrollText :innerStyle="textStyleFinal" :scrollDuration="scrollDuration" :text="content" />
     </view>
     <Text v-else :lines="wrap ? undefined : 1" :innerStyle="textStyleFinal" :text="content" />
     

+ 24 - 7
src/components/display/TextEllipsis.vue

@@ -1,10 +1,7 @@
 <script setup lang="ts">
 import { computed, ref } from 'vue';
-import Button from '../basic/Button.vue';
 import type { TextProps } from '../basic/Text.vue';
 import Text from '../basic/Text.vue';
-import FlexRow from '../layout/FlexRow.vue';
-import FlexView from '../layout/FlexView.vue';
 import FlexCol from '../layout/FlexCol.vue';
 
 export interface TextEllipsisProps extends TextProps {
@@ -36,6 +33,13 @@ function handleClick() {
   open.value = !open.value;
   emit(open.value ? 'expand' : 'collapse');
 }
+
+defineOptions({
+  options: {
+    virtualHost: true,
+    styleIsolation: "shared",
+  },
+})
 </script>
 
 <template>
@@ -50,9 +54,22 @@ function handleClick() {
       </slot>
     </Text>
     <slot v-if="expandable" name="button" :onClick="handleClick">
-      <FlexView direction="row" justify="flex-end" touchable @click="handleClick">
-        <Text :text="open ? closeText : openText" color="primary" />
-      </FlexView>
+      <Text 
+        innerClass="nana-text-ellipsis-expand"
+        touchable
+        :text="open ? closeText : openText"
+        color="primary" 
+        @click="handleClick"
+      />
     </slot>
   </FlexCol>
-</template>
+</template>
+
+<style>
+.nana-text-ellipsis-expand {
+  /* #ifndef APP-NVUE */
+  display: inline-block;
+  /* #endif */
+  text-align: right;
+}
+</style>

+ 39 - 23
src/components/display/Watermark.vue

@@ -1,6 +1,5 @@
 <template>
   <canvas 
-    type="2d"
     :id="id"
     :canvas-id="id"
     :class="[
@@ -8,23 +7,23 @@
       {'nana-watermark-full-page': props.fullPage}
     ]"
     :style="style"
-    :width="width"
-    :height="height"
+    :width="`${measuredWidth}px`"
+    :height="`${measuredHeight}px`"
   />
 </template>
 
 <script setup lang="ts">
-import { computed, nextTick, onMounted, ref, watch } from 'vue';
+import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue';
 import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
 import { RandomUtils } from '@imengyu/imengyu-utils';
 
 export interface WatermarkProps {
   /**
-   * 水印组件宽度。默认占满父容器
+   * 水印组件宽度(px)。默认占满父容器
    */
   width?: number,
   /**
-   * 水印组件高度。默认占满父容器
+   * 水印组件高度(px)。默认占满父容器
    */
   height?: number,
   /**
@@ -68,6 +67,7 @@ export interface WatermarkProps {
   contentColor?: string,
   /**
    * 水印图片
+   * @remark 小程序似乎不能使用本地图片,需要使用网络图片地址。
    */
   image?: string,
   /**
@@ -97,6 +97,7 @@ export interface WatermarkProps {
 }
 
 const id = `watermarkCanvas${RandomUtils.genNonDuplicateID(10)}`;
+const instance = getCurrentInstance();
 const props = withDefaults(defineProps<WatermarkProps>(), {
   zIndex: 999,
   content: '',
@@ -114,15 +115,18 @@ const props = withDefaults(defineProps<WatermarkProps>(), {
   opacity: () => propGetThemeVar('WatermarkOpacity', 0.4),
 });
 
+const measuredWidth = ref(0);
+const measuredHeight = ref(0);
+
 const theme = useTheme();
 const style = computed(() => ({
-  width: theme.resolveThemeSize(props.width),
-  height: theme.resolveThemeSize(props.height),
+  width: measuredWidth.value ? `${measuredWidth.value}px` : undefined,
+  height: measuredHeight.value ? `${measuredHeight.value}px` : undefined,
   zIndex: props.zIndex,
   opacity: props.opacity,
 }));
 
-const ctx = uni.createCanvasContext(id);
+const ctx = uni.createCanvasContext(id, instance);
 
 async function drawWatermark() {
 
@@ -138,24 +142,34 @@ async function drawWatermark() {
   await nextTick();
   await new Promise<void>((resolve) => {
     if (!vh || !vw) {
-      uni.createSelectorQuery().select(`#${id}`).boundingClientRect().exec((res) => {
-        if (res[0]) {
-          if (!vh)
-            vh = res[0].height;
-          if (!vw)
-            vw = res[0].width;
-        }
-        resolve();
-      });
+      uni.createSelectorQuery()
+        // #ifndef H5
+        .in(instance)
+        // #endif
+        .select(`#${id}`)
+        .boundingClientRect().exec((res) => {
+          if (res[0]) {
+            if (!vh)
+              vh = res[0].height;
+            if (!vw)
+              vw = res[0].width;
+          }
+          resolve();
+        });
     }
   });
 
+  measuredWidth.value = vw;
+  measuredHeight.value = vh;
+
   // 清除 canvas
   ctx.clearRect(0, 0, vw, vh);
 
   if (props.image) {
     // --- 绘制图片水印 ---
     await new Promise<void>((resolve) => {
+      console.log(props.image);
+      
       uni.getImageInfo({
         src: props.image,
         success(res) {
@@ -170,16 +184,18 @@ async function drawWatermark() {
           }
           ctx.draw();
           resolve();
-        }
+        },
+        fail:(fail)=>{
+          console.error('绘制水印图片失败', fail);
+          resolve();
+        },
+
       });
     }); 
   } else if (props.content) {
-    ctx.fillStyle = '#f00';
-    ctx.font = '57px Arial';  
-    ctx.fillText('Aaaaaaaaa', 0, 0);
     ctx.setTextAlign('center');
     ctx.setTextBaseline('middle');
-    ctx.fillStyle = theme.resolveThemeColor(props.contentColor)!;
+    ctx.setFillStyle(theme.resolveThemeColor(props.contentColor)!);
     ctx.font = props.contentFont;  
 
     for (let i = props.offsetX; i < vw; i += cw) {

+ 1 - 1
src/components/display/countdown/CountDownButton.vue

@@ -19,8 +19,8 @@
 
 <script setup lang="ts">
 import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
-import { type TextProps } from '@/components/basic/Text.vue';
 import { SimpleTimer } from '@/components/utils/Timer';
+import type { TextProps } from '@/components/basic/Text.vue';
 import Button from '@/components/basic/Button.vue';
 
 export interface CountDownButtonProps extends TextProps {

+ 2 - 2
src/components/feedback/BackToTop.vue

@@ -142,8 +142,6 @@ function onClick() {
   emit('click');
 }
 
-
-
 onPageScroll((e) => {
   if (props.customScrollValue)
     return;
@@ -153,12 +151,14 @@ onPageScroll((e) => {
 defineExpose<BackToTopInstance>({
   customScrollTop,
   onScroll(e) {
+    console.log('onScroll', e);
     if (props.customScrollValue)
       onScroll(e.detail.scrollTop);
   },
 })
 defineOptions({
   options: {
+    virtualHost: true,
     styleIsolation: "shared",
   },
 });

+ 19 - 6
src/components/feedback/DropdownMenu.vue

@@ -4,12 +4,11 @@
       <slot />
     </view>
     <scroll-view scroll-x>
-      <view class="nana-dropdown-menu-extra">
-        <DropdownMenuProvide isExtra>
-          <slot name="extra">
-          </slot>
-        </DropdownMenuProvide>
-      </view>
+      <DropdownMenuProvide :isExtra="true">
+        <view class="nana-dropdown-menu-extra">
+          <slot name="extra" />
+        </view>
+      </DropdownMenuProvide>
     </scroll-view>
   </view>
 </template>
@@ -44,6 +43,11 @@ export interface DropdownMenuProps {
    * 动画时长,单位ms,设置为 0 可以禁用动画
    */
   duration?: number;
+  /**
+   * 是否包含导航栏空间,这会影响弹出菜单的定位
+   * @default true
+   */
+  includeNavBarSpace?: boolean;
 }
 
 const props = withDefaults(defineProps<DropdownMenuProps>(), {
@@ -51,6 +55,7 @@ const props = withDefaults(defineProps<DropdownMenuProps>(), {
   backgroundColor: () => propGetThemeVar('DropdownMenuBackgroundColor', 'white'),
   direction: 'down',
   duration: 300,
+  includeNavBarSpace: true,
 });
 
 let closeCb: (() => void)|null = null;
@@ -66,6 +71,7 @@ export interface DropdownMenuContext {
   activeColor: Ref<string>;
   backgroundColor: Ref<string>;
   itemStyle: Ref<ViewStyle|undefined>;
+  includeNavBarSpace: Ref<boolean>;
   itemExtraStyle: Ref<ViewStyle|undefined>;
   direction: Ref<'up'|'down'>;
   duration: Ref<number>;
@@ -75,6 +81,7 @@ export interface DropdownMenuContext {
 provide<DropdownMenuContext>('DropdownMenuContext', {
   activeColor: toRef(props, 'activeColor'),
   backgroundColor: toRef(props, 'backgroundColor'),
+  includeNavBarSpace: toRef(props, 'includeNavBarSpace'),
   itemStyle: toRef(props, 'itemStyle'),
   itemExtraStyle: toRef(props, 'itemExtraStyle'),
   direction: toRef(props, 'direction'),
@@ -94,6 +101,12 @@ defineExpose<DropdownMenuInstance>({
   close,
 })
 
+defineOptions({
+  options: {
+    inheritAttrs: false,
+    styleIsolation: 'shared',
+  }
+})
 </script>
 
 <style lang="scss">

+ 27 - 11
src/components/feedback/DropdownMenuItem.vue

@@ -3,7 +3,7 @@
     :id="id"
     :class="[
       'nana-dropdown-menu-item', 
-      isExtra ? 'extra' : 'normal',
+      extra || isExtra ? 'extra' : 'normal',
       disabled ? 'disabled' : '',
       activeState ? 'active' : '',
       openState ? 'open' : '',
@@ -11,13 +11,14 @@
     :hover-class="openState ? '' : 'pressed'"
     :style="{
       backgroundColor: topContext.backgroundColor.value,
-      ...(isExtra ? itemExtraStyle : itemStyle),
-      ...(isExtra ? topContext.itemExtraStyle.value : topContext.itemStyle.value),
-      ...(isExtra && activeState ? itemExtraActiveStyle : {}),
-      ...(isExtra && openState ? itemExtraOpenStyle : {}),
+      ...((extra || isExtra) ? itemExtraStyle : itemStyle),
+      ...((extra || isExtra) ? topContext.itemExtraStyle.value : topContext.itemStyle.value),
+      ...((extra || isExtra) && activeState ? itemExtraActiveStyle : {}),
+      ...((extra || isExtra) && openState ? itemExtraOpenStyle : {}),
     }"
+    @click="handleClick"
   >
-    <view class="inner" @click="handleClick">
+    <view class="inner">
       <slot name="icon">
         <Icon 
           v-if="icon"
@@ -50,6 +51,7 @@
       :position="topContext.direction.value === 'up' ? 'bottom' : 'top'"
       :closeIcon="false"
       :closeable="true"
+      :safeArea="false"
       :inset="popupMargin"
       :duration="topContext.duration.value"
       size="auto"
@@ -74,7 +76,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, inject, ref, useSlots } from 'vue';
+import { computed, getCurrentInstance, inject, ref, useSlots, type Ref } from 'vue';
 import Icon from '../basic/Icon.vue';
 import Popup from '../dialog/Popup.vue';
 import { useTheme } from '../theme/ThemeDefine';
@@ -131,6 +133,8 @@ export interface DropdownMenuItemProps {
    * @default 'default'
    */
   activated?: boolean|'default'|'none';
+
+  extra?: boolean;
 }
 
 const id = `dropItem${RandomUtils.genNonDuplicateID(20)}`;
@@ -143,7 +147,8 @@ const props = withDefaults(defineProps<DropdownMenuItemProps>(), {
 const emit = defineEmits(['update:modelValue', 'click']);
 const slots = useSlots();
 
-const isExtra = inject('isExtra', false);
+const instance = getCurrentInstance()!;
+const isExtra = inject<Ref<boolean>>('dropdownMenuisExtra', ref(false));
 const topContext = inject<DropdownMenuContext>('DropdownMenuContext')!;
 
 const openState = ref(false);
@@ -224,6 +229,9 @@ async function updateDialogMargin() {
 
   const d = await new Promise<UniApp.NodeInfo>((resolve, reject) => {
     uni.createSelectorQuery()
+      // #ifdef MP
+      .in(instance)
+      // #endif
       .select(`#${id}`)
       .fields({
         id: true,
@@ -241,10 +249,12 @@ async function updateDialogMargin() {
   });
   const systemInfo = uni.getSystemInfoSync();
   let v = 0
-  // #ifdef H5
-  v += 44;
-  // #endif
   if (topContext.direction.value === 'up') {
+    
+    if (topContext.includeNavBarSpace.value) {
+      v += 44;
+      v += systemInfo.statusBarHeight || 0;
+    }
     v += d.top ?? 0;
     v = systemInfo.screenHeight - v;
     popupMargin.value[2] = `${v}px`;
@@ -306,6 +316,12 @@ defineExpose({
   open,
   toggle,
 })
+defineOptions({
+  options: {
+    inheritAttrs: false,
+    virtualHost: true,
+  }
+})
 </script>
 
 <style lang="scss">

+ 7 - 2
src/components/feedback/DropdownMenuProvide.vue

@@ -3,7 +3,7 @@
 </template>
 
 <script setup lang="ts">
-import { provide } from 'vue';
+import { provide, toRef } from 'vue';
 
 const props = defineProps({
   isExtra: {
@@ -12,8 +12,13 @@ const props = defineProps({
   }
 });
 
-provide('isExtra', props.isExtra);
+provide('dropdownMenuisExtra', toRef(props, 'isExtra'));
 
+defineOptions({
+  options: {
+    virtualHost: true,
+  }
+})
 </script>
 
 <style lang="scss">

+ 28 - 33
src/components/feedback/ShareSheet.vue

@@ -11,45 +11,41 @@
     @update:show="emit('update:show', $event)"
   >
     <slot :close="onCancelClick">
-      <FlexCol>
+      <FlexCol width="100%">
         <ShareSheetTitle :title="title" :description="description" />
         <slot name="content">
           <FlexCol v-if="Array.isArray(items[0])" position="relative" wrap>
-            
             <template v-for="(group, k) in (items as ShareSheetItem[][])" :key="k" >
-              <FlexRow position="relative" justify="space-between" align="center" wrap>
-                <ShareSheetButtons
-                  :items="group"
-                  :iconSize="iconSize"
-                  :itemStyle="{ 
-                    ...themeStyles.item.value,
-                    ...itemStyle, 
-                  }"
-                  :itemTextStyle="{
-                    ...themeStyles.itemText.value,
-                    ...itemTextStyle
-                  }"
-                  @click="onItemClick"
-                />
-              </FlexRow>
+              <ShareSheetButtons
+                :items="group"
+                :iconSize="iconSize"
+                :itemStyle="{ 
+                  ...themeStyles.item.value,
+                  ...itemStyle, 
+                }"
+                :itemTextStyle="{
+                  ...themeStyles.itemText.value,
+                  ...itemTextStyle
+                }"
+                @click="onItemClick"
+              />
               <Divider v-if="k < items.length - 1" />
             </template>
           </FlexCol>
-          <FlexRow v-else position="relative" justify="space-between" align="center" wrap>
-            <ShareSheetButtons
-              :items="(items as ShareSheetItem[])"
-              :iconSize="iconSize"
-              :itemStyle="{ 
-                ...themeStyles.item.value,
-                ...itemStyle, 
-              }"
-              :itemTextStyle="{
-                ...themeStyles.itemText.value,
-                ...itemTextStyle
-              }"
-              @click="onItemClick"
-            />
-          </FlexRow>
+          <ShareSheetButtons
+            v-else 
+            :items="(items as ShareSheetItem[])"
+            :iconSize="iconSize"
+            :itemStyle="{ 
+              ...themeStyles.item.value,
+              ...itemStyle, 
+            }"
+            :itemTextStyle="{
+              ...themeStyles.itemText.value,
+              ...itemTextStyle
+            }"
+            @click="onItemClick"
+          />
         </slot>
         <FlexCol v-if="showCancel" position="relative" :innerStyle="themeStyles.viewCancel.value">
           <ShareSheetItem
@@ -74,7 +70,6 @@ import ShareSheetItem from '../dialog/ActionSheetItem.vue';
 import ShareSheetTitle from '../dialog/ActionSheetTitle.vue';
 import Popup, { type PopupProps } from '../dialog/Popup.vue';
 import FlexCol from '../layout/FlexCol.vue';
-import FlexRow from '../layout/FlexRow.vue';
 import Divider from '../display/Divider.vue';
 import ShareSheetButtons from './ShareSheetButtons.vue';
 

+ 25 - 18
src/components/feedback/ShareSheetButtons.vue

@@ -1,20 +1,22 @@
 <template>
-  <IconButton
-    v-for="(item, k) in items"
-    :key="k"
-    :icon="getDefaultIconNames(item.icon)"
-    :disabled="item.disabled"
-    :color="item.color"
-    :size="iconSize"
-    :backgroundColor="item.backgroundColor"
-    direction="column"
-    justify="center"
-    align="center"
-    :buttonStyle="itemStyle"
-    @click="emit('click', item)"
-  >
-    <Text v-if="item.title" :innerStyle="itemTextStyle">{{ item.title }}</Text>
-  </IconButton>
+  <FlexRow position="relative" align="center" wrap>
+    <IconButton
+      v-for="(item, k) in items"
+      :key="k"
+      :icon="getDefaultIconNames(item.icon)"
+      :disabled="item.disabled"
+      :color="item.color"
+      :size="iconSize"
+      :backgroundColor="item.backgroundColor"
+      direction="column"
+      justify="center"
+      align="center"
+      :buttonStyle="itemStyle"
+      @click="emit('click', item)"
+    >
+      <Text v-if="item.title" :innerStyle="itemTextStyle">{{ item.title }}</Text>
+    </IconButton>
+  </FlexRow>
 </template>
 
 <script setup lang="ts">
@@ -31,6 +33,7 @@ import IconWechatAppQrcode from '../images/share_sheet_weapp_qrcode.png';
 import type { ViewStyle, TextStyle } from '../theme/ThemeDefine';
 import Text from '../basic/Text.vue';
 import IconButton from '../basic/IconButton.vue';
+import FlexRow from '../layout/FlexRow.vue';
 
 const props = defineProps<{
   items: ShareSheetItem[];
@@ -42,8 +45,6 @@ const emit = defineEmits<{
   (e: 'click', item: ShareSheetItem): void;
 }>();
 
-
-
 function getDefaultIconNames(name: string|undefined) {
   switch (name) {
     case 'mobile': return IconMobile;
@@ -58,4 +59,10 @@ function getDefaultIconNames(name: string|undefined) {
   }
   return name;
 }
+
+defineOptions({
+  options: {
+    virtualHost: true,
+  }
+})
 </script>

+ 5 - 0
src/components/form/Calendar.vue

@@ -654,6 +654,11 @@ provide<CalendarContext>('calendarContext', {
   pickStateText: toRef(props, 'pickStateText'),
 })
 
+defineOptions({
+  options: {
+    styleIsolation: 'shared',
+  }
+})
 </script>
 
 <style lang="scss">

+ 5 - 1
src/components/form/CalendarItem.vue

@@ -146,5 +146,9 @@ const finalBottomText = computed(() => {
   return '';
 })
 
-
+defineOptions({
+  options: {
+    virtualHost: true,
+  }
+})
 </script>

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

@@ -1,6 +1,6 @@
 <template>
   <FlexRow
-    touchable
+    :touchable="!disabled"
     :activeOpacity="activeOpacity"
     :innerStyle="{ 
       ...(block ? themeStyles.checkBoxFull.value : themeStyles.checkBox.value), 
@@ -60,7 +60,7 @@ import FlexRow from '../layout/FlexRow.vue';
 import CheckBoxDefaultButton from './CheckBoxDefaultButton.vue';
 import Text from '../basic/Text.vue';
 import { DynamicSize } from '../theme/ThemeTools';
-import { CellContextKey, type CellContext } from '../basic/CellContext';
+import { useCellContext } from '../basic/CellContext';
 
 
 export interface CheckBoxProps {
@@ -190,7 +190,7 @@ const themeStyles = themeContext.useThemeStyles({
 });
 
 const groupContext = inject<CheckBoxGroupContextInfo>('checkBoxGroupContext', null as any);
-const cellContext = inject<CellContext>(CellContextKey);
+const cellContext = useCellContext();
 const disabled = computed(() => props.disabled === true || groupContext?.disabled.value);
 
 

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

@@ -67,6 +67,8 @@ const value = computed(() => {
   let date = props.modelValue;
   if (!date)
     date = new Date();
+  if (!(date instanceof Date))
+    date = typeof date === 'string' ? DateUtils.parseDate(date) : new Date();
   if (props.showYears) 
     value.push(date.getFullYear());
   if (props.showMonths)

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

@@ -1,6 +1,6 @@
 <template>
   <FlexRow
-    touchable
+    :touchable="!disabled"
     :activeOpacity="activeOpacity"
     :innerStyle="{ 
       ...(block ? themeStyles.checkBoxFull.value : themeStyles.checkBox.value), 
@@ -62,7 +62,7 @@ import FlexRow from '../layout/FlexRow.vue';
 import CheckBoxDefaultButton from './CheckBoxDefaultButton.vue';
 import Text from '../basic/Text.vue';
 import type { RadioBoxGroupContextInfo } from './RadioGroup.vue';
-import { CellContextKey, type CellContext } from '../basic/CellContext';
+import { useCellContext } from '../basic/CellContext';
 
 export interface RadioBoxProps {
   /**
@@ -182,7 +182,7 @@ const themeStyles = themeContext.useThemeStyles({
 });
 
 const groupContext = inject<RadioBoxGroupContextInfo>('RadioBoxGroupContext', null as any);
-const cellContext = inject<CellContext>(CellContextKey);
+const cellContext = useCellContext();;
 const value = computed(() => {
   if (!groupContext)
     return false;

+ 6 - 2
src/components/form/Rate.vue

@@ -48,7 +48,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, toRef } from 'vue';
+import { computed, getCurrentInstance, toRef } from 'vue';
 import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
 import { useFieldChildValueInjector } from './FormContext';
 import type { IconProps } from '../basic/Icon.vue';
@@ -168,6 +168,7 @@ const starDeactiveStyle = computed(() => ({
 }));
 
 const id = RandomUtils.genNonDuplicateID(12);
+const instance = getCurrentInstance();
 const width = computed(() => (props.size + props.space) * props.count);
 
 const {
@@ -211,7 +212,10 @@ function handleTouchStart(e: any) {
     return;
   e.stopPropagation();
   const query = uni.createSelectorQuery();
-  query.select('#' + id).boundingClientRect((res) => {
+  query
+    .in(instance)
+    .select('#' + id)
+    .boundingClientRect((res) => {
     if (res)
       absLeft = (res as any).left;
     handleDrag(e.touches[0]?.clientX);

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

@@ -6,7 +6,6 @@
   >
     <canvas
       class="signature-canvas"
-      type="2d"
       disable-scroll
       :canvas-id="id"
       :style="canvasStyle"
@@ -86,7 +85,8 @@ let isDirty = true;
 async function initCanvas() {
   // 获取容器尺寸信息
   containerRect.value = await new Promise<UniApp.NodeInfo>((resolve) => {
-    uni.createSelectorQuery().in(instance)
+    uni.createSelectorQuery()
+      .in(instance)
       .select('.signature-container')
       .boundingClientRect(resolve as any)
       .exec();

+ 12 - 21
src/components/form/Slider.vue

@@ -1,21 +1,13 @@
 <template>
-  <slider 
-    v-if="native" 
-    :min="min"
-    :max="max"
-    :value="value"
-    :step="step"
-    :disabled="disabled"
-    @changing="(e: any) => doUpdateValue(e.detail.value)"
-    @change="(e: any) => doUpdateValue(e.detail.value)"
-  />
-  <view v-else 
+  <view
     class="nana-slider"
     :id="id"
     :style="{
       opacity: disabled ? 0.6 : 1,
       width: direction == 'horizontal' ? undefined : themeContext.resolveSize(dotSize),
       height: direction == 'vertical' ? undefined : themeContext.resolveSize(dotSize),
+      minWidth: themeContext.resolveSize(dotSize),
+      minHeight: themeContext.resolveSize(dotSize),
       ...innerStyle,
     }"
     @touchstart="handleTouchStart"
@@ -52,7 +44,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, toRef } from 'vue';
+import { computed, getCurrentInstance, onMounted, toRef } from 'vue';
 import { propGetThemeVar, useTheme, type ViewStyle } from '../theme/ThemeDefine';
 import { DynamicVar } from '../theme/ThemeTools';
 import { useFieldChildValueInjector } from './FormContext';
@@ -65,11 +57,6 @@ export interface SliderProps {
    */
   modelValue?: number;
   /**
-   * 是否使用原生 Slider 组件
-   * @default false
-   */
-  native?: boolean;
-  /**
    * 滑动方向
    * @default horizontal
    */
@@ -128,11 +115,11 @@ export interface SliderProps {
   innerStyle?: ViewStyle,
 }
 
+
 const emit = defineEmits([ 'update:modelValue' ])
 
 const props = withDefaults(defineProps<SliderProps>(), {
   modelValue: 50,
-  native: false,
   activeColor: () => propGetThemeVar('SliderActiveColor', 'primary'),
   inactiveColor: () => propGetThemeVar('SliderInactiveColor', 'background.switch'),
   dotColor: () => propGetThemeVar('SliderDotColor', 'white'),
@@ -164,8 +151,7 @@ const {
   (v) => emit('update:modelValue', v)
 );
 
-
-
+const instance = getCurrentInstance();
 const id = RandomUtils.genNonDuplicateID(12);
 let absLeft = 0;
 let absWidth = 0;
@@ -188,7 +174,12 @@ function handleTouchStart(e: any) {
     return;
   e.stopPropagation();
   const query = uni.createSelectorQuery();
-  query.select('#' + id).boundingClientRect((res) => {
+  query
+    // #ifdef MP
+    .in(instance)
+    // #endif
+    .select('#' + id)
+    .boundingClientRect((res) => {
     if (res) {
       absLeft = (res as any).left;
       absWidth = (res as any).width;

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

@@ -41,6 +41,8 @@ const value = computed(() => {
   let date = props.modelValue;
   if (!date)
     date = new Date();
+  if (!(date instanceof Date))
+    date = typeof date === 'string' ? DateUtils.parseDate(date) : new Date();
   if (props.showHours) 
     value.push(date.getHours());
   if (props.showMinute)

+ 12 - 13
src/components/form/Uploader.vue

@@ -19,19 +19,18 @@
         >
           <slot 
             name="uploadItem"
-            v-bind="{
-              key: index,
-              item: item,
-              onClick: () => onItemPress(item),
-              onDeleteClick: () => onItemDeletePress(item),
-              style: props.itemStyle,
-              imageStyle: props.itemImageStyle,
-              itemMaskStyle: props.itemMaskStyle,
-              itemMaskTextStyle: props.itemMaskTextStyle,
-              itemSize,
-              showDelete: showDelete,
-              defaultSource: props.itemDefaultSource,
-            }"
+            :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"
+              
           >
             <UploaderListItem
               :item="item"

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

@@ -32,14 +32,14 @@
         backgroundColor: themeContext.resolveThemeColor('UploaderListItemUploadingBackgroundColor', 'mask.primary'),
       }">
         <ActivityIndicator :color="itemMaskTextColor" :size="loadingSize" />
-        <Text :style="itemMaskTextStyle">{{item.message}}</Text>
+        <Text :color="itemMaskTextColor" :style="itemMaskTextStyle">{{item.message}}</Text>
       </FlexView>
       <FlexView v-if="item.state === 'fail'" :innerStyle="{
         ...itemMaskStyle,
         backgroundColor: themeContext.resolveThemeColor('UploaderListItemUploadingBackgroundColor', 'mask.danger'),
       }">
         <Icon icon="error" :color="itemMaskTextColor" :size="iconSize" />
-        <Text :style="itemMaskTextStyle">{{item.message}}</Text>
+        <Text :color="itemMaskTextColor" :style="itemMaskTextStyle">{{item.message}}</Text>
       </FlexView>
     </template>
 
@@ -70,7 +70,7 @@
       </FlexCol>
       <Icon v-if="item.state === 'success'" icon="select-bold" color="success" />
       <Icon v-else-if="item.state === 'fail'" icon="close-bold" color="danger" />
-      <ActivityIndicator v-else-if="item.state === 'uploading'" :size="22" />
+      <ActivityIndicator v-else-if="item.state === 'uploading'" :size="45" />
       <Width :size="20" />
       <IconButton v-if="showDelete" icon="trash" @click.stop="emit('delete')" />
     </FlexRow>

+ 14 - 2
src/components/index.scss

@@ -1,4 +1,14 @@
-@keyframes horizontalScrollText {
+@keyframes horizontalScrollTextRight {
+  0% {
+    transform: translateX(-120%);
+    margin-left: 0;
+  }
+  100% {
+    transform: translateX(0%);
+    margin-left: 100%;
+  }
+}
+@keyframes horizontalScrollTextLeft {
   0% {
     transform: translateX(0%);
     margin-left: 100%;
@@ -9,4 +19,6 @@
   }
 }
 
-
+wx-action-sheet-item {
+  padding: 0!important;
+}

+ 6 - 0
src/components/keyboard/PlateKeyBoardInputBox.vue

@@ -48,4 +48,10 @@ const themeStyles = themeContext.useThemeStyles({
     borderColor: DynamicColor('PlateKeyBoardInputBoxActiveColor', 'primary'),
   },
 });
+
+defineOptions({
+  options: {
+    virtualHost: true,
+  }
+})
 </script>

+ 5 - 1
src/components/keyboard/PlateKeyBoardKey.vue

@@ -68,5 +68,9 @@ const keyStyle = computed(() => ({
   ...context.keyStyle.value,
 }));
 
-
+defineOptions({
+  options: {
+    virtualHost: true,
+  }
+})
 </script>

+ 19 - 2
src/components/layout/FlexView.vue

@@ -1,6 +1,6 @@
 <template>
   <view
-    :id="innerId"
+    :id="innerId ?? id"
     :class="[
       'nana-flex-layout', { 
         'nana-flex-row': direction === 'row', 
@@ -26,9 +26,10 @@
 /**
  * 组件说明:Flex组件,用于一些布局中快速写容器,是一系列盒子的基础组件。
  */
-import { computed, onMounted, ref } from 'vue';
+import { computed, getCurrentInstance, onMounted, ref } from 'vue';
 import { useTheme, type ThemePaddingMargin } from '../theme/ThemeDefine';
 import { configMargin, configPadding } from '../theme/ThemeTools';
+import { RandomUtils } from '@imengyu/imengyu-utils';
 
 export type FlexDirection = "row"|"column"|'row-reverse'|'column-reverse';
 export type FlexJustifyType =  'flex-start' | 'flex-end' | 'center' |'space-between' |'space-around' |'space-evenly';
@@ -268,6 +269,22 @@ function handleTouchEnd() {
 onMounted(() => {
   emit('state', 'default')
 })
+    
+const instance = getCurrentInstance();
+const id = 'flex-item-' + RandomUtils.genNonDuplicateID(15);
+
+defineExpose({
+  measure() {
+    return new Promise((resolve) => {
+      const tabItem = uni.createSelectorQuery()
+        .in(instance)
+        .select(`#${id}`);
+      tabItem.boundingClientRect().exec((res) => {
+        resolve(res)
+      })
+    });
+  },
+})
 </script>
 
 <style>

+ 1 - 1
src/components/layout/space/SafeAreaMargin.vue

@@ -10,7 +10,7 @@
 </template>
 
 <script setup lang="ts">
-const systemInfo = (globalThis as any).uni ? uni.getSystemInfoSync() : undefined;
+const systemInfo = uni.getSystemInfoSync();
 const safeAreaInsets = systemInfo?.safeAreaInsets || {
   top: 0,
   bottom: 0,

+ 4 - 2
src/components/layout/space/SafeAreaPadding.vue

@@ -12,14 +12,16 @@
 </template>
 
 <script setup lang="ts">
-const systemInfo = (globalThis as any).uni ? uni.getSystemInfoSync() : undefined;
-const safeAreaInsets = systemInfo?.safeAreaInsets || {
+const systemInfo = uni.getSystemInfoSync();
+const safeAreaInsets = systemInfo.safeAreaInsets || {
   top: 0,
   bottom: 0,
   left: 0,
   right: 0,
 };
 
+//console.log(safeAreaInsets);
+
 export interface SafeAreaPaddingProps {
   top?: boolean;
   bottom?: boolean;

+ 2 - 2
src/components/layout/space/XBarSpace.vue

@@ -3,8 +3,8 @@
 </template>
 
 <script setup lang="ts">
-const systemInfo = (globalThis as any).uni ? uni.getSystemInfoSync() : undefined;
-const safeAreaBottom = systemInfo?.safeAreaInsets?.bottom || 0;// 底部安全区距离
+const systemInfo = uni.getSystemInfoSync();
+const safeAreaBottom = systemInfo.safeAreaInsets?.bottom || 0;// 底部安全区距离
 
 defineOptions({
   options: {

+ 20 - 9
src/components/list/FixedVirtualList.vue

@@ -8,6 +8,12 @@
       class="virtual-list-scroll"
       :scroll-y="direction === 'vertical'"
       :scroll-x="direction === 'horizontal'"
+      :scroll-left="$attrs.scrollLeft"
+      :scroll-anchoring="$attrs.scrollAnchoring"
+      :scroll-top="$attrs.scrollTop"
+      :scroll-into-view="$attrs.scrollIntoView"
+      :scroll-with-animation="$attrs.scrollWithAnimation"
+      :show-scrollbar="$attrs.showScrollbar"
       @scroll="handleScroll"
     >
       <slot name="prefix" />
@@ -22,14 +28,16 @@
         class="virtual-list-content" 
         :style="contentStyle"
       >
-        <view 
-          v-for="(item, index) in visibleItems" 
-          :key="dataKey ? ((item as Record<string, any>)[dataKey] ?? index) : index" 
-          :style="itemStyle"
-          class="virtual-list-item"
-        >
-          <slot name="item" :item="item" :index="startIndex + index"></slot>
-        </view>
+        <slot name="list" :visibleItems="visibleItems" className="virtual-list-item" :itemStyle="itemStyle">
+          <view 
+            v-for="(item, index) in visibleItems" 
+            :key="item.key" 
+            :style="itemStyle"
+            class="virtual-list-item"
+          >
+            <slot name="item" :key="item.key" :item="item.item" :index="startIndex + index"></slot>
+          </view>
+        </slot>
       </view>
 
       <slot v-if="data.length === 0" name="empty" />
@@ -167,7 +175,10 @@ const visibleItems = computed(() => {
     props.data.length, 
     startIndex.value + visibleCount.value
   );
-  return props.data.slice(startIndex.value, endIndex);
+  return props.data.slice(startIndex.value, endIndex).map((item, index) => ({
+    item,
+    key: props.dataKey ? ((item as Record<string, any>)[props.dataKey] ?? index) : index,
+  }));
 });
 
 // 占位容器样式(总滚动区域)

+ 46 - 33
src/components/list/IndexList.vue

@@ -9,34 +9,49 @@
     @scroll="handleScroll"
     v-bind="$attrs"
   >
-    <template #item="{ item, index }"> 
-      <slot v-if="item.isHeader" name="header" :header="item.header" :id="item.id">
-        <text 
-          :id="item.id"
-          :style="{ 
-            ...themeStyles.header.value,
-            ...headerStyle,
-          }"
-        >
-          {{ item.header }}
-        </text>
-      </slot>
-      <slot v-else name="item" :item="item.data" :index="index" :id="item.id">
-        <SimpleListItem
-          :id="item.id"
-          :item="item.data"
-          :index="index"
-          :dataDisplayProp="dataDisplayProp"
-          :colorProp="colorProp"
-          :disabledProp="disabledProp"
-          :showCheck="mode !== 'select'"
-          :checked="checkedList.indexOf(item) >= 0"
-          @click="onItemPress(item, index)"
-        >
-          <template v-if="$slots.itemContent" #itemContent>
-            <slot name="itemContent" :item="item.data" :index="index" :id="item.id" />
-          </template>
-        </SimpleListItem>
+    <template #list="{ visibleItems, className, itemStyle }">
+      <slot 
+        name="list" 
+        :visibleItems="visibleItems"
+        :className="className"
+        :itemStyle="itemStyle"
+        :onItemPress="onItemPress"
+      >
+        <view 
+          v-for="(row, index) in visibleItems" 
+          :key="row.key"
+          :style="itemStyle"
+          :class="className"
+        > 
+          <slot v-if="row.item.isHeader" name="header" :header="row.item.header" :id="row.item.id">
+            <text 
+              :id="row.item.id"
+              :style="{ 
+                ...themeStyles.header.value,
+                ...headerStyle,
+              }"
+            >
+              {{ row.item.header }}
+            </text>
+          </slot>
+          <slot v-else name="item" :item="row.item.data" :index="index" :id="row.item.id">
+            <SimpleListItem
+              :id="row.item.id"
+              :item="row.item.data"
+              :index="index"
+              :dataDisplayProp="dataDisplayProp"
+              :colorProp="colorProp"
+              :disabledProp="disabledProp"
+              :showCheck="mode !== 'select'"
+              :checked="checkedList.indexOf(row.item) >= 0"
+              @click="onItemPress(row.item, index)"
+            >
+              <template v-if="$slots.itemContent" #itemContent>
+                <slot name="itemContent" :item="row.item.data" :index="index" :id="row.item.id" />
+              </template>
+            </SimpleListItem>
+          </slot>
+        </view>
       </slot>
     </template>
     <template #empty>
@@ -49,22 +64,20 @@
         :activeIndex="activeIndex"
         :data="groupedData.index"
         @drag="handleDrag"
-
       />
     </template>
   </FixedVirtualList>
 </template>
 
 <script setup lang="ts" generic="T">
-import { computed, nextTick, provide, ref, watch, type ComputedRef, type Ref } from 'vue';
-import type { CheckBoxDefaultButtonProps } from '../form/CheckBoxDefaultButton.vue';
-import FlexCol from '../layout/FlexCol.vue';
+import { computed, nextTick, provide, ref, type Ref } from 'vue';
 import { useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
 import { DynamicColor, DynamicSize, DynamicSize2, DynamicVar } from '../theme/ThemeTools';
+import type { CheckBoxDefaultButtonProps } from '../form/CheckBoxDefaultButton.vue';
+import type { SimpleListContext } from './SimpleList.vue';
 import Empty from '../feedback/Empty.vue';
 import FixedVirtualList from './FixedVirtualList.vue';
 import SimpleListItem from './SimpleListItem.vue';
-import type { SimpleListContext } from './SimpleList.vue';
 import IndexBar from '../nav/IndexBar.vue';
 
 export interface IndexedListGrouperedData<T> {

+ 1 - 2
src/components/list/SimpleList.vue

@@ -17,13 +17,12 @@
         :disabledProp="disabledProp"
         :showCheck="mode !== 'select'"
         :checked="checkedList.indexOf(item) >= 0"
+        :overrideItem="Boolean($slots.itemContent)"
         @click="onItemPress(item, index)"
       >
-        <!-- #ifndef MP -->
         <template v-if="$slots.itemContent" #itemContent>
           <slot name="itemContent" :item="item" :index="index" />
         </template>
-        <!-- #endif -->
       </SimpleListItem>
     </template>
     <template #empty>

+ 15 - 13
src/components/list/SimpleListItem.vue

@@ -6,19 +6,20 @@
     :touchable="!disabledProp || !item[disabledProp]"
     @click="emit('click')"
   >
-    <slot name="itemContent">
-      <text 
-        :style="{
-          ...(checked ? context.checkedTextStyle.value : context.textStyle.value),
-          color: colorProp ? item[colorProp] : undefined,
-        }"
-      >{{
-        dataDisplayProp ?
-          (item as unknown as Record<string, string>)[dataDisplayProp] :
-          (item as unknown as string) 
-        }}
-      </text>
-    </slot>
+    <!--TODO: 在uniapp插槽问题修复后,此处可修改为插槽默认值-->
+    <slot v-if="overrideItem" name="itemContent" />
+    <text 
+      v-else
+      :style="{
+        ...(checked ? context.checkedTextStyle.value : context.textStyle.value),
+        color: colorProp ? item[colorProp] : undefined,
+      }"
+    >{{
+      dataDisplayProp ?
+        (item as unknown as Record<string, string>)[dataDisplayProp] :
+        (item as unknown as string) 
+      }}
+    </text>
     <CheckBoxDefaultButton
       v-if="showCheck"
       v-bind="context.checkProps.value"
@@ -42,6 +43,7 @@ export interface SimpleListItemProps {
   disabledProp?: string,
   item?: any,
   id?:string,
+  overrideItem?: boolean,
 }
 
 const props = withDefaults(defineProps<SimpleListItemProps>(), {});

+ 12 - 6
src/components/nav/IndexBar.vue

@@ -41,7 +41,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue';
+import { computed, getCurrentInstance } from 'vue';
 import { useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
 import { DynamicColor, DynamicSize, DynamicVar } from '../theme/ThemeTools';
 import { MathUtis, RandomUtils } from '@imengyu/imengyu-utils';
@@ -171,6 +171,7 @@ const themeStyles = themeContext.useThemeStyles({
 
 let dragging = false;
 let absTop = 0;
+const instance = getCurrentInstance();
 
 function handleDrag(e: number) {
   const y = MathUtis.limitNumber(e - absTop, 1, (props.itemSize + props.itemSpace) * props.data.length);
@@ -185,11 +186,16 @@ function handleTouchStart(e: any) {
   e.preventDefault();
   e.stopPropagation();
   const query = uni.createSelectorQuery();
-  query.select('#' + id).boundingClientRect((res) => {
-    if (res)
-      absTop = (res as any).top;
-    handleDrag(e.touches[0]?.clientY);
-  }).exec();
+  query
+    // #ifdef MP
+    .in(instance)
+    // #endif
+    .select('#' + id)
+    .boundingClientRect((res) => {
+      if (res)
+        absTop = (res as any).top;
+      handleDrag(e.touches[0]?.clientY);
+    }).exec();
 }
 function handleTouchMove(e: any) {
   if (!dragging) 

+ 16 - 16
src/components/nav/Pagination.vue

@@ -13,24 +13,24 @@
       <text :style="themeStyles.simpleText.value">{{ `${currentPage + 1}/${props.pageCount}` }}</Text>
     </slot>
     <template v-else>
-      <slot 
-        v-for="index in items"
-        name="item" 
-        :key="index"
-        :text="(index + 1).toString()"
-        :index="index"
-        :active="index === currentPage"
-        :onClick="() => emitChange(index)"
-      >
-        <PaginationItem
-          v-bind="$props"
-          :key="index"
+      <template  v-for="index in items" :key="index">
+        <slot
+          name="item"
           :text="(index + 1).toString()"
+          :index="index"
           :active="index === currentPage"
-          :touchable="true"
-          @click="() => emitChange(index)"
-        />
-      </slot>
+          :onClick="() => emitChange(index)"
+        >
+          <PaginationItem
+            v-bind="$props"
+            :key="index"
+            :text="(index + 1).toString()"
+            :active="index === currentPage"
+            :touchable="true"
+            @click="() => emitChange(index)"
+          />
+        </slot>
+      </template>
     </template>
     <slot v-if="showNextPrev" name="next" :onClick="onNextPress" :touchable="canNext">
       <PaginationItem 

+ 9 - 2
src/components/nav/SegmentedControl.vue

@@ -14,6 +14,9 @@
       :active="i === props.selectedIndex"
       :itemStyle="{
         ...themeStyles.item.value,
+        ...(fill ? { 
+          flexGrow: 1,
+        } : {}),
         borderStyle: 'solid',
         borderWidth: themeContext.resolveSize(themeVars.SegmentedControlBorderWidth),
         borderColor: themeContext.resolveThemeColor(tintColor),
@@ -47,7 +50,11 @@ export interface SegmentedControlProps {
    * 选中的条目索引。
    */
   selectedIndex?: number | undefined;
-
+  /**
+   * 是否让条目自动填充整个宽度。
+   * @default true
+   */
+  fill?: boolean | undefined;
   /**
    * 控制器的主颜色。
    * @default primary
@@ -72,6 +79,7 @@ export interface SegmentedControlProps {
 const emit = defineEmits(['update:selectedIndex']);
 const props = withDefaults(defineProps<SegmentedControlProps>(), {
   touchable: true,
+  fill: true,
   selectedIndex: 0,
   tintColor: () => propGetThemeVar('SegmentedControlTintColor', 'primary'),
   activeTextColor: () => propGetThemeVar('SegmentedControlActiveTextColor', 'white'),
@@ -99,7 +107,6 @@ const themeColorVars = themeContext.getColors({
 const themeStyles = themeContext.useThemeStyles({
   item: {
     display: 'flex',
-    flexGrow: 1,
     alignSelf: 'auto',
     justifyContent: 'center',
     alignContent: 'center',

+ 6 - 0
src/components/nav/SegmentedControlItem.vue

@@ -47,4 +47,10 @@ export interface SegmentedControlItemProps {
 
 const emit = defineEmits([ 'click' ]);
 const props = defineProps<SegmentedControlItemProps>();
+
+defineOptions({
+  options: {
+    virtualHost: true,
+  }
+})
 </script>

+ 8 - 4
src/components/nav/TabBarItem.vue

@@ -15,9 +15,13 @@
       </view>
       <Badge
         :containerStyle="{
-          height: props.hump ? theme.resolveSize(iconProps.size) : undefined,
-          position: props.hump ? 'absolute' : undefined,
-          bottom: props.hump ? theme.resolveSize(active ? humpHeight[0] : humpHeight[1]) : undefined,
+          ...(props.hump ? {
+            position: 'absolute',
+            height: theme.resolveSize(iconProps.size),
+            bottom: theme.resolveSize(active ? humpHeight[0] : humpHeight[1]),
+            left: '50%',
+            transform: 'translateX(-50%)',
+          } : {}),
         }"
         :content="props.badge === -1 ? '' : ((typeof props.badge === 'undefined' || props.badge === 0) ? 0 : props.badge)"
         :offset="{ x: 3, y: 0 }"
@@ -44,11 +48,11 @@
 import type { IconProps } from '@/components/basic/Icon.vue';
 import { useTheme, type TextStyle } from '@/components/theme/ThemeDefine';
 import { computed, inject, onMounted, onUpdated, ref } from 'vue';
+import { DynamicSize } from '../theme/ThemeTools';
 import FlexView from '../layout/FlexView.vue';
 import Text from '@/components/basic/Text.vue';
 import Icon from '@/components/basic/Icon.vue';
 import Badge, { type BadgeProps } from '../display/Badge.vue';
-import { DynamicSize, DynamicVar } from '../theme/ThemeTools';
 
 export interface TabBarItemProps {
   /**

+ 48 - 39
src/components/nav/Tabs.vue

@@ -17,50 +17,45 @@
         height: theme.resolveSize(props.height),
       }"
     >
-      <template
+      <FlexView
         v-for="(tab, index) in filteredTabs"
+        :ref="(ref) => tabsRefs[index] = ref"
         :key="index"
+        direction="row"
+        :pressedColor="themedUnderlayColor"
+        :innerStyle="itemStyle"
+        :width="itemWidthArr[index].width > 0 ?
+            theme.resolveSize(itemWidthArr[index].width - tabPaddingHorizontal * 2) :
+            undefined"
+        :touchable="!tab.disabled"
+        :flexShrink="0"
+        :padding="[ 0, tabPaddingHorizontal ]"
+        center
+        innerClass="tab-item"
+        @click="onTabClick(index)"
       >
         <slot name="tab" 
           :tab="tab"
           :index="index" 
           :width="itemWidthArr[index].width" 
           :active="currentIndex == index"
-          :onClick="() => onTabClick(index)"
-          :tabId="`${idPrefix}${index}`"
         >
-          <FlexView
-            :innerId="`${idPrefix}${index}`"
-            direction="row"
-            :pressedColor="themedUnderlayColor"
-            :innerStyle="itemStyle"
-            :width="itemWidthArr[index].width > 0 ?
-                theme.resolveSize(itemWidthArr[index].width - tabPaddingHorizontal * 2) :
-                undefined"
-            :touchable="!tab.disabled"
-            :flexShrink="0"
-            :padding="[ 0, tabPaddingHorizontal ]"
-            center
-            innerClass="tab-item"
-            @click="onTabClick(index)"
+          <Badge
+            content="0"
+            v-bind="tab.badgeProps"
           >
-            <Badge
-              content="0"
-              v-bind="tab.badgeProps"
+            <text
+              class="tab-item-text"
+              :style="{
+                color: tab.disabled ? themedDisableTextColor : (currentIndex == index ? themedActiveTextColor : themedTextColor),
+                ...(currentIndex == index ? activeTextStyle : textStyle),
+              }"
             >
-              <text
-                class="tab-item-text"
-                :style="{
-                  color: tab.disabled ? themedDisableTextColor : (currentIndex == index ? themedActiveTextColor : themedTextColor),
-                  ...(currentIndex == index ? activeTextStyle : textStyle),
-                }"
-              >
-                {{ tab.text }}
-              </text>
-            </Badge>
-          </FlexView>
+              {{ tab.text }}
+            </text>
+          </Badge>
         </slot>
-      </template>
+      </FlexView>
 
       <view 
 		    v-if="showIndicator"
@@ -80,7 +75,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, nextTick, onMounted, ref, watch } from 'vue';
+import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue';
 import type { BadgeProps } from '../display/Badge.vue';
 import Badge from '../display/Badge.vue';
 import FlexView from '../layout/FlexView.vue';
@@ -243,6 +238,7 @@ const themedTextColor = computed(() => theme.resolveThemeColor(props.textColor))
 const themedDisableTextColor = computed(() => theme.resolveThemeColor(props.disableTextColor));
  
 const mersuredTabs = ref<(number|undefined)[]>([]);
+const tabsRefs = ref<any[]>([]);
 const filteredTabs = computed(() => props.tabs.filter(tab => tab.visible !== false));
 const itemWidthArr = computed(() => {
   const result : {
@@ -255,6 +251,7 @@ const itemWidthArr = computed(() => {
         width: 0,
         indicatorWidth: 0,
       });
+      nextTick(() => measureTab(i))
     } else {
       const itemWidth = filteredTabs.value[i].width || 
         (
@@ -267,20 +264,22 @@ const itemWidthArr = computed(() => {
         width: itemWidth,
         indicatorWidth: itemIndicatorWidth,
       });
-      nextTick(() => measureTab(i))
+      if (itemWidth <= 0)
+        nextTick(() => measureTab(i))
     }
   }
   return result;
 });
 
-const currentScrollPosOld = ref(0);
+const currentCanMeasure = ref(true);
 const currentScrollPos = ref(0);
 const currentIndicatorPos = ref(0);
 const currentIndicatorWidth = ref(0);
 
 function measureTab(index: number) {
-  const tabItem = uni.createSelectorQuery().select(`#${idPrefix}${index}`);
-  tabItem.boundingClientRect().exec((res) => {
+  if (!currentCanMeasure.value)
+    return;
+  tabsRefs.value[index]?.measure().then((res: any) => {
     if (res[0])
       mersuredTabs.value[index] = res[0].width;
     else
@@ -296,7 +295,7 @@ function loadPos() {
 
   const targetWidth = uni.upx2px(current.indicatorWidth);
   let scrollLeft = 0;
-  let targetLeft = itemWidth / 2 - targetWidth / 2;
+  let targetLeft = 0;
 
   for (let i = currentIndex - 1; i >= 0; i--) {
     const width = itemWidthArr.value[i].width > 0 ? uni.upx2px(itemWidthArr.value[i].width) : mersuredTabs.value[i] || 0;
@@ -304,6 +303,10 @@ function loadPos() {
     scrollLeft += width;
   }
 
+
+  if (targetWidth < itemWidth)
+    targetLeft += itemWidth / 2 - targetWidth / 2;
+
   currentIndicatorPos.value = targetLeft;
   currentIndicatorWidth.value = targetWidth;
   currentScrollPos.value = scrollLeft + (currentIndex == itemWidthArr.value.length - 1 ? itemWidth : 0); 
@@ -313,7 +316,13 @@ watch(mersuredTabs, loadPos, { deep: true });
 watch(() => props.currentIndex, loadPos);
 
 onMounted(() => {
-  nextTick(loadPos);
+  nextTick(() => {
+    currentCanMeasure.value = true;
+    measureTab(0);
+    setTimeout(() => {
+      loadPos();
+    }, 200);
+  });
 })
 
 function onTabClick(index: number) {

+ 7 - 3
src/components/theme/ThemeTools.ts

@@ -7,12 +7,16 @@ export function solveUrl(v: string) {
     return v;
   return `url(${v})`;
 }
-const info = (globalThis as any).uni ? uni.getSystemInfoSync() : {
-  // #ifdef H5
+// #ifdef H5
+const info = {
   screenWidth: globalThis.window.innerWidth,
   screenHeight: globalThis.window.innerHeight,
-  // #endif
 }
+// #endif
+// #ifndef H5
+const info = uni.getSystemInfoSync() ;
+// #endif
+
 export const screenWidth = info.screenWidth;
 export const screenHeight = info.screenHeight;
 

+ 30 - 18
src/components/typography/HorizontalScrollText.vue

@@ -15,21 +15,17 @@
     <view 
       class="inner"
       :style="{
-        animation: scrollAble ? `${animDuration}ms linear 0s horizontalScrollText infinite` : 'none',
+        animation: scrollAble ? `${animDuration}ms linear 0s ${props.direction === 'left' ? 'horizontalScrollTextLeft' : 'horizontalScrollTextRight'} infinite` : 'none',
       }"
     >
-      <Text v-bind="$props" :innerClass="['text','real-text']">
-        <slot />
-      </Text>
+      <Text ref="realTextRef" v-bind="$props" :innerClass="['text']" />
     </view>
-    <Text v-bind="$props" :innerClass="['placeholder','text']">
-      <slot />
-    </Text>
+    <Text v-bind="$props" :innerClass="['placeholder','text']" />
   </view>
 </template>
 
 <script setup lang="ts">
-import { onMounted, onUpdated, ref } from 'vue';
+import { getCurrentInstance, onMounted, onUpdated, ref } from 'vue';
 import type { TextProps } from '../basic/Text.vue';
 import type { ViewStyle } from '../theme/ThemeDefine';
 import Text from '../basic/Text.vue';
@@ -37,27 +33,38 @@ import { RandomUtils, waitTimeOut } from '@imengyu/imengyu-utils';
 import { selectStyleType } from '../theme/ThemeTools';
 
 export interface HorizontalScrollTextProps extends TextProps {
+  /**
+   * 动画持续时间,单位毫秒
+   * @default 25000
+   */
   animDuration?: number,
+  /**
+   * 是否滚动
+   * @default true
+   */
   scroll?: boolean,
+  /**
+   * 外部样式
+   */
   outerStyle?: ViewStyle,
+  /**
+   * 滚动方向
+   * @default 'left'
+   */
+  direction?: 'left' | 'right',
 }
 
 const id = RandomUtils.genNonDuplicateID(12);
 const scrollAble = ref(false);
+const instance = getCurrentInstance();
+const realTextRef = ref();
 
 const props = withDefaults(defineProps<HorizontalScrollTextProps>(), {
   animDuration: 25000,
   scroll: true,
+  direction: 'left',
 });
 
-async function getTextWidth() {
-  return await new Promise<number>((resolve) => {
-    uni.createSelectorQuery()
-      .select(`#${id} .inner .real-text`)
-      .boundingClientRect((data) => resolve((data as UniApp.NodeInfo)?.width ?? 0))
-      .exec();
-  })
-}
 async function lodScrollInfo() {
   if (!props.scroll) {
     scrollAble.value = false;
@@ -67,15 +74,20 @@ async function lodScrollInfo() {
 
   const conWidth = await new Promise<number>((resolve) => {
     uni.createSelectorQuery()
+      // #ifdef MP
+      .in(instance)
+      // #endif
       .select(`#${id}`)
       .boundingClientRect((data) => resolve((data as UniApp.NodeInfo)?.width ?? 0))
       .exec();
   })
 
-  let textWidth = await getTextWidth();
+  let textWidth = await realTextRef.value.measureTextWidth();
+  console.log('textWidth', textWidth, 'conWidth', conWidth);
+  
   if (textWidth == conWidth) { 
     await waitTimeOut(200);
-    textWidth = await getTextWidth();
+    textWidth = await realTextRef.value.measureTextWidth();
   }
   scrollAble.value = conWidth < textWidth;
 }

+ 47 - 65
src/components/typography/VerticalScrollOneText.vue

@@ -9,24 +9,24 @@
       }"
       :text="oneStr"
     />
-    <Text 
-      v-bind="props"
-      :innerClass="['text','next', nextClass]"
-      :innerStyle="{
+    <view
+      class="scroll"
+      :style="{
+        transform: `translateY(${currentScrollTop ? '0%' : '-50%'})`,
         transition: anim ? `linear ${animDuration}ms all` : 'none',
-        ...$props.innerStyle || {},
       }"
-      :text="nextText"
-    />
-    <Text 
-      v-bind="props"
-      :innerClass="['text','current', currentClass]"
-      :innerStyle="{
-        transition: anim ? `linear ${animDuration}ms all` : 'none',
-        ...$props.innerStyle || {},
-      }"
-      :text="currentText"
-    />
+    >
+      <Text 
+        v-bind="props"
+        :innerStyle="$props.innerStyle"
+        :text="upText"
+      />
+      <Text 
+        v-bind="props"
+        :innerStyle="$props.innerStyle"
+        :text="downText"
+      />
+    </view>
   </view>
 </template>
 
@@ -67,61 +67,51 @@ const props = withDefaults(defineProps<VerticalScrollOneTextProps>(), {
   animDuration: 230,
   animDirection: 'auto',
 });
-const nextText = ref('');
-const nextClass = ref('');
-const currentText = ref('');
-const currentClass = ref('');
+const currentScrollTop = ref(false);
+const upText = ref('');
+const downText = ref('');
 const anim = ref(false);
 
 onMounted(() => {
-  currentText.value = props.oneStr;
-  currentClass.value = '';
-  nextClass.value = 'bottom';
+  upText.value = props.oneStr;
+  currentScrollTop.value = false;
 })
 
-let timer = 0;
-let lock = false;
+watch(() => props.oneStr, (currentStr, prevStr) => {
+  anim.value = false;
 
-watch(() => props.oneStr, (oneStr, oneStrPrev) => {
-  nextText.value = oneStr;
-  currentClass.value = '';
-  currentText.value = oneStrPrev;
+  if (currentStr === '' || prevStr === '') {
+    currentScrollTop.value = true;
+    upText.value = currentStr;
+    downText.value = currentStr;
+    return;
+  }
   
   const isDown = props.animDirection === 'down' ||
     //根据数字自动判断应该往上还是往下
     (props.animDirection === 'auto' && 
-      oneStr.length === 1 && oneStrPrev > oneStr && 
-      !(oneStrPrev === '9' && oneStr === '0'));
-
+      currentStr.length === 1 && prevStr > currentStr && 
+      !(prevStr === '9' && currentStr === '0'));
+      
   if (isDown) {
     //下
-    nextClass.value = 'bottom';
-    
+    currentScrollTop.value = true;
+    upText.value = prevStr;
+    downText.value = currentStr;
     setTimeout(() => {
-      currentClass.value = 'anim top';
-      nextClass.value = 'anim';
+      currentScrollTop.value = false;
       anim.value = true;
-    }, 20)
+    }, 90)
   } else {
     //上
-    nextClass.value = 'top';
-    
+    currentScrollTop.value = false;
+    upText.value = currentStr;
+    downText.value = prevStr;
     setTimeout(() => {
-      currentClass.value = 'anim bottom';
-      nextClass.value = 'anim';
+      currentScrollTop.value = true;
       anim.value = true;
-    }, 20)
+    }, 90)
   }
-  if (timer)
-    clearTimeout(timer);
-  timer = setTimeout(() => {
-    timer = 0;
-    anim.value = false;
-    currentText.value = oneStr;
-    currentClass.value = '';
-    nextText.value = '';
-    nextClass.value = 'bottom';
-  }, props.animDuration) as any;
 })
 </script>
 
@@ -136,22 +126,14 @@ watch(() => props.oneStr, (oneStr, oneStrPrev) => {
 
   .holder {
     visibility: hidden;
+    opacity: 0;
   }
-  .text {
+  .scroll {
     position: absolute;
-    left: 0;
-    right: 0;
+    display: flex;
+    flex-direction: column;
     top: 0;
-    bottom: 0;
-    transition: none;
-    transform: translateY(0); 
-
-    &.top {
-      transform: translateY(-100%);
-    }
-    &.bottom {
-      transform: translateY(100%); 
-    }
+    left: 0;
   }
 }
 </style>