Pārlūkot izejas kodu

完善表单组件问题

快乐的梦鱼 1 nedēļu atpakaļ
vecāks
revīzija
ea818bb185

+ 11 - 4
package-lock.json

@@ -24,9 +24,10 @@
         "@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001",
         "@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001",
         "@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
-        "@imengyu/imengyu-utils": "^0.0.16",
+        "@imengyu/imengyu-utils": "^0.0.17",
         "@imengyu/js-request-transform": "^0.3.7",
         "async-validator": "^4.2.5",
+        "crypto-js": "^4.2.0",
         "pinia": "^3.0.1",
         "tslib": "^2.8.1",
         "vue": "3.5.22",
@@ -3049,9 +3050,9 @@
       }
     },
     "node_modules/@imengyu/imengyu-utils": {
-      "version": "0.0.16",
-      "resolved": "https://registry.npmjs.org/@imengyu/imengyu-utils/-/imengyu-utils-0.0.16.tgz",
-      "integrity": "sha512-PhiHoat0ZL+2LGJR2W3MWVsd/dx+N4Tb3GGsOQQ+6HpBB8a/plhY1lNlqngDqlb3H6p6B6kap00Bdhcu9gYh+w==",
+      "version": "0.0.17",
+      "resolved": "https://registry.npmmirror.com/@imengyu/imengyu-utils/-/imengyu-utils-0.0.17.tgz",
+      "integrity": "sha512-cs2eB17qjIKnkQeb2j9FO2U/aevyohOr5IAIInCxr2/8QhOeHdWd+4bDmMdpxmEYEx0W+Hth8C5YUhMCJifONQ==",
       "license": "MIT",
       "dependencies": {
         "@imengyu/js-request-transform": "^0.3.6"
@@ -6312,6 +6313,12 @@
         "node": ">= 8"
       }
     },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+      "license": "MIT"
+    },
     "node_modules/css-font-size-keywords": {
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz",

+ 2 - 1
package.json

@@ -51,9 +51,10 @@
     "@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001",
     "@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001",
     "@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
-    "@imengyu/imengyu-utils": "^0.0.16",
+    "@imengyu/imengyu-utils": "^0.0.17",
     "@imengyu/js-request-transform": "^0.3.7",
     "async-validator": "^4.2.5",
+    "crypto-js": "^4.2.0",
     "pinia": "^3.0.1",
     "tslib": "^2.8.1",
     "vue": "3.5.22",

+ 1 - 1
src/App.vue

@@ -7,8 +7,8 @@
 import AppConfig from '@/common/config/AppCofig'
 import { onLaunch } from '@dcloudio/uni-app'
 import { useAuthStore } from './store/auth'
-import { getCurrentPageUrl, navTo } from "@imengyu/imengyu-utils/dist/uniapp/PageAction";
 import { configTheme } from './components/theme/ThemeDefine';
+import { getCurrentPageUrl, navTo } from './components/utils/PageAction';
 
 const authStore = useAuthStore();
 

+ 1 - 1
src/api/CommonContent.ts

@@ -2,7 +2,7 @@ import { DataModel, transformArrayDataModel, type NewDataModel } from '@imengyu/
 import ApiCofig from '@/common/config/ApiCofig';
 import { AppServerRequestModule } from './RequestModules';
 import { transformSomeToArray } from './Utils';
-import { RequestApiConfig, RequestOptions, type QueryParams } from '@imengyu/imengyu-utils/dist/request';
+import { RequestApiConfig, RequestOptions, type QueryParams } from '@imengyu/imengyu-utils';
 
 export class GetColumListParams extends DataModel<GetColumListParams> {
   

+ 6 - 19
src/api/RequestModules.ts

@@ -8,15 +8,16 @@
 
 import AppCofig, { isDev } from "../common/config/AppCofig";
 import ApiCofig from "@/common/config/ApiCofig";
-import uniappImplementer from "@imengyu/imengyu-utils/dist/request/implementer/Uniapp";
-import { appendGetUrlParams, appendPostParams } from "@imengyu/imengyu-utils/dist/request/utils/Utils";
 import { 
   RequestCoreInstance, RequestOptions, RequestApiError, RequestApiResult, type RequestApiErrorType,
   defaultResponseDataGetErrorInfo, defaultResponseDataHandlerCatch,
   RequestResponse,
-} from "@imengyu/imengyu-utils/dist/request";
+  UniappImplementer,
+  appendGetUrlParams, 
+  appendPostParams,
+  StringUtils,
+} from "@imengyu/imengyu-utils";
 import type { DataModel, KeyValue, NewDataModel } from "@imengyu/js-request-transform";
-import { StringUtils } from "@imengyu/imengyu-utils";
 
 /**
  * 不报告错误的 code
@@ -217,7 +218,7 @@ export function reportError<T extends DataModel>(instance: RequestCoreInstance<T
  */
 export class AppServerRequestModule<T extends DataModel> extends RequestCoreInstance<T> {
   constructor() {
-    super(uniappImplementer);
+    super(UniappImplementer);
     this.config.baseUrl = ApiCofig.serverProd;
     this.config.errCodes = []; //
     this.config.requestInceptor = requestInceptor;
@@ -225,18 +226,4 @@ export class AppServerRequestModule<T extends DataModel> extends RequestCoreInst
     this.config.responseErrReoprtInceptor = responseErrReoprtInceptor;
     this.config.reportError = reportError;
   }
-}
-/**
- * App服务请求模块
- */
-export class AppServerRequestModule2<T extends DataModel> extends RequestCoreInstance<T> {
-  constructor() {
-    super(uniappImplementer);
-    this.config.baseUrl = 'https://huli-app.wenlvti.net';
-    this.config.errCodes = []; //
-    this.config.requestInceptor = requestInceptor;
-    this.config.responseDataHandler = responseDataHandler;
-    this.config.responseErrReoprtInceptor = responseErrReoprtInceptor;
-    this.config.reportError = reportError;
-  }
 }

+ 0 - 59
src/common/components/ImageWrapper.vue

@@ -1,59 +0,0 @@
-<template>
-  <view class="image-wrapper">
-    <u-image 
-      :showLoading="true"
-      v-bind="$props"
-      :src="src || EmptyImage"
-      :style="{
-        width: width + 'px',
-        height: height + 'px',
-      }"
-      @click="onClick"
-    >
-      <template #loading>
-        <u-loading-icon color="red"></u-loading-icon>
-      </template>
-      <template #error>
-        <u-empty mode="page" text="图片加载失败" />
-      </template>
-    </u-image>
-  </view>
-</template>
-
-<script setup lang="ts">
-import EmptyImage from '@/static/EmptyImage.png';
-
-const props = defineProps({	
-  src: {
-    type: String,
-    required: true,
-  },
-  mode: {
-    type: String,
-    default: 'aspectFill',
-  },
-  canPreview: {
-    type: Boolean,
-    default: false,
-  },
-  width: {
-    type: [Number,String],
-    default: 0,
-  },
-  height: {
-    type: [Number,String],
-    default: 0,
-  },
-})
-
-const emit = defineEmits([ 'clcik' ])
-
-function onClick() {
-  if (props.canPreview) {
-    uni.previewImage({
-      urls: [props.src],
-    });
-  } 
-  emit('clcik')
-}
-</script>

