Parcourir la source

🎨 热点新鲜事页面问题修改

快乐的梦鱼 il y a 2 semaines
Parent
commit
7179815877
41 fichiers modifiés avec 448 ajouts et 345 suppressions
  1. 6 6
      src/components/README.md
  2. 3 3
      src/components/basic/Button.vue
  3. 26 8
      src/components/basic/IconButton.vue
  4. 1 2
      src/components/basic/Image.vue
  5. 9 19
      src/components/basic/Text.vue
  6. 17 0
      src/components/display/block/IconTextBlock.vue
  7. 9 3
      src/components/display/block/ImageBlock2.vue
  8. 7 1
      src/components/display/block/ImageBlock3.vue
  9. 25 0
      src/components/display/loading/Loadmore.vue
  10. 0 90
      src/components/dynamic/DynamicFormCate.vue
  11. 0 85
      src/components/dynamic/DynamicFormCateInner.vue
  12. 2 4
      src/components/dynamic/DynamicFormControl.vue
  13. 0 3
      src/components/dynamic/Images/IconAdd.vue
  14. 0 3
      src/components/dynamic/Images/IconCheck.vue
  15. 0 3
      src/components/dynamic/Images/IconDelete.vue
  16. 0 3
      src/components/dynamic/Images/IconDown.vue
  17. 0 1
      src/components/dynamic/Images/IconError.svg
  18. 0 3
      src/components/dynamic/Images/IconUp.vue
  19. 0 1
      src/components/dynamic/Images/IconWarning.svg
  20. 0 1
      src/components/dynamic/group/FormArrayGroup.vue
  21. 34 50
      src/components/dynamic/group/FormGroup.vue
  22. 22 3
      src/components/dynamic/wrappers/CheckBoxTreeListItem.vue
  23. 93 19
      src/components/feedback/SwipeRow.vue
  24. 3 0
      src/components/form/CalendarField.vue
  25. 2 0
      src/components/form/CascadePickerField.vue
  26. 2 0
      src/components/form/CascaderField.vue
  27. 2 0
      src/components/form/DatePickerField.vue
  28. 2 0
      src/components/form/DateTimePickerField.vue
  29. 34 0
      src/components/form/Picker.ts
  30. 1 5
      src/components/form/Picker.vue
  31. 3 0
      src/components/form/PickerField.vue
  32. 65 4
      src/components/form/SearchBar.vue
  33. 2 0
      src/components/form/TimePickerField.vue
  34. 4 6
      src/components/form/Uploader.vue
  35. 0 2
      src/components/form/UploaderField.vue
  36. 3 1
      src/components/layout/BaseView.ts
  37. 1 1
      src/components/layout/FlexView.vue
  38. 47 3
      src/components/nav/Pagination.vue
  39. 2 2
      src/components/theme/ThemeDefine.ts
  40. 1 1
      src/components/typography/HorizontalScrollText.vue
  41. 20 9
      src/pages/introduction/news.vue

+ 6 - 6
src/components/README.md

@@ -6,11 +6,11 @@ NaEasy UI 是一款简单的 UniApp 移动端UI组件库。
 
 ## 版本
 
-当前版本:INDEV 0.0.4-25121701
+当前版本:1.0.8-26012501
 
 ## 版权说明
 
-© 2024 imengyu. 保留所有权利。
+© 2026 imengyu. 保留所有权利。
 
 本组件库基于 MIT 许可证开源。您可以自由地使用、修改和分发本组件库,但必须保留原始的版权声明和许可证文本。
 
@@ -38,7 +38,7 @@ SOFTWARE.
 
 ### 使用注意事项
 
-1. 请确保在您的项目中适当标注本组件库的来源
-2. 对于商用项目,请确保遵守相关法律法规
-3. 如对组件库进行了修改或扩展,请在文档中明确说明
-4. 我们不对使用本组件库可能产生的任何损失或问题承担责任
+1. 请确保在您的项目中适当标注本组件库的来源
+2. 对于商用项目,请确保遵守相关法律法规
+3. 如对组件库进行了修改或扩展,请注意备份,防止更新库后被覆盖丢失。
+4. 我们不对使用本组件库可能产生的任何损失或问题承担责任

+ 3 - 3
src/components/basic/Button.vue

@@ -70,7 +70,7 @@ import Touchable from '../feedback/Touchable.vue';
 import type { ButtonGroupContext } from './ButtonGroup.vue';
 import { useChildLinkChild } from '../composeabe/ChildItem';
 
