Kaynağa Gözat

表单优化

快乐的梦鱼 5 gün önce
ebeveyn
işleme
ea6db0f877
54 değiştirilmiş dosya ile 776 ekleme ve 366 silme
  1. 82 0
      src/api/RequestModules.ts
  2. 79 0
      src/api/map/MapApi.ts
  3. 13 0
      src/common/components/dynamicf/ComponentConfigs.ts
  4. 1 0
      src/common/config/ApiCofig.ts
  5. 7 6
      src/components/basic/Button.vue
  6. 5 5
      src/components/basic/Cell.vue
  7. 4 4
      src/components/basic/IconButton.vue
  8. 2 1
      src/components/basic/Image.vue
  9. 4 3
      src/components/dialog/ActionSheetItem.vue
  10. 4 3
      src/components/dialog/DialogButton.vue
  11. 6 2
      src/components/dialog/Popup.vue
  12. 6 5
      src/components/display/NoticeBar.vue
  13. 4 2
      src/components/display/Tag.vue
  14. 0 15
      src/components/display/block/BackgroundBox.vue
  15. 3 5
      src/components/display/block/ImageBlock.vue
  16. 3 3
      src/components/display/block/ImageBlock2.vue
  17. 3 2
      src/components/display/block/ImageBlock3.vue
  18. 4 3
      src/components/display/block/TextBlock.vue
  19. 4 4
      src/components/display/block/TextLeftRightBlock.vue
  20. 3 2
      src/components/display/title/SubTitle.vue
  21. 1 0
      src/components/dynamic/DynamicForm.vue
  22. 31 12
      src/components/dynamic/DynamicFormControl.vue
  23. 38 0
      src/components/dynamic/index.ts
  24. 65 31
      src/components/dynamic/wrappers/PickerCityField.vue
  25. 6 0
      src/components/dynamic/wrappers/PickerIdField.vue
  26. 14 0
      src/components/dynamic/wrappers/RadioIdField.ts
  27. 33 0
      src/components/dynamic/wrappers/RadioIdField.vue
  28. 3 2
      src/components/dynamic/wrappers/RadioValue.vue
  29. 126 0
      src/components/feedback/Touchable.vue
  30. 3 3
      src/components/form/CalendarField.vue
  31. 4 3
      src/components/form/CalendarItem.vue
  32. 2 1
      src/components/form/Cascader.vue
  33. 7 3
      src/components/form/CascaderField.vue
  34. 6 6
      src/components/form/CheckBox.vue
  35. 3 2
      src/components/form/Field.vue
  36. 2 2
      src/components/form/FormContext.ts
  37. 4 2
      src/components/form/NumberInputBox.vue
  38. 1 3
      src/components/form/PickerUtils.ts
  39. 4 3
      src/components/form/Radio.vue
  40. 5 5
      src/components/form/UploaderListItem.vue
  41. 5 4
      src/components/keyboard/NumberKeyBoardKey.vue
  42. 4 2
      src/components/keyboard/PlateKeyBoardKey.vue
  43. 77 0
      src/components/layout/BaseView.ts
  44. 4 128
      src/components/layout/FlexView.vue
  45. 4 3
      src/components/layout/grid/GridItem.vue
  46. 4 3
      src/components/list/SimpleListItem.vue
  47. 3 3
      src/components/nav/PaginationItem.vue
  48. 3 3
      src/components/nav/SegmentedControlItem.vue
  49. 3 3
      src/components/nav/SideBarItem.vue
  50. 3 3
      src/components/nav/TabBarItem.vue
  51. 5 8
      src/components/nav/Tabs.vue
  52. 1 1
      src/pages/dig/forms/common.vue
  53. 60 57
      src/pages/dig/forms/forms.ts
  54. 5 5
      src/pages/user/index.vue

+ 82 - 0
src/api/RequestModules.ts

@@ -226,4 +226,86 @@ export class AppServerRequestModule<T extends DataModel> extends RequestCoreInst
     this.config.responseErrReoprtInceptor = responseErrReoprtInceptor;
     this.config.reportError = reportError;
   }
+}
+/**
+ * 地图服务请求模块
+ */
+export class MapServerRequestModule<T extends DataModel> extends RequestCoreInstance<T> {
+  constructor() {
+    super(UniappImplementer);
+    this.config.baseUrl = 'https://restapi.amap.com';
+    this.config.errCodes = []; //
+    this.config.requestInceptor = (url, req) => {
+      url = appendGetUrlParams(url, 'key', ApiCofig.amapServerKey);
+      return { newUrl: url, newReq: req };
+    };
+    this.config.responseDataHandler = (response, req, resultModelClass, instance, apiName) => {
+        return new Promise<RequestApiResult<T>>((resolve, reject) => {
+          const method = req.method || 'GET';
+          response.json().then((json) => {
+            if (response.ok) {
+              if (!json) {
+                reject(new RequestApiError(
+                  'businessError',
+                  '后端未返回数据',
+                  '',
+                  response.status,
+                  null,
+                  null,
+                  req,
+                  apiName,
+                  response.url
+                ));
+                return;
+              }
+              if (json.status != '1') {
+                handleError();
+                return;
+              }
+              resolve(new RequestApiResult(
+                resultModelClass ?? instance.config.modelClassCreator,
+                json?.code || response.status,
+                json.info,
+                json,
+                json
+              ));
+            }
+            else {
+              handleError();
+            }
+
+            function handleError() {
+              let errType : RequestApiErrorType = 'unknow';
+              let errString = json.info;
+              let errCodeStr = json.infocode;
+              if (errString) {
+                errType = 'businessError';
+              } else {
+                const res = defaultResponseDataGetErrorInfo(response, json);
+                errType = res.errType;
+                errString = res.errString;
+                errCodeStr = res.errCodeStr;
+              }
+
+              reject(new RequestApiError(
+                errType,
+                errString,
+                errCodeStr,
+                response.status,
+                null,
+                null,
+                req,
+                apiName,
+                response.url
+              ));
+            }
+          }).catch((err) => {
+            //错误统一处理
+            defaultResponseDataHandlerCatch(method, req, response, null, err, apiName, response.url, reject, instance);
+          });
+        });
+    };
+    this.config.responseErrReoprtInceptor = responseErrReoprtInceptor;
+    this.config.reportError = reportError;
+  }
 }

+ 79 - 0
src/api/map/MapApi.ts

@@ -0,0 +1,79 @@
+import { DataModel, transformArrayDataModel, type NewDataModel } from '@imengyu/js-request-transform';
+import { MapServerRequestModule } from '../RequestModules';
+import { type QueryParams } from '@imengyu/imengyu-utils';
+
+export class Poi extends DataModel<Poi> {
+  constructor() {
+    super(Poi, "Poi");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      location: { clientSide: 'splitCommaArray' },
+    };
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('Time'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
+  }
+  name = '';
+  location = [] as number[];
+  type = '';
+  address = '';
+  cityname = '';
+}
+
+export class MapApi extends MapServerRequestModule<DataModel> {
+
+  constructor() {
+    super();
+  }
+
+
+  searchPoi(name: string, page = 1, size = 10, querys?: QueryParams) {
+    return this.get(`/v3/place/text?keywords=${encodeURIComponent(name)}&citylimit=true&offset=${size}&page=${page}`, `搜索POI ${name} 第${page}页`, {
+      ...querys,
+    })
+      .then(res => transformArrayDataModel(Poi, res.data2 ?? [], ''))
+      .catch(e => { throw e });
+  }
+  regeo(lat: number, lng: number, querys?: QueryParams) {
+    return this.get(`/v3/geocode/regeo`, `查询经纬度(${lat}, ${lng})所属区县`, {
+      location: `${lng},${lat}`,
+      ...querys,
+    })
+      .then(res => (res.data as any).regeocode.addressComponent as {
+        country: string,
+        province: string,
+        city: string,
+        district: string,
+        adcode: string,
+        township: string,
+        citycode: string,
+        towncode: string,
+      })
+      .catch(e => { throw e });
+  }
+  loadCityData() {
+    return new Promise((resolve, reject) => {
+      uni.request({
+        url: 'https://mn.wenlvti.net/app_static/xiangan/city-data.json',
+        method: 'GET',
+        success(result) {
+          if (result.statusCode === 200) {
+            resolve(result.data);
+          } else {
+            reject(new Error(`请求失败,状态码:${result.statusCode}`));
+          }
+        },
+        fail(error) {
+          reject(error);
+        }
+      })
+    });
+  }
+}
+
+export default new MapApi();

+ 13 - 0
src/common/components/dynamicf/ComponentConfigs.ts

@@ -0,0 +1,13 @@
+import MapApi from "@/api/map/MapApi";
+import type { FormItemComponentAdditionalDefine } from "@/components/dynamic";
+
+export default [
+  {
+    name: 'select-city',
+    needArrow: true,
+    props: {
+      loadCityData: () => MapApi.loadCityData(),
+      loadDistrictInfo: (latlon: [number, number]) => MapApi.regeo(latlon[0], latlon[1]),
+    },
+  },
+] as FormItemComponentAdditionalDefine[];

+ 1 - 0
src/common/config/ApiCofig.ts

@@ -5,5 +5,6 @@
 export default {
   serverDev: 'https://mn.wenlvti.net/api',
   serverProd: 'https://mn.wenlvti.net/api',
+  amapServerKey: '8fd09264c33678141f609588c432df0e',
   mainBodyId: 2,
 }

+ 7 - 6
src/components/basic/Button.vue

@@ -1,18 +1,18 @@
 <template>
-  <FlexView
-    :pressedColor="type === 'custom' ? themeContext.resolveThemeColor(pressedColor) :
-      (themeContext.resolveThemeColor((plain || type === 'text' || type === 'default') ? 'pressed.notice' : 'pressed.' + type))"
+  <Touchable
     :innerStyle="{
       ...currentStyle.style,
       ...innerStyle,
     }"
     :innerClass="['nana-button', props.block ? 'nana-button-block' : 'nana-button-auto']"
