ソースを参照

完善表单组件问题

快乐的梦鱼 1 週間 前
コミット
d2c93389a7

+ 1 - 1
src/api/inhert/VillageInfoApi.ts

@@ -186,7 +186,7 @@ export class VillageInfoApi extends AppServerRequestModule<DataModel> {
       village_id: villageId,
       village_volunteer_id: villageVolunteerId,
     }, '获取信息详情'))
-      .then(res => transformArrayDataModel<T>(modelClassCreator, res.data2, `获取分类列表`, true))
+      .then(res => transformArrayDataModel<T>(modelClassCreator, res.data2 ?? [], `获取分类列表`, true))
       .catch(e => { throw e });
   }
   async updateInfo<T extends DataModel>(

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

@@ -1,10 +1,12 @@
 <template>
   <view class="d-flex flex-col">
-    <Parse v-if="modelValue" :content="modelValue" containerStyle="max-height:400px" />
-    <text v-else>未编写内容,点击编写</text>
-    <view class="d-flex flex-row gap-sss align-center mt-3">
-      <Button icon="browse" text="预览内容" size="small" @click="preview" />
-      <Button icon="edit" text="编辑内容" size="small" @click="edit" type="primary" />
+    <view class="richtext-preview-box">
+      <Parse v-if="modelValue" :content="modelValue" containerStyle="max-height:400px" />
+      <text v-else>未编写内容,点击编写</text>
+    </view>
+    <view class="d-flex flex-row gap-sss align-center mt-2">
+      <Button icon="browse" text="预览" size="small" @click="preview" />
+      <Button icon="edit" text="编辑" size="small" @click="edit" type="primary" />
     </view>
   </view>
 </template>
@@ -15,6 +17,7 @@ import { navTo } from '@/components/utils/PageAction';
 import Parse from '@/components/display/parse/Parse.vue';
 import Button from '@/components/basic/Button.vue';
 
+
 const props = defineProps({	
   modelValue: { 
     type: String,
@@ -49,4 +52,14 @@ onPageShow(() => {
     })
   }
 })
-</script>
+</script>
+
+<style lang="scss" scoped>
+.richtext-preview-box {
+  padding: 15rpx 20rpx;
+  border-radius: 20rpx;
+  background-color: #efefef;
+  flex: 1;
+
+}
+</style>

+ 2 - 2
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 from '../form/Form.vue';
+import Form, { type FormProps } from '../form/Form.vue';
 import type { FormDefine, FormDefineItem, FormExport } from '.';
 import type { Rules } from 'async-validator';
 
@@ -29,7 +29,7 @@ const props = defineProps({
     default: () => ({})
   },
   formProps: {
-    type: Object,
+    type: Object as PropType<Omit<FormProps, 'model' | 'rules'>>,
     default: () => ({}) 
   },
   formGlobalParams: {

+ 25 - 27
src/components/dynamic/DynamicFormControl.vue

@@ -124,41 +124,39 @@
         <PickerLonlat
           ref="itemRef"
           :modelValue="modelValue"
-          @update:modelValue="(v:any) =>{onValueChanged(v);formItemRef.onFieldChange(v)}"
+          @update:modelValue="(v:any) => onValueChanged(v)"
           v-bind="params"
         />
       </template>
       <template v-else-if="formDefineItem.type === 'picker-datetime'">
-        <DateTimePickerField
-          ref="itemRef"
-          :value="modelValue"
-          v-bind="params"
-          @update:modelValue="(e: any) => onValueChanged(e)"
-        />
+        <view>
+          <DateTimePickerField
+            ref="itemRef"
+            :value="modelValue"
+            v-bind="params"
+            @update:modelValue="(e: any) => onValueChanged(e)"
+          />
+        </view>
       </template>
       <template v-else-if="formDefineItem.type === 'picker-time'">
-        <TimePickerField
-          ref="itemRef"
-          :value="modelValue"
-          v-bind="params"
-          @update:modelValue="(e: any) => onValueChanged(e)"
-        />
+        <view>
+          <TimePickerField
+            ref="itemRef"
+            :value="modelValue"
+            v-bind="params"
+            @update:modelValue="(e: any) => onValueChanged(e)"
+          />
+        </view>
       </template>
       <template v-else-if="formDefineItem.type === 'picker-date'">
-        <DatePickerField
-          ref="itemRef"
-          :value="modelValue"
-          v-bind="params"
-          @update:modelValue="(e: any) => onValueChanged(e)"
-        />
-      </template>
-      <template v-else-if="formDefineItem.type === 'uploader-image'">
-        <ImageUploaderWrapper
-          ref="itemRef"
-          :value="modelValue"
-          v-bind="params"
-          @change="(e: any) => onValueChanged(e)"
-        />
+        <view>
+          <DatePickerField
+            ref="itemRef"
+            :value="modelValue"
+            v-bind="params"
+            @update:modelValue="(e: any) => onValueChanged(e)"
+          />
+        </view>
       </template>
       <!-- More components can be added here... -->
       <template v-else-if="formDefineItem.type === 'select-city'">

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

@@ -18,7 +18,7 @@
       <CheckBox
         v-for="value in data2"
         :key="value.value"
-        :value="value.value"
+        :name="value.value"
         :text="value.text" 
         :disabled="value.disable"
       />

+ 2 - 1
src/components/dynamic/wrappers/PickerLonlat.vue

@@ -2,12 +2,13 @@
   <Button
     type="primary"
     size="small"
-    :text="props.modelValue ? `${props.modelValue.join(',')}` : '请选择经纬度'"
+    :text="props.modelValue ? `${FormatUtils.formatCoordinates(props.modelValue[1], props.modelValue[0])}` : '请选择经纬度'"
     @click="openPicker"
   />
 </template>
 
 <script setup lang="ts">
+import { FormatUtils } from '@imengyu/imengyu-utils';
 import type { PickerLonlatProps } from './PickerLonlat';
 import Button from '@/components/basic/Button.vue';
 

+ 31 - 3
src/components/form/Field.vue

@@ -106,7 +106,20 @@
         <slot name="rightButton" />
         <Icon v-if="showRightArrow" icon="arrow-right" :size="themeContext.resolveThemeSize('FieldRightArrowSize', 30)" v-bind="rightArrowProps" />
       </FlexRow>
-      <text v-if="finalErrorMessage" :style="themeStyles.errorMessage.value">{{finalErrorMessage}}</text>
+      <FlexRow 
+        v-if="finalErrorMessage"
+        :gap="10"
+        :innerStyle="themeStyles.errorMessage.value"
+        align="center"
+      >
+        <Icon 
+          :icon="errorIcon"
+          :size="themeContext.resolveThemeSize('FieldErrorIconSize', 40)" 
+          v-bind="errorIconProps" 
+          :color="themeContext.resolveThemeColor('FieldErrorMessageColor', 'danger')"
+        />
+        <text :style="themeStyles.errorMessageText.value">{{finalErrorMessage}}</text>
+      </FlexRow>
       <text v-if="showWordLimit" :style="themeStyles.wordLimitText.value">{{wordLimitText}}</text>
 
     </FlexCol>
@@ -341,6 +354,15 @@ export interface FieldProps {
    */
   error?: boolean;
   /**
+   * 错误提示的图标
+   * @default 'prompt'
+   */
+  errorIcon?: string;
+  /**
+   * 错误提示图标的自定义属性
+   */
+  errorIconProps?: IconProps;
+  /**
    * 底部错误提示文案,为空时不展示
    */
   errorMessage?: string;
@@ -416,6 +438,8 @@ const props = withDefaults(defineProps<FieldProps>(), {
   autoHeight: true,
   maxLength: 100,
   modelValue: undefined,
+  errorIcon: () => propGetThemeVar('FieldErrorIcon', 'prompt'),
+  errorIconProps: () => propGetThemeVar('FieldErrorIconProps', propGetFormContext()?.fieldProps.value?.errorIconProps ?? {}),
 });
 
 //#region Context
@@ -506,13 +530,17 @@ const themeStyles = themeContext.useThemeStyles({
   },
   input: {
     flex: 1,
+    width: 'auto',
+    minWidth: '100rpx',
     paddingVertical: DynamicSize('FieldInputPaddingVertical', 0),
     paddingHorizontal: DynamicSize('FieldInputPaddingHorizontal', 0),
   },
-  errorMessage: {
+  errorMessageText: {
     fontSize: DynamicSize('FieldErrorMessageFontSize', 24),
     color: DynamicColor('FieldErrorMessageColor', 'danger'),
-    marginTop: DynamicSize('FieldErrorMessageMarginTop', 8),
+  },
+  errorMessage: {
+    marginTop: DynamicSize('FieldErrorMessageMarginTop', 12),
   },
   wordLimitText: {
     fontSize: DynamicSize('FieldWordLimitTextFontSize', 24),

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

@@ -1,7 +1,10 @@
 <template>
   <view class="nana-form-picker">
+    <slot v-if="loaded && (columns.length === 0 || (columns.length === 1 && columns[0].length === 0))" name="empty">
+      <Empty description="暂无可选数据" :innerStyle="{ position: 'absolute', width: '100%' }" />
+    </slot>
     <picker-view 
-      v-if="loaded"
+      v-else-if="loaded"
       :value="pickerSelectIndex" 
       class="picker-view"
       :style="{
@@ -20,6 +23,7 @@
 <script setup lang="ts">
 import { nextTick, onMounted, ref, watch } from 'vue';
 import { useTheme } from '../theme/ThemeDefine';
+import Empty from '../feedback/Empty.vue';
 
 const themeContext = useTheme();
 

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

@@ -29,6 +29,7 @@ import Popup from '../dialog/Popup.vue';
 import ActionSheetTitle, { type ActionSheetTitleProps } from '../dialog/ActionSheetTitle.vue';
 import Picker from './Picker.vue';
 import { usePickerFieldTempStorageData } from './PickerUtils';
+import Empty from '../feedback/Empty.vue';
 
 export interface PickerFieldProps extends Omit<PickerProps, 'value'> {
   modelValue?: (number|string)[];

+ 86 - 7
src/components/form/Stepper.vue

@@ -4,8 +4,16 @@
       <IconButton
         :icon="minusIcon"
         :disabled="disabled || value <= min"
-        :buttonStyle="themeStyles.button.value"
+        :buttonStyle="{ 
+          ...themeStyles.button.value,
+          ...selectObjectByType(size, 'medium', {
+            small: themeStyles.buttonSmall.value,
+            medium: themeStyles.buttonMedium.value,
+            large: themeStyles.buttonLarge.value,
+          })
+        }"
         :padding="5"
+        :size="iconSize"
         @click="minus"
       />
     </slot>
@@ -18,7 +26,14 @@
       :onBlur="onTextBlur"
       :editable="!disabled"
     >
-      <view :style="themeStyles.inputWrapper.value">
+      <view :style="{
+        ...themeStyles.inputWrapper.value,
+        ...selectObjectByType(size, 'medium', {
+          small: themeStyles.inputWrapperSmall.value,
+          medium: themeStyles.inputWrapperMedium.value,
+          large: themeStyles.inputWrapperLarge.value,
+        }),
+      }">
         <input
           :style="{
             ...themeStyles.input.value,
@@ -41,8 +56,16 @@
       <IconButton
         :icon="addIcon"
         :disabled="disabled || (max ? value >= max : undefined)"
-        :buttonStyle="themeStyles.button.value"
+        :buttonStyle="{ 
+          ...themeStyles.button.value,
+          ...selectObjectByType(size, 'medium', {
+            small: themeStyles.buttonSmall.value,
+            medium: themeStyles.buttonMedium.value,
+            large: themeStyles.buttonLarge.value,
+          })
+        }"
         :padding="5"
+        :size="iconSize"
         @click="add"
       />
     </slot>
@@ -50,12 +73,12 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref, toRef, watch } from 'vue';
+import { computed, onMounted, ref, toRef, watch } from 'vue';
 import IconButton from '../basic/IconButton.vue';
 import FlexRow from '../layout/FlexRow.vue';
 import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
 import { StringUtils } from '@imengyu/imengyu-utils';
-import { DynamicColor, DynamicSize } from '../theme/ThemeTools';
+import { DynamicColor, DynamicSize, selectObjectByType } from '../theme/ThemeTools';
 import { useFieldChildValueInjector } from './FormContext';
 
 export interface StepperProps {
@@ -80,6 +103,11 @@ export interface StepperProps {
    */
   inputWidth?: number;
   /**
+   * 组件大小
+   * @default 'medium'
+   */
+  size?: 'small' | 'medium' | 'large';
+  /**
    * 步长,每次点击时改变的值
    * @default 1
    */
@@ -154,17 +182,57 @@ const themeContext = useTheme();
 const themeStyles = themeContext.useThemeStyles({
   button: {
     borderRadius: DynamicSize('StepperButtonBorderRadius', 0),
+    backgroundColor: DynamicColor('StepperButtonBackgroundColor', 'light'),
+  },
+  buttonLarge: {
+    borderRadius: DynamicSize('StepperButtonBorderRadius', 0),
     paddingTop: DynamicSize('StepperButtonPaddingVertical', 8),
     paddingBottom: DynamicSize('StepperButtonPaddingVertical', 8),
     paddingLeft: DynamicSize('StepperButtonPaddingHorizontal', 8),
     paddingRight: DynamicSize('StepperButtonPaddingHorizontal', 8),
     backgroundColor: DynamicColor('StepperButtonBackgroundColor', 'light'),
   },
+  buttonMedium: {
+    paddingTop: DynamicSize('StepperButtonPaddingVertical', 4),
+    paddingBottom: DynamicSize('StepperButtonPaddingVertical', 4),
+    paddingLeft: DynamicSize('StepperButtonPaddingHorizontal', 4),
+    paddingRight: DynamicSize('StepperButtonPaddingHorizontal', 4),
+  },
+  buttonSmall: {
+    borderRadius: DynamicSize('StepperButtonBorderRadius', 0),
+    paddingTop: DynamicSize('StepperButtonPaddingVertical', 2),
+    paddingBottom: DynamicSize('StepperButtonPaddingVertical', 2),
+    paddingLeft: DynamicSize('StepperButtonPaddingHorizontal', 2),
+    paddingRight: DynamicSize('StepperButtonPaddingHorizontal', 2),
+  },
   inputWrapper: {
     display: 'flex',
     justifyContent: 'center',
     alignItems: 'center',
     alignSelf: 'stretch',
+    backgroundColor: DynamicColor('StepperInputBackgroundColor', 'light'),
+  },
+  inputWrapperSmall: {
+    paddingTop: DynamicSize('StepperInputPaddingVertical', 0),
+    paddingBottom: DynamicSize('StepperInputPaddingVertical', 0),
+    paddingLeft: DynamicSize('StepperInputPaddingHorizontal', 6),
+    paddingRight: DynamicSize('StepperInputPaddingHorizontal', 6),
+    marginLeft: DynamicSize('StepperInputMarginHorizontal', 2),
+    marginRight: DynamicSize('StepperInputMarginHorizontal', 2),
+    marginTop: DynamicSize('StepperInputMarginVertical', 0),
+    marginBottom: DynamicSize('StepperInputMarginVertical', 0),
+  },
+  inputWrapperMedium: {
+    paddingTop: DynamicSize('StepperInputPaddingVertical', 0),
+    paddingBottom: DynamicSize('StepperInputPaddingVertical', 0),
+    paddingLeft: DynamicSize('StepperInputPaddingHorizontal', 10),
+    paddingRight: DynamicSize('StepperInputPaddingHorizontal', 10),
+    marginLeft: DynamicSize('StepperInputMarginHorizontal', 4),
+    marginRight: DynamicSize('StepperInputMarginHorizontal', 4),
+    marginTop: DynamicSize('StepperInputMarginVertical', 0),
+    marginBottom: DynamicSize('StepperInputMarginVertical', 0),
+  },
+  inputWrapperLarge: {
     paddingTop: DynamicSize('StepperInputPaddingVertical', 0),
     paddingBottom: DynamicSize('StepperInputPaddingVertical', 0),
     paddingLeft: DynamicSize('StepperInputPaddingHorizontal', 20),
@@ -173,13 +241,24 @@ const themeStyles = themeContext.useThemeStyles({
     marginRight: DynamicSize('StepperInputMarginHorizontal', 8),
     marginTop: DynamicSize('StepperInputMarginVertical', 0),
     marginBottom: DynamicSize('StepperInputMarginVertical', 0),
-    backgroundColor: DynamicColor('StepperInputBackgroundColor', 'light'),
   },
   input: {
     textAlign: 'center',
     color: DynamicColor('StepperInputTextColor', 'text.content'),
   },
-})
+});
+
+const iconSize = computed(() => {
+  switch (props.size) {
+    case 'small':
+      return themeContext.resolveThemeSize('StepperIconSizeSmall', 24);
+    default:
+    case 'medium':
+      return themeContext.resolveThemeSize('StepperIconSizeMedium', 36);
+    case 'large':
+      return themeContext.resolveThemeSize('StepperIconSizeLarge', 48);
+  }
+});
 
 const {
   value,

+ 3 - 3
src/pages/dig/forms/common.vue

@@ -5,10 +5,10 @@
       <DynamicForm
         ref="formRef"
         :formDefine="formDefine"
-        :formProps="{
-          labelPosition: 'top',
+        :formProps="({
+          labelWidth: '160rpx',
           labelAlign: 'left',
-        }"
+        })"
         :formGlobalParams="querys"
       />
       <Height :height="20" />

+ 12 - 15
src/pages/dig/forms/forms.ts

@@ -3,9 +3,11 @@ import { useAliOssUploadCo } from "@/common/components/upload/AliOssUploadCo";
 import type { FormDefine, FormDefineItem, IFormItemCallbackAdditionalProps } from "@/components/dynamic";
 import type { FormGroupProps } from "@/components/dynamic/DynamicFormCate.vue";
 import type { CheckBoxListProps } from "@/components/dynamic/wrappers/CheckBoxList.vue";
+import type { CheckBoxToIntProps } from "@/components/dynamic/wrappers/CheckBoxToInt";
 import type { IDynamicFormItemSelectIdFormItemProps } from "@/components/dynamic/wrappers/PickerIdField";
 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 { UploaderFieldProps } from "@/components/form/UploaderField.vue";
 import type { NewDataModel } from "@imengyu/js-request-transform";
 
@@ -1430,7 +1432,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
         {
           label: '建筑数量', 
           name: 'num', 
-          type: 'nmber', 
+          type: 'number', 
           defaultValue: '',
           params: {
             min: 0,
@@ -1594,7 +1596,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
         }, 
         {
           name: '',
-          label: '平面坐标X',
+          label: '平面坐标XY',
           children: {
             type: 'group',
             props: { type: 'row' } as FormGroupProps,
@@ -1608,7 +1610,8 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
                 params: {
                   min: -250,
                   max: 250,
-                },
+
+                } as StepperProps,
                 itemParams: {
                   labelWidth: '0rpx',
                   labelPosition2: 'left',
@@ -1621,23 +1624,13 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
               },
               { 
                 label: '', 
-                name: 'aa', 
-                type: 'static-text', 
-                params: { text: 'Y' },
-                itemParams: {
-                  labelWidth: '0rpx',
-                  bottomMargin: false,
-                },
-              },
-              { 
-                label: '', 
                 name: 'mapY', 
                 type: 'number', 
                 defaultValue: 0,
                 params: {
                   min: -250,
                   max: 250,
-                },
+                } as StepperProps,
                 itemParams: {
                   labelWidth: '0rpx',
                   labelPosition2: 'left',
@@ -1792,7 +1785,9 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           name: 'isInheritor', 
           type: 'check-box-int', 
           defaultValue: 0,
-          params: {},
+          params: {
+            text: '是',
+          } as CheckBoxToIntProps ,
           rules:  [{
             required: true,
             message: '请选择是否确定传承人',
@@ -1897,6 +1892,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           params: {
             type: 'datetime',
           },
+          itemParams: { showRightArrow: true } as FieldProps,
           rules: [{
             required: true,
             message: '请选择具体传承时间',
@@ -1909,6 +1905,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           params: {
             type: 'datetime',
           },
+          itemParams: { showRightArrow: true } as FieldProps,
           rules: [{
             required: true,
             message: '请选择加入时间',

+ 1 - 1
src/pages/editor/preview.vue

@@ -1,6 +1,6 @@
 <template>
   <view>
-    <Empty v-if="!content" image="search" text="空内容,请先编写内容后再预览" />
+    <Empty v-if="!content" image="search" description="空内容,请先编写内容后再预览" />
     <view v-else class="p-3">
       <Parse :content="content" />
     </view>