-export type ButtomType = 'default'|'primary'|'success'|'warning'|'danger'|'custom'|'text';
+export type ButtonType = 'default'|'primary'|'success'|'warning'|'danger'|'custom'|'text';
 export type ButtomSizeType = 'small'|'medium'|'large'|'larger'|'mini';
 
 export interface ButtonProp {
@@ -82,7 +82,7 @@ export interface ButtonProp {
    * 按钮支持 default、primary、success、warning、danger、custom 自定义 六种类型
    * @default 'default'
    */
-  type?: ButtomType,
+  type?: ButtonType,
   /**
    * 占满父级主轴
    * @default false
@@ -328,7 +328,7 @@ const themeStyles = themeContext.useThemeStyles({
 
 //按钮样式生成
 const currentStyle = computed(() => {
-  const colorStyle = selectStyleType<ViewStyle, ButtomType>(props.type, 'default', 
+  const colorStyle = selectStyleType<ViewStyle, ButtonType>(props.type, 'default', 
     selectStyleType(props.scheme, 'default', {
       default: {
         default: themeStyles.buttonDefault.value,

+ 26 - 8
src/components/basic/IconButton.vue

@@ -2,7 +2,7 @@
   <Touchable
     :pressedColor="pressedBackgroundColor"
     :innerStyle="style"
-    touchable
+    :touchable="touchable"
     @click="(e) => emit('click', e)"
     v-bind="$attrs"
   >
@@ -19,7 +19,14 @@ import type { IconProps } from './Icon.vue';
 import Icon from './Icon.vue';
 import Touchable from '../feedback/Touchable.vue';
 
-export type IconButtonShapeType = 'round'|'square-full'|'custom';
+/**
+ * 图标按钮形状预设
+ * * round: 圆角按钮
+ * * round-full: 圆角按钮,宽度和高度相等
+ * * square-full: 方角按钮,宽度和高度相等
+ * * custom: 自定义按钮形状
+ */
+export type IconButtonShapeType = 'round'|'round-full'|'square-full'|'custom';
 
 export interface IconButtonProps extends IconProps {
   /**
@@ -32,6 +39,11 @@ export interface IconButtonProps extends IconProps {
    */
   padding?: number,
   /**
+   * 是否可点击
+   * @default true
+   */
+  touchable?: boolean|undefined;
+  /**
    * 按钮形状预设
    * @default round
    */
@@ -46,9 +58,9 @@ export interface IconButtonProps extends IconProps {
    */
   buttonStyle?: ViewStyle;
   /**
-   * 按钮大小
+   * 按钮大小。可以是一个数字,也可以是一个数组,数组的第一个元素是宽度,第二个元素是高度。
    */
-  buttonSize?: number|string;
+  buttonSize?: number|((number|string)[])|string;
   /**
    * 按钮背景颜色
    */
@@ -59,7 +71,8 @@ const emit = defineEmits(['click']);
 const theme = useTheme();
 const props = withDefaults(defineProps<IconButtonProps>(), {
   shape: 'custom',
-  pressedBackgroundColor: () => propGetThemeVar('IconButtonPressedColor', 'pressed.white')
+  pressedBackgroundColor: () => propGetThemeVar('IconButtonPressedColor', 'pressed.white'),
+  touchable: true,
 });
 const style = computed(() => {
   return {
@@ -67,12 +80,17 @@ const style = computed(() => {
     justifyContent: 'center',
     alignItems: 'center',
     padding: props.padding,
-    width: theme.resolveThemeSize(props.buttonSize),
-    height: theme.resolveThemeSize(props.buttonSize),
+    width: Array.isArray(props.buttonSize) ? theme.resolveThemeSize(props.buttonSize[0]) : theme.resolveThemeSize(props.buttonSize),
+    height: Array.isArray(props.buttonSize) ? theme.resolveThemeSize(props.buttonSize[1]) : theme.resolveThemeSize(props.buttonSize),
     backgroundColor: theme.resolveThemeColor(props.backgroundColor),
     ...selectStyleType(props.shape, 'round', {
       "round": {
-        borderRadius: theme.resolveThemeSize('IconButtonRoundBorderRadius', 50),
+        borderRadius: theme.resolveThemeSize('IconButtonRoundBorderRadius', 20),
+      },
+      "round-full": {
+        height: '100%',
+        aspectRatio: 1,
+        borderRadius: '50%',
       },
       "custom": {},
       "square-full": {

+ 1 - 2
src/components/basic/Image.vue

@@ -137,7 +137,7 @@ const props = withDefaults(defineProps<ImageProps>(), {
   loadingSize: () => propGetThemeVar('ImageLoadingSize', 50),
   touchable: false,
   round: () => propGetThemeVar('ImageRound', false),
-  radius: () => propGetThemeVar('ImageRadius', '0'),
+  radius: () => propGetThemeVar('ImageRadius', ''),
 })
 const emit = defineEmits([ 'click' ]);
 const showBackgroundEffect = computed(() => props.showBackgroundEffect && props.mode === 'aspectFit');
@@ -153,7 +153,6 @@ const style = computed(() => {
     borderRadius: props.round ? '50%' : themeContext.resolveThemeSize(props.radius), 
     backgroundColor: isErrorState.value || props.showGrey ? themeContext.resolveThemeColor('background.imageBox') : 'transparent',
     overflow: 'hidden',
-    flexShrink: 0,
     width: themeContext.resolveThemeSize(props.width),
     height: themeContext.resolveThemeSize(props.height),
     ...props.innerStyle,

+ 9 - 19
src/components/basic/Text.vue

@@ -169,29 +169,19 @@ function onClick(e: any) {
 const style = computed(() => {
   const o : Record<string, any> = {
   }
-  let color = props.color;
-  let backgroundColor = props.backgroundColor;
-  let fontSize = props.fontSize;
-  let fontWeight = props.fontWeight;
-  let fontFamily = props.fontFamily;
-  let fontStyle = props.fontStyle as any;
-  if (props.fontConfig) {
-    const style = getText(props.fontConfig, {});
-    color = props.color ?? style.color as string;
-    backgroundColor = props.backgroundColor ?? style.backgroundColor as string;
-    fontSize = props.fontSize ?? style.fontSize as string;
-    fontWeight = props.fontWeight ?? style.fontWeight as string;
-    fontFamily = props.fontFamily ?? style.fontFamily as string;
-    fontStyle = props.fontStyle ?? style.fontStyle as object;
-  }
+  const style = props.fontConfig ? getText(props.fontConfig) : undefined;
+  const color = props.color ?? style?.color as string;
+  const backgroundColor = props.backgroundColor ?? style?.backgroundColor as string;
+  const fontSize = props.fontSize ?? style?.fontSize as string;
+  const fontWeight = props.fontWeight ?? style?.fontWeight as string;
+  const fontFamily = props.fontFamily ?? style?.fontFamily as string;
+  const fontStyle = props.fontStyle ?? style?.fontStyle as string;
+
   if (color) o.color = resolveThemeColor(color, "text");
   if (backgroundColor) o.background = resolveThemeColor(backgroundColor);
   if (fontSize) o.fontSize = resolveThemeSize(fontSize);
   if (fontWeight) o.fontWeight = fontWeight;
-  if (fontStyle) {
-    for(const k in fontStyle)
-      o.fontStyle = k;
-  }
+  if (fontStyle) o.fontStyle = fontStyle;
   if (!props.wrap) o.whiteSpace = 'nowrap';
 
   if (fontFamily) o.fontFamily = fontFamily;

+ 17 - 0
src/components/display/block/IconTextBlock.vue

@@ -53,6 +53,10 @@ defineProps({
     type: [String,Number],
     default: '',
   },
+  extraMpSlotState: {
+    type: Boolean,
+    default: true,
+  },
   /**
    * 标题文字属性
    */
@@ -89,10 +93,23 @@ defineProps({
         <Text fontConfig="subText" :text="desc" v-bind="descProps" />
       </FlexCol>
     </FlexRow>
+    <!-- #ifndef MP -->
     <slot name="extra">
       <FlexRow :flexShrink="0">
         <Text fontConfig="subText" :text="extra" v-bind="extraProps" />
       </FlexRow>
     </slot>
+    <!-- #endif -->
+    <!-- #ifdef MP -->
+    <slot v-if="extraMpSlotState" name="extra">
+      <FlexRow :flexShrink="0">
+        <Text fontConfig="subText" :text="extra" v-bind="extraProps" />
+      </FlexRow>
+    </slot>
+    <FlexRow v-else :flexShrink="0">
+      <Text fontConfig="subText" :text="extra" v-bind="extraProps" />
+    </FlexRow>
+    <!-- #endif -->
+
   </FlexRow>
 </template>

+ 9 - 3
src/components/display/block/ImageBlock2.vue

@@ -3,15 +3,16 @@
     touchable
     v-bind="props"
     :flexShrink="0"
-    :innerStyle="{ borderRadius: theme.resolveThemeSize(imageRadius), overflow: 'hidden', }"
+    :innerStyle="{ borderRadius: theme.resolveThemeSize(radius), overflow: 'hidden', }"
     :width="width"
     @click="$emit('click')"
   >
     <Image 
       :src="src" 
-      width="100%"
       :height="imageHeight"
       :radius="imageRadius"
+      width="100%"
+      round
       mode="aspectFill"
     />
     <slot name="desc">
@@ -20,7 +21,12 @@
           :title="title"
           :desc="desc"
           :extra="extra"
-        />
+          :extraMpSlotState="Boolean($slots.extra)"
+        >
+          <template v-if="$slots.extra" #extra>
+            <slot name="extra" />
+          </template>
+        </IconTextBlock>
       </FlexCol>
     </slot>
   </Touchable>

+ 7 - 1
src/components/display/block/ImageBlock3.vue

@@ -20,8 +20,14 @@
           :title="title"
           :desc="desc"
           :extra="extra"
-        />
+          :extraMpSlotState="Boolean($slots.extra)"
+        >
+          <template v-if="$slots.extra" #extra>
+            <slot name="extra" />
+          </template>
+        </IconTextBlock>
       </slot>
+
     </FlexView>
   </Touchable>
 </template>

+ 25 - 0
src/components/display/loading/Loadmore.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
+import Button from '@/components/basic/Button.vue';
 import Text from '@/components/basic/Text.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import type { FlexProps } from '@/components/layout/FlexView.vue';
@@ -39,6 +40,16 @@ export interface LoadMoreProps extends FlexProps {
    * @default '点击加载更多'
    */ 
   loadmoreText?: string,
+  /**
+   * 重试按钮文字
+   * @default '重试'
+   */
+  retryText?: string,
+  /**
+   * 是否显示重试按钮
+   * @default true
+   */
+  showRetry?: boolean,
 }
 
 const props = withDefaults(defineProps<LoadMoreProps>(), {
@@ -47,7 +58,12 @@ const props = withDefaults(defineProps<LoadMoreProps>(), {
   errorText: '加载失败',
   nomoreText: '没有更多了',
   loadmoreText: '点击加载更多',
+  retryText: '重试',
+  showRetry: true,
 });
+const emit = defineEmits<{
+  (e: 'retry'): void;
+}>();
 
 const themeContext = useTheme();
 
@@ -76,5 +92,14 @@ const text = computed(() => {
       <Width :size="20" />
     </template>
     <Text :text="text" fontConfig="subText" />
+    <template v-if="props.showRetry && props.status === 'error'">
+      <Width :size="20" />
+      <Button
+        type="text"
+        textColor="primary"
+        :text="props.retryText"
+        @click="emit('retry')"
+      />
+    </template>
   </FlexRow>
 </template>

+ 0 - 90
src/components/dynamic/DynamicFormCate.vue

@@ -1,90 +0,0 @@
-<template>
-  <view 
-    v-if="formDefine.type === 'group'" 
-    :class="`form-group ${formDefine.props.type}`"
-  >
-    <text class="form-group-title" v-if="formDefineParentLabel">
-      {{ formDefineParentLabel }}
-    </text>
-    <DynamicFormCateInner
-      :formDefine="formDefine"
-      :formModel="formModel" 
-      :groupType="formDefine.props.type"
-    />
-  </view>
-  <DynamicFormCateInner
-    v-else
-    :formDefine="formDefine"
-    :formModel="formModel" 
-  />
-</template>
-
-<script setup lang="ts">
-import type { PropType } from 'vue';
-import type { FormDefine } from '.';
-import DynamicFormCateInner from './DynamicFormCateInner.vue';
-
-export interface FormGroupProps {
-  type: 'row' | 'column' | 'block';
-}
-
-const props = defineProps({
-  formDefineParentLabel: {
-    type: null,
-    default: '' 
-  },
-  formDefineParentKey: {
-    type: String,
-    default: '' 
-  },
-  formModel: {
-    type: Object,
-    default: () => ({})
-  },
-  formDefine: {
-    type: Object as PropType<FormDefine>,
-    default: () => ({})
-  },
-})
-</script>
-
-<style lang="scss">
-.form-group {
-  display: flex;
-  flex-direction: column;
-
-  &.block {
-    margin-bottom: 32rpx;
-    padding: 24rpx 0;
-    background: #fff;
-    border-radius: 10rpx;
-
-    .form-group-title {
-      display: block;
-      font-size: 28rpx;
-      color: #333;
-      margin-bottom: 16rpx;
-    }
-  }
-
-  .form-group-title {
-    display: block;
-    flex-shrink: 0;
-    font-size: 28rpx;
-    color: #333;
-    margin-bottom: 16rpx;
-    margin-left: 26rpx;
-  }
-
-  &.row {
-    flex-direction: row;
-    justify-content: space-between;
-    align-items: center;
-
-    .form-group-title {
-      display: inline-block;
-      margin-left: 30rpx;
-    }
-  }
-}
-</style>

+ 0 - 85
src/components/dynamic/DynamicFormCateInner.vue

@@ -1,85 +0,0 @@
-<template>
-  <view 
-    v-for="(item, key) in formDefine.items"
-    :key="key"
-    :class="[
-      'form-cate-inner',
-      groupType
-    ]"
-  >
-    <DynamicFormCate
-      v-if="item.children"
-      :formDefine="item.children"
-      :formModel="children" 
-      :formDefineParentKey="item.name"
-      :formDefineParentLabel="item.label"
-      :parentModel="formModel"
-      :topModel="topModel"
-    />
-    <DynamicFormControl
-      v-else
-      :modelValue="formModel[item.name] ?? null"
-      :formDefineItem="item"
-      :parentModel="formModel"
-      :topModel="topModel"
-      :isLast="key === formDefine.items.length - 1"
-      @update:modelValue="(v: any) => formModel[item.name] = v"
-    />
-  </view>
-</template>
-
-<script setup lang="ts">
-import { computed, type PropType } from 'vue';
-import type { FormDefine } from '.';
-import DynamicFormControl from './DynamicFormControl.vue';
-import DynamicFormCate from './DynamicFormCate.vue';
-
-const props = defineProps({	
-  topModel: {
-    type: Object,
-    default: () => ({})
-  },
-  parentModel: {
-    type: null,
-  },
-  formModel: {
-    type: Object,
-    default: () => ({})
-  },
-  formDefineParentKey: {
-    type: String,
-    default: ''
-  },
-  formDefine: {
-    type: Object as PropType<FormDefine>,
-    default: () => ({})
-  },
-  groupType: {
-    type: String,
-    default: '' 
-  }
-})
-
-const children = computed(() => {
-  if (props.formDefineParentKey && props.formDefine.propNestType == 'nest')
-    return props.formModel[props.formDefineParentKey];
-  return props.formModel;
-});
-
-</script>
-
-<style lang="scss">
-.form-cate-inner {
-  display: flex;
-  flex-direction: column;
-
-  &.row {
-    display: flex;
-    flex-direction: row;
-    align-items: center;
-  }
-}
-.form-static-text {
-  margin: 0 10rpx 20rpx 10px;
-}
-</style>

+ 2 - 4
src/components/dynamic/DynamicFormControl.vue

@@ -324,12 +324,10 @@ const props = defineProps({
     default: false
   },
   model: {
-    type: null,
-    default: null
+    type: null
   },
   parentModel: {
-    type: null,
-    default: null
+    type: null
   },
   rawModel: {
     type: Object as PropType<Record<string, unknown>>,

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 3
src/components/dynamic/Images/IconAdd.vue


+ 0 - 3
src/components/dynamic/Images/IconCheck.vue

@@ -1,3 +0,0 @@
-<template>
-  <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M369.792 704.32L930.304 128 1024 223.616 369.984 896l-20.288-20.864-0.128 0.128L0 516.8 96.128 423.68l273.664 280.64z"></path></svg>
-</template>

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 3
src/components/dynamic/Images/IconDelete.vue


+ 0 - 3
src/components/dynamic/Images/IconDown.vue

@@ -1,3 +0,0 @@
-<template>
-  <svg  viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M904 332c0-8.189-3.124-16.379-9.372-22.628-12.497-12.496-32.759-12.496-45.256 0L512 646.745 174.628 309.372c-12.497-12.496-32.758-12.496-45.255 0-12.497 12.498-12.497 32.758 0 45.256l360 360c12.497 12.496 32.758 12.496 45.255 0l360-360C900.876 348.379 904 340.189 904 332z"></path></svg>
-</template>

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 1
src/components/dynamic/Images/IconError.svg


+ 0 - 3
src/components/dynamic/Images/IconUp.vue

@@ -1,3 +0,0 @@
-<template>
-  <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M904 692c0 8.189-3.124 16.379-9.372 22.628-12.497 12.496-32.759 12.496-45.256 0L512 377.255 174.628 714.628c-12.497 12.496-32.758 12.496-45.255 0-12.497-12.498-12.497-32.758 0-45.256l360-360c12.497-12.496 32.758-12.496 45.255 0l360 360C900.876 675.621 904 683.811 904 692z"></path></svg>
-</template>

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 1
src/components/dynamic/Images/IconWarning.svg


+ 0 - 1
src/components/dynamic/group/FormArrayGroup.vue

@@ -1,5 +1,4 @@
 <template>
-  <!--TODO: 请修改为统一样式 -->
   <div class="dynamic-form-array-group">
     <!--列表-->
     <div :class="['list', direction ]">

+ 34 - 50
src/components/dynamic/group/FormGroup.vue

@@ -1,48 +1,29 @@
 <template>
-  <FlexCol 
-    :innerClass="[
-      'dynamic-form-group', 
-      {
-        'collapsed': collapsed,
-        'collapsible': collapsible,
-        'plain': plain,
-      }
-    ]"
-    :innerStyle="dynamicFormGroupStyle"
-  >
-    <Touchable
-      v-if="title" 
-      innerClass="title" 
-      direction="row"
-      :innerStyle="dynamicFormGroupTitleWraperStyle"
-      @click="collapsible ? collapsed = !collapsed : null"
-    >
-      <span :style="dynamicFormGroupTitleStyle">{{ title }}</span>
-      <FlexRow align="center">
-        <text :style="dynamicFormGroupTitleStyle" v-show="collapsed">点击展开更多</text>
-        <Icon 
-          v-if="collapsible"
-          :size="theme.resolveThemeSize('DynamicFormGroupIconSize', 22)" 
-          icon="arrow-down"
-          innerClass="collapsible-icon"
-        />
-      </FlexRow>
-    </Touchable>
+  <div :class="[
+    'dynamic-form-group', 
+    {
+      'collapsed': collapsed,
+      'collapsible': collapsible,
+      'plain': plain,
+    }
+  ]">
+    <h5 v-if="title" class="title" @click="collapsible ? collapsed = !collapsed : null">
+      <span class="title-text">{{ title }}</span>
+      <view class="title-right">
+        <span class="title-right-text" v-show="collapsed">点击展开更多</span>
+        <Icon :size="22" icon="arrow-down" v-if="collapsible" innerClass="collapsible-icon" />
+      </view>
+    </h5>
     <Row v-if="!collapsed" :justify="(justify as any)" :gutter="gutter">
       <slot />
     </Row>
-  </FlexCol>
+  </div>
 </template>
 
 <script lang="ts" setup>
-import { computed, ref } from "vue";
+import { ref } from "vue";
 import Row from "@/components/layout/grid/Row.vue";
 import Icon from "@/components/basic/Icon.vue";
-import { useTheme } from "@/components/theme/ThemeDefine";
-import FlexRow from "@/components/layout/FlexRow.vue";
-import Touchable from "@/components/feedback/Touchable.vue";
-import FlexCol from "@/components/layout/FlexCol.vue";
-
 const props = defineProps({
   /**
    * 标题
@@ -87,22 +68,8 @@ const props = defineProps({
     default: false,
   },
 });
-const theme = useTheme();
 
 const collapsed = ref(props.collapsed);
-const dynamicFormGroupStyle = computed(() => ({
-  backgroundColor: theme.resolveThemeColor('DynamicFormGroupBackgroundColor', 'white'),
-  borderRadius: theme.resolveThemeSize('DynamicFormGroupBorderRadius', 10),
-  padding: `${theme.resolveThemeSize('DynamicFormGroupPaddingVertical', 10)} ${theme.resolveThemeSize('DynamicFormGroupPaddingHorizontal', 0)}`,
-  marginBottom: theme.resolveThemeSize('DynamicFormGroupMarginBottom', 12),
-})); 
-const dynamicFormGroupTitleWraperStyle = computed(() => ({
-  marginBottom: theme.resolveThemeSize('DynamicFormGroupTitleMarginBottom', 12),
-})); 
-const dynamicFormGroupTitleStyle = computed(() => ({
-  fontSize: theme.resolveThemeSize('DynamicFormGroupTitleFontSize', 25),
-  color: theme.resolveThemeColor('DynamicFormGroupTitleColor', 'text.second')
-})); 
 
 defineOptions({
   options: {
@@ -115,6 +82,10 @@ defineOptions({
 
 <style lang="scss">
 .dynamic-form-group {
+  padding: 10px 0;
+  background-color: var(--dynamic-form-background-color);
+  border-radius: var(--dynamic-form-border-radius);
+
   &.collapsed {
     .collapsible-icon {
       transform: rotate(0deg);
@@ -125,19 +96,32 @@ defineOptions({
       cursor: pointer;
     }
   }
+
   .collapsible-icon {
     transform: rotate(180deg);
     transition: transform 0.3s ease-in-out;
+    width: 16px;
+    height: 16px;
   }
+
   .title {
     display: flex;
     align-items: center;
     justify-content: space-between;
+    color: var(--dynamic-form-text-color);
+    margin: 0;
+    margin-bottom: 12px;
 
     .title-right {
       display: flex;
       flex-direction: row;
       align-items: center;
+
+      .title-right-text {
+        font-size: 11px;
+        margin-right: 10rpx;
+        color: var(--dynamic-form-secondary-color);
+      }
     }
   }
 

+ 22 - 3
src/components/dynamic/wrappers/CheckBoxTreeListItem.vue

@@ -17,6 +17,12 @@
     </FlexRow>
     <CollapseBox :open="open" :anim="false">
       <FlexCol :padding="[10,0,10,30]">
+        <Loadmore 
+          v-if="loading || loadError"
+          :status="loading ? 'loading' : 'error'"
+          :errorText="loadError"
+          @retry="loadChildren"
+        />
         <CheckBoxTreeListItemWrapper
           v-for="child in item.children"
           :key="child.value"
@@ -36,20 +42,33 @@ import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import IconButton from '@/components/basic/IconButton.vue';
 import CheckBoxTreeListItemWrapper from './CheckBoxTreeListItemWrapper.vue';
+import Loadmore from '@/components/display/loading/Loadmore.vue';
 
 const props = defineProps<{
   item: CheckBoxTreeListItem,
 }>();
 
 const open = ref(false);
+const loading = ref(false);
+const loadError = ref('');
 const loadData = inject('loadData') as (pid?: number) => Promise<CheckBoxTreeListItem[]>;
 
 function toggleOpen() {
   open.value = !open.value;
   if (open.value && (!props.item.children || props.item.children.length === 0))
-    loadData(props.item.value).then((children) => {
-      props.item.children = children;
-    });
+    loadChildren();
+}
+function loadChildren() {
+  loading.value = true;
+  loadData(props.item.value).then((children) => {
+    props.item.children = children;
+    loadError.value = '';
+  }).catch((err) => {
+    props.item.children = [];
+    loadError.value = '' + err;
+  }).finally(() => {
+    loading.value = false;
+  });
 }
 </script>
 

+ 93 - 19
src/components/feedback/SwipeRow.vue

@@ -18,22 +18,40 @@
     >
       <view 
         :style="{
-          width: `${props.leftWidth}px`,
-          marginLeft: `-${props.leftWidth}px`,
+          width: leftWidth ? `${leftWidth}px` : 'auto',
+          marginLeft: `-${leftWidth}px`,
         }"
         class="left"
+        id="left"
       >
-        <slot name="left" />
+        <slot name="left">
+          <Button 
+            v-for="action in leftActions" 
+            :key="action.text" shape="square" 
+            :type="action.type" 
+            :text="action.text" 
+            @click="action.onClick" 
+          />
+        </slot>
       </view>
       <slot />
       <view 
+        id="right"
         :style="{
-          width: `${props.rightWidth}px`,
-          marginRight: `-${props.rightWidth}px`,
+          width: rightWidth ? `${rightWidth}px` : 'auto',
+          marginRight: `-${rightWidth}px`,
         }"
         class="right"
       >
-        <slot name="right" />
+        <slot name="right">
+          <Button 
+            v-for="action in rightActions" 
+            :key="action.text" shape="square" 
+            :type="action.type" 
+            :text="action.text" 
+            @click="action.onClick" 
+          />
+        </slot>
       </view>
     </view>
   </view>
@@ -41,21 +59,45 @@
 
 <script setup lang="ts">
 
-import { nextTick, ref } from 'vue';
+import { computed, getCurrentInstance, nextTick, onMounted, onUpdated, ref } from 'vue';
 import { useTheme } from '../theme/ThemeDefine';
 import { RandomUtils } from '@imengyu/imengyu-utils';
+import Button, { type ButtonType } from '../basic/Button.vue';
 
 const themeContext = useTheme();
 
+export interface SwipeRowAction {
+  /**
+   * 操作按钮的文本
+   */
+  text: string,
+  /**
+   * 操作按钮的类型
+   */
+  type: ButtonType,
+  /**
+   * 操作按钮的点击事件
+   */
+  onClick?: (e: any) => void,
+}
+
 export interface SwipeRowProps {
   /**
-   * 左侧宽度
+   * 手动设置左侧宽度,单位为px
    */
-  leftWidth?: number,
+  leftWidth?: number|undefined,
   /**
-   * 右侧宽度
+   * 左侧操作按钮
    */
-  rightWidth?: number,
+  leftActions?: SwipeRowAction[],
+  /**
+   * 手动设置右侧宽度,单位为px
+   */
+  rightWidth?: number|undefined,
+  /**
+   * 右侧操作按钮
+   */
+  rightActions?: SwipeRowAction[],
   /**
    * 是否禁用
    */
@@ -77,6 +119,31 @@ const props = withDefaults(defineProps<SwipeRowProps>(), {
 const themeStyles = themeContext.useThemeStyles({
 });
 
+const instance = getCurrentInstance();
+const measuredLeftWidth = ref(0);
+const measuredRightWidth = ref(0);
+
+function measureWidth() {
+  function measuredEle(id: string) {
+    uni.createSelectorQuery()
+      .in(instance)
+      .select(`#${id}`)
+      .boundingClientRect()
+      .exec((res) => {
+        if (res[0]) {
+          if (id === 'left')
+            measuredLeftWidth.value = res[0].width;
+          else if (id === 'right')
+            measuredRightWidth.value = res[0].width;
+        }
+      });
+  }
+  nextTick(() => {
+    measuredEle('left');
+    measuredEle('right');
+  });
+}
+
 const currentAnim = ref(false);
 const currentOffset = ref(0);
 
@@ -86,15 +153,16 @@ let pressed = false;
 let startOffset = 0;
 let lastMovedSize = 0;
 
+const leftWidth = computed(() => props.leftWidth && props.leftWidth > 0 ? props.leftWidth : measuredLeftWidth.value);
+const rightWidth = computed(() => props.rightWidth && props.rightWidth > 0 ? props.rightWidth : measuredRightWidth.value);
+
 function handleDrag(x: number) {
   const movedSize = x - startX;
   lastMovedSize = movedSize;
   if (movedSize > 0) {
-    if (props.leftWidth > 0)
-      currentOffset.value = Math.min(startOffset + movedSize, props.leftWidth);
+    currentOffset.value = Math.min(startOffset + movedSize, leftWidth.value);
   } else {
-    if (props.rightWidth > 0)
-      currentOffset.value = Math.max(startOffset + movedSize, -props.rightWidth);
+    currentOffset.value = Math.max(startOffset + movedSize, -rightWidth.value);
   }
 }
 
@@ -121,10 +189,10 @@ function handleTouchEnd(e: any) {
   currentAnim.value = true;
   nextTick(() => {
     if (Math.abs(lastMovedSize) > 10) {
-      if (currentOffset.value > props.leftWidth / 2)
-        currentOffset.value = props.leftWidth;
-      else if (currentOffset.value < -props.rightWidth / 2)
-        currentOffset.value = -props.rightWidth;
+      if (currentOffset.value > leftWidth.value / 2)
+        currentOffset.value = leftWidth.value;
+      else if (currentOffset.value < -rightWidth.value / 2)
+        currentOffset.value = -rightWidth.value;
       else 
         currentOffset.value = 0;
     } else if (props.autoClose)
@@ -132,6 +200,12 @@ function handleTouchEnd(e: any) {
   });
 }
 
+onMounted(() => {
+  measureWidth();
+});
+onUpdated(() => {
+  measureWidth();
+});
 </script>
 
 <style lang="scss">

+ 3 - 0
src/components/form/CalendarField.vue

@@ -45,6 +45,7 @@ import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSh
 import Calendar from './Calendar.vue';
 import Height from '../layout/space/Height.vue';
 import Text, { type TextProps } from '../basic/Text.vue';
+import { usePickerFieldInstance, type PickerFieldInstance } from './Picker';
 
 export interface CalendarFieldProps extends Omit<CalendarProps, 'modelValue'> {
   
@@ -167,6 +168,8 @@ watch(tempValue, (v) => {
   }
 })
 
+defineExpose<PickerFieldInstance>(usePickerFieldInstance(popupShow));
+  
 defineOptions({
   options: {
     styleIsolation: "shared",

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

@@ -37,6 +37,7 @@ import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSh
 import CascadePicker from './CascadePicker.vue';
 import { usePickerFieldTempStorageData } from './PickerUtils';
 import Text, { type TextProps } from '../basic/Text.vue';
+import { usePickerFieldInstance, type PickerFieldInstance } from './Picker';
 
 export interface CascadePickerFieldProps extends Omit<CascadePickerProps, 'value'> {
   /**
@@ -120,6 +121,7 @@ const {
   popupShow,
 );
 
+defineExpose<PickerFieldInstance>(usePickerFieldInstance(popupShow));
 defineOptions({
   options: {
     styleIsolation: "shared",

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

@@ -45,6 +45,7 @@ import Height from '../layout/space/Height.vue';
 import Text, { type TextProps } from '../basic/Text.vue';
 import PopupTitle from '../dialog/PopupTitle.vue';
 import { getCascaderText } from './CascaderUtils';
+import { usePickerFieldInstance } from './Picker';
 
 export interface CascaderFieldProps extends Omit<CascaderProps, 'modelValue'> {
   /**
@@ -158,6 +159,7 @@ watch(tempValue, (v) => {
 defineExpose({
   confirm: onConfirm,
   cancel: onCancel,
+  ...usePickerFieldInstance(popupShow),
 })
 defineOptions({
   options: {

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

@@ -37,6 +37,7 @@ import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSh
 import DatePicker from './DatePicker.vue';
 import { usePickerFieldTempStorageData } from './PickerUtils';
 import Text, { type TextProps } from '../basic/Text.vue';
+import { usePickerFieldInstance, type PickerFieldInstance } from './Picker';
 
 export interface DatePickerFieldProps extends Omit<DatePickerProps, 'modelValue'> {
   modelValue?: Date;
@@ -120,6 +121,7 @@ const {
   popupShow,
 );
 
+defineExpose<PickerFieldInstance>(usePickerFieldInstance(popupShow));
 defineOptions({
   options: {
     styleIsolation: "shared",

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

@@ -37,6 +37,7 @@ import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSh
 import DateTimePicker from './DateTimePicker.vue';
 import { usePickerFieldTempStorageData } from './PickerUtils';
 import Text, { type TextProps } from '../basic/Text.vue';
+import { usePickerFieldInstance, type PickerFieldInstance } from './Picker';
 
 export interface DateTimePickerFieldProps extends Omit<DateTimePickerProps, 'modelValue'> {
   modelValue?: Date;
@@ -122,6 +123,7 @@ const {
   undefined,
   popupShow,
 );
+defineExpose<PickerFieldInstance>(usePickerFieldInstance(popupShow));
 
 defineOptions({
   options: {

+ 34 - 0
src/components/form/Picker.ts

@@ -0,0 +1,34 @@
+import type { Ref } from "vue";
+
+export interface PickerItem {
+  /**
+   * 显示文本
+   */
+  text: string;
+  /**
+   * 选中值
+   */
+  value: string|number;
+}
+
+export interface PickerFieldInstance {
+  /**
+   * 显示
+   */
+  show(): void;
+  /**
+   * 隐藏
+   */
+  hide(): void;
+}
+
+export function usePickerFieldInstance(show: Ref<boolean>) : PickerFieldInstance {
+  return {
+    show() {
+      show.value = true;
+    },
+    hide() {
+      show.value = false;
+    }
+  }
+}

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

@@ -25,14 +25,10 @@
 import { nextTick, onMounted, ref, watch } from 'vue';
 import { useTheme } from '../theme/ThemeDefine';
 import Empty from '../feedback/Empty.vue';
+import type { PickerItem } from './Picker';
 
 const themeContext = useTheme();
 
-export interface PickerItem {
-  text: string;
-  value: string|number;
-}
-
 export interface PickerProps {
   /**
    * 选择器列

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

@@ -37,6 +37,7 @@ import Popup from '../dialog/Popup.vue';
 import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSheetTitle.vue';
 import Picker from './Picker.vue';
 import Text, { type TextProps } from '../basic/Text.vue';
+import { usePickerFieldInstance, type PickerFieldInstance } from './Picker';
 
 export interface PickerFieldProps extends Omit<PickerProps, 'value'> {
   modelValue?: (number|string)[];
@@ -123,6 +124,8 @@ const {
   popupShow,
 );
 
+defineExpose<PickerFieldInstance>(usePickerFieldInstance(popupShow));
+
 defineOptions({
   options: {
     styleIsolation: "shared",

+ 65 - 4
src/components/form/SearchBar.vue

@@ -3,7 +3,8 @@
     center 
     :innerStyle="{
       ...themeStyles.view.value, 
-      ...innerStyle
+      ...innerStyle,
+      background: theme.resolveThemeColor(inputBackgroundColor),
     }"
     :flexShrink="0"
     overflow="hidden"
@@ -43,7 +44,6 @@
       />
       <slot name="rightIcon" />
     </FlexRow>
-    <slot name="rightButton" :onCustomButtonClick="onCustomButtonClick" />
     <slot 
       v-if="(
         cancelState === 'show' || 
@@ -64,6 +64,24 @@
         @click="onCancelPressed"
       />
     </slot>
+    <slot
+      v-if="(
+        searchState === 'show' || 
+        (inputFocus && (searchState === 'show-active' || searchState === 'show-active-or-no-empty')) || 
+        (valueTemp !== '' && (searchState === 'show-no-empty' || searchState === 'show-active-or-no-empty'))
+      )"
+      name="searchButton" 
+      :onSearchButtonClick="onSearchButtonClick"
+    >
+      <IconButton
+        :icon="searchIcon"
+        :size="searchIconSize"
+        :color="searchIconColor"
+        v-bind="searchIconProps"
+        @click="onSearchButtonClick"
+      />
+    </slot>
+    <slot name="rightButton" :onCustomButtonClick="onCustomButtonClick"/>
   </FlexRow>
 </template>
 
@@ -73,8 +91,9 @@ import type { IconProps } from '../basic/Icon.vue';
 import { type ViewStyle, type TextStyle, useTheme, propGetThemeVar } from '../theme/ThemeDefine';
 import FlexRow from '../layout/FlexRow.vue';
 import Button from '../basic/Button.vue';
-import { DynamicColor, DynamicSize } from '../theme/ThemeTools';
+import { DynamicSize } from '../theme/ThemeTools';
 import Icon from '../basic/Icon.vue';
+import IconButton from '../basic/IconButton.vue';
 
 export interface SearchBarProp {
   /**
@@ -87,6 +106,11 @@ export interface SearchBarProp {
    */
   inputColor?: string;
   /**
+   * 输入框的背景颜色
+   * @default white
+   */
+  inputBackgroundColor?: string;
+  /**
    * 输入框的placeholder
    */
   placeholder?: string;
@@ -137,6 +161,35 @@ export interface SearchBarProp {
    */
   cancelState?: 'hidden'|'show'|'show-active'|'show-no-empty'|'show-active-or-no-empty';
   /**
+   * 搜索按钮显示
+   * * hidden 不显示
+   * * show 一直显示
+   * * show-active 当输入框激活时显示
+   * * show-no-empty 当输入框有文字时显示
+   * * show-active-or-no-empty 当输入框激活或者有文字时显示
+   * @default 'hidden'
+   */
+  searchState?: 'hidden'|'show'|'show-active'|'show-no-empty'|'show-active-or-no-empty';
+  /**
+   * 搜索按钮图标
+   * @default 'search'
+   */
+  searchIcon?: string;
+  /**
+   * 搜索按钮图标大小
+   * @default 40
+   */
+  searchIconSize?: string|number;
+  /**
+   * 搜索按钮图标颜色
+   * @default text.content
+   */
+  searchIconColor?: string;
+  /**
+   * 图标自定义属性
+   */
+  searchIconProps?: Partial<IconProps>;
+  /**
    * 自定义样式
    */
   innerStyle?: ViewStyle;
@@ -164,6 +217,11 @@ const theme = useTheme();
 
 const props = withDefaults(defineProps<SearchBarProp>(), {
   inputColor: () => propGetThemeVar('SearchBarTextColor', 'text.content'),
+  inputBackgroundColor: () => propGetThemeVar('SearchBarBackgroundColor', 'white'),
+  searchState: 'hidden',
+  searchIcon: () => propGetThemeVar('SearchBarSearchIcon', 'search'),
+  searchIconSize: () => propGetThemeVar('SearchBarSearchIconSize', 40),
+  searchIconColor: () => propGetThemeVar('SearchBarSearchIconColor', 'text.content'),
   placeholder: '',
   placeholderTextColor: () => propGetThemeVar('SearchBarCancelTextColor', 'text.second'),
   cancelText: '取消',
@@ -189,7 +247,6 @@ function updateValue(v: string) {
 const themeStyles = theme.useThemeStyles({
   view: {
     position: 'relative',
-    backgroundColor: DynamicColor('SearchBarBackgroundColor', 'white'),
     borderRadius: DynamicSize('SearchBarBorderRadius', 40),
     paddingHorizontal: DynamicSize('SearchBarPaddingHorizontal', 28),
     paddingVertical: DynamicSize('SearchBarPaddingVertical', 16),
@@ -231,6 +288,10 @@ function onCancelPressed() {
     updateValue('');
   emit('cancel');
 }
+function onSearchButtonClick() {
+  emit('search', valueTemp.value);
+  uni.hideKeyboard()
+}
 function onSumit() {
   emit('search', valueTemp.value);
   uni.hideKeyboard()

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

@@ -37,6 +37,7 @@ import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSh
 import TimePicker from './TimePicker.vue';
 import { usePickerFieldTempStorageData } from './PickerUtils';
 import Text, { type TextProps } from '../basic/Text.vue';
+import { usePickerFieldInstance, type PickerFieldInstance } from './Picker';
 
 export interface TimePickerFieldProps extends Omit<TimePickerProps, 'modelValue'> {
   modelValue?: Date;
@@ -115,6 +116,7 @@ const {
   undefined,
   popupShow,
 );
+defineExpose<PickerFieldInstance>(usePickerFieldInstance(popupShow));
 
 defineOptions({
   options: {

+ 4 - 6
src/components/form/Uploader.vue

@@ -52,7 +52,7 @@
           </slot>
           <!-- #endif -->
         </template>
-        <slot v-if="currentUpladList.length < maxUploadCount && !disabled" name="addButton" :onUploadPress="onUploadPress" :itemSize="itemSize">
+        <slot v-if="currentUpladList.length < maxUploadCount && !disabled && !readonly" name="addButton" :onUploadPress="onUploadPress" :itemSize="itemSize">
           <UploaderListAddItem :itemSize="itemSize" :style="itemStyle" @click="onUploadPress" :isListStyle="props.listType === 'list'" />
         </slot>
         <slot v-if="readonly && currentUpladList.length === 0">
@@ -292,16 +292,14 @@ const currentUpladList = ref<UploaderItem[]>(props.intitalItems?.concat() || [])
 
 //上传按钮点击
 function onUploadPress() {
-  if (props.disabled) {
+  if (props.disabled || props.readonly)
     return;
-  }
   if (props.maxUploadCount > 1 && props.maxUploadCount - currentUpladList.value.length <= 0) {
     props.onOverCount ?
       props.onOverCount(props.maxUploadCount, currentUpladList.value.length) :
       toast.value?.text(`最多上传 ${props.maxUploadCount} 个文件哦!`);
     return;
   }
-
   const items = props.onPickImage ?
     props.onPickImage() :
     new Promise<UploaderItem[]>((resolve, reject) => {
@@ -336,9 +334,10 @@ function onUploadPress() {
             },
             {
               name: '从微信聊天中选择',
-              subname: '可在录音机或者WPS文档分享给文件传输助手\n然后选择任意文档文件',
+              subname: '可在录音机或者WPS文档分享给文件传输助手\n然后选择任意文档/文件',
             },
           ],
+          showCancel: true,
           onSelect(index, name) {
           },
         }).then((index) => {
@@ -359,7 +358,6 @@ function onUploadPress() {
             });
           }
         });
-
       } else {
         chooseLocal();
       }

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

@@ -80,8 +80,6 @@ onMounted(() => {
 
 defineExpose({
   getUploaderRef: () => {
-    console.log('getUploaderRef', uploaderRef.value);
-    
     return uploaderRef.value
   },
 })

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

@@ -22,7 +22,9 @@ export function useBaseViewStyleBuilder(props: FlexProps) {
       backgroundColor: themeContext.resolveThemeColor(props.backgroundColor),
       width: themeContext.resolveThemeSize(props.width),
       height: themeContext.resolveThemeSize(props.height),
-      gap: themeContext.resolveThemeSize(props.gap),
+      gap: !Array.isArray(props.gap) ? themeContext.resolveThemeSize(props.gap) : props.gap,
+      rowGap: Array.isArray(props.gap) ? themeContext.resolveSize(props.gap[0]) : undefined,
+      columnGap: Array.isArray(props.gap) ? themeContext.resolveSize(props.gap[1]) : undefined,
       borderRadius: themeContext.resolveThemeSize(props.radius),
       overflow: props.overflow,
       border: props.border && !props.border.includes(' ') ? themeContext.getVar('border.' + props.border, undefined) : props.border,

+ 1 - 1
src/components/layout/FlexView.vue

@@ -105,7 +105,7 @@ export interface FlexProps {
   /**
    * 间距
    */
-  gap?: number|string,
+  gap?: number|number[]|string,
   /**
    * 背景颜色
    */

+ 47 - 3
src/components/nav/Pagination.vue

@@ -10,10 +10,27 @@
       />
     </slot>
     <slot v-if="simple" name="simple" :text="`${currentPage + 1}/${props.pageCount}`"">
+      <PaginationItem
+        v-if="showFirstAndLast"
+        v-bind="$props"
+        :text="firstText"
+        :touchable="canPrev"
+        :active="false"
+        @click="onFirstPress"
+      />
       <text :style="themeStyles.simpleText.value">{{ `${currentPage + 1}/${props.pageCount}` }}</text>
+      <PaginationItem
+        v-if="showFirstAndLast"
+        v-bind="$props"
+        :text="lastText"
+        :touchable="canNext"
+        :active="false"
+        @click="onLastPress"
+      />
     </slot>
-    <template v-else>
-      <template  v-for="index in items" :key="index">
+    <slot v-else name="items" :items="items" :currentPage="currentPage" :onChange="emitChange">
+      <template v-for="index in items" :key="index">
+        <!-- #ifndef MP -->
         <slot
           name="item"
           :text="(index + 1).toString()"
@@ -21,6 +38,7 @@
           :active="index === currentPage"
           :onClick="() => emitChange(index)"
         >
+        <!-- #endif -->
           <PaginationItem
             v-bind="$props"
             :key="index"
@@ -29,9 +47,11 @@
             :touchable="true"
             @click="() => emitChange(index)"
           />
+        <!-- #ifndef MP -->
         </slot>
+        <!-- #endif -->
       </template>
-    </template>
+    </slot>
     <slot v-if="showNextPrev" name="next" :onClick="onNextPress" :touchable="canNext">
       <PaginationItem 
         v-bind="$props"
@@ -63,10 +83,17 @@ export interface PaginationProps {
   pageCount: number;
   /**
    * 是否显示上一页下一页按钮,默认是
+   * @default true
    */
   showNextPrev?: boolean;
   /**
+   * 是否显示第一页和最后一页按钮,默认是
+   * @default true
+   */
+  showFirstAndLast?: boolean;
+  /**
    * 同时显示的页面按钮数量
+   * @default 5
    */
   showPageCount?: number;
   /**
@@ -75,13 +102,25 @@ export interface PaginationProps {
   simple?: boolean;
   /**
    * 上一页按钮文字
+   * @default '上一页'
    */
   prevText?: string;
   /**
    * 下一页按钮文字
+   * @default '下一页'
    */
   nextText?: string;
   /**
+   * 第一页按钮文字,仅在 simple 模式下显示
+   * @default '首页'
+   */
+  firstText?: string;
+  /**
+   * 最后一页按钮文字,仅在 simple 模式下显示
+   * @default '末页'
+   */
+  lastText?: string;
+  /**
    * 按钮按下颜色
    */
   pressedColor?: string;
@@ -123,6 +162,9 @@ const props = withDefaults(defineProps<PaginationProps>(), {
   simple: false,
   prevText: '上一页',
   nextText: '下一页',
+  firstText: '首页',
+  lastText: '末页',
+  showFirstAndLast: true,
   pressedColor: () => propGetThemeVar('PaginationPressedColor', 'pressed.white'),
   pressedTextColor: () => propGetThemeVar('PaginationPressedTextColor', 'text.second'),
   activeColor: () => propGetThemeVar('PaginationActiveColor', 'primary'),
@@ -159,6 +201,8 @@ function emitChange(num: number) {
 }
 const onPrevPress = () => emitChange(props.currentPage - 1);
 const onNextPress = () => emitChange(props.currentPage + 1);
+const onFirstPress = () => emitChange(0);
+const onLastPress = () => emitChange(props.pageCount - 1);
 
 const themeContext = useTheme();
 const themeStyles = themeContext.useThemeStyles({

+ 2 - 2
src/components/theme/ThemeDefine.ts

@@ -164,10 +164,10 @@ export function useTheme() {
       v = theme.value.varOverrides[key];
     return resolveSize(v ?? defaultValue);
   }
-  function getText(key: string, defaultValue: Record<string, string>) {
+  function getText(key: string, defaultValue?: Record<string, string>) {
     return theme.value.textConfigs[key]?? defaultValue;
   }
-  function getVar<T>(key: string, defaultValue: T) : T {
+  function getVar<T>(key: string, defaultValue?: T) : T {
     let rs = undefined;
     let type = '';
     if (key.includes('.'))

+ 1 - 1
src/components/typography/HorizontalScrollText.vue

@@ -114,6 +114,7 @@ defineOptions({
   white-space: nowrap;
   display: flex;
   flex-direction: row;
+  align-items: center;
 
   .placeholder {
     visibility: hidden;
@@ -124,7 +125,6 @@ defineOptions({
   }
   .inner {
     position: absolute;
-    top: 0;
 
     &.scroll {
       animation: 25000ms linear 0s infinite normal none running horizontalScrollText;

+ 20 - 9
src/pages/introduction/news.vue

@@ -3,20 +3,30 @@
   <CommonRoot>
     <FlexCol :padding="30" innerClass="bg-base">
       <FlexRow>
-        <SearchBar
-          v-model="searchText"
-          placeholder="搜索新闻"
-          @search="loadNews"
-        />
-        <FlexRow align="center">
+        <Touchable 
+          direction="row"
+          width="450"
+          align="center"
+          :activeOpacity="1"
+          @click="($refs.pickerField as any).show()"
+        >
           <Text>选择日期:</Text>
           <PickerField 
+            ref="pickerField"
             v-model="filterDate"
             placeholder="选择日期"
             :columns="[filterDates]" 
           />
           <Icon name="arrow-down" />
-        </FlexRow>
+        </Touchable>
+        <SearchBar
+          v-model="searchText"
+          inputBackgroundColor="transparent"
+          placeholder="搜索新闻"
+          cancelState="hidden"
+          searchState="show"
+          @search="loadNews"
+        />
       </FlexRow>
       <Box2LineImageRightShadow 
         v-for="(item, i) in newsLoader.list.value"
@@ -28,7 +38,7 @@
         titleColor="title-text"
         :image="item.thumbnail || item.image"
         :title="item.title"
-        :desc="`来源:${item.from}`"
+        :desc="item.from ? `来源:${item.from}` : ''"
         :badge="item.badge"
         :wideImage="true"
         @click="goDetails(item, item.id)"
@@ -41,7 +51,7 @@
 <script setup lang="ts">
 import { onMounted, ref, watch } from 'vue';
 import { type GetContentListItem, GetContentListParams } from '@/api/CommonContent';
-import { type PickerItem } from '@/components/form/Picker.vue';
+import { type PickerItem } from '@/components/form/Picker';
 import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
 import NewsIndexContent from '@/api/news/NewsIndexContent';
 import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
@@ -53,6 +63,7 @@ import PickerField from '@/components/form/PickerField.vue';
 import Box2LineImageRightShadow from '../parts/Box2LineImageRightShadow.vue';
 import Icon from '@/components/basic/Icon.vue';
 import Text from '@/components/basic/Text.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
 
 const searchText = ref('');
 const filterDate = ref(['']);