+ 3 - 2
src/common/components/RequireLogin.vue

@@ -4,14 +4,15 @@
     <view class="mb-3">
       <text>{{unLoginMessage}}</text>
     </view>
-    <u-button class="w-50" type="primary" @click="goLogin">去登录</u-button>
+    <Button :innerStyle="{width: '50%'}" type="primary" @click="goLogin">去登录</Button>
   </view>
 </template>
 
 <script setup lang="ts">
+import Button from '@/components/basic/Button.vue';
+import { navTo } from '@/components/utils/PageAction';
 import { useAuthStore } from '@/store/auth';
 import { computed } from 'vue';
-import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
 
 const authStore = useAuthStore();
 const isLogged = computed(() => authStore.isLogged);

+ 0 - 74
src/common/components/SimpleDropDownPicker.vue

@@ -1,74 +0,0 @@
-<template>
-  <view 
-    class="simple-dropdown-box" 
-    @click="show=true"
-  >
-    <text v-if="preText" class="preText">{{ preText }}</text>
-    {{ dispayText }} ▼
-  </view>
-  <u-picker 
-    :show="show" 
-    :columns="[columns]" 
-    keyName="name"
-    @cancel="show=false"
-    @confirm="confirm"
-  />
-</template>
-
-<script setup lang="ts">
-import { computed, ref, type PropType } from 'vue';
-
-export interface SimpleDropDownPickerItem {
-  id: number,
-  name: string,
-}
-
-const props = defineProps({	
-  columns: {
-    type: Object as PropType<SimpleDropDownPickerItem[]>,
-    default: null,
-  },
-  modelValue: {
-    type: Number,
-    default: null,
-  },
-  defaultText: {
-    type: String,
-    default: '请选择',
-  },
-  preText: {
-    type: String,
-    default: '',
-  },
-})
-
-const emit = defineEmits([
-  'update:modelValue', 
-])
-
-const show = ref(false);
-const dispayText = computed(() => {
-  if (props.modelValue) 
-    return props.columns.find(item => item.id == props.modelValue)?.name;
-  return props.defaultText;
-});
-
-function confirm(e: { value: SimpleDropDownPickerItem[] }) {
-  show.value = false;
-  emit('update:modelValue', e.value[0].id);
-}
-</script>
-
-<style lang="scss">
-.simple-dropdown-box {
-  position: relative;
-  padding: 16rpx 18rpx;
-  border-radius: 20rpx;
-  background: #FFFFFF;
-
-  .preText {
-    margin-right: 10rpx;
-    color: #525252;
-  }
-}
-</style>

+ 21 - 25
src/common/components/SimplePageContentLoader.vue

@@ -3,23 +3,18 @@
     v-if="loader?.loadStatus.value == 'loading'"
     style="min-height: 200rpx;display: flex;justify-content: center;align-items: center;"
   >
-    <u-loading-icon text="加载中" textSize="18" />
+    <LoadingPage loadingText="加载中" textSize="18" />
   </view>
   <view
     v-else-if="loader?.loadStatus.value == 'error'"
     style="min-height: 200rpx"
   >
-    <u-empty
-      mode="page"
-      :text="loader.loadError.value"
-    />
-    <view style="margin-top: 20rpx">
-      <u-row justify="center">
-        <u-col span="3">
-          <u-button text="刷新" @click="handleRetry" />
-        </u-col>
-      </u-row>
-    </view>
+    <Empty
+      image="error"
+      :description="loader.loadError.value"
+    >
+      <Button type="primary" text="刷新" @click="handleRetry" />
+    </Empty>
   </view>
   <template v-else-if="loader?.loadStatus.value == 'finished' || loader?.loadStatus.value == 'nomore'">
     <slot />
@@ -28,20 +23,17 @@
     v-if="showEmpty || loader?.loadStatus.value == 'nomore'"
     style="min-height: 200rpx"
   >