-    :touchable="touchable && !loading"
     center
     direction="row"
     v-bind="viewProps"
+    :pressedColor="type === 'custom' ? themeContext.resolveThemeColor(pressedColor) :
+      (themeContext.resolveThemeColor((plain || type === 'text' || type === 'default') ? 'pressed.notice' : 'pressed.' + type))"
+    :touchable="touchable && !loading"
     @state="(v) => state = v"
-    @click="emit('click')"
+    @click="emit('click', $event)"
   >
     <slot name="leftIcon">
       <ActivityIndicator 
@@ -55,7 +55,7 @@
         v-bind="rightIconProps"
       />
     </slot>
-  </FlexView>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
@@ -67,6 +67,7 @@ import Text from './Text.vue';
 import FlexView, { type FlexProps } from '../layout/FlexView.vue';
 import ActivityIndicator from './ActivityIndicator.vue';
 import Icon from './Icon.vue';
+import Touchable from '../feedback/Touchable.vue';
 
 export type ButtomType = 'default'|'primary'|'success'|'warning'|'danger'|'custom'|'text';
 export type ButtomSizeType = 'small'|'medium'|'large'|'larger'|'mini';

+ 5 - 5
src/components/basic/Cell.vue

@@ -1,5 +1,5 @@
 <template>
-  <FlexView
+  <Touchable
     direction="row"
     :touchable="touchable || Boolean($attrs['onClick'])"
     :pressedColor="pressedColor"
@@ -76,20 +76,20 @@
         </slot>
       </FlexRow>
     </slot>
-  </FlexView>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
 import { computed, provide } from 'vue';
 import { propGetThemeVar, useTheme, type ThemePaddingMargin } from '../theme/ThemeDefine';
-import type { IconProps } from './Icon.vue';
+import { CellContextKey, type CellContext } from './CellContext';
 import { configPadding } from '../theme/ThemeTools';
+import type { IconProps } from './Icon.vue';
 import FlexRow from '../layout/FlexRow.vue';
 import FlexCol from '../layout/FlexCol.vue';
 import Text from './Text.vue';
 import Icon from './Icon.vue';
