Kaynağa Gözat

更新组件库

快乐的梦鱼 3 ay önce
ebeveyn
işleme
e9fde2e20a
48 değiştirilmiş dosya ile 1139 ekleme ve 132 silme
  1. 1 1
      src/components/README.md
  2. 19 0
      src/components/anim/SimpleTransition.vue
  3. 60 2
      src/components/basic/ActivityIndicator.vue
  4. 11 4
      src/components/basic/Button.vue
  5. 9 0
      src/components/basic/CellContext.ts
  6. 3 2
      src/components/basic/IconButton.vue
  7. 8 7
      src/components/demo/DemoPage.vue
  8. 1 6
      src/components/dialog/Dialog.vue
  9. 1 1
      src/components/dialog/DialogInner.vue
  10. 6 5
      src/components/dialog/Popup.vue
  11. 0 1
      src/components/display/Divider.vue
  12. 5 1
      src/components/display/Tag.vue
  13. 1 1
      src/components/display/countdown/CountDownButton.vue
  14. 19 20
      src/components/display/loading/LoadingPage.vue
  15. 37 3
      src/components/display/loading/Loadmore.vue
  16. 7 1
      src/components/feedback/DropdownMenuItem.vue
  17. 1 2
      src/components/feedback/Notify.vue
  18. 8 6
      src/components/feedback/SwipeRow.vue
  19. 3 1
      src/components/feedback/Toast.vue
  20. 3 1
      src/components/feedback/Touchable.vue
  21. 3 3
      src/components/form/Calendar.vue
  22. 2 2
      src/components/form/CalendarField.vue
  23. 538 0
      src/components/form/CalendarUtils.ts
  24. 37 1
      src/components/form/CascadePickerField.vue
  25. 35 1
      src/components/form/DatePickerField.vue
  26. 35 1
      src/components/form/DateTimePickerField.vue
  27. 76 2
      src/components/form/FormContext.ts
  28. 14 3
      src/components/form/PickerField.vue
  29. 19 13
      src/components/form/Rate.vue
  30. 51 10
      src/components/form/Signature.vue
  31. 12 0
      src/components/form/Slider.vue
  32. 1 1
      src/components/form/Switch.vue
  33. 31 1
      src/components/form/TimePickerField.vue
  34. 1 1
      src/components/form/Uploader.vue
  35. 1 1
      src/components/form/UploaderField.vue
  36. 3 3
      src/components/keyboard/NumberKeyBoardKey.vue
  37. 1 0
      src/components/layout/BaseView.ts
  38. 1 0
      src/components/list/SimpleList.vue
  39. 3 0
      src/components/nav/IndexBar.vue
  40. 26 8
      src/components/nav/SegmentedControl.vue
  41. 6 5
      src/components/nav/SegmentedControlItem.vue
  42. 2 0
      src/components/nav/TabBar.vue
  43. 1 0
      src/components/nav/Tabs.vue
  44. 2 0
      src/components/theme/Theme.ts
  45. 15 3
      src/components/theme/ThemeDefine.ts
  46. 16 4
      src/components/typography/A.vue
  47. 1 2
      src/components/typography/VerticalScrollOneText.vue
  48. 3 2
      src/components/typography/VerticalScrollTexts.vue

+ 1 - 1
src/components/README.md

@@ -6,7 +6,7 @@ NaEasy UI 是一款简单的 UniApp 移动端UI组件库。
 
 ## 版本
 
-当前版本:INDEV 0.0.0
+当前版本:INDEV 0.0.1
 
 ## 版权说明
 

+ 19 - 0
src/components/anim/SimpleTransition.vue

@@ -12,10 +12,29 @@
 <script setup lang="ts">
 import { nextTick, onMounted, ref, watch } from 'vue';
 
+/**
+ * 简单过渡组件
+ */
 export interface TransitionProps {
+  /**
+   * 是否显示
+   * @default false
+   */
   show: boolean;
+  /**
+   * 是否启用动画
+   * @default true
+   */
   anim?: boolean;
+  /**
+   * 动画持续时间(毫秒)
+   * @default 500
+   */
   duration?: number,
+  /**
+   * 动画名称
+   * @default 'v'
+   */
   name?: string;
 }
 

+ 60 - 2
src/components/basic/ActivityIndicator.vue

@@ -3,7 +3,11 @@
     class="nana-activity-indicator"
     :style="style"
   >
-
+    <!-- #ifndef APP-NVUE -->
+    <svg class="chrome-spinner" viewBox="0 0 50 50">
+      <circle cx="25" cy="25" r="20" class="single-ring" :stroke-width="props.strokeWidth" />
+    </svg>
+    <!-- #endif -->
   </view>
 </template>
 
