快乐的梦鱼 2 napja
szülő
commit
711a20db63

+ 129 - 0
src/common/components/form/Recorder.vue

@@ -0,0 +1,129 @@
+<script setup lang="ts">
+import IconButton from '@/components/basic/IconButton.vue';
+import Text from '@/components/basic/Text.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import { FormatUtils, LogUtils, SimpleTimer, TimeUtils } from '@imengyu/imengyu-utils';
+import { computed, ref } from 'vue';
+
+const emit = defineEmits(['update:modelValue', 'recordDone'])
+const props = defineProps({
+  modelValue: { 
+    type: String,
+    default: null 
+  },
+});
+
+
+const TAG = 'Recorder'; 
+const manager = uni.getRecorderManager();
+const state = ref(false);
+const paused = ref(false);
+const recordTime = ref(0);
+const recordTimeInterval = new SimpleTimer(undefined, () => {
+  recordTime.value++;
+}, 1000);
+const recordTimeString = computed(() => {
+  const times = TimeUtils.splitMillSeconds(recordTime.value * 1000);
+  return `${FormatUtils.formatNumberWithZero(times.minutes, 2)}:${FormatUtils.formatNumberWithZero(times.seconds, 2)}`;
+});
+
+manager.onError((err) => {
+  LogUtils.printLog(TAG, 'error', '录音错误', err)
+  uni.showModal({
+    title: '录音错误',
+    content: err.errMsg,
+  })
+})
+manager.onPause(() => {
+  LogUtils.printLog(TAG, 'info', '录音暂停')
+})
+manager.onStart((result) => {
+  uni.hideLoading();
+  state.value = true;
+  paused.value = false;
+  recordTimeInterval.start();
+  LogUtils.printLog(TAG, 'info', '录音开始')
+})
+manager.onStop((result) => {
+  uni.hideLoading();
+  state.value = false;
+  paused.value = false;
+  recordTime.value = 0;
+  recordTimeInterval.stop();
+  LogUtils.printLog(TAG, 'info', '录音结束', result);
+  uni.showToast({ title: '录音成功' });
+  emit('update:modelValue', result.tempFilePath);
+  emit('recordDone', result.tempFilePath);
+})
+
+function startRecord() {
+  if (state.value)
+    return;
+  uni.showLoading();
+  recordTime.value = 0;
+  state.value = true;
+  manager.start({
+    format: 'mp3',
+    duration: 60000,
+  })
+}
+function stopRecord() {
+  if (!state.value)
+    return;
+  uni.showLoading();
+  manager.stop();
+}
+function toggleRecord() {
+  if (!state.value)
+    return;
+  if (!paused.value) {
+    manager.pause();
+    recordTimeInterval.stop();
+    paused.value = true;
+  } else {
+    manager.resume();
+    recordTimeInterval.start();
+    paused.value = false;
+  }
+}
+
+defineOptions({
+  options: {
+    styleIsolation: "shared",
+    virtualHost: true,
+  }
+})
+</script>
+
+<template>
+  <FlexRow flex="1 1 100%" align="center" justify="space-between">
+    <Text :text="`${state?(paused?'暂停中':'录音中'):'点击开始录音'} ${ recordTimeString }/10:00 `"/>
+    <FlexRow align="center" :gap="15">
+      <IconButton v-if="!state" 
+        icon="record-filling"
+        :size="40" 
+        :buttonSize="60"
+        color="danger"
+        shape="round"
+        @click="startRecord" 
+      />
+      <template v-else>
+        <IconButton 
+          icon="stop-filling" 
+          shape="round"
+          :size="30" 
+          :buttonSize="50"
+          color="danger"
+          @click="stopRecord" 
+        />
+        <IconButton 
+          :icon="paused?'play-filling':'pause-filling'" shape="round" 
+          :size="30" 
+          :buttonSize="50"
+          color="danger"
+          @click="toggleRecord" 
+        />
+      </template>
+    </FlexRow>
+  </FlexRow>
+</template>

+ 7 - 6
src/common/components/form/RichTextEditor.vue

@@ -2,7 +2,7 @@
   <view class="d-flex flex-col">
     <view class="richtext-preview-box">
       <Parse v-if="modelValue" :content="modelValue" containerStyle="max-height:400px" />
-      <text v-else>未编写内容,点击编写</text>
+      <text v-else>{{placeholder}}</text>
     </view>
     <view class="d-flex flex-row gap-sss align-center mt-2">
       <Button icon="browse" text="预览" size="small" @click="preview" />
@@ -26,7 +26,11 @@ const props = defineProps({
   maxLength: {
     type: Number,
     default: -1,
-  }
+  },
+  placeholder: {
+    type: String,
+    default: '未编写内容,点击编写',
+  },
 })
 const emit = defineEmits(['update:modelValue'])
 let editorOpened = false;