-    <u-empty
-      mode="data"
-      :text="emptyView?.text ?? '暂无数据'"
-    />
-    <view v-if="emptyView?.button" style="margin-top: 20rpx">
-      <u-row justify="center">
-        <u-col span="3">
-          <u-button
-            :text="emptyView?.buttonText ?? '刷新'" 
+    <Empty
+      image="search"
+      :description="emptyView?.text ?? '暂无数据'"
+    >
+      <Button 
+        v-if="emptyView?.button"  
+        type="primary"
+        :text="emptyView?.buttonText ?? '刷新'"
             @click="() => emptyView?.buttonClick ? emptyView?.buttonClick() : handleRetry()"
-          />
-        </u-col>
-      </u-row>
-    </view>
+      />
+    </Empty>
   </view>
   <image 
     v-if="lazy && !loaded"
@@ -56,6 +48,10 @@
 <script setup lang="ts">
 import { onMounted, ref, type PropType } from 'vue';
 import type { ISimplePageContentLoader } from '../composeabe/SimplePageContentLoader';
+import Empty from '@/components/feedback/Empty.vue';
+import Button from '@/components/basic/Button.vue';
+import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
+import LoadingPage from '@/components/display/loading/LoadingPage.vue';
 
 const props = defineProps({	
   loader: {

+ 8 - 3
src/common/components/SimplePageListLoader.vue

@@ -1,18 +1,23 @@
 <template>
-  <u-loadmore 
+  <Loadmore
     v-if="
     loader.loadStatus.value == 'loading' 
     || (loader.loadStatus.value == 'nomore' && !$slots.empty)" 
     :status="loader.loadStatus.value" 
   />
   <slot v-else-if="loader.loadStatus.value == 'nomore' && $slots.empty" name="empty" />
-  <u-loadmore v-else-if="loader.loadStatus.value == 'error'" status="loadmore" :loadmoreText="loader.loadError.value" @loadmore="handleRetry" />
-
+  <Loadmore 
+    v-else-if="loader.loadStatus.value == 'error'"
+    status="loadmore" 
+    :loadmoreText="loader.loadError.value" 
+    @loadmore="handleRetry" 
+  />
 </template>
 
 <script setup lang="ts">
 import type { PropType } from 'vue';
 import type { ISimplePageListLoader } from '../composeabe/SimplePageListLoader';
+import Loadmore from '@/components/display/loading/Loadmore.vue';
 
 const props = defineProps({	
   loader: {

+ 1 - 1
src/common/components/form/RichTextEditor.vue

@@ -10,8 +10,8 @@
 </template>
 
 <script setup lang="ts">
-import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
 import { onPageShow } from '@dcloudio/uni-app';
+import { navTo } from '@/components/utils/PageAction';
 import Parse from '@/components/display/parse/Parse.vue';
 import Button from '@/components/basic/Button.vue';
 

+ 89 - 0
src/common/components/upload/AliOssUploadCo.ts

@@ -0,0 +1,89 @@
+import { Base64Utils, RandomUtils, StringUtils } from "@imengyu/imengyu-utils";
+import { hmacSha1Base64 } from "./hmac";
+import type { UploaderAction } from "@/components/form/Uploader";
+
+const client = {
+  region: 'oss-cn-shenzhen',
+  accessKeyId: 'LTAI5t5e7wAQ1FvUA4LCsNs5',
+  accessKeySecret: 'lhF0SimpatPMHNjmjtIKsWsYwTmJhx',
+  bucket: 'minnanwenhua', 
+};
+
+function generatePolicyAndSignature(key: string, accessKeySecret: string) {
+  try {
+    const expiration = new Date(Date.now() + 3600 * 1000).toISOString();
+    const policyText = {
+      expiration: expiration,
+      conditions: [
+        ["content-length-range", 0, 10485760 * 10], // 100MB限制
+        ["starts-with", "$key", key.substring(0, key.lastIndexOf('/') + 1)]
+      ]
+    };
+    const policyBase64 = Base64Utils.encode(JSON.stringify(policyText));
+    const signature = hmacSha1(policyBase64, accessKeySecret);
+    return {
+      policy: policyBase64,
+      signature: signature
+    };
+  } catch (error) {
+    console.error("生成policy和signature失败:", error);
+    throw error;
+  }
+}
+function hmacSha1(message: string, key: string) {
+  try {
+    return hmacSha1Base64(message, key);
+  } catch (error) {
+    console.error("HMAC-SHA1签名生成失败:", error);
+    throw error;
+  }
+}
+
+function uploadOSS(uploadPath: string, filePath: string, onProgress?: (progress: number) => void) {
+  return new Promise<string>((resolve, reject) => {
+
+    const key = uploadPath;
+    const { policy, signature } = generatePolicyAndSignature(key, client.accessKeySecret);
+
+    const formData = {
+      key,
+      policy: policy,
+      OSSAccessKeyId: client.accessKeyId,
+      signature: signature,
+      success_action_status: '200'
+    };
+
+    uni.uploadFile({
+      url: `https://${client.bucket}.${client.region}.aliyuncs.com`,
+      filePath,
+      name: 'file',
+      formData,
+      success: res => {
+        if (res.statusCode === 200 || res.statusCode === 204) {
+          resolve(`https://${client.bucket}.${client.region}.aliyuncs.com/${uploadPath}`)
+        } else {
+          reject(new Error('上传失败' + res.statusCode))
+        }
+      },
+      fail: reject
+    }).onProgressUpdate(({ progress }) => onProgress?.(progress))
+  })
+}
+
+export function useAliOssUploadCo(subPath: string) {
+  return (action: UploaderAction) => {
+    const name = StringUtils.path.getFileName(action.item.filePath);
+    const uploadPath = `${subPath}/${name.split('.')[0]}-${RandomUtils.genNonDuplicateID(26)}.${StringUtils.path.getFileExt(name)}`;      
+    uploadOSS(uploadPath, action.item.filePath, (progress) => {
+      action.onProgress?.(progress)
+    }).then((res) => {  
+      action.onFinish?.({
+        uploadedUrl: res,
+        previewUrl: res,
+      });
+    }).catch((err) => {
+      action.onError?.(err);
+    });
+  }
+}
+

+ 19 - 0
src/common/components/upload/ImageUploadCo.ts

@@ -0,0 +1,19 @@
+import CommonContent from "@/api/CommonContent";
+import type { UploaderAction } from "@/components/form/Uploader";
+
+export function useImageSimpleUploadCo(additionData?: Record<string, any>) {
+  return (action: UploaderAction) => {
+    action.onStart('正在上传');
+    CommonContent.uploadFile(action.item.filePath, 'image', 'file', additionData)
+      .then((res) => {
+        action.onProgress(100);
+        action.onFinish({
+          uploadedUrl: res.fullurl,
+          previewUrl: res.fullurl,
+        }, '上传成功');
+      }).catch((err) => {
+        action.onError?.(err);
+      })
+  }
+}
+

+ 9 - 0
src/common/components/upload/hmac.ts

@@ -0,0 +1,9 @@
+import CryptoJS from 'crypto-js'
+
+export function hmacSha1Base64(text: string, secret: string) {
+  const hash = CryptoJS.HmacSHA1(text, secret)
+  return CryptoJS.enc.Base64.stringify(hash)
+}
+export function hmacSha1Hex(text: string, secret: string) {
+  return CryptoJS.HmacSHA1(text, secret).toString(CryptoJS.enc.Hex)
+}

+ 2 - 2
src/common/composeabe/RequireLogin.ts

@@ -1,6 +1,6 @@
+import { confirm } from "@/components/utils/DialogAction";
+import { navTo } from "@/components/utils/PageAction";
 import { useAuthStore } from "@/store/auth";
-import { confirm } from "@imengyu/imengyu-utils/dist/uniapp/DialogAction";
-import { navTo } from "@imengyu/imengyu-utils/dist/uniapp/PageAction";
 
 export function useReqireLogin() {
   const authStore = useAuthStore();

+ 1 - 1
src/components/display/loading/LoadingPage.vue

@@ -31,7 +31,7 @@ export interface LoadingPageProps {
 const props = withDefaults(defineProps<LoadingPageProps>(), {
   loadingText: '加载中',
   indicatorColor: 'primary',
-})
+});
 </script>
 
 <template>

+ 15 - 6
src/components/dynamic/DynamicFormControl.vue

@@ -62,11 +62,21 @@
         />
       </template>
       <template v-else-if="formDefineItem.type === 'select'">
-        <PickerField 
+        <view>
+          <NaPickerField 
+            ref="itemRef"
+            :modelValue="modelValue"
+            @update:modelValue="onValueChanged"
+            v-bind="(params as any as PickerFieldProps)"
+          />
+        </view>
+      </template>
+      <template v-else-if="formDefineItem.type === 'uploader'">
+        <UploaderField
           ref="itemRef"
           :modelValue="modelValue"
           @update:modelValue="onValueChanged"
-          v-bind="(params as any as PickerFieldProps)"
+          v-bind="(params as any as UploaderFieldProps)"
         />
       </template>
       <template v-else-if="formDefineItem.type === 'select-id'">
@@ -179,7 +189,7 @@ import { computed, inject, onBeforeUnmount, onMounted, ref, type PropType } from
 import type { FormDefineItem, IFormItemCallback } from '.';
 import Field from '../form/Field.vue';
 import Stepper from '../form/Stepper.vue';
-import PickerField, { type PickerFieldProps } from '../form/PickerField.vue';
+import NaPickerField, { type PickerFieldProps } from '../form/PickerField.vue';
 import CheckBox from '../form/CheckBox.vue';
 import Switch from '../form/Switch.vue';
 import RadioValue from './wrappers/RadioValue.vue';
@@ -188,14 +198,13 @@ import CheckBoxList from './wrappers/CheckBoxList.vue';
 import CheckBoxToInt from './wrappers/CheckBoxToInt.vue';
 import type { IDynamicFormItemRadioValueFormItemProps } from './wrappers/RadioValue';
 import type { IDynamicFormItemSelectIdFormItemProps } from './wrappers/PickerIdField';
-import type { CascaderProps } from '../form/Cascader.vue';
-import CascaderField, { type CascaderFieldProps } from '../form/CascaderField.vue';
 import PickerCityField from './wrappers/PickerCityField.vue';
 import PickerLonlat from './wrappers/PickerLonlat.vue';
 import DateTimePickerField from '../form/DateTimePickerField.vue';
 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';
 
 const props = defineProps({	
   parentModel: {
@@ -238,7 +247,7 @@ function evaluateCallbackObj(val: Record<string, unknown|IFormItemCallback<unkno
 
 const params = computed(() => evaluateCallbackObj(props.formDefineItem.params as any))
 const label = computed(() => evaluateCallback(props.formDefineItem.label) as string)
-const show = computed(() => props.formDefineItem.hidden == undefined || evaluateCallback(props.formDefineItem.hidden) !== false)
+const show = computed(() => props.formDefineItem.show === undefined || evaluateCallback(props.formDefineItem.show))
 
 const itemRef = ref();
 const emit = defineEmits([ 'update:modelValue' ]);

+ 2 - 2
src/components/dynamic/index.ts

@@ -70,9 +70,9 @@ export interface FormDefineItem {
   //todo:联动
 
   /**
-   * 是否隐藏。当为undefined时,默认显示。
+   * 是否显示。当为undefined时,默认显示。
    */
-  hidden?: boolean|IFormItemCallback<boolean>|undefined,
+  show?: boolean|IFormItemCallback<boolean>|undefined,
 
   /**
    * 当前条目组件加载时发生事件

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

@@ -35,7 +35,11 @@
 
     <!-- 输入框区域 -->
     <FlexCol :flex="inputFlex">
-      <FlexRow align="center" :innerStyle="themeStyles.inputWapper2.value">
+      <FlexRow 
+        justify="space-between"
+        align="center" 
+        :innerStyle="themeStyles.inputWapper2.value"
+      >
         <slot name="prefix" />
         <slot name="leftButton" />
         <slot name="control" />
@@ -496,7 +500,6 @@ const themeStyles = themeContext.useThemeStyles({
     paddingVertical: DynamicSize('FieldLabelPaddingVertical', 8),
   },
   inputWapper2: {
-    justifyContent: 'space-between',
     align: 'center',
     alignSelf: 'center',
     width: '100%',

+ 12 - 2
src/components/form/Picker.vue

@@ -47,6 +47,11 @@ export interface PickerProps {
    * @default 750
    */
   pickerWidth?: string|number,
+  /**
+   * 是否在columns只有一列情况下value返回一个值
+   * @default false
+   */
+  singleValue?: boolean,
 }
 
 const emit = defineEmits([ 'update:value', 'selectTextChange' ]);
@@ -72,7 +77,11 @@ function bindChange(e: any) {
   });
   pickerSelectIndex.value = val;
 
-  emit('update:value', value);
+
+  if (props.singleValue && props.columns.length === 1)
+    emit('update:value', value[0]);
+  else 
+    emit('update:value', value);
   emit('selectTextChange', val.map((p, i) => {
     const cols = props.columns[i];
     if (!cols || cols.length === 0) 
@@ -81,7 +90,8 @@ function bindChange(e: any) {
   }).join(' '));
 }
 function loadValues() {
-  props.value.forEach((v,i) => {
+  const value = typeof props.value === 'number' || typeof props.value === 'string' ? [props.value] : props.value ?? [];
+  value.forEach((v,i) => {
     const index = props.columns[i]?.findIndex((item) => item.value === v);
     pickerSelectIndex.value[i] = index < 0 ? 0 : index;
   })

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

@@ -32,11 +32,29 @@ import { usePickerFieldTempStorageData } from './PickerUtils';
 
 export interface PickerFieldProps extends Omit<PickerProps, 'value'> {
   modelValue?: (number|string)[];
+  /**
+   * 标题
+   */
   title?: string,
+  /**
+   * 标题属性
+   */
   titleProps?: Omit<ActionSheetTitleProps, 'title'>,
+  /**
+   * 是否显示选中的文本
+   */
   showSelectText?: boolean,
+  /**
+   * 占位符
+   */
   placeholder?: string,
+  /**
+   * 初始值
+   */
   initalValue?: (number|string)[];
+  /**
+   * 是否立即更新值
+   */
   shouldUpdateValueImmediately?: boolean,
 }
 
@@ -50,6 +68,7 @@ const props = withDefaults(defineProps<PickerFieldProps>(), {
     confirmText: '确定',
   }),
   showSelectText: true,
+  singleValue: false,
 });
 
 const popupShow = ref(false);

+ 77 - 0
src/components/form/Uploader.ts

@@ -0,0 +1,77 @@
+export interface UploaderItem {
+  /**
+   * 上传文件源路径
+   */
+  filePath: string;
+  /**
+   * 上传完成后的文件路径
+   */
+  uploadedPath?: string;
+  /**
+   * 文件的大小(B)
+   */
+  size?: number;
+  /**
+   * 指示当前文件是否是图片,如果设置为 true,则预览时会调用 ImagePreview 打开,否则会调用 Linking.openURL 预览资源。
+   */
+  isImage?: boolean;
+  /**
+   * 在已上传列表中显示的预览图像,为空时使用 filePath
+   */
+  previewPath?: string;
+  /**
+   * 当前状态
+   */
+  state: 'notstart'|'uploading'|'success'|'fail';
+  /**
+   * 当失败时显示
+   */
+  message?: string;
+  /**
+   * 当前上传进度,0-100
+   */
+  progress?: number;
+}
+export interface UploaderAction {
+  /**
+   * 当前上传条目
+   */
+  item: UploaderItem;
+  /**
+   * 当上传进度变化时需要调用
+   * @param precent 当前上传百分比,0-100
+   */
+  onProgress: (precent: number) => void;
+  /**
+   * 当开始上传时需要调用
+   */
+  onStart: (message?: string) => void;
+  /**
+   * 当上传失败时需要调用
+   * @param error 当前错误信息,会显示在条目中
+   */
+  onError: (error: unknown) => void;
+  /**
+   * 当上传完成时需要调用
+   * @param result 上传完成后的结果
+   * @param message 上传完成后的消息,会显示在条目中
+   */
+  onFinish: (result: {
+    /**
+     * 上传完成后的文件路径
+     */
+    uploadedUrl: string;
+    /**
+     * 上传完成后的预览图像路径,为空时不修改
+     */
+    previewUrl?: string;
+  }, message?: string) => void;
+}
+
+export function stringUrlToUploaderItem(url: string): UploaderItem {
+  return {
+    filePath: url,
+    uploadedPath: url,
+    state: 'success',
+  }
+}

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

@@ -64,68 +64,15 @@ import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../th
 import type { ToastInstance } from '../feedback/Toast.vue';
 import Toast from '../feedback/Toast.vue';
 import DialogRoot, { type DialogAlertRoot } from '../dialog/DialogRoot.vue';
-import FlexRow from '../layout/FlexRow.vue';
 import UploaderListAddItem from './UploaderListAddItem.vue';
 import UploaderListItem from './UploaderListItem.vue';
 import FlexView from '../layout/FlexView.vue';
 import FlexCol from '../layout/FlexCol.vue';
+import type { UploaderAction, UploaderItem } from './Uploader';
+import { LogUtils } from '@imengyu/imengyu-utils';
 
 const themeContext = useTheme();
-
-export interface UploaderItem {
-  /**
-   * 上传文件源路径
-   */
-  filePath: string;
-  /**
-   * 文件的大小(B)
-   */
-  size?: number;
-  /**
-   * 指示当前文件是否是图片,如果设置为 true,则预览时会调用 ImagePreview 打开,否则会调用 Linking.openURL 预览资源。
-   */
-  isImage?: boolean;
-  /**
-   * 在已上传列表中显示的预览图像,为空时使用 filePath
-   */
-  previewPath?: string;
-  /**
-   * 当前状态
-   */
-  state: 'notstart'|'uploading'|'success'|'fail';
-  /**
-   * 当失败时显示
-   */
-  message?: string;
-  /**
-   * 当前上传进度,0-100
-   */
-  progress?: number;
-}
-export interface UploaderAction {
-  /**
-   * 当前上传条目
-   */
-  item: UploaderItem;
-  /**
-   * 当上传进度变化时需要调用
-   * @param precent 当前上传百分比,0-100
-   */
-  onProgress: (precent: number) => void;
-  /**
-   * 当开始上传时需要调用
-   */
-  onStart: (message?: string) => void;
-  /**
-   * 当上传失败时需要调用
-   * @param error 当前错误信息,会显示在条目中
-   */
-  onError: (error: unknown) => void;
-  /**
-   * 当上传完成时需要调用
-   */
-  onFinish: (message?: string) => void;
-}
+const TAG = 'Uploader';
 
 export interface UploaderProps {
   /**
@@ -295,7 +242,7 @@ const isImageExt = [
 const toast = ref<ToastInstance>();
 const dialog = ref<DialogAlertRoot>();
 
-const emit = defineEmits([ 'click' ]);
+const emit = defineEmits([ 'click', 'updateList' ]);
 const props = withDefaults(defineProps<UploaderProps>(), {
   disabled: false,
   maxUploadCount: 1,
@@ -381,7 +328,7 @@ function onItemPress(item: UploaderItem) {
 //条目预览
 function onItemPreview(item: UploaderItem) {
   //判断后缀是不是图片
-  const previewPath = item.previewPath || item.filePath;
+  const previewPath = item.previewPath || item.uploadedPath || item.filePath;
   if (item.isImage) {
     uni.previewImage({
       urls: [ 
@@ -416,6 +363,7 @@ function updateListItem(item: UploaderItem) {
     index >= 0 ? newList[index] = { ...item } : newList.push(item);
     return newList;
   })(currentUpladList.value);
+  emit('updateList', currentUpladList.value);
 }
 //删除列表条目
 function deleteListItem(item: UploaderItem) {
@@ -428,6 +376,8 @@ function startUploadItem(item: UploaderItem) {
       resolve();
       return;
     }
+    LogUtils.printLog(TAG, 'message', `调用上传文件 ${item.filePath}`);
+
     props.upload({
       item,
       onError(error) {
@@ -435,13 +385,18 @@ function startUploadItem(item: UploaderItem) {
         item.message = ('' + error) || '上传失败';
         updateListItem(item);
         reject(error);
+        LogUtils.printLog(TAG, 'error', `上传文件 ${item.filePath} 失败,错误信息:${error}`);
       },
-      onFinish(message) {
+      onFinish(result, message) {
         item.state = 'success';
         item.message = message || '上传完成';
         item.progress = 100;
+        item.uploadedPath = result.uploadedUrl;
+        if (result.previewUrl)
+          item.previewPath = result.previewUrl;
         updateListItem(item);
         resolve();
+        LogUtils.printLog(TAG, 'success', `上传文件 ${item.filePath} 成功,上传路径:${result.uploadedUrl}`);
       },
       onProgress(precent) {
         item.state = 'uploading';
@@ -454,6 +409,7 @@ function startUploadItem(item: UploaderItem) {
         item.message = message || '上传中...';
         item.progress = 0;
         updateListItem(item);
+        LogUtils.printLog(TAG, 'message', `上传文件 ${item.filePath} 开始,信息:${item.message}`);
       },
     });
   });

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

@@ -0,0 +1,60 @@
+<template>
+  <Uploader
+    ref="uploaderRef"
+    v-bind="props"
+    :maxUploadCount="single ? 1 : maxUploadCount"
+    @updateList="handleListChange"
+  />
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref, toRef } from 'vue';
+import { useFieldChildValueInjector } from './FormContext';
+import type { UploaderInstance, UploaderProps } from './Uploader.vue';
+import { stringUrlToUploaderItem, type UploaderItem } from './Uploader';
+import Uploader from './Uploader.vue';
+
+export interface UploaderFieldProps extends Omit<UploaderProps, 'value'> {
+  modelValue?: string[];
+  single?: boolean;
+}
+
+const uploaderRef = ref<UploaderInstance>();
+const emit = defineEmits([ 'update:modelValue' ]);
+const props = withDefaults(defineProps<UploaderFieldProps>(), {
+  modelValue: undefined,
+  showDelete: true,
+  showUpload: true,
+  uploadWhenAdded: true,
+});
+
+const {
+  value,
+  updateValue,
+} = useFieldChildValueInjector(
+  toRef(props, 'modelValue'), 
+  (v) => emit('update:modelValue', v),
+  undefined,
+  () => { /*uploaderRef.value?.pick()*/ },
+);
+
+function handleListChange(list: UploaderItem[]) {
+  updateValue(list.map((item) => item.uploadedPath).filter((item) => item) as string[]);
+}
+
+onMounted(() => {
+  setTimeout(() => {
+    uploaderRef.value?.setList(props.single 
+      ? (value.value ? [ stringUrlToUploaderItem(value.value as any as string) ] : [])
+      : props.modelValue?.map((item) => stringUrlToUploaderItem(item)) ?? []
+    )
+  }, 200);
+})
+
+defineOptions({
+  options: {
+    styleIsolation: "shared",
+    virtualHost: true,
+  }
+})
+</script>

+ 1 - 1
src/pages/dig/composeable/TaskEntryForm.ts

@@ -1,5 +1,5 @@
 import { useLoadQuerys } from "@/common/composeabe/LoadQuerys";
-import { navTo } from "@imengyu/imengyu-utils/dist/uniapp/PageAction";
+import { navTo } from "@/components/utils/PageAction";
 
 export function useTaskEntryForm() {
   const { querys } = useLoadQuerys({ 

+ 1 - 1
src/pages/dig/details.vue

@@ -127,10 +127,10 @@
 
 <script setup lang="ts">
 import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
-import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
 import { computed } from 'vue';
 import { useAuthStore } from '@/store/auth';
 import { useCollectStore } from '@/store/collect';
+import { navTo } from '@/components/utils/PageAction';
 
 const { querys } = useLoadQuerys({ 
   id: 0,  

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

@@ -22,8 +22,7 @@ import { ref } from 'vue';
 import { showError } from '@/common/composeabe/ErrorDisplay';
 import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
 import { getVillageInfoForm } from './forms';
-import { backAndCallOnPageBack } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
-import { RequestApiError } from '@imengyu/imengyu-utils/dist/request';
+import { RequestApiError } from '@imengyu/imengyu-utils';
 import VillageInfoApi, { CommonInfoModel } from '@/api/inhert/VillageInfoApi';
 import DynamicForm from '@/components/dynamic/DynamicForm.vue';
 import LoadingPage from '@/components/display/loading/LoadingPage.vue';
@@ -32,6 +31,7 @@ import CommonRoot from '@/components/dialog/CommonRoot.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import type { FormDefine, FormExport } from '@/components/dynamic';
 import Height from '@/components/layout/space/Height.vue';
+import { backAndCallOnPageBack } from '@/components/utils/PageAction';
 
 const popupRef = ref();
 const loading = ref(false);

+ 40 - 21
src/pages/dig/forms/forms.ts

@@ -1,10 +1,12 @@
 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 { FormGroupProps } from "@/components/dynamic/DynamicFormCate.vue";
 import type { CheckBoxListProps } from "@/components/dynamic/wrappers/CheckBoxList.vue";
-import type { IDynamicFormItemSelectIdFormItemProps, IDynamicFormItemSelectIdOption } from "@/components/dynamic/wrappers/PickerIdField";
+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 { UploaderFieldProps } from "@/components/form/UploaderField.vue";
 import type { NewDataModel } from "@imengyu/js-request-transform";
 
 type SingleForm = [NewDataModel, FormDefine]
@@ -1442,10 +1444,13 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
         {
           label: '分布图', 
           name: 'distribution', 
-          type: 'uploader-image', 
+          type: 'uploader', 
           defaultValue: '',
           params: {
-          },
+            upload: useAliOssUploadCo('xiangyuan/distribution'),
+            maxFileSize: 1024 * 1024 * 20,
+            single: true,
+          } as UploaderFieldProps,
           rules:  [{
             required: true,
             message: '请上传分布图',
@@ -1662,10 +1667,13 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
         {
           label: '图片', 
           name: 'images', 
-          type: 'uploader-image', 
+          type: 'uploader', 
           defaultValue: '',
           params: {
-          },
+            upload: useAliOssUploadCo('xiangyuan/relic'),
+            maxFileSize: 1024 * 1024 * 20,
+            maxUploadCount: 20,
+          } as UploaderFieldProps,
           rules:  [] 
         },
         {
@@ -1883,8 +1891,8 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           label: '具体传承时间', 
           name: 'otherInheritanceTime',
           type: 'picker-datetime', 
-          hidden: { callback(model, rawModel) {
-            return !(rawModel.inheritanceTime === 150);
+          show: { callback(model, rawModel) {
+            return (rawModel.inheritanceTime === 150);
           } },
           params: {
             type: 'datetime',
@@ -1989,7 +1997,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           label: '公交车介绍', 
           name: 'busIntro', 
           type: 'text', 
-          hidden: { callback: (_, rawModel) => !(rawModel.isBus === 1) },
+          show: { callback: (_, rawModel) => (rawModel.isBus === 1) },
           defaultValue: '',
           params: {
             placeholder: '请输入公交车介绍',
@@ -2029,21 +2037,25 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
         {
           label: '景区全景图', 
           name: 'panorama', 
-          type: 'uploader-image', 
+          type: 'uploader', 
           defaultValue: '',
           params: {
-            placeholder: '请上传景区全景图',
-          },
+            upload: useAliOssUploadCo('xiangyuan/travel/panorama'),
+            maxFileSize: 1024 * 1024 * 20,
+            single: true,
+          } as UploaderFieldProps,
           rules:  [] 
         },
         {
           label: '其他图', 
           name: 'otherImage', 
-          type: 'uploader-image', 
+          type: 'uploader', 
           defaultValue: '',
           params: {
-            placeholder: '请上传其他图',
-          },
+            upload: useAliOssUploadCo('xiangyuan/travel/guide'),
+            maxFileSize: 1024 * 1024 * 20,
+            single: true,
+          } as UploaderFieldProps,
           rules:  [] 
         },
         //解说牌
@@ -2179,7 +2191,8 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
               { value: 0, text: '无' },
               { value: 1, text: '有' },
               { value: 2, text: '其他' }
-            ]]
+            ]],
+            singleValue: true,
           } as PickerFieldProps,
           itemParams: {
             showRightArrow: true,
@@ -2193,9 +2206,11 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           label: '其他医疗点', 
           name: 'otherMedicalPoint', 
           type: 'text', 
-          hidden: { callback: (_, rawModel) => !(rawModel.medicalPoint === 2) },
+          show: { callback: (_, rawModel) => (rawModel.medicalPoint === 2) },
           defaultValue: '',
-          params: {},
+          params: {
+            placeholder: '请输入其他医疗点',
+          },
           rules:  [] 
         },
         //医疗点
@@ -2209,7 +2224,8 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
               { value: 0, text: '无' },
               { value: 1, text: '有' },
               { value: 2, text: '其他' }
-            ]]
+            ]],
+            singleValue: true,
           } as PickerFieldProps,
           itemParams: {
             showRightArrow: true,
@@ -2223,7 +2239,7 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
           label: '其他游览车', 
           name: 'otherTourBus', 
           type: 'text', 
-          hidden: { callback: (_, rawModel) => !(rawModel.tourBus === 2) },
+          show: { callback: (_, rawModel) => (rawModel.tourBus === 2) },
           defaultValue: '',
           params: {
             placeholder: '请输入其他游览车',
@@ -2467,10 +2483,13 @@ const villageInfoForm : Record<string, Record<number, SingleForm>> = {
         {
           label: '图片视频', 
           name: 'images', 
-          type: 'uploader-image', 
+          type: 'uploader', 
           defaultValue: '',
           params: {
-          },
+            upload: useAliOssUploadCo('xiangyuan/activity'),
+            maxFileSize: 1024 * 1024 * 20,
+            maxUploadCount: 20,
+          } as UploaderFieldProps,
           rules:  [] 
         },
       ] 

+ 2 - 3
src/pages/dig/forms/list.vue

@@ -35,13 +35,11 @@
 </template>
 
 <script setup lang="ts">
-import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
-import ImageWrapper from '@/common/components/ImageWrapper.vue';
 import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
 import { ref } from 'vue';
 import { DataDateUtils } from '@imengyu/js-request-transform';
-import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
 import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
 import VillageInfoApi from '@/api/inhert/VillageInfoApi';
 import Image from '@/components/basic/Image.vue';
 import Empty from '@/components/feedback/Empty.vue';
@@ -50,6 +48,7 @@ import SearchBar from '@/components/form/SearchBar.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Text from '@/components/basic/Text.vue';
+import { navTo } from '@/components/utils/PageAction';
 
 const searchText = ref('');
 const listLoader = useSimplePageListLoader<{

+ 2 - 2
src/pages/editor/editor.vue

@@ -21,11 +21,11 @@
 
 <script setup lang="ts">
 import { showError } from '@/common/composeabe/ErrorDisplay';
-import { confirm } from '@imengyu/imengyu-utils/dist/uniapp/DialogAction';
-import { back, backAndCallOnPageBack } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
 import spEditor from '@/uni_modules/sp-editor/components/sp-editor/sp-editor.vue';
 import XBarSpace from '@/components/layout/space/XBarSpace.vue';
 import Button from '@/components/basic/Button.vue';
+import { confirm } from '@/components/utils/DialogAction';
+import { back, backAndCallOnPageBack } from '@/components/utils/PageAction';
 
 function cancel() {
   confirm({

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

@@ -28,8 +28,6 @@
 </template>
 
 <script setup lang="ts">
-import { confirm } from '@imengyu/imengyu-utils/dist/uniapp/DialogAction';
-import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
 import { useAuthStore } from '@/store/auth';
 import { computed } from 'vue';
 import UserHead from '@/static/images/home/UserHead.png';
@@ -42,6 +40,8 @@ import Icon from '@/components/basic/Icon.vue';
 import H4 from '@/components/typography/H4.vue';
 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';
 
 const authStore = useAuthStore();
 const userInfo = computed(() => authStore.userInfo);

+ 1 - 1
src/pages/user/login.vue

@@ -61,7 +61,6 @@
 <script setup lang="ts">
 import baseLogo from '/static/logo.png';
 import { useAuthStore } from '@/store/auth';
-import { back, navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
 import { onMounted, ref } from 'vue';
 import { showError } from '@/common/composeabe/ErrorDisplay';
 import FlexCol from '@/components/layout/FlexCol.vue';
@@ -75,6 +74,7 @@ import type { Rules } from 'async-validator';
 import { closeToast, toast } from '@/components/dialog/CommonRoot';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import CommonRoot from '@/components/dialog/CommonRoot.vue';
+import { navTo } from '@/components/utils/PageAction';
 
 const type = ref('wechat');
 const authStore = useAuthStore();

+ 1 - 1
src/pages/user/update/profile.vue

@@ -37,7 +37,6 @@
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
 import { useAuthStore } from '@/store/auth';
-import { navTo } from '@imengyu/imengyu-utils/dist/uniapp/PageAction';
 import UserApi from '@/api/auth/UserApi';
 import DefaultAvatar from '/static/images/home/UserHead.png';
 import CommonContent from '@/api/CommonContent';
@@ -47,6 +46,7 @@ import FlexCol from '@/components/layout/FlexCol.vue';
 import Button from '@/components/basic/Button.vue';
 import Height from '@/components/layout/space/Height.vue';
 import type { Rules } from 'async-validator';
+import { navTo } from '@/components/utils/PageAction';
 
 const authStore = useAuthStore();
 const formRef = ref<any>(null);