@@ -21,6 +25,10 @@ export interface ActivityIndicatorProps {
    */
   size?: string|number,
   /**
+   * 加载中圆圈宽度
+   */
+  strokeWidth?: number,
+  /**
    * 自定义样式
    */
   innerStyle?: ViewStyle,
@@ -36,6 +44,7 @@ const props = withDefaults(defineProps<ActivityIndicatorProps>(), {
 const style = computed(() => {
   return {
     borderTopColor: themeContext.resolveThemeColor(props.color),
+    color: themeContext.resolveThemeColor(props.color),
     width: themeContext.resolveThemeSize(props.size),
     height: themeContext.resolveThemeSize(props.size),
     ...props.innerStyle,
@@ -43,7 +52,55 @@ const style = computed(() => {
 });
 </script>
 
-<style>
+<style lang="scss">
+/* #ifndef APP-NVUE */
+
+/* 旋转动画 */
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+/* 线段长度变化动画 */
+@keyframes dash {
+  0% {
+    stroke-dasharray: 10 190; /* 最短状态 */
+    stroke-dashoffset: 0;
+  }
+  50% {
+    stroke-dasharray: 100 100; /* 最长状态 */
+    stroke-dashoffset: -40;
+  }
+  100% {
+    stroke-dasharray: 10 190; /* 回到最短 */
+    stroke-dashoffset: -200;
+  }
+}
+
+
+.nana-activity-indicator {
+  .chrome-spinner {
+    width: 100%;
+    height: 100%;
+    animation: spin 1.5s linear infinite;
+    
+    .single-ring {
+      fill: none;
+      stroke-width: 5;
+      stroke-linecap: round;
+      stroke: currentColor; /* 单色灰色 */
+      stroke-dasharray: 20 160; /* 控制线段长度 */
+      animation: dash 1.5s ease-in-out infinite;
+    }
+  }
+}
+/* #endif */
+
+/* #ifdef APP-NVUE */
 @keyframes spin {
   from {
     transform: rotate(0deg);
@@ -61,4 +118,5 @@ const style = computed(() => {
   animation: spin 1s linear infinite;
   box-sizing: border-box;
 }
+/* #endif */
 </style>

+ 11 - 4
src/components/basic/Button.vue

@@ -9,7 +9,7 @@
     direction="row"
     v-bind="viewProps"
     :pressedColor="type === 'custom' ? themeContext.resolveThemeColor(pressedColor) :
-      (themeContext.resolveThemeColor((plain || type === 'text' || type === 'default') ? 'pressed.notice' : 'pressed.' + type))"
+      (themeContext.resolveThemeColor((plain || type === 'text') ? 'pressed.notice' : 'pressed.' + type))"
     :touchable="touchable && !loading"
     @state="(v) => state = v"
     @click="emit('click', $event)"
@@ -61,10 +61,10 @@
 <script setup lang="ts">
 import { computed, ref } from 'vue';
 import { useTheme, type ViewStyle } from '../theme/ThemeDefine';
-import { configPadding, configThemePaddingMarginOneSide, DynamicColor, DynamicSize, DynamicVar, selectStyleType } from '../theme/ThemeTools';
+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 FlexView, { type FlexProps } from '../layout/FlexView.vue';
 import ActivityIndicator from './ActivityIndicator.vue';
 import Icon from './Icon.vue';
 import Touchable from '../feedback/Touchable.vue';
@@ -282,6 +282,10 @@ const themeStyles = themeContext.useThemeStyles({
     paddingVertical: DynamicSize('ButtonPaddingVerticalMini', 5),
     paddingHorizontal: DynamicSize('ButtonPaddingHorizontalMini', 6),
   },
+  buttonDefault: {
+    backgroundColor: DynamicColor('ButtonDefaultBackgroundColor', 'button'),
+    color: DynamicColor('ButtonDefaultColor', 'black'),
+  },
   buttonPrimary: {
     backgroundColor: DynamicColor('ButtonPrimaryBackgroundColor', 'primary'),
     color: DynamicColor('ButtonPrimaryColor', 'white'),
@@ -318,7 +322,7 @@ const currentStyle = computed(() => {
       color: themeContext.resolveThemeColor(props.color),
     },
   } : {
-    default: themeStyles.plainButtonDefault.value,
+    default: themeStyles.buttonDefault.value,
     primary: themeStyles.buttonPrimary.value,
     success: themeStyles.buttonSuccess.value,
     warning: themeStyles.buttonWarning.value,
@@ -379,6 +383,9 @@ const textColorFinal = computed(() => (
 <style>
 .nana-button {
   width: auto;
+  user-select: none;
+  cursor: pointer;
+  appearance: none;
 }
 .nana-button-inner {
   display: flex;

+ 9 - 0
src/components/basic/CellContext.ts

@@ -3,9 +3,18 @@ import { inject } from "vue";
 export const CellContextKey = Symbol("CellContext");
 
 export interface CellContext {
+  /**
+   * 子组件设置单元格点击事件。注意:只能设置一次,后续设置会覆盖之前的设置。
+   * @param listener 点击事件
+   * @returns 
+   */
   setOnClickListener: (listener: () => void) => void;
 }
 
+/**
+ * 获取单元格上下文
+ * @returns 单元格上下文
+ */
 export function useCellContext() {
   return inject<CellContext>(CellContextKey, null as any);
 }

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

@@ -16,10 +16,11 @@ import { computed } from 'vue';
 import { propGetThemeVar, useTheme, type ViewStyle } from '../theme/ThemeDefine';
 import { selectStyleType } from '../theme/ThemeTools';
 import type { IconProps } from './Icon.vue';
-import type { ImageButtonShapeType } from './ImageButton.vue';
 import Icon from './Icon.vue';
 import Touchable from '../feedback/Touchable.vue';
 
+export type IconButtonShapeType = 'round'|'square-full'|'custom';
+
 export interface IconButtonProps extends IconProps {
   /**
    * 按钮按下时的背景颜色
@@ -34,7 +35,7 @@ export interface IconButtonProps extends IconProps {
    * 按钮形状预设
    * @default round
    */
-  shape?: ImageButtonShapeType;
+  shape?: IconButtonShapeType;
   /**
    * 是否禁用
    * @default false

+ 8 - 7
src/components/demo/DemoPage.vue

@@ -14,14 +14,15 @@ if (props.title) {
 </script>
 
 <template>
-  <CommonRoot />
-  <view class="nana-demo-page">
-    <view class="header">
-      <text class="title">{{ title }}</text>
-      <text v-if="desc" class="desc">{{ desc }}</text>
+  <CommonRoot>
+    <view class="nana-demo-page">
+      <view class="header">
+        <text class="title">{{ title }}</text>
+        <text v-if="desc" class="desc">{{ desc }}</text>
+      </view>
+      <slot />
     </view>
-    <slot />
-  </view>
+  </CommonRoot>
 </template>
 
 <style>

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

@@ -132,11 +132,7 @@ export interface DialogProps extends Omit<PopupProps, 'onClose'|'position'|'rend
   /**
    * 对话框宽度
    */
-  width?: number|undefined;
-  /**
-   * 当对话框关闭时的回调
-   */
-  onClose?: () => void;
+  width?: number|string|undefined;
   /**
    * 当对话框点击取消时的回调
    */
@@ -156,7 +152,6 @@ const props = withDefaults(defineProps<DialogProps>(), {
 const emit = defineEmits([ 'close', 'update:show' ]);
 
 function onClose() {
-  props.onClose?.();
   emit('close');
   emit('update:show', false);
 }

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

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

+ 6 - 5
src/components/dialog/Popup.vue

@@ -115,13 +115,12 @@
 </template>
 
 <script setup lang="ts">
-import { computed, nextTick, ref, watch } from 'vue';
+import { computed, ref, watch } from 'vue';
 import { useTheme, type ViewStyle } from '../theme/ThemeDefine';
 import { selectStyleType } from '../theme/ThemeTools';
+import { SimpleDelay } from '@imengyu/imengyu-utils';
 import PopupTitle from './PopupTitle.vue';
 import SafeAreaPadding from '../layout/space/SafeAreaPadding.vue';
-import { SimpleDelay } from '../utils/Timer';
-
 
 /**
  * Popup 的显示位置
@@ -264,14 +263,16 @@ watch(() => props.show, (v) => {
     lateStopTimer = new SimpleDelay(undefined, () => {
       lateStopTimer = undefined;
       show2.value = false;
-    }, props.duration).start();
+    }, props.duration)
+    lateStopTimer.start();
   } else {
     if (lateStopTimer)
       lateStopTimer.stop();
     lateStopTimer = new SimpleDelay(undefined, () => {
       lateStopTimer = undefined;
       showAnimState.value = true
-    }, 20).start();
+    }, 20)
+    lateStopTimer.start();
   }
 });
 </script>

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

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

+ 5 - 1
src/components/display/Tag.vue

@@ -155,6 +155,10 @@ const themeStyles = themeContext.useThemeStyles({
     borderColor: DynamicColor('TagPlainDangerBorderColor', 'danger'),
     color: DynamicColor('TagPlainDangerColor', 'danger'),
   },
+  tagDefault: {
+    backgroundColor: DynamicColor('TagDefaultBackgroundColor', 'button'),
+    color: DynamicColor('TagDefaultColor', 'black'),
+  },
   tagPrimary: {
     backgroundColor: DynamicColor('TagPrimaryBackgroundColor', 'primary'),
     color: DynamicColor('TagPrimaryColor', 'white'),
@@ -207,7 +211,7 @@ const style = computed(() => {
       warning: themeStyles.plainTagWarning.value,
       danger: themeStyles.plainTagDanger.value,
     } : {
-      default: themeStyles.plainTagDefault.value,
+      default: themeStyles.tagDefault.value,
       primary: themeStyles.tagPrimary.value,
       success: themeStyles.tagSuccess.value,
       warning: themeStyles.tagWarning.value,

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

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

+ 19 - 20
src/components/display/loading/LoadingPage.vue

@@ -1,10 +1,10 @@
 <script setup lang="ts">
 import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
 import Text, { type TextProps } from '@/components/basic/Text.vue';
-import FlexView from '@/components/layout/FlexView.vue';
-import type { ViewStyle } from '@/components/theme/ThemeDefine';
+import FlexView, { type FlexProps } from '@/components/layout/FlexView.vue';
+import { propGetThemeVar, useTheme, type ViewStyle } from '@/components/theme/ThemeDefine';
 
-export interface LoadingPageProps {
+export interface LoadingPageProps extends FlexProps {
   /**
    * 加载器下方文字
    */
@@ -28,18 +28,30 @@ export interface LoadingPageProps {
   innerStyle?: ViewStyle,
 }
 
+const themeContext = useTheme();
+
 const props = withDefaults(defineProps<LoadingPageProps>(), {
   loadingText: '加载中',
-  indicatorColor: 'primary',
+  indicatorColor: () => propGetThemeVar('LoadingPageIndicatorColor', 'primary'),
 });
 </script>
 
 <template>
-  <FlexView innerClass="nana-loading-page-view">
+  <FlexView 
+    position="absolute" 
+    direction="column"
+    :backgroundColor="themeContext.resolveThemeColor('LoadingPageBackgroundColor', 'mask.white')"
+    :left="0" 
+    :right="0" 
+    :top="0"
+    :bottom="0"
+    center
+    v-bind="$attrs"
+  >
     <ActivityIndicator
       :color="indicatorColor"
       :innerStyle="indicatorStyle"
-      size="large"
+      :size="50"
     />
     <Text
       v-if="loadingText"
@@ -48,17 +60,4 @@ const props = withDefaults(defineProps<LoadingPageProps>(), {
     />
     <slot />
   </FlexView>
-</template>
-
-<style>
-.nana-loading-page-view {
-  position: absolute;
-  left: 0;
-  right: 0;
-  top: 0;
-  bottom: 0;
-  justify-content: center;
-  align-items: center;
-  z-index: 20;
-}
-</style>
+</template>

+ 37 - 3
src/components/display/loading/Loadmore.vue

@@ -2,15 +2,42 @@
 import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
 import Text from '@/components/basic/Text.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
+import type { FlexProps } from '@/components/layout/FlexView.vue';
 import Width from '@/components/layout/space/Width.vue';
+import { useTheme } from '@/components/theme/ThemeDefine';
 import { computed } from 'vue';
 
-export interface LoadMoreProps {
-  status?: ''|'loading'|'finished'|'error'|'nomore'|'loadmore',
+export type LoadMoreStatus = ''|'loading'|'finished'|'error'|'nomore'|'loadmore';
+
+export interface LoadMoreProps extends FlexProps {
+  /**
+   * 加载更多状态
+   */
+  status?: LoadMoreStatus,
+  /**
+   * 加载中文字
+   * @default '正在努力加载中...'
+   */
   loadingText?: string,
+  /**
+   * 加载完毕文字
+   * @default '加载完毕'
+   */
   finishedText?: string,
+  /**
+   * 加载失败文字
+   * @default '加载失败'
+   */
   errorText?: string,
+  /**
+   * 没有更多了文字
+   * @default '没有更多了'
+   */
   nomoreText?: string,
+  /**
+   * 点击加载更多文字
+   * @default '点击加载更多'
+   */ 
   loadmoreText?: string,
 }
 
@@ -22,6 +49,8 @@ const props = withDefaults(defineProps<LoadMoreProps>(), {
   loadmoreText: '点击加载更多',
 });
 
+const themeContext = useTheme();
+
 const text = computed(() => {
   switch (props.status) {
     case 'loading': return props.loadingText;
@@ -35,7 +64,12 @@ const text = computed(() => {
 </script>
 
 <template>
-  <FlexRow :padding="30" align="center" backgroundColor="background.bar">
+  <FlexRow 
+    :padding="[10,20]"
+    align="center" 
+    :backgroundColor="themeContext.resolveThemeColor('LoadMoreBackgroundColor', 'background.bar')" 
+    v-bind="$attrs"
+  >
     <ActivityIndicator v-if="props.status === 'loading'" :size="30" />
     <Width :size="20" />
     <Text :text="text" />

+ 7 - 1
src/components/feedback/DropdownMenuItem.vue

@@ -250,15 +250,21 @@ async function updateDialogMargin() {
   const systemInfo = uni.getSystemInfoSync();
   let v = 0
   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`;
   } else {
+    if (topContext.includeNavBarSpace.value) {
+      //H5 浏览器需要额外添加44px
+      // #ifdef H5
+      v += 44;
+      // #endif
+    }
     v += (d.height ?? 0);
     v += d.top ?? 0;
     popupMargin.value[0] = `${v}px`;

+ 1 - 2
src/components/feedback/Notify.vue

@@ -90,8 +90,7 @@
 
 <script setup lang="ts">
 import { reactive, ref } from 'vue';
-import { SimpleDelay } from '../utils/Timer';
-import { ArrayUtils } from '@imengyu/imengyu-utils';
+import { SimpleDelay, ArrayUtils } from '@imengyu/imengyu-utils';
 import { useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
 import { DynamicColor, DynamicSize, DynamicSize2, DynamicVar, selectStyleType } from '../theme/ThemeTools';
 import type { IconProps } from '../basic/Icon.vue';

+ 8 - 6
src/components/feedback/SwipeRow.vue

@@ -5,7 +5,9 @@
     @touchstart="handleTouchStart"
     @touchmove="handleTouchMove"
     @touchend="handleTouchEnd"
-    @click="handleClick"
+    @mousedown="handleTouchStart"
+    @mousemove="handleTouchMove"
+    @mouseup="handleTouchEnd"
   >
     <view 
       class="inner"
@@ -80,6 +82,7 @@ const currentOffset = ref(0);
 
 const id = RandomUtils.genNonDuplicateID(12);
 let startX = 0;
+let pressed = false;
 let startOffset = 0;
 let lastMovedSize = 0;
 
@@ -98,18 +101,21 @@ function handleDrag(x: number) {
 function handleTouchStart(e: any) {
   if (props.disabled)
     return;
+  pressed = true;
   e.stopPropagation();
   currentAnim.value = false;
   startOffset = currentOffset.value ;
   startX = e.touches[0]?.clientX;
+  lastMovedSize = 0;
 }
 function handleTouchMove(e: any) {
-  if (props.disabled)
+  if (props.disabled || !pressed)
     return;
   e.stopPropagation();
   handleDrag(e.touches[0]?.clientX);
 }
 function handleTouchEnd(e: any) {
+  pressed = false;
   if (props.disabled)
     return;
   currentAnim.value = true;
@@ -125,10 +131,6 @@ function handleTouchEnd(e: any) {
       currentOffset.value = 0;
   });
 }
-function handleClick() {
-  if (props.autoClose)
-    currentOffset.value = 0;
-}
 
 </script>
 

+ 3 - 1
src/components/feedback/Toast.vue

@@ -99,7 +99,9 @@ export interface ToastProps {
 }
 export interface ToastShowProps {
   /**
-   * 自动关闭的延时,单位ms。为0不会自动关闭
+   * 自动关闭的延时,单位ms。
+   * * 0:根据内容长度自动设置关闭时间,最小3000ms,最大15000ms
+   * * -1:一直显示,直到手动关闭
    */
   duration?: number;
   /**

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

@@ -94,8 +94,10 @@ function handleMouseLeave() {
     emit('state', 'default')
 }
 function handleClick(e: Event) {
-  if (props.touchable)
+  if (props.touchable) {
+    emit('state', 'default')
     emit('click', e);
+  }
 }
 function handleTouchEnd() {
   if (props.touchable) {

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

@@ -127,7 +127,7 @@ import FlexRow from '../layout/FlexRow.vue';
 import Height from '../layout/space/Height.vue';
 import CalendarItem from './CalendarItem.vue';
 import type { TextProps } from '../basic/Text.vue';
-import calendar, { getFestival } from '../utils/Calendar';
+import calendar, { getFestival } from './CalendarUtils';
 import type { FlexProps } from '../layout/FlexView.vue';
 import FixedVirtualList from '../list/FixedVirtualList.vue';
 
@@ -410,8 +410,8 @@ const dayGrids = computed(() => {
 });
 const dayGridsScroll = computed(() => {
 
-  const startDate = DateUtils.parseDate(props.startDate, props.dateFormat);
-  const endDate = DateUtils.parseDate(props.endDate, props.dateFormat);
+  const startDate = DateUtils.parseDate(props.startDate);
+  const endDate = DateUtils.parseDate(props.endDate);
   const endYear = endDate.getFullYear();
   const endMonth = endDate.getMonth();
   let startYear = startDate.getFullYear();

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

@@ -26,10 +26,10 @@
     </slot>
   </Popup>
   <Text 
-    v-if="showSelectText && "
+    v-if="showSelectText"
     :size="30"
     :color="selectText ? 'text.content' : 'text.second'"
-    :text="selectText" 
+    :text="selectText || placeholder" 
     :maxWidth="300"
     v-bind="textProps"
   />

+ 538 - 0
src/components/form/CalendarUtils.ts

@@ -0,0 +1,538 @@
+/**
+* @1900-2100区间内的公历、农历互转
+*/
+export const calendar = {
+  /**
+    * 农历1900-2100的润大小信息表
+    * @Array Of Property
+    * @return Hex 
+    */
+  lunarInfo: [0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,//1900-1909
+    0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,//1910-1919
+    0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,//1920-1929
+    0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,//1930-1939
+    0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,//1940-1949
+    0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,//1950-1959
+    0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,//1960-1969
+    0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,//1970-1979
+    0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,//1980-1989
+    0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0,//1990-1999
+    0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,//2000-2009
+    0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,//2010-2019
+    0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,//2020-2029
+    0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,//2030-2039
+    0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,//2040-2049
+    /**Add By JJonline@JJonline.Cn**/
+    0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,//2050-2059
+    0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,//2060-2069
+    0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,//2070-2079
+    0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,//2080-2089
+    0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,//2090-2099
+    0x0d520],//2100
+
+  /**
+    * 公历每个月份的天数普通表
+    * @Array Of Property
+    * @return Number 
+    */
+  solarMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
+
+  /**
+    * 天干地支之天干速查表
+    * @Array Of Property trans["甲","乙","丙","丁","戊","己","庚","辛","壬","癸"]
+    * @return Cn string 
+    */
+  Gan: ["\u7532", "\u4e59", "\u4e19", "\u4e01", "\u620a", "\u5df1", "\u5e9a", "\u8f9b", "\u58ec", "\u7678"],
+
+  /**
+    * 天干地支之地支速查表
+    * @Array Of Property 
+    * @trans["子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"]
+    * @return Cn string 
+    */
+  Zhi: ["\u5b50", "\u4e11", "\u5bc5", "\u536f", "\u8fb0", "\u5df3", "\u5348", "\u672a", "\u7533", "\u9149", "\u620c", "\u4ea5"],
+
+  /**
+    * 天干地支之地支速查表<=>生肖
+    * @Array Of Property 
+    * @trans["鼠","牛","虎","兔","龙","蛇","马","羊","猴","鸡","狗","猪"]
+    * @return Cn string 
+    */
+  Animals: ["\u9f20", "\u725b", "\u864e", "\u5154", "\u9f99", "\u86c7", "\u9a6c", "\u7f8a", "\u7334", "\u9e21", "\u72d7", "\u732a"],
+
+  /**
+    * 24节气速查表
+    * @Array Of Property 
+    * @trans["小寒","大寒","立春","雨水","惊蛰","春分","清明","谷雨","立夏","小满","芒种","夏至","小暑","大暑","立秋","处暑","白露","秋分","寒露","霜降","立冬","小雪","大雪","冬至"]
+    * @return Cn string 
+    */
+  solarTerm: ["\u5c0f\u5bd2", "\u5927\u5bd2", "\u7acb\u6625", "\u96e8\u6c34", "\u60ca\u86f0", "\u6625\u5206", "\u6e05\u660e", "\u8c37\u96e8", "\u7acb\u590f", "\u5c0f\u6ee1", "\u8292\u79cd", "\u590f\u81f3", "\u5c0f\u6691", "\u5927\u6691", "\u7acb\u79cb", "\u5904\u6691", "\u767d\u9732", "\u79cb\u5206", "\u5bd2\u9732", "\u971c\u964d", "\u7acb\u51ac", "\u5c0f\u96ea", "\u5927\u96ea", "\u51ac\u81f3"],
+
+  /**
+    * 1900-2100各年的24节气日期速查表
+    * @Array Of Property 
+    * @return 0x string For splice
+    */
+  sTermInfo: ['9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f',
+    '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
+    '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f',
+    'b027097bd097c36b0b6fc9274c91aa', '9778397bd19801ec9210c965cc920e', '97b6b97bd19801ec95f8c965cc920f',
+    '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd197c36c9210c9274c91aa',
+    '97b6b97bd19801ec95f8c965cc920e', '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2',
+    '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e', '97bcf97c3598082c95f8e1cfcc920f',
+    '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e',
+    '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722',
+    '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f',
+    '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
+    '97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd07f595b0b6fc920fb0722',
+    '9778397bd097c36b0b6fc9210c8dc2', '9778397bd19801ec9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f',
+    '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
+    '97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
+    '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bd07f1487f595b0b0bc920fb0722',
+    '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
+    '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
+    '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722',
+    '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
+    '97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b97bd19801ec9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
+    '9778397bd097c36b0b6fc9210c91aa', '97b6b97bd197c36c9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722',
+    '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
+    '97b6b7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
+    '9778397bd097c36b0b70c9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
+    '7f0e397bd097c35b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
+    '7f0e27f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
+    '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
+    '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
+    '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
+    '97b6b7f0e47f531b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
+    '9778397bd097c36b0b6fc9210c91aa', '97b6b7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
+    '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '977837f0e37f149b0723b0787b0721',
+    '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c35b0b6fc9210c8dc2',
+    '977837f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
+    '7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
+    '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
+    '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
+    '977837f0e37f14998082b0723b06bd', '7f07e7f0e37f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
+    '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721',
+    '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5',
+    '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f531b0b0bb0b6fb0722',
+    '7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35',
+    '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
+    '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721',
+    '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd',
+    '7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35',
+    '7ec967f0e37f14998082b0723b06bd', '7f07e7f0e37f14998083b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
+    '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14898082b0723b02d5', '7f07e7f0e37f14998082b0787b0721',
+    '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66aa89801e9808297c35', '665f67f0e37f14898082b0723b02d5',
+    '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66a449801e9808297c35',
+    '665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
+    '7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
+    '7f07e7f0e47f531b0723b0b6fb0721', '7f0e26665b66a449801e9808297c35', '665f67f0e37f1489801eb072297c35',
+    '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722'],
+
+  /**
+    * 数字转中文速查表
+    * @Array Of Property 
+    * @trans ['日','一','二','三','四','五','六','七','八','九','十']
+    * @return Cn string 
+    */
+  nStr1: ["\u65e5", "\u4e00", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341"],
+
+  /**
+    * 日期转农历称呼速查表
+    * @Array Of Property 
+    * @trans ['初','十','廿','卅']
+    * @return Cn string 
+    */
+  nStr2: ["\u521d", "\u5341", "\u5eff", "\u5345"],
+
+  /**
+    * 月份转农历称呼速查表
+    * @Array Of Property 
+    * @trans ['正','一','二','三','四','五','六','七','八','九','十','冬','腊']
+    * @return Cn string 
+    */
+  nStr3: ["\u6b63", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341", "\u51ac", "\u814a"],
+
+  /**
+    * 返回农历y年一整年的总天数
+    * @param lunar Year
+    * @return Number
+    * @eg:let count = calendar.lYearDays(1987) ;//count=387
+    */
+  lYearDays: function (y: number) {
+    let i, sum = 348;
+    for (i = 0x8000; i > 0x8; i >>= 1) { sum += (calendar.lunarInfo[y - 1900] & i) ? 1 : 0; }
+    return (sum + calendar.leapDays(y));
+  },
+
+  /**
+    * 返回农历y年闰月是哪个月;若y年没有闰月 则返回0
+    * @param lunar Year
+    * @return Number (0-12)
+    * @eg:let leapMonth = calendar.leapMonth(1987) ;//leapMonth=6
+    */
+  leapMonth: function (y: number) { //闰字编码 \u95f0
+    return (calendar.lunarInfo[y - 1900] & 0xf);
+  },
+
+  /**
+    * 返回农历y年闰月的天数 若该年没有闰月则返回0
+    * @param lunar Year
+    * @return Number (0、29、30)
+    * @eg:let leapMonthDay = calendar.leapDays(1987) ;//leapMonthDay=29
+    */
+  leapDays: function (y: number) {
+    if (calendar.leapMonth(y)) {
+      return ((calendar.lunarInfo[y - 1900] & 0x10000) ? 30 : 29);
+    }
+    return (0);
+  },
+
+  /**
+    * 返回农历y年m月(非闰月)的总天数,计算m为闰月时的天数请使用leapDays方法
+    * @param lunar Year
+    * @return Number (-1、29、30)
+    * @eg:let MonthDay = calendar.monthDays(1987,9) ;//MonthDay=29
+    */
+  monthDays: function (y: number, m: number) {
+    if (m > 12 || m < 1) { return -1 }//月份参数从1至12,参数错误返回-1
+    return ((calendar.lunarInfo[y - 1900] & (0x10000 >> m)) ? 30 : 29);
+  },
+
+  /**
+    * 返回公历(!)y年m月的天数
+    * @param solar Year
+    * @return Number (-1、28、29、30、31)
+    * @eg:let solarMonthDay = calendar.leapDays(1987) ;//solarMonthDay=30
+    */
+  solarDays: function (y: number, m: number) {
+    if (m > 12 || m < 1) { return -1 } //若参数错误 返回-1
+    let ms = m - 1;
+    if (ms == 1) { //2月份的闰平规律测算后确认返回28或29
+      return (((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)) ? 29 : 28);
+    } else {
+      return (calendar.solarMonth[ms]);
+    }
+  },
+
+  /**
+   * 农历年份转换为干支纪年
+   * @param  lYear 农历年的年份数
+   * @return Cn string
+   */
+  toGanZhiYear: function (lYear: number) {
+    let ganKey = (lYear - 3) % 10;
+    let zhiKey = (lYear - 3) % 12;
+    if (ganKey == 0) ganKey = 10;//如果余数为0则为最后一个天干
+    if (zhiKey == 0) zhiKey = 12;//如果余数为0则为最后一个地支
+    return calendar.Gan[ganKey - 1] + calendar.Zhi[zhiKey - 1];
+
+  },
+
+  /**
+   * 公历月、日判断所属星座
+   * @param  cMonth [description]
+   * @param  cDay [description]
+   * @return Cn string
+   */
+  toAstro: function (cMonth: number, cDay: number) {
+    let s = "\u9b54\u7faf\u6c34\u74f6\u53cc\u9c7c\u767d\u7f8a\u91d1\u725b\u53cc\u5b50\u5de8\u87f9\u72ee\u5b50\u5904\u5973\u5929\u79e4\u5929\u874e\u5c04\u624b\u9b54\u7faf";
+    let arr = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22];
+    return s.substr(cMonth * 2 - (cDay < arr[cMonth - 1] ? 2 : 0), 2) + "\u5ea7";//座
+  },
+
+  /**
+    * 传入offset偏移量返回干支
+    * @param offset 相对甲子的偏移量
+    * @return Cn string
+    */
+  toGanZhi: function (offset: number) {
+    return calendar.Gan[offset % 10] + calendar.Zhi[offset % 12];
+  },
+
+  /**
+    * 传入公历(!)y年获得该年第n个节气的公历日期
+    * @param y公历年(1900-2100);n二十四节气中的第几个节气(1~24);从n=1(小寒)算起 
+    * @return day Number
+    * @eg:let _24 = calendar.getTerm(1987,3) ;//_24=4;意即1987年2月4日立春
+    */
+  getTerm: function (y: number, n: number) {
+    if (y < 1900 || y > 2100) { return -1; }
+    if (n < 1 || n > 24) { return -1; }
+    let _table = calendar.sTermInfo[y - 1900];
+    let _info = [
+      parseInt('0x' + _table.substr(0, 5)).toString(),
+      parseInt('0x' + _table.substr(5, 5)).toString(),
+      parseInt('0x' + _table.substr(10, 5)).toString(),
+      parseInt('0x' + _table.substr(15, 5)).toString(),
+      parseInt('0x' + _table.substr(20, 5)).toString(),
+      parseInt('0x' + _table.substr(25, 5)).toString()
+    ];
+    let _calday = [
+      _info[0].substr(0, 1),
+      _info[0].substr(1, 2),
+      _info[0].substr(3, 1),
+      _info[0].substr(4, 2),
+
+      _info[1].substr(0, 1),
+      _info[1].substr(1, 2),
+      _info[1].substr(3, 1),
+      _info[1].substr(4, 2),
+
+      _info[2].substr(0, 1),
+      _info[2].substr(1, 2),
+      _info[2].substr(3, 1),
+      _info[2].substr(4, 2),
+
+      _info[3].substr(0, 1),
+      _info[3].substr(1, 2),
+      _info[3].substr(3, 1),
+      _info[3].substr(4, 2),
+
+      _info[4].substr(0, 1),
+      _info[4].substr(1, 2),
+      _info[4].substr(3, 1),
+      _info[4].substr(4, 2),
+
+      _info[5].substr(0, 1),
+      _info[5].substr(1, 2),
+      _info[5].substr(3, 1),
+      _info[5].substr(4, 2),
+    ];
+    return parseInt(_calday[n - 1]);
+  },
+
+  /**
+    * 传入农历数字月份返回汉语通俗表示法
+    * @param lunar month
+    * @return Cn string
+    * @eg:let cnMonth = calendar.toChinaMonth(12) ;//cnMonth='腊月'
+    */
+  toChinaMonth: function (m: number) { // 月 => \u6708
+    if (m > 12 || m < 1) { return -1 } //若参数错误 返回-1
+    let s = calendar.nStr3[m - 1];
+    s += "\u6708";//加上月字
+    return s;
+  },
+
+  /**
+    * 传入农历日期数字返回汉字表示法
+    * @param lunar day
+    * @return Cn string
+    * @eg:let cnDay = calendar.toChinaDay(21) ;//cnMonth='廿一'
+    */
+  toChinaDay: function (d: number) { //日 => \u65e5
+    let s;
+    switch (d) {
+      case 10:
+        s = '\u521d\u5341'; break;
+      case 20:
+        s = '\u4e8c\u5341'; break;
+        break;
+      case 30:
+        s = '\u4e09\u5341'; break;
+        break;
+      default:
+        s = calendar.nStr2[Math.floor(d / 10)];
+        s += calendar.nStr1[d % 10];
+    }
+    return (s);
+  },
+
+  /**
+    * 年份转生肖[!仅能大致转换] => 精确划分生肖分界线是“立春”
+    * @param y year
+    * @return Cn string
+    * @eg:let animal = calendar.getAnimal(1987) ;//animal='兔'
+    */
+  getAnimal: function (y: number) {
+    return calendar.Animals[(y - 4) % 12]
+  },
+
+  /**
+    * 传入阳历年月日获得详细的公历、农历object信息 <=>JSON
+    * @param y  solar year
+    * @param m  solar month
+    * @param d  solar day
+    * @return JSON object
+    * @eg:console.log(calendar.solar2lunar(1987,11,01));
+    */
+  solar2lunar: function (y: number, m: number, d: number) { //参数区间1900.1.31~2100.12.31
+    let objDate : Date;
+    if (
+      y < 1900 || y > 2100 || //年份限定、上限
+      (y == 1900 && m == 1 && d < 31) //下限
+    )
+      throw new Error('年份超出范围');
+    objDate = new Date(y, m - 1, d)
+
+    let i, leap = 0, temp = 0;
+    //修正ymd参数
+    y = objDate.getFullYear(), m = objDate.getMonth() + 1, d = objDate.getDate();
+    let offset = (Date.UTC(objDate.getFullYear(), objDate.getMonth(), objDate.getDate()) - Date.UTC(1900, 0, 31)) / 86400000;
+    for (i = 1900; i < 2101 && offset > 0; i++) { temp = calendar.lYearDays(i); offset -= temp; }
+    if (offset < 0) { offset += temp; i--; }
+
+    //是否今天
+    let isTodayObj = new Date(), isToday = false;
+    if (isTodayObj.getFullYear() == y && isTodayObj.getMonth() + 1 == m && isTodayObj.getDate() == d) {
+      isToday = true;
+    }
+    //星期几
+    let nWeek = objDate.getDay(), cWeek = calendar.nStr1[nWeek];
+    if (nWeek == 0) { nWeek = 7; }//数字表示周几顺应天朝周一开始的惯例
+    //农历年
+    let year = i;
+
+    leap = calendar.leapMonth(i); //闰哪个月
+    let isLeap = false;
+
+    //效验闰月
+    for (i = 1; i < 13 && offset > 0; i++) {
+      //闰月
+      if (leap > 0 && i == (leap + 1) && isLeap == false) {
+        --i;
+        isLeap = true; temp = calendar.leapDays(year); //计算农历闰月天数
+      }
+      else {
+        temp = calendar.monthDays(year, i);//计算农历普通月天数
+      }
+      //解除闰月
+      if (isLeap == true && i == (leap + 1)) { isLeap = false; }
+      offset -= temp;
+    }
+
+    if (offset == 0 && leap > 0 && i == leap + 1)
+      if (isLeap) {
+        isLeap = false;
+      } else {
+        isLeap = true; --i;
+      }
+    if (offset < 0) { offset += temp; --i; }
+    //农历月
+    let month = i;
+    //农历日
+    let day = offset + 1;
+
+    //天干地支处理
+    let sm = m - 1;
+    let gzY = calendar.toGanZhiYear(year);
+
+    //月柱 1900年1月小寒以前为 丙子月(60进制12)
+    let firstNode = calendar.getTerm(year, (m * 2 - 1));//返回当月「节」为几日开始
+    let secondNode = calendar.getTerm(year, (m * 2));//返回当月「节」为几日开始
+
+    //依据12节气修正干支月
+    let gzM = calendar.toGanZhi((y - 1900) * 12 + m + 11);
+    if (d >= firstNode) {
+      gzM = calendar.toGanZhi((y - 1900) * 12 + m + 12);
+    }
+
+    //传入的日期的节气与否
+    let isTerm = false;
+    let Term = null;
+    if (firstNode == d) {
+      isTerm = true;
+      Term = calendar.solarTerm[m * 2 - 2];
+    }
+    if (secondNode == d) {
+      isTerm = true;
+      Term = calendar.solarTerm[m * 2 - 1];
+    }
+    //日柱 当月一日与 1900/1/1 相差天数
+    let dayCyclical = Date.UTC(y, sm, 1, 0, 0, 0, 0) / 86400000 + 25567 + 10;
+    let gzD = calendar.toGanZhi(dayCyclical + d - 1);
+    //该日期所属的星座
+    let astro = calendar.toAstro(m, d);
+
+    return { 'lYear': year, 'lMonth': month, 'lDay': day, 'Animal': calendar.getAnimal(year), 'IMonthCn': (isLeap ? "\u95f0" : '') + calendar.toChinaMonth(month), 'IDayCn': calendar.toChinaDay(day), 'cYear': y, 'cMonth': m, 'cDay': d, 'gzYear': gzY, 'gzMonth': gzM, 'gzDay': gzD, 'isToday': isToday, 'isLeap': isLeap, 'nWeek': nWeek, 'ncWeek': "\u661f\u671f" + cWeek, 'isTerm': isTerm, 'Term': Term, 'astro': astro };
+  },
+
+  /**
+    * 传入农历年月日以及传入的月份是否闰月获得详细的公历、农历object信息 <=>JSON
+    * @param y  lunar year
+    * @param m  lunar month
+    * @param d  lunar day
+    * @param isLeapMonth  lunar month is leap or not.[如果是农历闰月第四个参数赋值true即可]
+    * @return JSON object
+    * @eg:console.log(calendar.lunar2solar(1987,9,10));
+    */
+  lunar2solar: function (y: number, m: number, d: number, isLeapMonth: boolean = false) {   //参数区间1900.1.31~2100.12.1
+    let leapOffset = 0;
+    let leapMonth = calendar.leapMonth(y);
+    let leapDay = calendar.leapDays(y);
+    if (isLeapMonth && (leapMonth != m)) { return -1; }//传参要求计算该闰月公历 但该年得出的闰月与传参的月份并不同
+    if (y == 2100 && m == 12 && d > 1 || y == 1900 && m == 1 && d < 31) { return -1; }//超出了最大极限值 
+    let day = calendar.monthDays(y, m);
+    let _day = day;
+    //bugFix 2016-9-25 
+    //if month is leap, _day use leapDays method 
+    if (isLeapMonth) {
+      _day = calendar.leapDays(y);
+    }
+    if (y < 1900 || y > 2100 || d > _day) { return -1; }//参数合法性效验
+
+    //计算农历的时间差
+    let offset = 0;
+    for (let i = 1900; i < y; i++) {
+      offset += calendar.lYearDays(i);
+    }
+    let leap = 0, isAdd = false;
+    for (let i = 1; i < m; i++) {
+      leap = calendar.leapMonth(y);
+      if (!isAdd) {//处理闰月
+        if (leap <= i && leap > 0) {
+          offset += calendar.leapDays(y); isAdd = true;
+        }
+      }
+      offset += calendar.monthDays(y, i);
+    }
+    //转换闰月农历 需补充该年闰月的前一个月的时差
+    if (isLeapMonth) { offset += day; }
+    //1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点)
+    let stmap = Date.UTC(1900, 1, 30, 0, 0, 0);
+    let calObj = new Date((offset + d - 31) * 86400000 + stmap);
+    let cY = calObj.getUTCFullYear();
+    let cM = calObj.getUTCMonth() + 1;
+    let cD = calObj.getUTCDate();
+
+    return calendar.solar2lunar(cY, cM, cD);
+  }
+};
+
+/**
+ * 获取今日的节日
+ * @return {string} 今日的节日
+ */
+export function getFestival(calendar: Date){
+  let str = '';
+  const month = calendar.getMonth();
+  const date = calendar.getDate();
+  
+  if ((month == 0) && (date == 1)) str = "元旦";
+  if ((month == 2) && (date == 12)) str = "植树节";
+  if ((month == 3) && (date == 5)) str = "清明节";
+  if ((month == 4) && (date == 1)) str = "劳动节";
+  if ((month == 4) && (date == 4)) str = "青年节";
+  if ((month == 5) && (date == 1)) str = "儿童节";
+  if ((month == 7) && (date == 1)) str = "建军节"
+  if ((month == 9) && (date == 1)) str = "国庆节";
+  if ((month == 11) && (date == 24)) str = "平安夜";
+  if ((month == 11) && (date == 25)) str = "圣诞节";
+
+  return str;
+}
+
+export default calendar

+ 37 - 1
src/components/form/CascadePickerField.vue

@@ -18,7 +18,14 @@
       @selectTextChange="onSelectTextChange"
     />
   </Popup>
-  <text v-if="showSelectText">{{ selectText || placeholder }}</text>
+  <Text
+    v-if="showSelectText"
+    :size="30"
+    :color="selectText ? 'text.content' : 'text.second'"
+    :text="selectText || placeholder" 
+    :maxWidth="300"
+    v-bind="textProps"
+  />
 </template>
 
 <script setup lang="ts">
@@ -29,15 +36,44 @@ import Popup from '../dialog/Popup.vue';
 import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSheetTitle.vue';
 import CascadePicker from './CascadePicker.vue';
 import { usePickerFieldTempStorageData } from './PickerUtils';
+import Text, { type TextProps } from '../basic/Text.vue';
 
 export interface CascadePickerFieldProps extends Omit<CascadePickerProps, 'value'> {
+  /**
+   * 绑定值
+   */
   modelValue?: (number|string)[];
+  /**
+   * 标题
+   */
   title?: string,
+  /**
+   * 标题属性
+   */
   titleProps?: Omit<ActionSheetTitleProps, 'title'>,
+  /**
+   * 是否显示选择的文本
+   * @default true
+   */
   showSelectText?: boolean,
+  /**
+   * 占位符
+   * @default '请选择选项'
+   */
   placeholder?: string,
+  /**
+   * 初始值
+   */
   initalValue?: (number|string)[];
+  /**
+   * 是否立即更新绑定值
+   * @default false
+   */
   shouldUpdateValueImmediately?: boolean,
+  /**
+   * 显示的文本属性
+   */
+  textProps?: TextProps,
 }
 
 const emit = defineEmits([ 'update:modelValue', 'cancel', 'confirm', 'selectTextChange', 'tempValueChange' ]);

+ 35 - 1
src/components/form/DatePickerField.vue

@@ -18,7 +18,14 @@
       @selectTextChange="onSelectTextChange"
     />
   </Popup>
-  <text v-if="showSelectText">{{ selectText || placeholder }}</text>
+  <Text
+    v-if="showSelectText"
+    :size="30"
+    :color="selectText ? 'text.content' : 'text.second'"
+    :text="selectText || placeholder" 
+    :maxWidth="300"
+    v-bind="textProps"
+  />
 </template>
 
 <script setup lang="ts">
@@ -29,15 +36,42 @@ import Popup from '../dialog/Popup.vue';
 import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSheetTitle.vue';
 import DatePicker from './DatePicker.vue';
 import { usePickerFieldTempStorageData } from './PickerUtils';
+import Text, { type TextProps } from '../basic/Text.vue';
 
 export interface DatePickerFieldProps extends Omit<DatePickerProps, 'modelValue'> {
   modelValue?: Date;
+  /**
+   * 标题
+   * @default '请选择时间'
+   */
   title?: string,
+  /**
+   * 标题属性
+   */
   titleProps?: Omit<ActionSheetTitleProps, 'title'>,
+  /**
+   * 是否显示选择的文本
+   * @default true
+   */
   showSelectText?: boolean,
+  /**
+   * 占位符
+   * @default '请选择时间'
+   */
   placeholder?: string,
+  /**
+   * 初始值
+   */
   initalValue?: Date,
+  /**
+   * 是否立即更新绑定值
+   * @default false
+   */
   shouldUpdateValueImmediately?: boolean,
+  /**
+   * 显示的文本属性
+   */
+  textProps?: TextProps,
 }
 
 const emit = defineEmits([ 'update:modelValue', 'cancel', 'confirm', 'selectTextChange', 'tempValueChange' ]);

+ 35 - 1
src/components/form/DateTimePickerField.vue

@@ -18,7 +18,14 @@
       @selectTextChange="onSelectTextChange"
     />
   </Popup>
-  <text v-if="showSelectText">{{ selectText || placeholder }}</text>
+  <Text
+    v-if="showSelectText"
+    :size="30"
+    :color="selectText ? 'text.content' : 'text.second'"
+    :text="selectText || placeholder" 
+    :maxWidth="300"
+    v-bind="textProps"
+  />
 </template>
 
 <script setup lang="ts">
@@ -29,15 +36,42 @@ import Popup from '../dialog/Popup.vue';
 import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSheetTitle.vue';
 import DateTimePicker from './DateTimePicker.vue';
 import { usePickerFieldTempStorageData } from './PickerUtils';
+import Text, { type TextProps } from '../basic/Text.vue';
 
 export interface DateTimePickerFieldProps extends Omit<DateTimePickerProps, 'modelValue'> {
   modelValue?: Date;
+  /**
+   * 标题
+   * @default '请选择时间'
+   */
   title?: string,
+  /**
+   * 标题属性
+   */
   titleProps?: Omit<ActionSheetTitleProps, 'title'>,
+  /**
+   * 是否显示选择的文本
+   * @default true
+   */
   showSelectText?: boolean,
+  /**
+   * 占位符
+   * @default '请选择时间'
+   */
   placeholder?: string,
+  /**
+   * 初始值
+   */
   initalValue?: Date,
+  /**
+   * 是否立即更新绑定值
+   * @default false
+   */
   shouldUpdateValueImmediately?: boolean,
+  /**
+   * 显示的文本属性
+   */
+  textProps?: TextProps,
 }
 
 const emit = defineEmits([ 'update:modelValue', 'cancel', 'confirm', 'selectTextChange', 'tempValueChange' ]);

+ 76 - 2
src/components/form/FormContext.ts

@@ -2,15 +2,45 @@ import { type InjectionKey, inject, provide, type Ref, computed, ref, watch } fr
 import type { FieldProps } from "./Field.vue";
 import { useCellContext } from "../basic/CellContext";
 
+/**
+ * 校验触发时机
+ * - blur: 失去焦点时触发校验
+ * - change: 值改变时触发校验
+ * - submit: 提交表单时触发校验
+ */
 export type ValidTrigger = "blur" | "change" | "submit";
 
+/**
+ * 表单项上下文
+ */
 export type FormItemContext = {
   getFieldName: (ref: any) => string,
+  /**
+   * 触发表单条目获得焦点事件
+   */
   onFieldFocus: () => void;
+  /**
+   * 触发表单条目失去焦点事件
+   */
   onFieldBlur: () => void;
+  /**
+   * 触发表单条目值改变事件
+   * @param newValue 新值
+   */
   onFieldChange: (newValue: unknown) => void;
+  /**
+   * 清除表单条目校验状态
+   */
   clearValidate: () => void;
+  /**
+   * 设置表单条目点击事件监听器,设置后表单项允许点击,点击后会触发点击事件。
+   * @param listener 点击事件监听器
+   */
   setOnClickListener: (listener: (() => void)|undefined) => void;
+  /**
+   * 获取表单组件中的当前值
+   * @returns 表单组件中的当前值
+   */
   getFormModelValue(): any;
 };
 export type FormItemInternalContext = {
@@ -20,8 +50,8 @@ export type FormItemInternalContext = {
   setErrorState: (errorMessage: string|null) => void;
   setBlurState(): void;
 };
-
 export type FormContext = {
+  //由表单项组件调用
   onFieldFocus: (item: FormItemInternalContext) => void;
   onFieldBlur: (item: FormItemInternalContext) => void;
   onFieldChange: (item: FormItemInternalContext, newValue: unknown) => void;
@@ -44,21 +74,55 @@ export type FormContext = {
   getItemRequieed: (item: FormItemInternalContext) => boolean;
 };
 
+/**
+ * 用于Props默认值回调中获取表单上下文
+ * @returns FormContext
+ */
 export function propGetFormContext() {
   return inject<FormContext>('formContext', null as any);
 }
 
-export const FormItemContextContextKey: InjectionKey<FormItemContext> = Symbol('ContextProps');
+export const FormItemContextContextKey: InjectionKey<FormItemContext> = Symbol('FormItemContext');
 
+/**
+ * 用于注入表单项上下文
+ * @returns FormItemContext
+ */
 export function useInjectFormItemContext() : FormItemContext {
   const context = inject<FormItemContext>(FormItemContextContextKey, null as any);
   provide(FormItemContextContextKey, null as any as FormItemContext);
   return context as FormItemContext;
 }
+/**
+ * 用于注入表单上下文
+ * @returns FormContext
+ */
 export function useInjectFormContext() : FormContext {
   return inject<FormContext>('formContext', null as any);
 }
 
+/**
+ * 用于注入表单项子组件值,用于实现表单项值的双向绑定。
+ * 
+ * 组件可以通过返回的 `value` 属性获取当前值,通过 `updateValue` 方法更新值,
+ * 即使外部未绑定 `modelValue` 属性,也可以正常工作。
+ * 
+ * ```ts
+ * const {
+    value,
+    updateValue,
+  } = useFieldChildValueInjector(
+    toRef(props, 'modelValue'), 
+    (v) => emit('update:modelValue', v)
+  );
+ * ```
+ * @param propsModelValue 组件外部传入的modelValue
+ * @param emit 组件外部的emit
+ * @param secondParentContext 二级父组件上下文,用于更新二级父组件的值。
+ * @param fieldClick 表单项点击事件监听器,设置后表单项允许点击,点击后会触发点击事件。
+ * @param initialValue 初始值
+ * @returns 
+ */
 export function useFieldChildValueInjector<T>(
   propsModelValue: Ref<T>, 
   emit: (v: T) => void, 
@@ -83,6 +147,10 @@ export function useFieldChildValueInjector<T>(
     shadowRefValue.value = v;    
   })
 
+  /**
+   * 更新表单项值
+   * @param newValue 新值
+   */
   function updateValue(newValue: T) {
     if (secondParentContext)
       secondParentContext.updateValue(newValue);
@@ -100,8 +168,14 @@ export function useFieldChildValueInjector<T>(
   }
 
   return {
+    /**
+     * 临时值
+     */
     value: value,
     updateValue,
+    /**
+     * 表单项上下文
+     */
     context,
   }
 }

+ 14 - 3
src/components/form/PickerField.vue

@@ -18,18 +18,25 @@
       @selectTextChange="onSelectTextChange"
     />
   </Popup>
-  <text v-if="showSelectText">{{ selectText || placeholder }}</text>
+  <Text
+    v-if="showSelectText"
+    :size="30"
+    :color="selectText ? 'text.content' : 'text.second'"
+    :text="selectText || placeholder" 
+    :maxWidth="300"
+    v-bind="textProps"
+  />
 </template>
 
 <script setup lang="ts">
 import { ref, toRef } from 'vue';
 import { useFieldChildValueInjector } from './FormContext';
+import { usePickerFieldTempStorageData } from './PickerUtils';
 import type { PickerProps } from './Picker.vue';
 import Popup from '../dialog/Popup.vue';
 import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSheetTitle.vue';
 import Picker from './Picker.vue';
-import { usePickerFieldTempStorageData } from './PickerUtils';
-import Empty from '../feedback/Empty.vue';
+import Text, { type TextProps } from '../basic/Text.vue';
 
 export interface PickerFieldProps extends Omit<PickerProps, 'value'> {
   modelValue?: (number|string)[];
@@ -57,6 +64,10 @@ export interface PickerFieldProps extends Omit<PickerProps, 'value'> {
    * 是否立即更新值
    */
   shouldUpdateValueImmediately?: boolean,
+  /**
+   * 显示的文本属性
+   */
+  textProps?: TextProps,
 }
 
 const emit = defineEmits([ 'update:modelValue', 'cancel', 'confirm', 'selectTextChange', 'tempValueChange' ]);

+ 19 - 13
src/components/form/Rate.vue

@@ -4,12 +4,16 @@
     class="nana-rate"
     @touchstart="handleTouchStart"
     @touchmove="handleTouchMove"
+    @touchend="handleTouchEnd"
+    @mouseup="handleTouchEnd"
+    @mousedown="handleTouchStart"
+    @mousemove="handleTouchMove"
   >
     <template
       v-for="i of count"
       :key="i"
     >
-      <view v-if="i === Math.floor(value) && i < Math.ceil(value)" class="active-half">
+      <view v-if="i - 1 === Math.floor(value) && i - 1 < Math.ceil(value)" class="active-half">
         <view :style="(starActiveHalfStyle as any)">
           <Icon 
             :icon="icon"
@@ -28,7 +32,7 @@
         />
       </view>
       <Icon 
-        v-else-if="i < value" 
+        v-else-if="i <= value" 
         :icon="icon" 
         v-bind="props.starActiveStyle"
         :color="disabled ? starDisableColor : starActiveColor"
@@ -36,7 +40,7 @@
         :style="starActiveStyle"
       />
       <Icon
-        v-else-if="i >= value" 
+        v-else-if="i > value" 
         :icon="voidIcon" 
         v-bind="props.starStyle" 
         :color="disabled ? starDisableColor : starColor" 
@@ -174,37 +178,33 @@ const width = computed(() => (props.size + props.space) * props.count);
 const {
   value,
   updateValue,
-  context,
 } = useFieldChildValueInjector(
   toRef(props, 'modelValue'), 
   (v) => emit('update:modelValue', v)
 );
 
 let absLeft = 0;
+let pressed = false;
 
 function handleDrag(x: number) {
   let v = (x - absLeft) / rpx2px(width.value) * props.count;
 
   if (props.half) {
     let demcial = v - Math.floor(v);
-    v++;
     if (demcial < 0.2) demcial = 0;
     else if (demcial > 0.8) demcial = 1;
     else demcial = 0.5;
     v = Math.floor(v) + demcial;
   }
   else {
-    if (v <= 1 && props.canbeZero) {
-      v = Math.round(v);
-    }
-    else {
-      v++;
-      v = Math.ceil(v);
-    }
+    v = Math.ceil(v);
   }
+  if (v === 0 && !props.canbeZero)
+    v = props.half ? 0.5 : 1;
 
-  if (v !== value.value)
+  if (v !== value.value) {
     updateValue(v);
+  }
 }
 
 function handleTouchStart(e: any) {
@@ -219,14 +219,20 @@ function handleTouchStart(e: any) {
     if (res)
       absLeft = (res as any).left;
     handleDrag(e.touches[0]?.clientX);
+    pressed = true;
   }).exec();
 }
 function handleTouchMove(e: any) {
   if (props.readonly || props.disabled)
     return;
+  if (!pressed)
+    return;
   e.stopPropagation();
   handleDrag(e.touches[0]?.clientX);
 }
+function handleTouchEnd() {
+  pressed = false;
+}
 
 
 

+ 51 - 10
src/components/form/Signature.vue

@@ -1,18 +1,21 @@
 <template>
   <view 
     class="signature-container"
-    :style="containerStyle" 
-    ref="containerRef"
+    :style="containerStyle"
   >
     <canvas
       class="signature-canvas"
       disable-scroll
+      :id="id"
       :canvas-id="id"
       :style="canvasStyle"
       @touchstart="handleTouchStart"
       @touchmove="handleTouchMove"
       @touchend="handleTouchEnd"
       @touchcancel="handleTouchEnd"
+      @mousedown="handleMouseDown"
+      @mousemove="handleMouseMove"
+      @mouseup="handleMouseUp"
     ></canvas>
   </view>
 </template>
@@ -80,6 +83,7 @@ const currentLine = ref<Line | null>(null);
 const containerRect = ref<UniApp.NodeInfo>();
 let timer = 0;
 let isDrawing = false;
+let absPos = [0,0];
 let isDirty = true;
 
 async function initCanvas() {
@@ -114,10 +118,8 @@ function render() {
   canvasContext.value.draw();
 }
 
-function handleTouchStart(e: any) {
-  if (!canvasContext.value) return;
+function startDrag(x: number, y: number) {
   isDrawing = true;
-  const { x, y } = e.touches[0];
   // 创建新线条
   currentLine.value = {
     points: [{ x, y }],
@@ -125,22 +127,61 @@ function handleTouchStart(e: any) {
     width: props.lineWidth || 3
   };
 }
-function handleTouchMove(e: any) {
-  if (!isDrawing || !currentLine.value || !canvasContext.value) 
+function doDrag(x: number, y: number) {
+  if (!currentLine.value)
     return;
-  const { x, y } = e.touches[0];
   currentLine.value.points.push({ x, y });
   isDirty = true;
 }
-function handleTouchEnd() {
+function endDrag() {
   if (!isDrawing || !currentLine.value) 
     return;
-
   isDrawing = false;
   lines.value.push(currentLine.value);
   currentLine.value = null;
 }
 
+function handleTouchStart(e: any) {
+  if (!canvasContext.value) return;
+  const { x, y } = e.touches[0];
+  startDrag(x, y);
+}
+function handleTouchMove(e: any) {
+  if (!isDrawing || !currentLine.value || !canvasContext.value) 
+    return;
+  const { x, y } = e.touches[0];
+  doDrag(x, y);
+}
+function handleTouchEnd() {
+  endDrag();
+}
+
+function handleMouseDown(e: any) {
+  if (!canvasContext.value) 
+    return;
+  //H5鼠标事件需要减去canvas位置
+  uni.createSelectorQuery()
+    .in(instance)
+    .select(`#${id}`)
+    .boundingClientRect()
+    .exec((res) => {
+      if (res[0]) {
+        const { clientX, clientY } = e.touches[0];
+        absPos = [res[0].left, res[0].top];
+        startDrag(clientX - res[0].left, clientY - res[0].top);
+      }
+    });
+}
+function handleMouseMove(e: any) {
+  if (!isDrawing || !currentLine.value || !canvasContext.value) 
+    return;
+  const { clientX, clientY } = e.touches[0];
+  doDrag(clientX - absPos[0], clientY - absPos[1]);
+}
+function handleMouseUp() {
+  endDrag();
+}
+
 function drawBackground() {
   if (!canvasContext.value || !canvasWidth.value || !canvasHeight.value) return;
   const ctx = canvasContext.value;

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

@@ -12,6 +12,11 @@
     }"
     @touchstart="handleTouchStart"
     @touchmove="handleTouchMove"
+    @touchend="handleTouchEnd"
+    @touchcancel="handleTouchEnd"
+    @mousedown="handleTouchStart"
+    @mousemove="handleTouchMove"
+    @mouseup="handleTouchEnd"
   >
     <view
       class="dot"
@@ -155,6 +160,7 @@ const instance = getCurrentInstance();
 const id = RandomUtils.genNonDuplicateID(12);
 let absLeft = 0;
 let absWidth = 0;
+let pressed = false;
 
 function doUpdateValue(v: number) {
   v = Math.max(props.min, Math.min(props.max, v));
@@ -183,6 +189,7 @@ function handleTouchStart(e: any) {
     if (res) {
       absLeft = (res as any).left;
       absWidth = (res as any).width;
+      pressed = true;
     }
     handleDrag(e.touches[0]?.clientX);
   }).exec();
@@ -190,9 +197,14 @@ function handleTouchStart(e: any) {
 function handleTouchMove(e: any) {
   if (props.disabled)
     return;
+  if (!pressed)
+    return;
   e.stopPropagation();
   handleDrag(e.touches[0]?.clientX);
 }
+function handleTouchEnd() {
+  pressed = false;
+}
 
 defineOptions({
   options: {

+ 1 - 1
src/components/form/Switch.vue

@@ -40,7 +40,7 @@
         }">
           <ActivityIndicator
             v-if="loading" 
-            :size="Math.max(size / 2 - 5, 10)"
+            :size="Math.max(size * 0.6  - 2, 10)"
             :color="themeContext.resolveThemeColor(value ? color : inverseColor)" 
           />
         </view>

+ 31 - 1
src/components/form/TimePickerField.vue

@@ -18,7 +18,14 @@
       @selectTextChange="onSelectTextChange"
     />
   </Popup>
-  <text v-if="showSelectText">{{ selectText || placeholder }}</text>
+  <Text
+    v-if="showSelectText"
+    :size="30"
+    :color="selectText ? 'text.content' : 'text.second'"
+    :text="selectText || placeholder" 
+    :maxWidth="300"
+    v-bind="textProps"
+  />
 </template>
 
 <script setup lang="ts">
@@ -29,15 +36,38 @@ import Popup from '../dialog/Popup.vue';
 import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSheetTitle.vue';
 import TimePicker from './TimePicker.vue';
 import { usePickerFieldTempStorageData } from './PickerUtils';
+import Text, { type TextProps } from '../basic/Text.vue';
 
 export interface TimePickerFieldProps extends Omit<TimePickerProps, 'modelValue'> {
   modelValue?: Date;
+  /**
+   * 标题
+   */
   title?: string,
+  /**
+   * 标题属性
+   */
   titleProps?: Omit<ActionSheetTitleProps, 'title'>,
+  /**
+   * 是否显示选择的文本
+   */
   showSelectText?: boolean,
+  /**
+   * 占位符
+   */
   placeholder?: string,
+  /**
+   * 初始值
+   */
   initalValue?: Date,
+  /**
+   * 是否立即更新值
+   */
   shouldUpdateValueImmediately?: boolean,
+  /**
+   * 显示的文本属性
+   */
+  textProps?: TextProps,
 }
 
 const emit = defineEmits([ 'update:modelValue', 'cancel', 'confirm', 'selectTextChange', 'tempValueChange' ]);

+ 1 - 1
src/components/form/Uploader.vue

@@ -59,7 +59,7 @@
 </template>
 
 <script setup lang="ts">
-import { h, ref } from 'vue';
+import { ref } from 'vue';
 import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
 import type { ToastInstance } from '../feedback/Toast.vue';
 import Toast from '../feedback/Toast.vue';

+ 1 - 1
src/components/form/UploaderField.vue

@@ -35,7 +35,7 @@ const {
   toRef(props, 'modelValue'), 
   (v) => emit('update:modelValue', v),
   undefined,
-  () => { /*uploaderRef.value?.pick()*/ },
+  //() => { /*uploaderRef.value?.pick()*/ },
 );
 
 function handleListChange(list: UploaderItem[]) {

+ 3 - 3
src/components/keyboard/NumberKeyBoardKey.vue

@@ -37,13 +37,13 @@ export interface NumberKeyBoardKeyProps {
 const emit = defineEmits([ 'click' ]);
 const props = withDefaults(defineProps<NumberKeyBoardKeyProps>(), {
   action: '',
-  widthCount: 1,
-  heightCount: 1,
+  widthCount: 2,
+  heightCount: 2,
   side: false
 });
 
 const context = inject<NumberKeyBoardContext>('NumberKeyBoardContext') as NumberKeyBoardContext;
-const keyWidthReal = computed(() => props.widthCount * context.keyWidthBase.value + context.keyMargin.value * (props.widthCount - 1));
+const keyWidthReal = computed(() => props.widthCount * context.keyWidthBase.value + context.keyMargin.value * (props.widthCount - 1) - 5);
 const keyHeightReal = computed(() => props.heightCount * context.keyHeight.value + (props.side ? context.keyMargin.value * (props.heightCount - 1) : 0));
 const keyStyle = computed(() => ({
   ...themeStyles.key.value,

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

@@ -8,6 +8,7 @@ export function useBaseViewStyleBuilder(props: FlexProps) {
   const themeContext = useTheme();
   const commonStyle = computed(() => {
     const obj : Record<string, any> = {
+      display: 'flex',
       flexDirection: props.direction,
       flexBasis: props.flexBasis,
       flexGrow: props.flexGrow,

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

@@ -45,6 +45,7 @@
       :disabledProp="disabledProp"
       :showCheck="mode !== 'select'"
       :checked="checkedList.indexOf(item) >= 0"
+      :overrideItem="Boolean($slots.itemContent)"
       @click="onItemPress(item, index)"
     >
       <template v-if="$slots.itemContent" #itemContent>

+ 3 - 0
src/components/nav/IndexBar.vue

@@ -11,9 +11,12 @@
         ...themeStyles.barInner.value,
         ...innerBarStyle,
       }"
+      @mousedown="handleTouchStart"
       @touchstart="handleTouchStart"
       @touchmove="handleTouchMove"
+      @mousemove="handleTouchMove"
       @touchend="handleTouchEnd"
+      @mouseup="handleTouchEnd"
       @touchcancel="handleTouchEnd"
     > 
       <view 

+ 26 - 8
src/components/nav/SegmentedControl.vue

@@ -4,11 +4,16 @@
       v-for="(item, i) in values"
       :key="i"
       :index="i"
+      :plain="true"
       :maxLen="(values?.length ?? 0) - 1"
       :radius="themeContext.resolveSize(radius)!"
-      :activeColor="themeContext.resolveThemeColor(tintColor)!"
-      :activeTextColor="themeContext.resolveThemeColor(activeTextColor)!"
-      :pressedColor="themeContext.resolveThemeColor(themeColorVars.SegmentedControlPressedColor)!"
+      :activeColor="themeContext.resolveThemeColor(tintColor)"
+      :activeTextColor="themeContext.resolveThemeColor(activeTextColor)"
+      :normalColor="plain ? 
+        themeContext.resolveThemeColor('SegmentedControlPlainBackgroundColor') :
+        themeContext.resolveThemeColor('SegmentedControlBackgroundColor', 'background.switch')
+      "
+      :pressedColor="themeContext.resolveThemeColor(themeColorVars.SegmentedControlPressedColor)"
       :label="typeof item === 'object' ? item.label : item"
       :disabled="!touchable || (typeof item === 'object' ? item.disabled || false : false)"
       :active="i === props.selectedIndex"
@@ -17,9 +22,11 @@
         ...(fill ? { 
           flexGrow: 1,
         } : {}),
-        borderStyle: 'solid',
-        borderWidth: themeContext.resolveSize(themeVars.SegmentedControlBorderWidth),
-        borderColor: themeContext.resolveThemeColor(tintColor),
+        ...(plain ? {
+          borderStyle: 'solid',
+          borderWidth: themeContext.resolveSize(themeVars.SegmentedControlBorderWidth),
+          borderColor: themeContext.resolveThemeColor(tintColor),
+        } : {}),
       }"
       :itemTextStyle="themeStyles.itemText.value"
       @click="onItemPress(i)"
@@ -29,8 +36,9 @@
 
 <script setup lang="ts">
 import FlexRow from '../layout/FlexRow.vue';
+import FlexView from '../layout/FlexView.vue';
 import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
-import { DynamicSize, DynamicSize2 } from '../theme/ThemeTools';
+import { DynamicColor, DynamicSize, DynamicSize2 } from '../theme/ThemeTools';
 import SegmentedControlItem from './SegmentedControlItem.vue';
 
 
@@ -45,7 +53,11 @@ export interface SegmentedControlProps {
    * @default true
    */
   touchable?: boolean | undefined;
-
+  /**
+   * 是否为简单外框模式。
+   * @default false
+   */
+  plain?: boolean | undefined,
   /**
    * 选中的条目索引。
    */
@@ -80,6 +92,7 @@ const emit = defineEmits(['update:selectedIndex']);
 const props = withDefaults(defineProps<SegmentedControlProps>(), {
   touchable: true,
   fill: true,
+  plain: false,
   selectedIndex: 0,
   tintColor: () => propGetThemeVar('SegmentedControlTintColor', 'primary'),
   activeTextColor: () => propGetThemeVar('SegmentedControlActiveTextColor', 'white'),
@@ -105,6 +118,11 @@ const themeColorVars = themeContext.getColors({
 });
 
 const themeStyles = themeContext.useThemeStyles({
+  indicator: {
+    transition: 'left 0.3s ease-in-out',
+    backgroundColor: DynamicColor('SegmentedControlIndicatorColor', 'button')!,
+    padding: DynamicSize2('SegmentedControlIndicatorPaddingVertical', 'SegmentedControlIndicatorPaddingHorizontal', 4, 8),
+  },
   item: {
     display: 'flex',
     alignSelf: 'auto',

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

@@ -14,7 +14,7 @@
         borderTopRightRadius: radius,
         borderBottomRightRadius: radius,
       } : {}),
-      backgroundColor: active ? activeColor : undefined,
+      backgroundColor: active ? activeColor : normalColor,
     }"
     :pressedColor="pressedColor"
     :touchable="!active && !disabled"
@@ -22,7 +22,7 @@
   >
     <text :style="{
       ...itemTextStyle,
-      color: active ? activeTextColor : activeColor,
+      color: active ? activeTextColor : undefined,
     }">{{ label }}</Text>
   </Touchable>
 </template>
@@ -38,9 +38,10 @@ export interface SegmentedControlItemProps {
   disabled: boolean,
   radius: string,
   maxLen: number,
-  activeColor: string,
-  activeTextColor: string,
-  pressedColor: string,
+  activeColor?: string,
+  normalColor?: string,
+  activeTextColor?: string,
+  pressedColor?: string,
   itemStyle: ViewStyle,
   itemTextStyle: TextStyle,
 }

+ 2 - 0
src/components/nav/TabBar.vue

@@ -47,10 +47,12 @@ export interface TabBarProps {
   selectedTabIndex?: number,
   /**
    * 预留底部安全区间距
+   * @default false
    */
   xbarSpace?: boolean,
   /**
    * 是否固定在底部
+   * @default false
    */
   fixed?: boolean,
 }

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

@@ -342,6 +342,7 @@ function onTabClick(index: number) {
     position: relative;
     padding-top: 20rpx;
     padding-bottom: 30rpx;
+    box-sizing: content-box;
   }
   .tab-item-text {
     font-size: 15px;

+ 2 - 0
src/components/theme/Theme.ts

@@ -19,6 +19,7 @@ export const DefaultTheme : ThemeConfig = {
   colorConfigs: {
     default: {
       default: 'transparent',
+      button: '#dddddd',
       primary: '#007AFF',
       secondary: '#0462C7',
       success: '#4CAF50',
@@ -49,6 +50,7 @@ export const DefaultTheme : ThemeConfig = {
     },
     mask: {
       default: 'rgba(0, 0, 0, 0.3)',
+      white: 'rgba(250, 250, 250, 0.8)',
       primary: 'rgba(9, 96, 172, 0.2)',
       info: 'rgba(33, 150, 243, 0.2)',
       success: 'rgba(39, 137, 80, 0.3)',

+ 15 - 3
src/components/theme/ThemeDefine.ts

@@ -7,6 +7,10 @@ export const ThemeKey = Symbol("NanaThemeKey");
 
 let defaultSizeUnit = 'rpx';
 
+/**
+ * 配置默认的尺寸单位
+ * @param unit 
+ */
 export function configDefaultSizeUnit(unit: string) {
   defaultSizeUnit = unit;
 }
@@ -30,15 +34,20 @@ export function propGetThemeVar<T>(key: string, defaultValue?: T) : T {
   const theme = (inject(ThemeKey, DefaultTheme) as ThemeConfig);
   return theme?.varOverrides[key] ?? defaultValue;
 }
-
+/**
+ * 在PropDefault回调中使用主题默认值的函数(回调)
+ * @param cb 回调函数,参数为获取主题变量的函数和主题配置
+ * @returns 回调函数的返回值
+ */
 export function propGetThemeVar2(cb: (getVar: (key: string, defaultValue: any) => any, theme: ThemeConfig) => any) {
   const theme = (inject(ThemeKey, DefaultTheme) as ThemeConfig);
   return cb((key, defaultValue) => getVar(theme, key, defaultValue), theme);
 }
+
+
 export function provideTheme(theme: ThemeConfig) {
   provide(ThemeKey, theme);
 }
-
 export function provideSomeThemeColor(record: Record<string, any>) {
   const v = {
     ...DefaultTheme,
@@ -49,7 +58,6 @@ export function provideSomeThemeColor(record: Record<string, any>) {
   } as ThemeConfig;
   provide(ThemeKey, v);
 }
-
 export function provideSomeThemeVar(record: Record<string, any>) {
   const v = {
     ...DefaultTheme,
@@ -258,6 +266,10 @@ export interface ThemePaddingMargin {
   b?: number,
 }
 
+/**
+ * 配置主题变量,建议在App.vue中调用
+ * @param cb 回调函数,参数为默认主题配置,返回值为新的主题配置
+ */
 export function configTheme(cb: (defaultTheme: ThemeConfig) => ThemeConfig) {
   provide(ThemeKey, cb(ObjectUtils.clone(DefaultTheme)));
 }

+ 16 - 4
src/components/typography/A.vue

@@ -14,15 +14,16 @@ export interface AProps extends TextProps {
   href?: string;
   /**
    * 链接类型
+   * auto 表示自动判断,根据 href 是否以 / 开头判断是否为 uni-app 页面,以 http 或 https 开头判断为外部链接。
    * uni-page 表示跳转 uni-app 页面(uni.navigateTo 打开)
    * url 表示跳转外部链接,h5 环境下会使用 window.open 打开,app 会调用 plus 打开浏览器,小程序不支持。
    * @default 'uni-page'
    */
-  linkType?: "uni-page" | "url" | "custom" | "back";
+  linkType?: 'auto' | "uni-page" | "url" | "custom" | "back";
 }
 
 const props = withDefaults(defineProps<AProps>(), {
-  linkType: "uni-page",
+  linkType: "auto",
 });
 const emit = defineEmits([ "click"	])
 
@@ -33,7 +34,18 @@ function onClick(e: any) {
   }
   if (props.href) {
 
-    if (props.linkType === "url") {
+    let linkType = props.linkType;
+    if (props.linkType === "auto") {
+      if (props.href.startsWith("/")) {
+        linkType = "uni-page";
+      } else if (props.href.startsWith("http") || props.href.startsWith("https")) {
+        linkType = "url";
+      } else {
+        linkType = "custom";
+      }
+    }
+
+    if (linkType === "url") {
       // #ifdef APP-PLUS
       plus.runtime.openURL(props.href)
       // #endif
@@ -51,7 +63,7 @@ function onClick(e: any) {
       // #endif
       return;
     }
-    if (props.linkType === "uni-page") {
+    if (linkType === "uni-page") {
       uni.navigateTo({ url: props.href })
       return;
     }

+ 1 - 2
src/components/typography/VerticalScrollOneText.vue

@@ -67,14 +67,13 @@ const props = withDefaults(defineProps<VerticalScrollOneTextProps>(), {
   animDuration: 230,
   animDirection: 'auto',
 });
-const currentScrollTop = ref(false);
+const currentScrollTop = ref(true);
 const upText = ref('');
 const downText = ref('');
 const anim = ref(false);
 
 onMounted(() => {
   upText.value = props.oneStr;
-  currentScrollTop.value = false;
 })
 
 watch(() => props.oneStr, (currentStr, prevStr) => {

+ 3 - 2
src/components/typography/VerticalScrollTexts.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import { onMounted, ref, watchEffect } from 'vue';
-import { SimpleTimer } from '../utils/Timer';
+import { SimpleTimer } from '@imengyu/imengyu-utils';
 import FlexRow from '../layout/FlexRow.vue';
 import VerticalScrollOneText, { type VerticalScrollOneTextProps } from './VerticalScrollOneText.vue';
 
@@ -27,7 +27,8 @@ let index = 0;
 watchEffect((onCleanUp) => {
   const timer = new SimpleTimer(undefined, () => {
     loadText()
-  }, interval).start();
+  }, interval)
+  timer.start();
 
   onCleanUp(() => {
     timer.stop();