@@ -64,10 +68,7 @@ onPageShow(() => {
 
 <style lang="scss" scoped>
 .richtext-preview-box {
-  padding: 15rpx 20rpx;
-  border-radius: 20rpx;
-  background-color: #efefef;
   flex: 1;
-
+  min-height: 400rpx;
 }
 </style>

+ 0 - 81
src/common/scss/common.scss

@@ -764,58 +764,6 @@ button[type="primary"] {
   color: #FFFFFF;
   font-weight: 600;
 }
-.form-title {
-  font-size: 32rpx;
-  color: #333;
-  margin-bottom: 28rpx;
-  margin-top: 30rpx;
-  display: flex;
-  align-items: center;
-  .line {
-    background: #FF8719;
-    border-radius: 3rpx;
-    margin-right: 22rpx;
-    width: 6rpx;
-    height: 34rpx;
-  }
-}
-.form-block {
-  margin-bottom: 32rpx;
-  padding: 24rpx 26rpx;
-  background: #fff;
-  border-radius: 10rpx;
-}
-
-::v-deep .uni-forms-item__label {
-  font-weight: 600;
-  font-size: 28rpx;
-  color: #23262D;
-  line-height: 36rpx;
-}
-
-::v-deep .uni-select {
-  border-radius: 10rpx;
-  //border: 1px solid #ececec;
-  padding: 16rpx 24rpx;
-  font-size: 28rpx;
-  background: #fff;
-}
-
-::v-deep .uni-date-x--border {
-  border-radius: 10rpx;
-  //border: 1px solid #BFBFBF;
-}
-
-::v-deep .uni-input-placeholder, ::v-deep .uni-textarea-placeholder, ::v-deep .uni-select__input-placeholder, ::v-deep .uni-date__x-input, ::v-deep .is-disabled .uni-easyinput__placeholder-class {
-  font-weight: 400;
-  font-size: 28rpx;
-  color: #999999;
-}
-
-::v-deep .uni-easyinput__content.is-disabled {
-  background-color: #fff;
-  cursor: pointer;
-}
 
 .address-select {
   position: relative;
@@ -829,35 +777,6 @@ button[type="primary"] {
   pointer-events: none; /* 禁用内部元素的点击事件 */
 }
 
-::v-deep .uni-easyinput__content.is-disabled {
-  background-color: #fff;
-  cursor: pointer;
-  width: 100%; // 确保输入框宽度占满
-}
-
-::v-deep .popup-content {
-  text-align: center;
-  background: #FFFFFF;
-  border-radius: 20rpx;
-  padding: 22rpx 44rpx 37rpx;
-
-  image {
-    width: 158rpx;
-    height: 186rpx;
-    display: block;
-    margin: 0 auto;
-    margin-bottom: 12rpx;
-  }
-
-  text {
-    font-weight: 500;
-    font-size: 24rpx;
-    color: #666666;
-  }
-}
-.uni-datetime-picker--btn{
-  background:#FF8719!important;
-}
 .bottom-actions {
   position: fixed;
   bottom: 0;

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

@@ -45,12 +45,22 @@ export interface IconButtonProps extends IconProps {
    * 按钮样式
    */
   buttonStyle?: ViewStyle;
+  /**
+   * 按钮大小
+   */
+  buttonSize?: number|string;
+  /**
+   * 按钮背景颜色
+   * @default 'button'
+   */
+  backgroundColor?: string;
 }
 
 const emit = defineEmits(['click']);
 const theme = useTheme();
 const props = withDefaults(defineProps<IconButtonProps>(), {
   shape: 'custom',
+  backgroundColor: 'button',
   pressedBackgroundColor: () => propGetThemeVar('IconButtonPressedColor', 'pressed.white')
 });
 const style = computed(() => {
@@ -59,6 +69,9 @@ const style = computed(() => {
     justifyContent: 'center',
     alignItems: 'center',
     padding: props.padding,
+    width: theme.resolveThemeSize(props.buttonSize),
+    height: theme.resolveThemeSize(props.buttonSize),
+    backgroundColor: theme.resolveThemeColor(props.backgroundColor),
     ...selectStyleType(props.shape, 'round', {
       "round": {
         borderRadius: theme.resolveThemeSize('IconButtonRoundBorderRadius', 50),

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 4 - 0
src/components/data/DefaultIcon.json


+ 18 - 3
src/components/dynamic/DynamicForm.vue

@@ -19,7 +19,7 @@
 import { computed, onMounted, provide, reactive, ref, toRef, watch, type PropType } from 'vue';
 import { waitTimeOut } from '@imengyu/imengyu-utils';
 import DynamicFormCate from './DynamicFormCate.vue';
-import Form, { type FormProps } from '../form/Form.vue';
+import Form, { type FormInstance, type FormProps } from '../form/Form.vue';
 import type { FormDefine, FormDefineItem, FormExport } from '.';
 import type { Rules } from 'async-validator';
 
@@ -38,7 +38,7 @@ const props = defineProps({
   },
 });
 
-const formRef = ref<any>();
+const formRef = ref<FormInstance>();
 const formModel = ref<any>(null);
 const formRules = computed(() => {
   const rules: Rules = {};
@@ -107,6 +107,9 @@ function loadFormData(value?: Record<string, any>) {
   formModel.value = obj;
 }
 async function submitForm<T = Record<string, any>>() : Promise<T|null> {
+  if (!formRef.value)
+    throw new Error('表单实例不存在');
+
   await formRef.value.clearValidate();
   await waitTimeOut(50);
   
@@ -140,8 +143,20 @@ onMounted(() => {
 
 defineExpose<FormExport>({
   getFormModel: () => formModel.value,
-  getFormRef: () => formRef.value,
+  getFormRef: () => {
+    if (!formRef.value)
+      throw new Error('表单实例不存在');
+    return formRef.value
+  },
   getFormData: () => formModel.value,
+  getFormItemRef: (path: string) => {
+    if (!formRef.value)
+      throw new Error('表单实例不存在');
+    const filed = formRef.value.getField(path);
+    if (!filed)
+      throw new Error(`表单项${path}不存在`);
+    return filed.getExpectedRef();
+  },
   initFormData,
   loadFormData,
   submitForm,

+ 1 - 1
src/components/dynamic/DynamicFormCate.vue

@@ -55,7 +55,7 @@ const props = defineProps({
 
   &.block {
     margin-bottom: 32rpx;
-    padding: 24rpx 26rpx;
+    padding: 24rpx 0;
     background: #fff;
     border-radius: 10rpx;
 

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

@@ -22,6 +22,7 @@
       :formDefineItem="item"
       :parentModel="formModel"
       :topModel="topModel"
+      :isLast="key === formDefine.items.length - 1"
       @update:modelValue="(v: any) => formModel[item.name] = v"
     />
   </view>

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

@@ -16,6 +16,7 @@
       :modelValue="modelValue"
       @update:modelValue="onValueChanged"
       :maxlength="260"
+      :showBottomBorder="!isLast"
       :required="Boolean(formDefineItem.rules?.length)"
       v-bind="params"
     />
@@ -26,6 +27,7 @@
       :label="label"
       :name="formDefineItem.fullName"
       :modelValue="modelValue"
+      :showBottomBorder="!isLast"
       :required="Boolean(formDefineItem.rules?.length)"
       @update:modelValue="onValueChanged"
       v-bind="params"
@@ -37,6 +39,8 @@
       :name="formDefineItem.fullName"
       :required="Boolean(formDefineItem.rules?.length)"
       :showRightArrow="extraDefine?.needArrow"
+      :showBottomBorder="!isLast"
+      :requireChildRef="() => itemRef"
       v-bind="{ 
         ...extraDefine?.itemProps || {},
         ...formDefineItem.itemParams,
@@ -182,6 +186,14 @@
           v-bind="params"
         />
       </template>
+      <template v-else-if="formDefineItem.type === 'recorder'">
+        <Recorder
+          ref="itemRef"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="params"
+        />
+      </template>
       <!-- 业务代码结束 -->
       <template v-else>
         <text>Fallback: unknow form type {{ formDefineItem.type }}</text>
@@ -217,6 +229,7 @@ import type { RadioIdFieldProps } from './wrappers/RadioIdField';
 //自定义额外的组件默认配置,修改为自定义文件路径
 //业务代码开始
 import ComponentConfigs from '@/common/components/dynamicf/ComponentConfigs';
+import Recorder from '@/common/components/form/Recorder.vue';
 //业务代码结束
 
 const props = defineProps({	
@@ -230,6 +243,10 @@ const props = defineProps({
     type: Object as PropType<FormDefineItem>,
     default: () => ({})
   },
+  isLast: {
+    type: Boolean,
+    default: false,
+  }
 });
 
 const formItemRef = ref();

+ 6 - 0
src/components/dynamic/index.ts

@@ -148,6 +148,12 @@ export interface FormExport {
    */
   getFormRef(): FormInstance;
   /**
+   * 获取表单条目的实例
+   * @param path 表单条目的路径
+   * @returns 表单条目的实例
+   */
+  getFormItemRef(path: string): any;
+  /**
    * 获取表单数据
    * @returns 表单数据
    */

+ 57 - 9
src/components/form/Field.vue

@@ -4,13 +4,14 @@
     :pressedColor="themeContext.resolveThemeColor('FieldPressedColor', 'pressed.white')"
     :innerStyle="{ 
       ...themeStyles.field.value, 
+      ...(showBottomBorder ? themeStyles.fieldBorder.value : {}),
       ...(labelPosition === 'top' ? themeStyles.fieldVertical.value : {}), 
       ...fieldStyle, 
       ...(focused ? activeFieldStyle : {}),
       ...(error || finalErrorMessage ? errorFieldStyle : {})
     }"
     :direction="labelPosition === 'top' ? 'column' : 'row'"
-    :center="labelPosition === 'top' ? false : true"
+    :justify="labelPosition === 'top' ? 'flex-start' : 'center'"
     @click="onClick"
   >
     <!-- 左边的标签区域 -->
@@ -112,6 +113,7 @@
         <slot name="rightButton" />
         <Icon v-if="showRightArrow" icon="arrow-right" :size="themeContext.resolveThemeSize('FieldRightArrowSize', 30)" v-bind="rightArrowProps" />
       </FlexRow>
+      <!-- 错误提示图标和文本 -->
       <FlexRow 
         v-if="finalErrorMessage"
         :gap="10"
@@ -126,6 +128,21 @@
         />
         <text :style="themeStyles.errorMessageText.value">{{finalErrorMessage}}</text>
       </FlexRow>
+      <!-- 额外的提示信息 -->
+      <FlexRow 
+        v-if="extraMessage"
+        :gap="10"
+        :innerStyle="themeStyles.extraMessage.value"
+        align="center"
+      >
+        <Icon 
+          :icon="extraIcon"
+          :size="themeContext.resolveThemeSize('FieldExtraIconSize', 40)" 
+          v-bind="extraIconProps" 
+          :color="themeContext.resolveThemeColor('FieldExtraIconColor', 'text.second')"
+        />
+        <text :style="themeStyles.extraMessageText.value">{{extraMessage}}</text>
+      </FlexRow>
       <text v-if="showWordLimit" :style="themeStyles.wordLimitText.value">{{wordLimitText}}</text>
 
     </FlexCol>
@@ -156,6 +173,7 @@ import FlexCol from '../layout/FlexCol.vue';
 import FlexRow from '../layout/FlexRow.vue';
 import FlexView from '../layout/FlexView.vue';
 import Touchable from '../feedback/Touchable.vue';
+import type { TextProps } from '../basic/Text.vue';
 
 
 export interface FieldInstance {
@@ -334,6 +352,19 @@ export interface FieldProps {
    * @default Color.danger
    */
   errorTextColor?: string;
+
+  /**
+   * 额外的提示信息
+   */
+  extraMessage?: string;
+  extraMessageProps?: TextProps;
+  /**
+   * 额外的提示图标
+   * @default 'prompt'
+   */
+  extraIcon?: string;
+  extraIconProps?: IconProps;
+
   /**
    * 文本框水印文字颜色
    * @default Color.grey
@@ -394,9 +425,16 @@ export interface FieldProps {
    */
   showRightArrow?: boolean;
   /**
+   * 是否显示底部边框
+   * @default true
+   */
+  showBottomBorder?: boolean;
+  /**
    * 右边箭头的自定义属性
    */
   rightArrowProps?: IconProps;
+
+  requireChildRef?: () => any,
 }
 
 defineOptions({
@@ -427,6 +465,7 @@ const props = withDefaults(defineProps<FieldProps>(), {
   inputStyle: () => propGetThemeVar('FieldInputStyle', propGetFormContext()?.fieldProps.value?.inputStyle ?? {}),
   activeInputStyle: () => propGetThemeVar('FieldActiveInputStyle', propGetFormContext()?.fieldProps.value?.activeInputStyle ?? {}),
   required: false,
+  showBottomBorder: () => propGetFormContext()?.fieldProps.value?.showBottomBorder ?? true,
   center: () => propGetFormContext()?.fieldProps.value?.center ?? true,
   showWordLimit: () => propGetFormContext()?.fieldProps.value?.showWordLimit ?? false,
   clearButton: () => propGetFormContext()?.fieldProps.value?.clearButton ?? false,
@@ -447,17 +486,23 @@ const props = withDefaults(defineProps<FieldProps>(), {
   modelValue: undefined,
   errorIcon: () => propGetThemeVar('FieldErrorIcon', 'prompt'),
   errorIconProps: () => propGetThemeVar('FieldErrorIconProps', propGetFormContext()?.fieldProps.value?.errorIconProps ?? {}),
+  extraMessage: '',
+  extraMessageProps: () => propGetThemeVar('FieldExtraMessageProps', propGetFormContext()?.fieldProps.value?.extraMessageProps ?? {}),
+  extraIcon: () => propGetThemeVar('FieldExtraIcon', 'prompt'),
+  extraIconProps: () => propGetThemeVar('FieldExtraIconProps', propGetFormContext()?.fieldProps.value?.extraIconProps ?? {}),
 });
 
 //#region Context
 
 const formContextProps = inject<FormContext>('formContext', null as any);
 const error = ref<string|null>(null);
-let childRef : any = null;
 const childOnClickListener = ref<(() => void)|undefined>(undefined);
 
 //Context for parent
 const formItemInternalContext : FormItemInternalContext = {
+  getExpectedRef: () => {
+    return props.requireChildRef?.() ?? fieldInstance;
+  },
   getValidateTrigger: () => props.validateTrigger || formContextProps?.validateTrigger.value || 'submit',
   getFieldName: () => props.name ?? '',
   setErrorState(errorMessage) { error.value = errorMessage; },
@@ -466,14 +511,11 @@ const formItemInternalContext : FormItemInternalContext = {
   },
   setBlurState() {
     inputRef.value?.blur();
-    childRef.value?.blur();
   },
 };
 //Context for custom children
 const formItemContext : FormItemContext = {
-  getFieldName: (ref: any) => {
-    if (ref)
-      childRef = ref;
+  getFieldName: () => {
     return props.name ?? uniqueId;
   },
   onFieldFocus: () => {
@@ -510,6 +552,8 @@ const themeStyles = themeContext.useThemeStyles({
     backgroundColor: DynamicColor('FieldBackgroundColor', 'background.cell'),
     paddingVertical: DynamicSize('FieldPaddingVertical', 16),
     paddingHorizontal: DynamicSize('FieldPaddingHorizontal', 20),
+  },
+  fieldBorder: {
     borderBottomWidth: DynamicSize('FieldBorderBottomWidth', '1px'),
     borderBottomColor: DynamicColor('FieldBorderBottomColor', 'border.cell'),
     borderBottomStyle: 'solid',
@@ -549,6 +593,13 @@ const themeStyles = themeContext.useThemeStyles({
   errorMessage: {
     marginTop: DynamicSize('FieldErrorMessageMarginTop', 12),
   },
+  extraMessageText: {
+    fontSize: DynamicSize('FieldExtraMessageFontSize', 24),
+    color: DynamicColor('FieldExtraMessageColor', 'text.second'),
+  },
+  extraMessage: {
+    marginTop: DynamicSize('FieldExtraMessageMarginTop', 12),
+  },
   wordLimitText: {
     fontSize: DynamicSize('FieldWordLimitTextFontSize', 24),
     color: DynamicColor('FieldWordLimitTextColor', 'text.second'),
@@ -647,15 +698,12 @@ function onClick() {
 const fieldInstance : FieldInstance = {
   focus() {
     inputRef.value?.focus();
-    childRef.value?.focus();
   },
   blur() {
     inputRef.value?.blur();
-    childRef.value?.blur();
   },
   clear() {
     inputRef.value?.clear();
-    childRef.value?.clear();
   },
   isFocused() :boolean {
     return focused.value;

+ 48 - 6
src/components/form/Form.vue

@@ -7,13 +7,13 @@
 </template>
 
 <script setup lang="ts">
+import { onMounted, provide, ref, toRefs } from 'vue';
+import { ObjectUtils, waitTimeOut } from '@imengyu/imengyu-utils';
+import Schema from 'async-validator';
 import type { Rule, Rules, ValidateError } from 'async-validator';
 import type { ViewStyle } from '../theme/ThemeDefine';
-import type { FieldInstance, FieldProps } from './Field.vue';
-import { computed, onMounted, provide, ref, toRefs } from 'vue';
-import Schema from 'async-validator';
+import type { FieldProps } from './Field.vue';
 import type { FormContext, FormItemInternalContext } from './FormContext';
-import { ObjectUtils, waitTimeOut } from '@imengyu/imengyu-utils';
 
 
 export type FormValueType = Date|null|number|string|boolean|number[]|string[]|boolean[]|null[]|FormValueType[];
@@ -121,7 +121,7 @@ const currentFocusField = ref<FormItemInternalContext>();
 const intitalModel = ref<Record<string, unknown>|null>(null);
 const formItems = ref(new Map<string, FormItemInternalContext>());
 
-function accessFormModel(keyName: string, isSet: boolean, setValue: unknown) : unknown {
+function accessFormModel(keyName: string, isSet: boolean|undefined, setValue: unknown) : unknown {
   const keys = keyName.split('.');
   let ret : unknown = undefined;
   let obj = props.model as Record<string, unknown>;
@@ -234,7 +234,6 @@ function blur() {
     currentFocusField.value = undefined;
   }
 }
-
 //Clear valid error state
 function clearValidate(name?: string|string[]) {
   if (name instanceof Array) {
@@ -334,14 +333,57 @@ async function submit(valid = true) {
 
 
 export interface FormInstance {
+  /**
+   * 取消表单内部的输入框激活(通常在提交时,可以调用此方法,关闭输入框)
+   */
   blur(): void;
+  /**
+   * 清除表单验证错误信息
+   * @param name 要清除验证错误信息的字段名,不指定则清除所有字段
+   */
   clearValidate: (name?: string | string[]) => void;
+  /**
+   * 重置表单字段值
+   * @param name 要重置的字段名,不指定则重置所有字段
+   */
   resetFields: (name?: string | string[]) => void;
+  /**
+   * 验证表单字段值
+   * @param name 要验证的字段名,不指定则验证所有字段
+   * @returns 验证结果
+   */
   validate(name?: string | string[]): Promise<void>;
+  /**
+   * 提交表单
+   * @param valid 是否验证表单字段值,默认值为 true
+   * @returns 提交结果
+   */
   submit(valid?: boolean): Promise<void>;
+  /**
+   * 访问表单字段值
+   * @param name 要访问的字段名
+   * @param isSet 是否设置字段值,否则为读取,默认值为 false
+   * @param defaultValue 字段不存在时的默认值,默认值为 null
+   * @returns 字段值
+   */
+  accessFormModel: (name: string, isSet?: boolean | undefined, defaultValue?: any) => any;
+  /**
+   * 获取当前激活的输入框字段上下文
+   * @returns 当前激活的输入框字段上下文
+   */
+  getCurrentFocusField: () => FormItemInternalContext | undefined;
+  /**
+   * 获取表单字段上下文
+   * @param name 要获取的字段名
+   * @returns 表单字段上下文
+   */
+  getField: (name: string) => FormItemInternalContext | undefined;
 }
 
 defineExpose<FormInstance>({
+  accessFormModel,
+  getCurrentFocusField: () => currentFocusField.value,
+  getField: (name: string) => formItems.value.get(name),
   blur,
   clearValidate,
   resetFields,

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

@@ -14,7 +14,7 @@ export type ValidTrigger = "blur" | "change" | "submit";
  * 表单项上下文
  */
 export type FormItemContext = {
-  getFieldName: (ref: any) => string,
+  getFieldName: () => string,
   /**
    * 触发表单条目获得焦点事件
    */
@@ -44,6 +44,15 @@ export type FormItemContext = {
   getFormModelValue(): any;
 };
 export type FormItemInternalContext = {
+  /**
+   * 获取表单组件的实例引用,如果没有子组件,则返回 Field 自身引用。
+   * 
+   * 本函数专用于动态表单,直接使用的情况可以在模板中绑定ref获取实例引用。
+   * 
+   * 只有 Field 组件设置了 requireChildRef 回调才能返回子组件实例引用,否则只会返回 Field 组件实例引用。
+   * @returns 
+   */
+  getExpectedRef: () => any,
   getFieldName: () => string,
   getValidateTrigger: () => ValidTrigger;
   getUniqueId: () => string,

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

@@ -205,7 +205,7 @@ export interface UploaderInstance {
    */
   setList: (list: UploaderItem[]) => void;
   /**
-   * 强制从已上传列表更新某个条目。如果条目在列表中不存在,则会添加到末尾。
+   * 强制从已上传列表更新某个条目。如果条目在列表中不存在(按文件路径判断),则会添加到末尾。
    */
   updateListItem: (item: UploaderItem) => void;
   /**
@@ -213,6 +213,13 @@ export interface UploaderInstance {
    */
   deleteListItem: (item: UploaderItem) => void;
   /**
+   * 添加条目到已上传列表,并且自动开始上传。
+   * 注:不会限制最大上传数和不限制最大文件大小。
+   * * 单选情况下会替换已上传列表中的条目。
+   * * 多选情况下会添加到末尾。
+   */
+  addItemAndUpload: (item: UploaderItem) => void;
+  /**
    * 开始手动上传所有条目
    */
   startUploadAll: () => Promise<void>;
@@ -473,6 +480,13 @@ defineExpose<UploaderInstance>({
   updateListItem(item) {
     updateListItem(item);
   },
+  addItemAndUpload(item) {
+    //如果是单选模式,且已存在上传项,则替换已存在项
+    if (props.maxUploadCount === 1 && currentUpladList.value.length > 0)
+      deleteListItem(currentUpladList.value[0]);
+    currentUpladList.value.push(item);
+    startUploadItem(item);
+  },
   pick() {
     onUploadPress();
   },

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

@@ -44,11 +44,15 @@ function handleListChange(list: UploaderItem[]) {
 
 onMounted(() => {
   setTimeout(() => {
-    uploaderRef.value?.setList(props.single 
+    uploaderRef.value?.setList(props.single || typeof value.value === 'string' 
       ? (value.value ? [ stringUrlToUploaderItem(value.value as any as string) ] : [])
       : props.modelValue?.map((item) => stringUrlToUploaderItem(item)) ?? []
     )
   }, 200);
+});
+
+defineExpose({
+  getUploaderRef: () => uploaderRef.value,
 })
 
 defineOptions({

+ 0 - 109
src/components/utils/Base64.ts

@@ -1,109 +0,0 @@
-export default {
-  encode: encode,
-  decode: decode,
-}
-
-var _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
-
-/**
- * 对字符串进行 Base64 编码
- * @param input 字符串
- */
-function encode(input : string) {
-    var output = "";
-    var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
-    var i = 0;
-    input = _utf8_encode(input);
-    while (i < input.length) {
-        chr1 = input.charCodeAt(i++);
-        chr2 = input.charCodeAt(i++);
-        chr3 = input.charCodeAt(i++);
-        enc1 = chr1 >> 2;
-        enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
-        enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
-        enc4 = chr3 & 63;
-        if (isNaN(chr2)) {
-            enc3 = enc4 = 64
-        } else {
-            if (isNaN(chr3)) {
-                enc4 = 64
-            }
-        }
-        output = output + _keyStr.charAt(enc1) + _keyStr.charAt(enc2) + _keyStr.charAt(enc3) + _keyStr.charAt(enc4)
-    }
-    return output
-}
-/**
- * 对 Base64字符串 进行 Base64 解码
- * @param input Base64字符串
- */
-function decode(input : string) {
-    var output = "";
-    var chr1, chr2, chr3;
-    var enc1, enc2, enc3, enc4;
-    var i = 0;
-    input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
-    while (i < input.length) {
-        enc1 = _keyStr.indexOf(input.charAt(i++));
-        enc2 = _keyStr.indexOf(input.charAt(i++));
-        enc3 = _keyStr.indexOf(input.charAt(i++));
-        enc4 = _keyStr.indexOf(input.charAt(i++));
-        chr1 = (enc1 << 2) | (enc2 >> 4);
-        chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
-        chr3 = ((enc3 & 3) << 6) | enc4;
-        output = output + String.fromCharCode(chr1);
-        if (enc3 != 64) {
-            output = output + String.fromCharCode(chr2)
-        }
-        if (enc4 != 64) {
-            output = output + String.fromCharCode(chr3)
-        }
-    }
-    output = _utf8_decode(output);
-    return output
-}
-function _utf8_encode(string: string) {
-    string = string.replace(/\r\n/g, "\n");
-    var utftext = "";
-    for (var n = 0; n < string.length; n++) {
-        var c = string.charCodeAt(n);
-        if (c < 128) {
-            utftext += String.fromCharCode(c)
-        } else {
-            if ((c > 127) && (c < 2048)) {
-                utftext += String.fromCharCode((c >> 6) | 192);
-                utftext += String.fromCharCode((c & 63) | 128)
-            } else {
-                utftext += String.fromCharCode((c >> 12) | 224);
-                utftext += String.fromCharCode(((c >> 6) & 63) | 128);
-                utftext += String.fromCharCode((c & 63) | 128)
-            }
-        }
-    }
-    return utftext
-}
-function _utf8_decode(utftext: string) {
-    var string = "";
-    var i = 0;
-    var c1, c2, c3;
-    var c = c1 = c2 = 0;
-    while (i < utftext.length) {
-        c = utftext.charCodeAt(i);
-        if (c < 128) {
-            string += String.fromCharCode(c);
-            i++
-        } else {
-            if ((c > 191) && (c < 224)) {
-                c2 = utftext.charCodeAt(i + 1);
-                string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
-                i += 2
-            } else {
-                c2 = utftext.charCodeAt(i + 1);
-                c3 = utftext.charCodeAt(i + 2);
-                string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
-                i += 3
-            }
-        }
-    }
-    return string
-}

+ 0 - 547
src/components/utils/Calendar.ts

@@ -1,547 +0,0 @@
-/**
-* @1900-2100区间内的公历、农历互转
-* @charset UTF-8
-* @Author  Jea杨(JJonline@JJonline.Cn) 
-* @Time    2014-7-21
-* @Time    2016-8-13 Fixed 2033hex、Attribution Annals
-* @Time    2016-9-25 Fixed lunar LeapMonth Param Bug
-* @Version 1.0.2
-* @公历转农历:calendar.solar2lunar(1987,11,01); //[you can ignore params of prefix 0]
-* @农历转公历:calendar.lunar2solar(1987,09,10); //[you can ignore params of prefix 0]
-*/
-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

+ 0 - 100
src/components/utils/Timer.ts

@@ -1,100 +0,0 @@
-/**
- * Copyrigt (C) 2022 imengyu.top
- */
-
-export class SimpleDelay<T = any> {
-
-  private executor: (data: T) => void;
-  private interval: number;
-  private data: T;
-  private timer = 0;
-
-  public constructor(data: T, executor: (d: T) => void, interval: number) {
-    this.executor = executor;
-    this.interval = interval;
-    this.data = data;
-  }
-
-  public start() {
-    if (this.timer)
-      clearTimeout(this.timer);
-    this.timer = setTimeout(() => 
-      this.executor(this.data), 
-      this.interval
-    ) as unknown as number;
-    return this;
-  }
-  public stop() {
-    if (this.timer) {
-      clearTimeout(this.timer);
-      this.timer = 0;
-    }
-    return this;
-  }
-
-}
-export class SimpleTimer<T = any> {
-
-  private executor: (data: T) => void;
-  private interval: number;
-  private data: T;
-  private timer = 0;
-  private executorTimeLimitWarnCount = 0;
-  private lasExecuteTime = 0;
-
-  public constructor(data: T, executor: (d: T) => void, interval: number) {
-    this.executor = executor;
-    this.interval = interval;
-    this.data = data;
-  }
-
-  public start() {
-    if (this.timer)
-      clearInterval(this.timer);
-    this.timer = setInterval(() => {
-
-      const startTime = new Date();
-      this.executor(this.data);
-
-      //计算执行时间
-      const executeTime = new Date().getTime() - startTime.getTime();
-
-      //如果与上一次触发时间过近,则取消
-      const lastInterval = startTime.getTime() - this.lasExecuteTime
-      if (this.lasExecuteTime > 0 && lastInterval < (this.interval - 200)) {
-        console.warn('The execution time of the timer is too fast, lastInterval: ' + lastInterval, 'executor:', this.executor);
-        return;
-      }
-
-      this.lasExecuteTime = startTime.getTime();
-
-      //如果执行时间过长,则警告
-      if (executeTime > this.interval * 0.5 && this.executorTimeLimitWarnCount < 1) {
-        console.warn('The execution time of the timer is too long, exceeding the timing for ' + this.interval + 'ms. executor:', this.executor, 'count:', this.executorTimeLimitWarnCount);
-        this.executorTimeLimitWarnCount++;
-      } else
-        this.executorTimeLimitWarnCount--;
-
-    }, this.interval) as unknown as number;
-    return this; 
-  }
-  public stop() {
-    if (this.timer) {
-      clearInterval(this.timer);
-      this.timer = 0;
-    }
-    return this; 
-  }
-
-}
-
-export function exactDelay(cb: () => void, ms: number) {
-  const startTime = new Date().getTime();
-
-  return setTimeout(() => {
-    const execTime = new Date().getTime() - startTime;
-    if (execTime > (ms + (ms < 500 ? 100 : 1000)))
-      console.warn('warn: exactDelay slow! execTime/ms:', execTime, '/', ms);
-    cb();
-  }, ms);
-}

+ 75 - 48
src/pages/dig/forms/forms.ts

@@ -9,6 +9,7 @@ import type { RadioIdFieldProps } from "@/components/dynamic/wrappers/RadioIdFie
 import type { FieldProps } from "@/components/form/Field.vue";
 import type { PickerFieldProps } from "@/components/form/PickerField.vue";
 import type { StepperProps } from "@/components/form/Stepper.vue";
+import type { UploaderInstance } from "@/components/form/Uploader.vue";
 import type { UploaderFieldProps } from "@/components/form/UploaderField.vue";
 import type { NewDataModel } from "@imengyu/js-request-transform";
 import type { Ref } from "vue";
@@ -511,46 +512,55 @@ const villageInfoFoodProductsForm : SingleForm = [VillageBulidingInfo, () => ({
 const villageCommonContent : (model: Ref<FormExport>) => FormDefine = (model) => ({
   items: [
     {
-      label: '标题',
-      name: 'title',
-      type: 'text',
-      defaultValue: '',
-      params: {
-        placeholder: '请输入标题',
-      },
-      rules: [{
-        required: true,
-        message: '请输入标题',
-      }]
-    },
-    {
-      label: '内容',
-      name: 'content',
-      type: 'richtext',
-      defaultValue: '',
-      params: {
-        placeholder: '请输入内容',
-        maxLength: 1000,
-      },
-      rules: [{
-        required: true,
-        message: '请输入内容',
-      }]
-    },
-    {
-      label: '图片',
-      name: 'images',
-      type: 'uploader',
-      defaultValue: '',
-      params: {
-        upload: useAliOssUploadCo('xiangyuan/common'),
-        maxFileSize: 1024 * 1024 * 20,
-        maxUploadCount: 20,
-      } as UploaderFieldProps,
-      rules: [{
-        required: true,
-        message: '请输入内容',
-      }]
+      name: '',
+      children: {
+        type: 'group',
+        props: {
+          type: 'block',
+        } as FormGroupProps,
+        propNestType: 'nest', 
+        items: [
+          {
+            label: '文章标题',
+            name: 'title',
+            type: 'text',
+            defaultValue: '',
+            params: {
+              placeholder: '请输入标题',
+            },
+            rules: [{
+              required: true,
+              message: '请输入文章标题',
+            }]
+          },
+          {
+            label: '主要内容',
+            name: 'content',
+            type: 'richtext',
+            defaultValue: '',
+            params: {
+              placeholder: '请输入介绍内容',
+              maxLength: 1000,
+            },
+            rules: [{
+              required: true,
+              message: '请输入内容',
+            }]
+          },
+          {
+            label: '相关图片',
+            name: 'images',
+            type: 'uploader',
+            defaultValue: '',
+            params: {
+              upload: useAliOssUploadCo('xiangyuan/common'),
+              maxFileSize: 1024 * 1024 * 20,
+              maxUploadCount: 20,
+            } as UploaderFieldProps,
+            rules: []
+          },
+        ],
+      }
     },
   ]
 });
@@ -1042,17 +1052,17 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           children: {
             type: 'group',
             props: {
-              class: 'form-block',
-            },
+              type: 'block',
+            } as FormGroupProps,
             propNestType: 'nest', 
             items: [
               {
                 label: '概括',
                 name: 'overview',
-                type: 'richtext',
+                type: 'textarea',
                 defaultValue: '',
                 params: {
-                  placeholder: '请输入概括',
+                  placeholder: '请输入村落整体概括信息',
                   maxLength: 300,
                   showWordLimit: true, 
                 } as FieldProps,
@@ -1069,8 +1079,8 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           children: {
             type: 'group',
             props: {
-              class: 'form-block',
-            },
+              type: 'block',
+            } as FormGroupProps,
             propNestType: 'nest', 
             items: [
               {
@@ -1079,7 +1089,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
                 type: 'textarea', 
                 defaultValue: '',
                 params: {
-                  placeholder: '请输入突出价值',
+                  placeholder: '请输入村落突出价值信息',
                   maxLength: 1000,
                   showWordLimit: true, 
                 } as FieldProps,
@@ -1121,7 +1131,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
       items: [
         ...villageCommonContent(m).items,
         {
-          label: '视频',
+          label: '口述历史视频/录音',
           name: 'video',
           type: 'uploader',
           defaultValue: '',
@@ -1131,6 +1141,23 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
             maxFileSize: 1024 * 1024 * 20,
             single: true,
           } as UploaderFieldProps,
+          itemParams: {
+            extraMessage: '您可以上传已经录制好的口述历史视频/录音,也可以点击下方按钮录制新的音频。但小程序中录音时长最长10分钟,如需更长时间,请使用系统相机拍摄。',
+          } as FieldProps,
+        },
+        {
+          label: '',
+          name: 'video1',
+          type: 'recorder',
+          defaultValue: '',
+          params: {
+            onRecordDone: (path: string) => {
+              (m.value.getFormItemRef('video')?.getUploaderRef() as UploaderInstance).addItemAndUpload({
+                filePath: path,
+                state: 'notstart',
+              });
+            }
+          },
         },
       ],
     })],

+ 21 - 6
src/pages/dig/forms/list.vue

@@ -9,20 +9,32 @@
       />
       <Button type="primary" @click="newData">+ 新增</Button>
     </FlexRow>
+    <Height :height="20" />
     <FlexCol>
-      <FlexRow 
-        class="item" 
-        hover-class="pressed"
+      <Touchable 
         v-for="item in listLoader.list.value"
         :key="item.id" 
+        :gap="20"
+        :padding="[15,20]"
+        :radius="15"
+        align="center"
+        backgroundColor="white"
+        direction="row"
+        touchable
         @click="goDetail(item.id)"
       >
-        <Image :src="item.image" width="170rpx" height="190rpx" />
+        <Image 
+          :src="item.image"
+          :width="100"
+          :height="100"
+          :radius="10"
+          round
+        />
         <FlexCol>
-          <Text :size="36">{{ item.title }}</Text>
+          <H4 :size="36">{{ item.title }}</H4>
           <Text :size="23">{{ item.date }}</Text>
         </FlexCol>
-      </FlexRow>
+      </Touchable>
     </FlexCol>
     <SimplePageListLoader :loader="listLoader" :noEmpty="true">
       <template #empty>
@@ -49,6 +61,9 @@ import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Text from '@/components/basic/Text.vue';
 import { navTo } from '@/components/utils/PageAction';
+import Height from '@/components/layout/space/Height.vue';
+import H4 from '@/components/typography/H4.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
 
 const searchText = ref('');
 const listLoader = useSimplePageListLoader<{