-import FlexView from '../layout/FlexView.vue';
-import { CellContextKey, type CellContext } from './CellContext';
+import Touchable from '../feedback/Touchable.vue';
 
 export interface CellProp {
   /**

+ 4 - 4
src/components/basic/IconButton.vue

@@ -1,5 +1,5 @@
 <template>
-  <FlexView
+  <Touchable
     :pressedColor="pressedBackgroundColor"
     :innerStyle="style"
     touchable
@@ -8,17 +8,17 @@
   >
     <Icon v-if="icon" v-bind="props" />
     <slot />
-  </FlexView>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
 import { computed } from 'vue';
 import { propGetThemeVar, useTheme, type ViewStyle } from '../theme/ThemeDefine';
-import FlexView from '../layout/FlexView.vue';
+import { selectStyleType } from '../theme/ThemeTools';
 import type { IconProps } from './Icon.vue';
 import type { ImageButtonShapeType } from './ImageButton.vue';
-import { selectStyleType } from '../theme/ThemeTools';
 import Icon from './Icon.vue';
+import Touchable from '../feedback/Touchable.vue';
 
 export interface IconButtonProps extends IconProps {
   /**

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

@@ -143,7 +143,8 @@ function handleClick() {
       urls: [ props.src ],
     })
   }
-  emit('click');
+  if (props.touchable)
+    emit('click');
 }
 function loadSrcState() {
   if (props.src) {

+ 4 - 3
src/components/dialog/ActionSheetItem.vue

@@ -1,6 +1,7 @@
 <template>
-  <FlexCol 
+  <Touchable
     center
+    direction="column"
     :touchable="!disabled"
     :innerStyle="themeStyles.item.value"
     :pressedColor="themeContext.resolveThemeColor('ActionSheetItemPressedColor', 'pressed.white')"
@@ -16,11 +17,11 @@
       {{ name }}
     </text>
     <text v-if="subname" :style="themeStyles.itemSubTitle.value">{{subname}}</text>
-  </FlexCol>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
-import FlexCol from '../layout/FlexCol.vue';
+import Touchable from '../feedback/Touchable.vue';
 import { useTheme } from '../theme/ThemeDefine';
 import { DynamicColor, DynamicSize } from '../theme/ThemeTools';
 

+ 4 - 3
src/components/dialog/DialogButton.vue

@@ -1,11 +1,12 @@
 <template>
-  <FlexRow
+  <Touchable
     :innerStyle="{
       ...themeStyles.dialogButton.value,
       ...vertical ? {} : themeStyles.dialogButtonHorz.value,
     }"
     :pressedColor="themeContext.resolveThemeColor(pressedColor)"
     touchable
+    direction="row"
     @click="loading ? undefined : $emit('click')"
   >
     <ActivityIndicator 
@@ -22,12 +23,12 @@
     >
       {{props.text}}
     </text>
-  </FlexRow>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
 import ActivityIndicator from '../basic/ActivityIndicator.vue';
-import FlexRow from '../layout/FlexRow.vue';
+import Touchable from '../feedback/Touchable.vue';
 import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
 import { DynamicColor, DynamicSize } from '../theme/ThemeTools';
 

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

@@ -42,7 +42,9 @@
         backgroundColor: mask ? themeContext.resolveThemeColor(maskColor) : '',
         transitionDuration: `${duration}ms`,
       }"
-      @click="handleClose"
+      @mousedown.stop="handleClose"
+      @touchstart.stop="handleClose"
+      @click.stop="handleClose"
     >  
     </view>
     <view 
@@ -231,8 +233,10 @@ const props = withDefaults(defineProps<PopupProps>(), {
   size: '30%',
 });
 
+function handleClick(e: Event) {
+  e.stopPropagation();
+}
 function handleClose(e: Event) {
-  
   e.stopPropagation();
   if (props.closeable)
     doClose();

+ 6 - 5
src/components/display/NoticeBar.vue

@@ -1,6 +1,7 @@
 <template>
-  <FlexRow 
+  <Touchable
     touchable 
+    direction="row"
     :flexGrow="0" 
     :flexShrink="0"
     :innerStyle="{
@@ -31,19 +32,19 @@
       :color="textColor"
       @click="emit('close')"
     />
-  </FlexRow>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
 import { computed } from 'vue';
+import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
+import { DynamicSize, DynamicSize2 } from '../theme/ThemeTools';
 import type { IconProps } from '../basic/Icon.vue';
 import Icon from '../basic/Icon.vue';
 import IconButton from '../basic/IconButton.vue';
 import Text from '../basic/Text.vue';
-import FlexRow from '../layout/FlexRow.vue';
-import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
-import { DynamicSize, DynamicSize2 } from '../theme/ThemeTools';
 import HorizontalScrollText from '../typography/HorizontalScrollText.vue';
+import Touchable from '../feedback/Touchable.vue';
 
 export interface NoticeBarProps {
   /**

+ 4 - 2
src/components/display/Tag.vue

@@ -1,7 +1,8 @@
 <template>
-  <FlexRow 
+  <Touchable 
     center 
     innerClass="nana-tag"
+    direction="row"
     :innerStyle="{
       ...style,
       ...innerStyle,
@@ -25,7 +26,7 @@
         :color="(props.textColor || style.color as string)"
       />
     </FlexRow>
-  </FlexRow>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
@@ -34,6 +35,7 @@ import { propGetThemeVar, useTheme, type ViewStyle } from '../theme/ThemeDefine'
 import { DynamicColor, DynamicSize, DynamicSize2, selectStyleType } from '../theme/ThemeTools';
 import FlexRow from '../layout/FlexRow.vue';
 import Icon from '../basic/Icon.vue';
+import Touchable from '../feedback/Touchable.vue';
 
 export type TagTypes = 'default'|'primary'|'success'|'warning'|'danger';
 

+ 0 - 15
src/components/display/block/BackgroundBox.vue

@@ -5,12 +5,6 @@
     :flexShrink="0"
     v-bind="$props"
     :innerStyle="style" 
-    :touchable="touchable"
-    :class="[
-      touchable ? 'nana-press-small' : '',
-    ]"
-    @click="emit('click')"
-    @state="(s) => emit('state', s)"
   >
     <slot />
   </FlexView>
@@ -86,13 +80,6 @@ const props = defineProps({
     default: undefined
   },
   /**
-   * 背景是否可点击。
-   */
-  touchable: {
-    type: Boolean,
-    default: false
-  },
-  /**
    * 背景渐变角度。
    * 只有 color1 和 color2 都定义时有效。
    *
@@ -198,8 +185,6 @@ const style = computed(() => {
   }
   return o;
 })
-
-const emit = defineEmits([ 'click', 'state' ])
 </script>
 
 <style lang="scss">

+ 3 - 5
src/components/display/block/ImageBlock.vue

@@ -1,5 +1,5 @@
 <template>
-  <FlexView
+  <Touchable
     touchable
     position="relative"
     overflow="hidden"
@@ -34,7 +34,7 @@
         <text class="nana-image-desc">{{ desc }}</text>
       </slot>
     </BackgroundBox>
-  </FlexView>
+  </Touchable>
 </template>
 
 <script lang="ts">
@@ -50,11 +50,9 @@ export default {}
 
 <script setup lang="ts">
 import { useTheme } from '@/components/theme/ThemeDefine';
-import FlexView from '../../layout/FlexView.vue';
-import Image from '../../basic/Image.vue';
-import Text from '../../basic/Text.vue';
 import BackgroundBox from './BackgroundBox.vue';
 import VideoMark from '/static/images/VideoMark.png';
+import Touchable from '@/components/feedback/Touchable.vue';
 
 const theme = useTheme();
 

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

@@ -1,5 +1,5 @@
 <template>
-  <FlexView
+  <Touchable
     touchable
     backgroundColor="white"
     overflow="hidden"
@@ -18,7 +18,7 @@
     <slot name="desc">
       <Text class="nana-image-desc">{{ desc }}</Text>
     </slot>
-  </FlexView>
+  </Touchable>
 </template>
 
 <script lang="ts">
@@ -34,9 +34,9 @@ export default {}
 
 <script setup lang="ts">
 import { useTheme } from '@/components/theme/ThemeDefine';
-import FlexView from '../../layout/FlexView.vue';
 import Image from '../../basic/Image.vue';
 import Text from '../../basic/Text.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
 
 const theme = useTheme();
 

+ 3 - 2
src/components/display/block/ImageBlock3.vue

@@ -1,5 +1,5 @@
 <template>
-  <FlexView
+  <Touchable
     touchable
     backgroundColor="white"
     direction="row"
@@ -22,7 +22,7 @@
         <Text class="nana-image-desc">{{ desc }}</Text>
       </slot>
     </FlexView>
-  </FlexView>
+  </Touchable>
 </template>
 
 <script lang="ts">
@@ -41,6 +41,7 @@ import { useTheme } from '@/components/theme/ThemeDefine';
 import FlexView from '../../layout/FlexView.vue';
 import Image from '../../basic/Image.vue';
 import Text from '../../basic/Text.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
 
 const theme = useTheme();
 

+ 4 - 3
src/components/display/block/TextBlock.vue

@@ -14,6 +14,7 @@ import FlexCol from '../../layout/FlexCol.vue';
 import FlexRow from '../../layout/FlexRow.vue';
 import Text, { type TextProps } from '../../basic/Text.vue';
 import type { PropType } from 'vue';
+import Touchable from '@/components/feedback/Touchable.vue';
 
 defineEmits([ 'click' ])
 defineProps({	
@@ -131,10 +132,11 @@ defineProps({
 </script>
 
 <template>
-  <FlexRow
+  <Touchable
     :touchable="touchable"
     justify="space-between"
     align="center"
+    direction="row"
     v-bind="viewProps"
     @click="$emit('click')"
   >
@@ -176,8 +178,7 @@ defineProps({
     <slot name="suffix">
       <Text class="nana-text-suffix" v-bind="suffixProps">{{ suffix }}</Text>
     </slot>
-  </FlexRow>
-
+  </Touchable>
 </template>
 
 <style lang="scss">

+ 4 - 4
src/components/display/block/TextLeftRightBlock.vue

@@ -10,10 +10,9 @@ export default {}
 </script>
 
 <script setup lang="ts">
-import FlexCol from '../../layout/FlexCol.vue';
-import FlexRow from '../../layout/FlexRow.vue';
 import Text, { type TextProps } from '../../basic/Text.vue';
 import type { PropType } from 'vue';
+import Touchable from '@/components/feedback/Touchable.vue';
 
 defineEmits([ 'click' ])
 defineProps({	
@@ -100,11 +99,12 @@ defineProps({
 </script>
 
 <template>
-  <FlexRow
+  <Touchable
     :touchable="touchable"
     justify="space-between"
     align="flex-start"
     width="fill"
+    direction="row"
     v-bind="viewProps"
     @click="$emit('click')"
   >
@@ -127,7 +127,7 @@ defineProps({
         :text="text2 || text2Empty"
       />
     </slot>
-  </FlexRow>
+  </Touchable>
 
 </template>
 

+ 3 - 2
src/components/display/title/SubTitle.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import Icon from '@/components/basic/Icon.vue';
 import Text from '@/components/basic/Text.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Width from '@/components/layout/space/Width.vue';
 import { useTheme } from '@/components/theme/ThemeDefine';
@@ -48,11 +49,11 @@ const badgeStyle = theme.useThemeStyle({
       </FlexRow>
     </slot>
     <slot name="right">
-      <FlexRow v-if="showMore" align="center" touchable @click="$emit('moreClicked')">
+      <Touchable v-if="showMore" align="center" touchable @click="$emit('moreClicked')">
         <Text fontConfig="subText" :text="moreText" />
         <Width :size="10" />
         <Icon icon="arrow-right" :size="26" />
-      </FlexRow>
+      </Touchable>
     </slot>
   </FlexRow>
 </template>

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

@@ -139,6 +139,7 @@ onMounted(() => {
 });
 
 defineExpose<FormExport>({
+  getFormModel: () => formModel.value,
   getFormRef: () => formRef.value,
   getFormData: () => formModel.value,
   initFormData,

+ 31 - 12
src/components/dynamic/DynamicFormControl.vue

@@ -36,7 +36,11 @@
       :label="label"
       :name="formDefineItem.fullName"
       :required="Boolean(formDefineItem.rules?.length)"
-      v-bind="formDefineItem.itemParams"
+      :showRightArrow="extraDefine?.needArrow"
+      v-bind="{ 
+        ...extraDefine?.itemProps || {},
+        ...formDefineItem.itemParams,
+      }"
     >
       <!-- <text>fullName: {{formDefineItem.fullName}}</text> -->
       <template v-if="formDefineItem.type === 'number'">
@@ -63,6 +67,14 @@
           v-bind="(params as any as RadioValueProps)"
         />
       </template>
+      <template v-else-if="formDefineItem.type === 'radio-id'">
+        <RadioIdField
+          ref="itemRef"
+          :modelValue="modelValue"
+          @update:modelValue="onValueChanged"
+          v-bind="(params as any as RadioIdFieldProps)"
+        />
+      </template>
       <template v-else-if="formDefineItem.type === 'select'">
         <view>
           <NaPickerField 
@@ -160,15 +172,8 @@
           />
         </view>
       </template>
-      <!-- More components can be added here... -->
-      <template v-else-if="formDefineItem.type === 'select-city'">
-        <CityPicker
-          ref="itemRef"
-          :modelValue="modelValue"
-          @update:modelValue="onValueChanged"
-          v-bind="params"
-        />
-      </template>
+      <!-- 在下方添加自定义组件 -->
+      <!-- 业务代码开始 -->
       <template v-else-if="formDefineItem.type === 'richtext'">
         <RichTextEditor
           ref="itemRef"
@@ -177,6 +182,7 @@
           v-bind="params"
         />
       </template>
+      <!-- 业务代码结束 -->
       <template v-else>
         <text>Fallback: unknow form type {{ formDefineItem.type }}</text>
       </template>
@@ -185,7 +191,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, inject, onBeforeUnmount, onMounted, ref, type PropType } from 'vue';
+import { computed, inject, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue';
 import type { FormDefineItem, IFormItemCallback } from '.';
 import Field from '../form/Field.vue';
 import Stepper from '../form/Stepper.vue';
@@ -205,6 +211,13 @@ import TimePickerField from '../form/TimePickerField.vue';
 import DatePickerField from '../form/DatePickerField.vue';
 import RichTextEditor from '@/common/components/form/RichTextEditor.vue';
 import UploaderField, { type UploaderFieldProps } from '../form/UploaderField.vue';
+import RadioIdField from './wrappers/RadioIdField.vue';
+import type { RadioIdFieldProps } from './wrappers/RadioIdField';
+
+//自定义额外的组件默认配置,修改为自定义文件路径
+//业务代码开始
+import ComponentConfigs from '@/common/components/dynamicf/ComponentConfigs';
+//业务代码结束
 
 const props = defineProps({	
   parentModel: {
@@ -245,7 +258,13 @@ function evaluateCallbackObj(val: Record<string, unknown|IFormItemCallback<unkno
   return newObj;
 }
 
-const params = computed(() => evaluateCallbackObj(props.formDefineItem.params as any))
+const extraDefine = computed(() => ComponentConfigs.find((item) => item.name === props.formDefineItem.type))
+const params = computed(() => {
+  return {
+    ...extraDefine.value?.props || {},
+    ...evaluateCallbackObj(props.formDefineItem.params as any)
+  } as Record<string, unknown>
+})
 const label = computed(() => evaluateCallback(props.formDefineItem.label) as string)
 const show = computed(() => props.formDefineItem.show === undefined || evaluateCallback(props.formDefineItem.show))
 

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

@@ -1,5 +1,7 @@
 import type { RuleItem } from "async-validator";
 import type { FormInstance } from "../form/Form.vue";
+import type { FieldProps } from "../form/Field.vue";
+import type { Ref } from "vue";
 
 export interface FormDefine {
   /**
@@ -7,7 +9,16 @@ export interface FormDefine {
    */
   type?: 'flat'|'page'|'group',
   props?: any;
+  /**
+   * 表单属性嵌套类型
+   * * flat: 扁平属性,直接在表单对象上定义属性
+   * * nest: 嵌套属性,属性值为对象,对象下有子属性
+   * * array: 数组属性,属性值为数组,数组下有子属性
+   */
   propNestType?: 'flat'|'nest'|'array',
+  /**
+   * 子条目定义
+   */
   items: FormDefineItem[];
 }
 
@@ -33,6 +44,28 @@ export declare type IFormItemCallback<T> = {
 };
 export type IFormItemCallbackAdditionalProps<T> = { [P in keyof T]?: T[P]|IFormItemCallback<T[P]> }
 
+/**
+ * 表单动态组件属性定义
+ */
+export interface FormItemComponentAdditionalDefine {
+  /**
+   * 组件名称
+   */
+  name: string,
+  /**
+   * 是否需要显示右侧箭头
+   */
+  needArrow?: boolean,
+  /**
+   * 传递给表单条目的参数
+   */
+  itemProps?: FieldProps,
+  /**
+   * 传递给组件的参数
+   */
+  props?: Record<string, unknown>|unknown,
+}
+
 export interface FormDefineItem {
   /**
    * 表单项显示标签
@@ -105,6 +138,11 @@ export interface FormExport {
    */
   initFormData(data: () => any): void;
   /**
+   * 获取表单数据模型
+   * @returns 表单数据模型
+   */
+  getFormModel(): Ref<Record<string, any>>;
+  /**
    * 获取表单实例
    * @returns 表单实例
    */

+ 65 - 31
src/components/dynamic/wrappers/PickerCityField.vue

@@ -1,48 +1,82 @@
 <template>
-  <CascaderField
-    :modelValue="modelValue"
-    @update:modelValue="$emit('update:modelValue', $event)"
-    textKey="name"
-    valueKey="code"
-    childrenKey="children"
-    placeholder="请选择省市区" 
-    :data="(ChinaCityData.data.value as CascaderItem[]) || []"
-    v-bind="$attrs"
-  />
+  <FlexRow width="100%" justify="space-between" align="center">
+    <CascaderField
+      v-if="ChinaCityData.data.value"
+      ref="fieldRef"
+      :modelValue="modelValue"
+      @update:modelValue="$emit('update:modelValue', $event)"
+      textKey="name"
+      :valueKey="stringValue ? 'name' : 'code'"1
+      childrenKey="children"
+      placeholder="请选择省市区" 
+      :data="(ChinaCityData.data.value as CascaderItem[]) || []"
+      v-bind="$attrs"
+    />
+    <Button type="primary" size="mini" icon="map" @click="selectCityFromMap">地图选择</Button>
+  </FlexRow>
 </template>
 
 <script setup lang="ts">
+import { ref, type PropType } from 'vue';
 import { useDataLoader } from '@/components/composeabe/DataLoader';
 import type { CascaderItem } from '@/components/form/Cascader.vue';
+import Button from '@/components/basic/Button.vue';
 import CascaderField from '@/components/form/CascaderField.vue';
-import type { PropType } from 'vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
 
 const props = defineProps({
   modelValue: {
     type: Array as PropType<string[]>,
     default: () => [],
   },
-  cityDataUrl: {
-    type: String,
-    default: 'https://mn.wenlvti.net/app_static/xiangan/city-data.json',
-  }
+  loadCityData: {
+    type: Function as PropType<() => Promise<CascaderItem[]>>,
+    default: () => Promise.resolve([]),
+  },
+  loadDistrictInfo: {
+    type: Function as PropType<(latlon: [number,number]) => Promise<{
+      province: string,
+      city: string,
+      district: string,
+      township: string,
+      towncode: string,
+      adcode: string,
+    }>>,
+    default: null,
+  },
+  /**
+   * 是否返回字符串值,否则返回选中城市code
+   */
+  stringValue: {
+    type: Boolean,
+    default: true,
+  },
 })
-defineEmits(['update:modelValue'])
-
-const ChinaCityData = useDataLoader(() => {
-  return new Promise((resolve, reject) => {
-    uni.request({
-      url: props.cityDataUrl,
-      method: 'GET',
-      success: (res) => {
-        resolve(res.data)
-      },
-      fail: (err) => {
-        reject(err)
-      }
-    })
-  })
-}, {
+const emit = defineEmits(['update:modelValue', 'selectedTownship'])
+const ChinaCityData = useDataLoader(() => props.loadCityData(), {
   immediate: true,
 });
+
+const fieldRef = ref();
+
+function selectCityFromMap() {
+  uni.chooseLocation({
+    success: (res) => {
+      props.loadDistrictInfo([res.latitude, res.longitude]).then((info) => {
+        console.log(info);
+        emit('update:modelValue', props.stringValue ? 
+          [info.province, info.city, info.district] : 
+          [info.adcode ])
+        emit('selectedTownship', props.stringValue ? info.township : info.towncode, info.towncode)
+        setTimeout(() => fieldRef.value?.confirm(), 200);
+      })
+    }
+  })
+}
+
+defineOptions({
+  options: {
+    virtualHost: true,
+  }
+})
 </script>

+ 6 - 0
src/components/dynamic/wrappers/PickerIdField.vue

@@ -20,4 +20,10 @@ const loader = useDataLoader<PickerIdFieldOption[]>(async () => {
 }, {
   immediate: true,
 });
+
+defineExpose({
+  reload() {
+    loader.load();
+  }
+})
 </script>

+ 14 - 0
src/components/dynamic/wrappers/RadioIdField.ts

@@ -0,0 +1,14 @@
+import type { RadioBoxGroupProps } from "@/components/form/RadioGroup.vue";
+
+export interface RadioIdFieldOption {
+  text: string,
+  value: string|number,
+}
+
+export interface RadioIdFieldProps extends Omit<RadioBoxGroupProps, 'modelValue'> {
+  /**
+   * 加载选项数据
+   * @returns 
+   */
+  loadData: () => Promise<RadioIdFieldOption[]>;
+}

+ 33 - 0
src/components/dynamic/wrappers/RadioIdField.vue

@@ -0,0 +1,33 @@
+<template>
+  <RadioValue
+    :options="loader.data.value"
+    :modelValue="modelValue"
+    @update:modelValue="emits('update:modelValue', $event)"
+    :disabled="disabled"
+    v-bind="$attrs"
+  />
+</template>
+
+<script lang="ts" setup>
+import { ref, watch, onMounted } from 'vue';
+import RadioValue from './RadioValue.vue';
+import type { RadioIdFieldOption, RadioIdFieldProps } from './RadioIdField';
+import { useDataLoader } from '@/components/composeabe/DataLoader';
+
+const props = defineProps<RadioIdFieldProps & {
+  modelValue: number|string,
+}>();
+const loader = useDataLoader<RadioIdFieldOption[]>(async () => await props.loadData(), {
+  immediate: true,
+});
+const emits = defineEmits([
+  'update:modelValue',
+]);
+
+defineExpose({
+  reload() {
+    loader.load();
+  }
+})
+
+</script>

+ 3 - 2
src/components/dynamic/wrappers/RadioValue.vue

@@ -56,9 +56,10 @@ const emits = defineEmits([
 const selectValue = ref<string|null>('');
 
 function setRadioValue() {
-  selectValue.value = props.options.find(k => (k.value === props.modelValue))?.text || null;
+  const options = props.options || [];
+  selectValue.value = options.find(k => (k.value === props.modelValue))?.text || null;
   if (selectValue.value === null)
-    selectValue.value = props.options.find(k => (typeof k.value === typeof props.modelValue))?.text || null;
+    selectValue.value = options.find(k => (typeof k.value === typeof props.modelValue))?.text || null;
 }
 
 watch(() => props.modelValue, () => {

+ 126 - 0
src/components/feedback/Touchable.vue

@@ -0,0 +1,126 @@
+<template>
+  <view
+    :id="innerId ?? id"
+    :class="[
+      'nana-flex-layout',
+      innerClass,
+    ]"
+    :style="finalStyle"
+    @mouseenter="handleMouseEnter"
+    @mouseleave="handleMouseLeave"
+    @mousedown="handleTouchStart"
+    @mouseup="handleTouchEnd"
+    @touchstart="handleTouchStart"
+    @touchend="handleTouchEnd"
+    @click.native.stop="handleClick"
+  >
+    <slot></slot>
+  </view>
+</template>
+
+<script setup lang="ts">
+
+/**
+ * 组件说明:Flex组件,用于一些布局中快速写容器,是一系列盒子的基础组件。
+ */
+import { computed, getCurrentInstance, onMounted, ref } from 'vue';
+import { useTheme } from '../theme/ThemeDefine';
+import { RandomUtils } from '@imengyu/imengyu-utils';
+import { useBaseViewStyleBuilder } from '../layout/BaseView';
+import type { FlexProps } from '../layout/FlexView.vue';
+
+export interface TouchableFlexProps extends FlexProps {
+  /**
+   * 是否可以点击
+   */
+  touchable?: boolean,
+  /**
+   * 按下时的颜色
+   */
+  pressedColor?: string,
+  /**
+   * 按下时的透明度(仅在 pressedColor 未设置时有效)
+   */
+  activeOpacity?: number,
+}
+
+const props = withDefaults(defineProps<TouchableFlexProps>(), {
+  activeOpacity: 0.7,
+  touchable: false,
+});
+
+const themeContext = useTheme();
+const { commonStyle } = useBaseViewStyleBuilder(props);
+
+const finalStyle = computed(() => {
+  const obj : Record<string, any> = {};
+  if (props.pressedColor != undefined) { 
+    if (isPressed.value)
+      obj.backgroundColor = themeContext.resolveThemeColor(props.pressedColor);
+  } else if (props.activeOpacity != undefined) 
+    obj.opacity = isPressed.value ? props.activeOpacity : 1;
+  const o = {
+    ...commonStyle.value,
+    ...obj
+  } 
+  for (const key in o) {
+    if (o[key] === undefined)
+      delete o[key]; 
+  }
+  return o;
+})
+
+defineOptions({
+  options: {
+    styleIsolation: "shared",
+    virtualHost: true
+  }
+})
+const emit = defineEmits([ "click", "state" ]);
+const isPressed = ref(false)
+
+function handleTouchStart() {
+  if (props.touchable) {
+    isPressed.value = true; // 按下时改变状态
+    emit('state', 'active');
+  }
+}
+function handleMouseEnter() {
+  if (props.touchable)
+    emit('state', 'active')
+}
+function handleMouseLeave() {
+  if (props.touchable)
+    emit('state', 'default')
+}
+function handleClick(e: Event) {
+  if (props.touchable)
+    emit('click', e);
+}
+function handleTouchEnd() {
+  if (props.touchable) {
+    isPressed.value = false; // 释放时恢复状态
+    emit('state', 'default');
+  }
+}
+
+onMounted(() => {
+  emit('state', 'default')
+})
+    
+const instance = getCurrentInstance();
+const id = 'flex-item-' + RandomUtils.genNonDuplicateID(15);
+
+defineExpose({
+  measure() {
+    return new Promise((resolve) => {
+      const tabItem = uni.createSelectorQuery()
+        .in(instance)
+        .select(`#${id}`);
+      tabItem.boundingClientRect().exec((res) => {
+        resolve(res)
+      })
+    });
+  },
+})
+</script>

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

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

+ 4 - 3
src/components/form/CalendarItem.vue

@@ -1,11 +1,12 @@
 <template>
-  <FlexCol 
+  <Touchable
     position="relative"
     center width="14.28%"
     :height="height"
     :backgroundColor="itemColor"
     :padding="[ 0,0,10,0]"
     touchable
+    direction="column"
     @click="() => emit('click', props.date)"
     v-bind="calendarContext.itemProps.value"
   >
@@ -33,7 +34,7 @@
     >
       {{ finalBottomText || '&nbsp;' }}
     </Text>
-  </FlexCol>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
@@ -42,7 +43,7 @@ import { DateUtils } from '@imengyu/imengyu-utils';
 import type { TextProps } from '../basic/Text.vue';
 import type { CalendarContext } from './Calendar.vue';
 import Text from '../basic/Text.vue';
-import FlexCol from '../layout/FlexCol.vue';
+import Touchable from '../feedback/Touchable.vue';
 
 export interface CalendarItemProps {
   disabled?: boolean,

+ 2 - 1
src/components/form/Cascader.vue

@@ -241,8 +241,9 @@ function handleTabChange(v: number) {
   headerTabCurrent.value = v;
 }
 
-watch(() => props.modelValue, () => {
+watch(() => props.modelValue, (v) => {
   loadValue();
+  updateText(v);
 })
 onMounted(() => {
   loadValue();

+ 7 - 3
src/components/form/CascaderField.vue

@@ -13,9 +13,9 @@
       @close="onCancel"
     />
     <Cascader 
-      v-if="popupShow"
       v-bind="props"
-      v-model="tempValue"
+      :modelValue="tempValue"
+      @update:modelValue="(v:any) => tempValue = v"
       @selectTextChange="onSelectTextChange"
       @pickEnd="onPickEnd"
     />
@@ -26,7 +26,7 @@
   <Text 
     v-if="showSelectText"
     :size="30"
-    color="text.second"
+    :color="selectText ? 'text.content' : 'text.second'"
     :text="selectText || placeholder" 
     :maxWidth="300"
     v-bind="textProps"
@@ -130,6 +130,10 @@ function onPickEnd() {
     onConfirm();
 }
 
+defineExpose({
+  confirm: onConfirm,
+  cancel: onCancel,
+})
 defineOptions({
   options: {
     styleIsolation: "shared",

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

@@ -1,5 +1,5 @@
 <template>
-  <FlexRow
+  <Touchable
     :touchable="!disabled"
     :activeOpacity="activeOpacity"
     :innerStyle="{ 
@@ -8,6 +8,7 @@
     }"
     innerClass="nana-check"
     center
+    direction="row"
     @click.stop="switchOn"
   >
     <slot name="check" v-if="checkPosition === 'left'" icon="check" :on="value" :disabled="disabled" :shape="shape">
@@ -47,21 +48,20 @@
         :icon="props.icon"
       />
     </slot>
-  </FlexRow>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
 import { computed, inject, onMounted, toRef, watch } from 'vue';
+import { useCellContext } from '../basic/CellContext';
 import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
 import { useFieldChildValueInjector } from './FormContext';
+import { DynamicSize } from '../theme/ThemeTools';
 import { StringUtils } from '@imengyu/imengyu-utils';
 import type { CheckBoxGroupContextInfo } from './CheckBoxGroup.vue';
-import FlexRow from '../layout/FlexRow.vue';
 import CheckBoxDefaultButton from './CheckBoxDefaultButton.vue';
 import Text from '../basic/Text.vue';
-import { DynamicSize } from '../theme/ThemeTools';
-import { useCellContext } from '../basic/CellContext';
-
+import Touchable from '../feedback/Touchable.vue';
 
 export interface CheckBoxProps {
   /**

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

@@ -1,5 +1,5 @@
 <template>
-  <FlexView
+  <Touchable
     :touchable="touchable || childOnClickListener !== undefined"
     :pressedColor="themeContext.resolveThemeColor('FieldPressedColor', 'pressed.white')"
     :innerStyle="{ 
@@ -141,7 +141,7 @@
       v-bind="clearButtonProps"
       @click="onClear"
     />
-  </FlexView>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
@@ -155,6 +155,7 @@ import IconButton from '../basic/IconButton.vue';
 import FlexCol from '../layout/FlexCol.vue';
 import FlexRow from '../layout/FlexRow.vue';
 import FlexView from '../layout/FlexView.vue';
+import Touchable from '../feedback/Touchable.vue';
 
 
 export interface FieldInstance {

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

@@ -79,8 +79,8 @@ export function useFieldChildValueInjector<T>(
     return shadowRefValue.value
   });
 
-  watch(propsModelValue, () => {
-    shadowRefValue.value = propsModelValue.value;
+  watch(() => propsModelValue.value, (v) => {
+    shadowRefValue.value = v;    
   })
 
   function updateValue(newValue: T) {

+ 4 - 2
src/components/form/NumberInputBox.vue

@@ -1,5 +1,5 @@
 <template>
-  <FlexCol
+  <Touchable
     :innerStyle="{
       ...themeStyles.box.value,
       marginHorizontal: themeContext.resolveSize(gutter),
@@ -9,6 +9,7 @@
     }"
     :pressedColor="themeContext.resolveThemeColor('pressed.white')"
     :touchable="!disableKeyPad"
+    direction="column"
     @click="emit('click')"
   >
     <text 
@@ -23,7 +24,7 @@
       v-if="!value && active && showCursur" class="number-input-cursor"
       :style="themeStyles.inputCursor.value"
     />
-  </FlexCol>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
@@ -32,6 +33,7 @@ import { useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
 import { DynamicColor, DynamicSize, selectStyleType } from '../theme/ThemeTools';
 import FlexCol from '../layout/FlexCol.vue';
 import type { NumberInputBorderType } from './NumberInput.vue';
+import Touchable from '../feedback/Touchable.vue';
 
 export interface NumberInputBoxProps {
   value?: string,

+ 1 - 3
src/components/form/PickerUtils.ts

@@ -31,9 +31,7 @@ export function usePickerFieldTempStorageData<T>(
     emit('confirm', value.value);
   }
 
-  watch(value, (v) => {
-    tempValue.value = v;
-  });
+  watch(value, (v) => tempValue.value = v);
   watch(tempValue, (v) => {
     emit('tempValueChange', tempValue.value);
     if (shouldUpdateValueImmediately)

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

@@ -1,5 +1,5 @@
 <template>
-  <FlexRow
+  <Touchable
     :touchable="!disabled"
     :activeOpacity="activeOpacity"
     :innerStyle="{ 
@@ -8,6 +8,7 @@
     }"
     innerClass="nana-check"
     center
+    direction="row"
     @click.stop="switchOn"
   >
     <slot v-if="checkPosition === 'left'" icon="check" :on="value" :disabled="disabled" :shape="shape">
@@ -50,7 +51,7 @@
         :type="props.icon != 'check-mark' ? 'icon' : 'radio'"
       />
     </slot>
-  </FlexRow>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
@@ -58,11 +59,11 @@ import { computed, inject, onMounted } from 'vue';
 import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
 import { StringUtils } from '@imengyu/imengyu-utils';
 import { DynamicSize } from '../theme/ThemeTools';
-import FlexRow from '../layout/FlexRow.vue';
 import CheckBoxDefaultButton from './CheckBoxDefaultButton.vue';
 import Text from '../basic/Text.vue';
 import type { RadioBoxGroupContextInfo } from './RadioGroup.vue';
 import { useCellContext } from '../basic/CellContext';
+import Touchable from '../feedback/Touchable.vue';
 
 export interface RadioBoxProps {
   /**

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

@@ -1,5 +1,5 @@
 <template>
-  <FlexView 
+  <Touchable 
     touchable 
     overflow="hidden"
     :width="isListStyle ? undefined : itemSize.width"
@@ -66,7 +66,7 @@
           uploading: 'primary',
           success: 'success',
           fail: 'danger',
-        })" :value="item.progress" />
+        })" :value="item.progress || 0" />
       </FlexCol>
       <Icon v-if="item.state === 'success'" icon="select-bold" color="success" />
       <Icon v-else-if="item.state === 'fail'" icon="close-bold" color="danger" />
@@ -74,7 +74,7 @@
       <Width :size="20" />
       <IconButton v-if="showDelete" icon="trash" @click.stop="emit('delete')" />
     </FlexRow>
-  </FlexView>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
@@ -91,8 +91,6 @@ import FlexCol from '../layout/FlexCol.vue';
 import FlexView from '../layout/FlexView.vue';
 import FlexRow from '../layout/FlexRow.vue';
 import Progress from '../display/Progress.vue';
-import type { UploaderItem } from './Uploader.vue';
-
 import IconApk from '../images/files/apk.png';
 import IconAudio from '../images/files/audio.png';
 import IconDefault from '../images/files/default.png';
@@ -105,6 +103,8 @@ import IconZip from '../images/files/zip.png';
 import IconPdf from '../images/files/pdf.png';
 import Width from '../layout/space/Width.vue';
 import Height from '../layout/space/Height.vue';
+import Touchable from '../feedback/Touchable.vue';
+import type { UploaderItem } from './Uploader';
 
 export interface UploaderListItemProps {
   item: UploaderItem;

+ 5 - 4
src/components/keyboard/NumberKeyBoardKey.vue

@@ -1,14 +1,15 @@
 <template>
-  <FlexCol
+  <Touchable
     v-if="text"
     :innerStyle="keyStyle"
     :pressedColor="action === 'finish' ? context.keyFinishPressedColor.value : context.keyPressedColor.value"
     touchable
+    direction="column"
     @click="emit('click', action, text)"
   >
     <Icon v-if="icon" :icon="text" :innerStyle="context.keyTextStyle.value" />
     <Text v-else :innerStyle="action === 'finish' ? context.keyTextStyleFinish.value : context.keyTextStyle.value">{{ text }}</Text>
-  </FlexCol>
+  </Touchable>
   <view
     v-else
     :style="keyStyle"
@@ -20,9 +21,9 @@ import { computed, inject } from 'vue';
 import { useTheme, type ViewStyle } from '../theme/ThemeDefine';
 import { DynamicSize } from '../theme/ThemeTools';
 import type { NumberKeyBoardContext } from './NumberKeyBoardInner.vue';
-import Text from '../basic/Text.vue';
-import FlexCol from '../layout/FlexCol.vue';
+import Text from '../basic/Text.vue';;
 import Icon from '../basic/Icon.vue';
+import Touchable from '../feedback/Touchable.vue';
 
 export interface NumberKeyBoardKeyProps {
   text: string;

+ 4 - 2
src/components/keyboard/PlateKeyBoardKey.vue

@@ -1,6 +1,7 @@
 <template>
-  <FlexCol
+  <Touchable
     v-if="text"
+    direction="column"
     :innerStyle="keyStyle"
     :pressedColor="context.keyPressedColor.value"
     :touchable="text!==''"
@@ -8,7 +9,7 @@
   >
     <Icon v-if="icon" :icon="text" :innerStyle="context.keyTextStyle.value" />
     <Text v-else :innerStyle="context.keyTextStyle.value">{{ text }}</Text>
-  </FlexCol>
+  </Touchable>
   <view
     v-else
     :style="keyStyle"
@@ -23,6 +24,7 @@ import type { PlateKeyBoardContext } from './PlateKeyBoardInner.vue';
 import FlexCol from '../layout/FlexCol.vue';
 import Text from '../basic/Text.vue';
 import Icon from '../basic/Icon.vue';
+import Touchable from '../feedback/Touchable.vue';
 
 export interface PlateKeyBoardKeyProps {
   text: string, 

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

@@ -0,0 +1,77 @@
+import { computed } from "vue";
+import { useTheme } from "../theme/ThemeDefine";
+import type { FlexProps } from "./FlexView.vue";
+import { configMargin, configPadding } from "../theme/ThemeTools";
+
+export function useBaseViewStyleBuilder(props: FlexProps) {
+  
+  const themeContext = useTheme();
+  const commonStyle = computed(() => {
+    const obj : Record<string, any> = {
+      flexDirection: props.direction,
+      flexBasis: props.flexBasis,
+      flexGrow: props.flexGrow,
+      flexShrink: props.flexShrink,
+      justifyContent: props.center ? (props.justify || 'center') : props.justify,
+      alignItems: props.center ? (props.align || 'center') : props.align,
+      position: props.position,
+      alignSelf: props.alignSelf,
+      flexWrap: props.wrap ? 'wrap' : 'nowrap',
+      backgroundColor: themeContext.resolveThemeColor(props.backgroundColor),
+      width: themeContext.resolveThemeSize(props.width),
+      height: themeContext.resolveThemeSize(props.height),
+      gap: themeContext.resolveThemeSize(props.gap),
+      borderRadius: themeContext.resolveThemeSize(props.radius),
+      overflow: props.overflow,
+      ...(props.innerStyle ? props.innerStyle : {}),
+    }
+    
+    //内边距样式
+    configPadding(obj, themeContext.theme, props.padding as any);
+    //外边距样式
+    configMargin(obj, themeContext.theme, props.margin as any);
+
+    if (obj.paddingVertical) {
+      if (obj.paddingTop === undefined)
+        obj.paddingTop = obj.paddingVertical;
+      if (obj.paddingBottom === undefined)
+        obj.paddingBottom = obj.paddingVertical;
+      obj.paddingVertical = undefined;
+    }
+    if (obj.paddingHorizontal) {
+      if (obj.paddingLeft === undefined)
+        obj.paddingLeft = obj.paddingHorizontal;
+      if (obj.paddingRight === undefined)
+        obj.paddingRight = obj.paddingHorizontal;
+      obj.paddingHorizontal = undefined;
+    }
+    if (obj.marginVertical) {
+      obj.marginTop = obj.marginVertical;
+      obj.marginBottom = obj.marginVertical;
+      obj.marginVertical = undefined;
+    }
+    if (obj.marginHorizontal) {
+      obj.marginLeft = obj.marginHorizontal;
+      obj.marginRight = obj.marginHorizontal;
+      obj.marginHorizontal = undefined;
+    }
+
+    //绝对距样式
+    if (typeof props.left !== 'undefined')
+      obj.left = themeContext.resolveThemeSize(props.left);
+    if (typeof props.right !== 'undefined')
+      obj.right = themeContext.resolveThemeSize(props.right);
+    if (typeof props.top !== 'undefined')
+      obj.top = themeContext.resolveThemeSize(props.top);
+    if (typeof props.bottom !== 'undefined')
+      obj.bottom = themeContext.resolveThemeSize(props.bottom);
+    if (typeof props.flex !== 'undefined')
+      obj.flex = props.flex;
+
+    return obj
+  });
+
+  return {
+    commonStyle
+  }
+}

+ 4 - 128
src/components/layout/FlexView.vue

@@ -9,15 +9,8 @@
       innerClass,
     ]"
     :style="finalStyle"
-    @mouseenter="handleMouseEnter"
-    @mouseleave="handleMouseLeave"
-    @mousedown="handleTouchStart"
-    @mouseup="handleTouchEnd"
-    @touchstart="handleTouchStart"
-    @touchend="handleTouchEnd"
-    @click="handleClick"
   >
-    <slot></slot>
+    <slot />
   </view>
 </template>
 
@@ -30,6 +23,7 @@ import { computed, getCurrentInstance, onMounted, ref } from 'vue';
 import { useTheme, type ThemePaddingMargin } from '../theme/ThemeDefine';
 import { configMargin, configPadding } from '../theme/ThemeTools';
 import { RandomUtils } from '@imengyu/imengyu-utils';
+import { useBaseViewStyleBuilder } from './BaseView';
 
 export type FlexDirection = "row"|"column"|'row-reverse'|'column-reverse';
 export type FlexJustifyType =  'flex-start' | 'flex-end' | 'center' |'space-between' |'space-around' |'space-evenly';
@@ -114,22 +108,10 @@ export interface FlexProps {
    */
   gap?: number|string,
   /**
-   * 是否可以点击
-   */
-  touchable?: boolean,
-  /**
    * 背景颜色
    */
   backgroundColor?: string,
   /**
-   * 按下时的颜色
-   */
-  pressedColor?: string,
-  /**
-   * 按下时的透明度(仅在 pressedColor 未设置时有效)
-   */
-  activeOpacity?: number,
-  /**
    * 宽度
    */
   width?: number|string,
@@ -137,90 +119,16 @@ export interface FlexProps {
    * 高度
    */
   height?: number|string,
-
   overflow?: 'visible'|'hidden'|'scroll'|'auto'
 }
 
 const props = withDefaults(defineProps<FlexProps>(), {
   direction: "column",
   backgroundColor: '',
-  activeOpacity: 0.7,
-  touchable: false,
-});
-
-const themeContext = useTheme();
-
-const commonStyle = computed(() => {
-  const obj : Record<string, any> = {
-    flexDirection: props.direction,
-    flexBasis: props.flexBasis,
-    flexGrow: props.flexGrow,
-    flexShrink: props.flexShrink,
-    justifyContent: props.center ? (props.justify || 'center') : props.justify,
-    alignItems: props.center ? (props.align || 'center') : props.align,
-    position: props.position,
-    alignSelf: props.alignSelf,
-    flexWrap: props.wrap ? 'wrap' : 'nowrap',
-    backgroundColor: themeContext.resolveThemeColor(props.backgroundColor),
-    width: themeContext.resolveThemeSize(props.width),
-    height: themeContext.resolveThemeSize(props.height),
-    gap: themeContext.resolveThemeSize(props.gap),
-    borderRadius: themeContext.resolveThemeSize(props.radius),
-    overflow: props.overflow,
-    ...(props.innerStyle ? props.innerStyle : {}),
-  }
-  
-  //内边距样式
-  configPadding(obj, themeContext.theme, props.padding as any);
-  //外边距样式
-  configMargin(obj, themeContext.theme, props.margin as any);
-
-  if (obj.paddingVertical) {
-    if (obj.paddingTop === undefined)
-      obj.paddingTop = obj.paddingVertical;
-    if (obj.paddingBottom === undefined)
-      obj.paddingBottom = obj.paddingVertical;
-    obj.paddingVertical = undefined;
-  }
-  if (obj.paddingHorizontal) {
-    if (obj.paddingLeft === undefined)
-      obj.paddingLeft = obj.paddingHorizontal;
-    if (obj.paddingRight === undefined)
-      obj.paddingRight = obj.paddingHorizontal;
-    obj.paddingHorizontal = undefined;
-  }
-  if (obj.marginVertical) {
-    obj.marginTop = obj.marginVertical;
-    obj.marginBottom = obj.marginVertical;
-    obj.marginVertical = undefined;
-  }
-  if (obj.marginHorizontal) {
-    obj.marginLeft = obj.marginHorizontal;
-    obj.marginRight = obj.marginHorizontal;
-    obj.marginHorizontal = undefined;
-  }
-
-  //绝对距样式
-  if (typeof props.left !== 'undefined')
-    obj.left = themeContext.resolveThemeSize(props.left);
-  if (typeof props.right !== 'undefined')
-    obj.right = themeContext.resolveThemeSize(props.right);
-  if (typeof props.top !== 'undefined')
-    obj.top = themeContext.resolveThemeSize(props.top);
-  if (typeof props.bottom !== 'undefined')
-    obj.bottom = themeContext.resolveThemeSize(props.bottom);
-  if (typeof props.flex !== 'undefined')
-    obj.flex = props.flex;
-
-  return obj
 });
+const { commonStyle } = useBaseViewStyleBuilder(props);
 const finalStyle = computed(() => {
   const obj : Record<string, any> = {};
-  if (props.pressedColor != undefined) { 
-    if (isPressed.value)
-      obj.backgroundColor = themeContext.resolveThemeColor(props.pressedColor);
-  } else if (props.activeOpacity != undefined) 
-    obj.opacity = isPressed.value ? props.activeOpacity : 1;
   const o = {
     ...commonStyle.value,
     ...obj
@@ -237,39 +145,7 @@ defineOptions({
     styleIsolation: "shared",
     virtualHost: true
   }
-})
-const emit = defineEmits([ "click", "state" ]);
-const isPressed = ref(false)
-
-function handleTouchStart() {
-  if (props.touchable) {
-    isPressed.value = true; // 按下时改变状态
-    emit('state', 'active');
-  }
-}
-function handleMouseEnter() {
-  if (props.touchable)
-    emit('state', 'active')
-}
-function handleMouseLeave() {
-  if (props.touchable)
-    emit('state', 'default')
-}
-function handleClick(e: Event) {
-  if (props.touchable)
-    emit('click', e);
-}
-function handleTouchEnd() {
-  if (props.touchable) {
-    isPressed.value = false; // 释放时恢复状态
-    emit('state', 'default');
-  }
-}
-
-onMounted(() => {
-  emit('state', 'default')
-})
-    
+});
 const instance = getCurrentInstance();
 const id = 'flex-item-' + RandomUtils.genNonDuplicateID(15);
 

+ 4 - 3
src/components/layout/grid/GridItem.vue

@@ -1,5 +1,5 @@
 <template>
-  <FlexView
+  <Touchable
     center
     :direction="(flexDirection as FlexDirection)"
     :innerStyle="style"
@@ -35,16 +35,17 @@
         :text="title"
       />
     </slot>
-  </FlexView>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
 import type { IconProps } from '@/components/basic/Icon.vue';
 import { propGetThemeVar, useTheme } from '@/components/theme/ThemeDefine';
 import { computed, inject } from 'vue';
-import FlexView, { type FlexDirection } from '../FlexView.vue';
+import { type FlexDirection } from '../FlexView.vue';
 import Text from '@/components/basic/Text.vue';
 import Icon from '@/components/basic/Icon.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
 
 /**
  * 网格块按钮。包含一个图标和文字。

+ 4 - 3
src/components/list/SimpleListItem.vue

@@ -1,9 +1,10 @@
 <template>
-  <FlexRow
+  <Touchable
     :id="id"
     :innerStyle="checked ? context.checkedItemStyle.value : context.itemStyle.value"
     :pressedColor="context.pressedColor.value"
     :touchable="!disabledProp || !item[disabledProp]"
+    direction="row"
     @click="emit('click')"
   >
     <!--TODO: 在uniapp插槽问题修复后,此处可修改为插槽默认值-->
@@ -25,13 +26,13 @@
       v-bind="context.checkProps.value"
       :on="checked"
     />
-  </FlexRow>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
 import { inject } from 'vue';
-import FlexRow from '../layout/FlexRow.vue';
 import CheckBoxDefaultButton from '../form/CheckBoxDefaultButton.vue';
+import Touchable from '../feedback/Touchable.vue';
 import type { SimpleListContext } from './SimpleList.vue';
 
 

+ 3 - 3
src/components/nav/PaginationItem.vue

@@ -2,7 +2,7 @@
 import { computed, ref } from 'vue';
 import { useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
 import { DynamicSize } from '../theme/ThemeTools';
-import FlexView from '../layout/FlexView.vue';
+import Touchable from '../feedback/Touchable.vue';
 
 const themeContext = useTheme();
 const themeStyles = themeContext.useThemeStyles({
@@ -41,7 +41,7 @@ const pressed = computed(() => state.value === 'active');
 </script>
 
 <template>
-  <FlexView
+  <Touchable
     :pressedColor="themeContext.resolveThemeColor(pressedColor)"
     :innerStyle="{   
       backgroundColor: themeContext.resolveThemeColor(active ? activeColor : (touchable ? deactiveColor : pressedColor)),
@@ -61,5 +61,5 @@ const pressed = computed(() => state.value === 'active');
       ...textStyle,
       color: themeContext.resolveThemeColor(touchable ? (pressed ? pressedTextColor : (active ? activeTextColor : deactiveTextColor)) : pressedTextColor),
     }">{{ text}}</text>
-  </FlexView>
+  </Touchable>
 </template>

+ 3 - 3
src/components/nav/SegmentedControlItem.vue

@@ -1,5 +1,5 @@
 <template>
-  <FlexView
+  <Touchable
     :innerStyle="{
       ...itemStyle,
       ...disabled ? { opacity: 0.5 } : {},
@@ -24,11 +24,11 @@
       ...itemTextStyle,
       color: active ? activeTextColor : activeColor,
     }">{{ label }}</Text>
-  </FlexView>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
-import FlexView from '../layout/FlexView.vue';
+import Touchable from '../feedback/Touchable.vue';
 import type { TextStyle, ViewStyle } from '../theme/ThemeDefine';
 
 export interface SegmentedControlItemProps {

+ 3 - 3
src/components/nav/SideBarItem.vue

@@ -1,5 +1,5 @@
 <template>
-  <FlexView
+  <Touchable
     :activeOpacity="touchable ? 0.8 : 0.4" 
     :innerStyle="{
       ...themeStyles.sideItem.value,
@@ -27,7 +27,7 @@
         <Text v-bind="textProps">{{props.text}}</Text>
       </slot>
     </Badge>
-  </FlexView>
+  </Touchable>
 </template>
 
 
@@ -37,9 +37,9 @@ import type { BadgeProps } from '../display/Badge.vue';
 import { useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
 import type { SideBarContext } from './SideBar.vue';
 import { DynamicColor, DynamicSize } from '../theme/ThemeTools';
-import FlexView from '../layout/FlexView.vue';
 import Badge from '../display/Badge.vue';
 import Text from '../basic/Text.vue';
+import Touchable from '../feedback/Touchable.vue';
 
 
 export interface SideBarItemProps {

+ 3 - 3
src/components/nav/TabBarItem.vue

@@ -1,5 +1,5 @@
 <template>
-  <FlexView
+  <Touchable
     center
     direction="column"
     :innerStyle="{
@@ -41,7 +41,7 @@
         :color="color"
       /> 
     </slot>
-  </FlexView>
+  </Touchable>
 </template>
 
 <script setup lang="ts">
@@ -49,10 +49,10 @@ import type { IconProps } from '@/components/basic/Icon.vue';
 import { useTheme, type TextStyle } from '@/components/theme/ThemeDefine';
 import { computed, inject, onMounted, onUpdated, ref } from 'vue';
 import { DynamicSize } from '../theme/ThemeTools';
-import FlexView from '../layout/FlexView.vue';
 import Text from '@/components/basic/Text.vue';
 import Icon from '@/components/basic/Icon.vue';
 import Badge, { type BadgeProps } from '../display/Badge.vue';
+import Touchable from '../feedback/Touchable.vue';
 
 export interface TabBarItemProps {
   /**

+ 5 - 8
src/components/nav/Tabs.vue

@@ -17,7 +17,7 @@
         height: theme.resolveSize(props.height),
       }"
     >
-      <FlexView
+      <Touchable
         v-for="(tab, index) in filteredTabs"
         :ref="(ref) => tabsRefs[index] = ref"
         :key="index"
@@ -55,7 +55,7 @@
             </text>
           </Badge>
         </slot>
-      </FlexView>
+      </Touchable>
 
       <view 
 		    v-if="showIndicator"
@@ -75,14 +75,11 @@
 </template>
 
 <script setup lang="ts">
-import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue';
+import { computed, nextTick, onMounted, ref, watch } from 'vue';
+import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
 import type { BadgeProps } from '../display/Badge.vue';
 import Badge from '../display/Badge.vue';
-import FlexView from '../layout/FlexView.vue';
-import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
-import { RandomUtils } from '@imengyu/imengyu-utils';
-
-const idPrefix = RandomUtils.genNonDuplicateIDHEX(8) + '-tab-item-';
+import Touchable from '../feedback/Touchable.vue';
 
 export interface TabsItemData {
   /**

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

@@ -85,7 +85,7 @@ const { querys } = useLoadQuerys({
   try {
     const [model, forms] = getVillageInfoForm(querys.subType, querys.subId);
     formRef.value.initFormData(() => new model());
-    formDefine.value = forms;
+    formDefine.value = forms(formRef as any);
     if (querys.id >= 0)
       formData = await VillageInfoApi.getInfo(
         querys.subType, 

+ 60 - 57
src/pages/dig/forms/forms.ts

@@ -1,19 +1,21 @@
 import VillageInfoApi, { CommonInfoModel, VillageBulidingInfo, VillageEnvInfo } from "@/api/inhert/VillageInfoApi";
 import { useAliOssUploadCo } from "@/common/components/upload/AliOssUploadCo";
-import type { FormDefine, FormDefineItem, IFormItemCallbackAdditionalProps } from "@/components/dynamic";
+import type { FormDefine, FormDefineItem, FormExport, 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 { PickerIdFieldProps } from "@/components/dynamic/wrappers/PickerIdField";
+import type { RadioIdFieldProps } from "@/components/dynamic/wrappers/RadioIdField";
 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";
+import type { Ref } from "vue";
 
-type SingleForm = [NewDataModel, FormDefine]
+type SingleForm = [NewDataModel, (model: Ref<FormExport>) => FormDefine]
 
-const villageInfoBuildingForm : SingleForm = [VillageBulidingInfo, {
+const villageInfoBuildingForm : SingleForm = [VillageBulidingInfo, () => ({
   items: [
     {
       label: '建筑名称', 
@@ -414,8 +416,8 @@ const villageInfoBuildingForm : SingleForm = [VillageBulidingInfo, {
       rules: []
     },
   ] 
-}]
-const villageInfoFolkCultureForm : SingleForm = [VillageBulidingInfo, {
+})]
+const villageInfoFolkCultureForm : SingleForm = [VillageBulidingInfo, () => ({
   items: [
     {
       label: '名称',
@@ -475,8 +477,8 @@ const villageInfoFolkCultureForm : SingleForm = [VillageBulidingInfo, {
       rules:  []
     }, 
   ]
-}];
-const villageInfoFoodProductsForm : SingleForm = [VillageBulidingInfo, {
+})];
+const villageInfoFoodProductsForm : SingleForm = [VillageBulidingInfo, () => ({
   items: [
     {
       label: '名称',
@@ -505,8 +507,8 @@ const villageInfoFoodProductsForm : SingleForm = [VillageBulidingInfo, {
       }]
     },
   ]
-}];
-const villageCommonContent : FormDefine = {
+})];
+const villageCommonContent : (model: Ref<FormExport>) => FormDefine = (model) => ({
   items: [
     {
       label: '标题',
@@ -551,13 +553,13 @@ const villageCommonContent : FormDefine = {
       }]
     },
   ]
-};
+});
  
 //TODO: 关联的文化资源ID
 
 const villageInfoForm : Record<string, Record<number, SingleForm>> = {
   'overview': {
-    [1]: [CommonInfoModel, {
+    [1]: [CommonInfoModel, (form) => ({
       items: [
         { 
           label: '村落名称', 
@@ -578,7 +580,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           type: 'text', 
           defaultValue: '',
           params: {
-            placeholder: '请输入村落编码',
+            placeholder: '请输入村落编码,例如330106',
           },
           rules:  [{
             required: true,
@@ -591,13 +593,16 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           type: 'select-city', 
           defaultValue: () => [],
           params: {
-            placeholder: '请选择村落地址',
+            placeholder: '请点击这里选择村落地址或右侧者从地图选择',
+            onSelectedTownship: (v: string, code: string) => {
+              form.value.getFormData().township = v;
+              form.value.getFormData().code = code;
+            }
           },
-          itemParams: { showRightArrow: true } as FieldProps,
           rules:  [{
             required: true,
             message: '请选择村落地址',
-          }] 
+          }],
         },
         { 
           label: '村落乡镇', 
@@ -615,25 +620,23 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
         { 
           label: '村落类型', 
           name: 'villageType',
-          type: 'select-id', 
+          type: 'radio-id', 
           params: {
             loadData: async () => 
             (await VillageInfoApi.getCategoryChildList(94))
               .map((p) => ({
                 value: p.id,
                 text: p.title,
-              })),
-            placeholder: '请选择村落类型',
-          } as PickerIdFieldProps,
-          itemParams: { showRightArrow: true } as FieldProps,
+            })),
+          } as RadioIdFieldProps,
           rules: [{
             required: true,
             message: '请选择类型',
           }],
         },
       ]
-    }],
-    [2]: [VillageEnvInfo, {
+    })],
+    [2]: [VillageEnvInfo, () => ({
       items: [
         { 
           label: '经纬度', 
@@ -735,8 +738,8 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           }],
         },
       ]
-    }],
-    [3]: [CommonInfoModel, {
+    })],
+    [3]: [CommonInfoModel, () => ({
       items: [
         { 
           label: '非遗最高级别', 
@@ -833,8 +836,8 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           rules:  [] 
         }, 
       ]
-    }],
-    [4]: [CommonInfoModel, {
+    })],
+    [4]: [CommonInfoModel, () => ({ 
       items: [
         {
           name: '',
@@ -1031,8 +1034,8 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           }
         },
       ] 
-    }],
-    [5]: [CommonInfoModel, {
+    })],
+    [5]: [CommonInfoModel, () => ({
       items: [
         {
           name: '',
@@ -1089,14 +1092,14 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           }
         },
       ] 
-    }],
+    })],
   },
   'cultural': {
     [1]: [CommonInfoModel, villageCommonContent],
     [2]: [CommonInfoModel, villageCommonContent],
-    [3]: [CommonInfoModel, {
+    [3]: [CommonInfoModel, (m) => ({
       items: [
-        ...(villageCommonContent.items.slice(0, 2)),
+        ...(villageCommonContent(m).items.slice(0, 2)),
         {
           label: '扫描件或图片',
           name: 'images',
@@ -1113,10 +1116,10 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           }]
         },
       ],
-    }],
-    [4]: [CommonInfoModel, {
+    })],
+    [4]: [CommonInfoModel, (m) => ({
       items: [
-        ...villageCommonContent.items,
+        ...villageCommonContent(m).items,
         {
           label: '视频',
           name: 'video',
@@ -1130,10 +1133,10 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           } as UploaderFieldProps,
         },
       ],
-    }],
+    })],
   },
   'story': {
-    [0]: [CommonInfoModel, {
+    [0]: [CommonInfoModel, () => ({
       items: [
         {
           label: '标题',
@@ -1175,10 +1178,10 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           }]
         }
       ]
-    }],
+    })],
   },
   'figure': {
-    [0]: [CommonInfoModel, {
+    [0]: [CommonInfoModel, () => ({
       items: [
         {
           label: '标题',
@@ -1222,10 +1225,10 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           }]
         }
       ]
-    }],
+    })],
   },
   'element': {
-    [0]: [CommonInfoModel, {
+    [0]: [CommonInfoModel, () => ({
       items: [
         {
           label: '名称', 
@@ -1361,10 +1364,10 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           }] 
         },
       ] 
-    }]
+    })]
   },
   'environment': {
-    [0]: [CommonInfoModel, {
+    [0]: [CommonInfoModel, () => ({
       items: [
         { 
           label: '名称', 
@@ -1445,7 +1448,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           }] 
         }, 
       ] 
-    }]
+    })]
   },
   'building': {
     [1]: villageInfoBuildingForm,
@@ -1453,7 +1456,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
     [3]: villageInfoBuildingForm,
   },
   'distribution': {
-    [0]: [CommonInfoModel, {
+    [0]: [CommonInfoModel, () => ({
       items: [
         {
           label: '建筑数量', 
@@ -1498,10 +1501,10 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           }] 
         }, 
       ] 
-    }],
+    })],
   },
   'relic': {
-    [0]: [CommonInfoModel, {
+    [0]: [CommonInfoModel, () => ({
       items: [
         {
           label: '建筑名称', 
@@ -1724,7 +1727,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           rules: []
         },
       ] 
-    }],
+    })],
   },
   'folk_culture': {
     [1]: villageInfoFolkCultureForm,
@@ -1734,7 +1737,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
     [5]: villageInfoFolkCultureForm,
   },
   'ich': {
-    [0]: [CommonInfoModel, {
+    [0]: [CommonInfoModel, () => ({
       items: [
         {
           label: '名称及管理编号', 
@@ -1942,10 +1945,10 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           }],
         },
       ] 
-    }],
+    })],
   },
   'travel_guide': {
-    [0]: [CommonInfoModel, {
+    [0]: [CommonInfoModel, () => ({
       items: [
         {
           label: '入村路线', 
@@ -2299,10 +2302,10 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           rules:  [] 
         },
       ] 
-    }] 
+    })], 
   },
   'route': {
-    [1]: [CommonInfoModel, {
+    [1]: [CommonInfoModel, () => ({
       items: [
         {
           label: '游览路线', 
@@ -2383,8 +2386,8 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           }] 
         },
       ] 
-    }],
-    [2]: [CommonInfoModel, {
+    })],
+    [2]: [CommonInfoModel, () => ({
       items: [
         {
           label: '活动标题', 
@@ -2439,8 +2442,8 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           }],
         },
       ] 
-    }],
-    [3]: [CommonInfoModel, {
+    })],
+    [3]: [CommonInfoModel, () => ({
       items: [
         {
           label: '特色', 
@@ -2530,7 +2533,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           rules:  [] 
         },
       ] 
-    }]
+    })]
   },
   'food_product': {
     [1]: villageInfoFoodProductsForm,

+ 5 - 5
src/pages/user/index.vue

@@ -10,13 +10,13 @@
         round
       />
       <Width :width="20" />
-      <FlexCol v-if="userInfo" touchable @click="navTo('/pages/user/update/profile')" :flex="1">
+      <Touchable v-if="userInfo" direction="column" touchable @click="navTo('/pages/user/update/profile')" :flex="1">
         <H4>{{ userInfo.nickname }}</H4>
         <text class="extra"><text class="label">守护编号</text><text>{{ userInfo.id }}</text><text class="label point-label">积分</text><text>{{ userInfo.totalCheckins }}</text></text>
-      </FlexCol>
-      <FlexCol v-else touchable @click="navTo('/pages/user/login')" :flex="1">
+      </Touchable>
+      <Touchable v-else direction="column" touchable @click="navTo('/pages/user/login')" :flex="1">
         <H4 class="nickname">请登录</H4>
-      </FlexCol>
+      </Touchable>
       <Width :width="20" />
       <Icon icon="arrow-right" />
     </FlexRow>
@@ -33,7 +33,6 @@ import { computed } from 'vue';
 import UserHead from '@/static/images/home/UserHead.png';
 import CellGroup from '@/components/basic/CellGroup.vue';
 import Cell from '@/components/basic/Cell.vue';
-import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Image from '@/components/basic/Image.vue';
 import Icon from '@/components/basic/Icon.vue';
@@ -42,6 +41,7 @@ import Width from '@/components/layout/space/Width.vue';
 import Height from '@/components/layout/space/Height.vue';
 import { confirm } from '@/components/utils/DialogAction';
 import { navTo } from '@/components/utils/PageAction';
+import Touchable from '@/components/feedback/Touchable.vue';
 
 const authStore = useAuthStore();
 const userInfo = computed(() => authStore.userInfo);