huangxw há 3 semanas atrás
pai
commit
2c259e974a

+ 60 - 221
.eslintrc-auto-import.json

@@ -1,274 +1,113 @@
 {
   "globals": {
-    "ComponentInternalInstance": true,
-    "CheckboxValueType": true,
+    "Component": true,
+    "ComponentPublicInstance": true,
+    "ComputedRef": true,
+    "DirectiveBinding": true,
+    "EffectScope": true,
+    "ExtractDefaultPropTypes": true,
+    "ExtractPropTypes": true,
+    "ExtractPublicPropTypes": true,
+    "InjectionKey": true,
+    "MaybeRef": true,
+    "MaybeRefOrGetter": true,
     "PropType": true,
-    "DateModelType": true,
-    "asyncComputed": true,
-    "autoResetRef": true,
+    "Ref": true,
+    "Slot": true,
+    "Slots": true,
+    "VNode": true,
+    "WritableComputedRef": true,
+    "acceptHMRUpdate": true,
     "computed": true,
-    "computedAsync": true,
-    "computedEager": true,
-    "computedInject": true,
-    "computedWithControl": true,
-    "controlledComputed": true,
-    "controlledRef": true,
     "createApp": true,
-    "createEventHook": true,
-    "createGlobalState": true,
-    "createInjectionState": true,
-    "createReactiveFn": true,
-    "createSharedComposable": true,
-    "createUnrefFn": true,
+    "createPinia": true,
     "customRef": true,
-    "debouncedRef": true,
-    "debouncedWatch": true,
+    "debounce": true,
     "defineAsyncComponent": true,
     "defineComponent": true,
-    "eagerComputed": true,
+    "defineStore": true,
     "effectScope": true,
-    "extendRef": true,
+    "getActivePinia": true,
     "getCurrentInstance": true,
     "getCurrentScope": true,
-    "ignorableWatch": true,
+    "h": true,
     "inject": true,
-    "isDefined": true,
     "isProxy": true,
     "isReactive": true,
     "isReadonly": true,
     "isRef": true,
-    "makeDestructurable": true,
+    "mapActions": true,
+    "mapGetters": true,
+    "mapState": true,
+    "mapStores": true,
+    "mapWritableState": true,
     "markRaw": true,
     "nextTick": true,
     "onActivated": true,
+    "onAddToFavorites": true,
+    "onBackPress": true,
     "onBeforeMount": true,
     "onBeforeUnmount": true,
     "onBeforeUpdate": true,
-    "onClickOutside": true,
     "onDeactivated": true,
+    "onError": true,
     "onErrorCaptured": true,
-    "onKeyStroke": true,
-    "onLongPress": true,
+    "onHide": true,
+    "onLaunch": true,
+    "onLoad": true,
     "onMounted": true,
+    "onNavigationBarButtonTap": true,
+    "onNavigationBarSearchInputChanged": true,
+    "onNavigationBarSearchInputClicked": true,
+    "onNavigationBarSearchInputConfirmed": true,
+    "onNavigationBarSearchInputFocusChanged": true,
+    "onPageNotFound": true,
+    "onPageScroll": true,
+    "onPullDownRefresh": true,
+    "onReachBottom": true,
+    "onReady": true,
     "onRenderTracked": true,
     "onRenderTriggered": true,
+    "onResize": true,
     "onScopeDispose": true,
     "onServerPrefetch": true,
-    "onStartTyping": true,
+    "onShareAppMessage": true,
+    "onShareTimeline": true,
+    "onShow": true,
+    "onTabItemTap": true,
+    "onThemeChange": true,
+    "onUnhandledRejection": true,
+    "onUnload": true,
     "onUnmounted": true,
     "onUpdated": true,
-    "pausableWatch": true,
+    "onWatcherCleanup": true,
     "provide": true,
-    "reactify": true,
-    "reactifyObject": true,
     "reactive": true,
-    "reactiveComputed": true,
-    "reactiveOmit": true,
-    "reactivePick": true,
     "readonly": true,
     "ref": true,
-    "refAutoReset": true,
-    "refDebounced": true,
-    "refDefault": true,
-    "refThrottled": true,
-    "refWithControl": true,
     "resolveComponent": true,
-    "resolveDirective": true,
-    "resolveRef": true,
-    "resolveUnref": true,
+    "setActivePinia": true,
+    "setMapStoreSuffix": true,
     "shallowReactive": true,
     "shallowReadonly": true,
     "shallowRef": true,
-    "syncRef": true,
-    "syncRefs": true,
-    "templateRef": true,
-    "throttledRef": true,
-    "throttledWatch": true,
+    "storeToRefs": true,
     "toRaw": true,
-    "toReactive": true,
     "toRef": true,
     "toRefs": true,
+    "toValue": true,
     "triggerRef": true,
-    "tryOnBeforeMount": true,
-    "tryOnBeforeUnmount": true,
-    "tryOnMounted": true,
-    "tryOnScopeDispose": true,
-    "tryOnUnmounted": true,
     "unref": true,
-    "unrefElement": true,
-    "until": true,
-    "useActiveElement": true,
-    "useArrayEvery": true,
-    "useArrayFilter": true,
-    "useArrayFind": true,
-    "useArrayFindIndex": true,
-    "useArrayFindLast": true,
-    "useArrayJoin": true,
-    "useArrayMap": true,
-    "useArrayReduce": true,
-    "useArraySome": true,
-    "useArrayUnique": true,
-    "useAsyncQueue": true,
-    "useAsyncState": true,
     "useAttrs": true,
-    "useBase64": true,
-    "useBattery": true,
-    "useBluetooth": true,
-    "useBreakpoints": true,
-    "useBroadcastChannel": true,
-    "useBrowserLocation": true,
-    "useCached": true,
-    "useClipboard": true,
-    "useCloned": true,
-    "useColorMode": true,
-    "useConfirmDialog": true,
-    "useCounter": true,
     "useCssModule": true,
-    "useCssVar": true,
     "useCssVars": true,
-    "useCurrentElement": true,
-    "useCycleList": true,
-    "useDark": true,
-    "useDateFormat": true,
-    "useDebounce": true,
-    "useDebounceFn": true,
-    "useDebouncedRefHistory": true,
-    "useDeviceMotion": true,
-    "useDeviceOrientation": true,
-    "useDevicePixelRatio": true,
-    "useDevicesList": true,
-    "useDisplayMedia": true,
-    "useDocumentVisibility": true,
-    "useDraggable": true,
-    "useDropZone": true,
-    "useElementBounding": true,
-    "useElementByPoint": true,
-    "useElementHover": true,
-    "useElementSize": true,
-    "useElementVisibility": true,
-    "useEventBus": true,
-    "useEventListener": true,
-    "useEventSource": true,
-    "useEyeDropper": true,
-    "useFavicon": true,
-    "useFetch": true,
-    "useFileDialog": true,
-    "useFileSystemAccess": true,
-    "useFocus": true,
-    "useFocusWithin": true,
-    "useFps": true,
-    "useFullscreen": true,
-    "useGamepad": true,
-    "useGeolocation": true,
-    "useIdle": true,
-    "useImage": true,
-    "useInfiniteScroll": true,
-    "useIntersectionObserver": true,
-    "useInterval": true,
-    "useIntervalFn": true,
-    "useKeyModifier": true,
-    "useLastChanged": true,
-    "useLocalStorage": true,
-    "useMagicKeys": true,
-    "useManualRefHistory": true,
-    "useMediaControls": true,
-    "useMediaQuery": true,
-    "useMemoize": true,
-    "useMemory": true,
-    "useMounted": true,
-    "useMouse": true,
-    "useMouseInElement": true,
-    "useMousePressed": true,
-    "useMutationObserver": true,
-    "useNavigatorLanguage": true,
-    "useNetwork": true,
-    "useNow": true,
-    "useObjectUrl": true,
-    "useOffsetPagination": true,
-    "useOnline": true,
-    "usePageLeave": true,
-    "useParallax": true,
-    "usePermission": true,
-    "usePointer": true,
-    "usePointerLock": true,
-    "usePointerSwipe": true,
-    "usePreferredColorScheme": true,
-    "usePreferredContrast": true,
-    "usePreferredDark": true,
-    "usePreferredLanguages": true,
-    "usePreferredReducedMotion": true,
-    "usePrevious": true,
-    "useRafFn": true,
-    "useRefHistory": true,
-    "useResizeObserver": true,
-    "useScreenOrientation": true,
-    "useScreenSafeArea": true,
-    "useScriptTag": true,
-    "useScroll": true,
-    "useScrollLock": true,
-    "useSessionStorage": true,
-    "useShare": true,
+    "useId": true,
+    "useModel": true,
     "useSlots": true,
-    "useSorted": true,
-    "useSpeechRecognition": true,
-    "useSpeechSynthesis": true,
-    "useStepper": true,
-    "useStorage": true,
-    "useStorageAsync": true,
-    "useStyleTag": true,
-    "useSupported": true,
-    "useSwipe": true,
-    "useTemplateRefsList": true,
-    "useTextDirection": true,
-    "useTextSelection": true,
-    "useTextareaAutosize": true,
-    "useThrottle": true,
-    "useThrottleFn": true,
-    "useThrottledRefHistory": true,
-    "useTimeAgo": true,
-    "useTimeout": true,
-    "useTimeoutFn": true,
-    "useTimeoutPoll": true,
-    "useTimestamp": true,
-    "useTitle": true,
-    "useToNumber": true,
-    "useToString": true,
-    "useToggle": true,
-    "useTransition": true,
-    "useUrlSearchParams": true,
-    "useUserMedia": true,
-    "useVModel": true,
-    "useVModels": true,
-    "useVibrate": true,
-    "useVirtualList": true,
-    "useWakeLock": true,
-    "useWebNotification": true,
-    "useWebSocket": true,
-    "useWebWorker": true,
-    "useWebWorkerFn": true,
-    "useWindowFocus": true,
-    "useWindowScroll": true,
-    "useWindowSize": true,
+    "useTemplateRef": true,
     "watch": true,
-    "watchArray": true,
-    "watchAtMost": true,
-    "watchDebounced": true,
     "watchEffect": true,
-    "watchIgnorable": true,
-    "watchOnce": true,
-    "watchPausable": true,
     "watchPostEffect": true,
-    "watchSyncEffect": true,
-    "watchThrottled": true,
-    "watchTriggerable": true,
-    "watchWithFilter": true,
-    "whenever": true,
-    "ImportOption": true,
-    "TreeType": true,
-    "FieldOption": true,
-    "PageData": true,
-    "storeToRefs": true,
-    "DictDataOption": true,
-    "UploadOption": true
+    "watchSyncEffect": true
   }
 }

+ 0 - 2
src/App.vue

@@ -5,7 +5,6 @@ import { autoLogin } from '@/utils/routeGuard';
 import { getToken, setToken, removeToken } from '@/utils/auth';
 onLaunch(async () => {
     console.log('App Launch');
-
     // 应用启动时检查登录状态
     try {
         // await autoLogin();
@@ -25,7 +24,6 @@ onHide(() => {
 <style lang="scss">
 @import "uview-plus/index.scss";
 @import '@/assets/styles/public.scss';
-@import '@/assets/styles/common.scss';
 @import '@/assets/styles/uview-plus.scss';
 
 // 设置背景色

+ 9 - 13
src/components/ut-action-sheet/ut-action-sheet.vue

@@ -1,11 +1,10 @@
 <template>
-    <view @click.stop="showModel = true">
+    <picker :range="options" range-key="name" @change="selectChange" :disabled="false">
         <slot></slot>
-    </view>
-    <up-action-sheet :actions="options" :title="title" :show="showModel" @select="selectChange" :closeOnClickOverlay="true" :closeOnClickAction="true" @close="close"></up-action-sheet>
+    </picker>
 </template>
-<script setup>
-import { ref, watch } from 'vue';
+<script setup lang="ts">
+import { ref, watch, computed } from 'vue';
 
 const props = defineProps({
     show: {
@@ -31,7 +30,7 @@ const props = defineProps({
     },
 });
 const options = computed(() => {
-    return props.tabs.map((item) => {
+    return props.tabs.map((item: any) => {
         return {
             name: item.label,
             value: item.value,
@@ -49,15 +48,12 @@ const close = () => {
     emit('close');
 };
 
-const clickItem = (value) => {
-    checkeds.value[value] = !checkeds.value[value];
-};
 // 未选择之前的
 const initialCheckeds = ref({});
-const selectChange = (item) => {
-    console.log(item);
-    emit('update:modelValue', item.value);
-    emit('change', item.value);
+const selectChange = (item: any) => {
+    const selectedValues = options.value[item.detail.value].value;
+    emit('update:modelValue', selectedValues);
+    emit('change', selectedValues);
     close();
 };
 watch(

+ 196 - 0
src/components/ut-album/ut-album.vue

@@ -0,0 +1,196 @@
+<template>
+    <view class="ut-album" :style="albumStyle">
+        <view v-for="(item, index) in displayUrls" :key="index" class="album-item" :style="getItemStyle(index)"
+            @click="handleItemClick(index)">
+            <image :src="getThumbnailUrl(index)" :mode="singleMode" class="album-image" />
+            <!-- 超出数量显示 +N -->
+            <view v-if="index === maxCount - 1 && urls.length > maxCount" class="album-more">
+                <text class="more-text">+{{ urls.length - maxCount }}</text>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue';
+
+const props = defineProps({
+    urls: {
+        type: Array,
+        default: []
+    },
+    singleMode: {
+        type: String,
+        default: 'aspectFill'
+    },
+    space: {
+        type: String,
+        default: '14rpx'
+    },
+    rowCount: {
+        type: Number,
+        default: 3
+    },
+    keyName: {
+        type: String,
+        default: ''
+    },
+    unit: {
+        type: String,
+        default: 'rpx'
+    },
+    // 统一的图片尺寸(单个和多个都使用这个)
+    multipleSize: {
+        type: String,
+        default: '160rpx'
+    },
+    // 兼容旧版本的单张图片尺寸(如果没有设置multipleSize则使用这个)
+    singleMaxSize: {
+        type: String,
+        default: '180rpx'
+    },
+    previewFullImage: {
+        type: Boolean,
+        default: true
+    },
+    maxCount: {
+        type: Number,
+        default: 9
+    },
+    // 缩略图系数
+    factor: {
+        type: Number,
+        default: 4
+    },
+    t: {
+        type: String,
+        default: '0'
+    }
+})
+
+// 显示的图片数组(限制数量)
+const displayUrls = computed(() => {
+    return props.urls.slice(0, props.maxCount);
+});
+
+// 计算相册容器样式
+const albumStyle = computed(() => {
+    return {
+        display: 'flex',
+        flexWrap: 'wrap',
+        gap: props.space,
+        width: '100%'
+    };
+});
+
+// 统一的图片尺寸(单个和多个都使用相同尺寸)
+const getImageSize = () => {
+    // 统一使用 multipleSize
+    return props.multipleSize;
+};
+
+// 获取原始图片URL
+const getImageUrl = (index) => {
+    const item = props.urls[index];
+    if (props.keyName && typeof item === 'object') {
+        return item[props.keyName] || '';
+    }
+    return item || '';
+}
+
+// 获取缩略图URL
+const getThumbnailUrl = (index) => {
+    const url = getImageUrl(index);
+    if (!url) return '';
+
+    const size = uni.$u.getPx(getImageSize()) * props.factor;
+
+    return `${url}?w=${size}&h=${size}&t=${props.t}`;
+}
+
+// 获取原图URL(移除参数)
+const getOriginalUrl = (index) => {
+    const url = getImageUrl(index);
+    return url ? url.split('?')[0] : '';
+}
+
+// 获取所有原图URL数组
+const getAllOriginalUrls = () => {
+    return props.urls.map((item, index) => getOriginalUrl(index));
+}
+
+// 获取单个图片项的样式 - 统一宽高
+const getItemStyle = (index) => {
+    const size = getImageSize();
+
+    // 无论单张还是多张,都使用相同的尺寸
+    return {
+        width: size,
+        height: size,
+        flexShrink: 0
+    };
+}
+
+// 处理图片点击
+const handleItemClick = (index) => {
+    if (props.previewFullImage) {
+        previewImg(index);
+    }
+}
+
+// 预览图片(使用原图)
+const previewImg = (index) => {
+    if (!props.previewFullImage) return;
+
+    const originalUrls = getAllOriginalUrls();
+
+    if (originalUrls.length > 0) {
+        uni.previewImage({
+            urls: originalUrls,
+            current: index
+        });
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+.ut-album {
+    width: 100%;
+
+    .album-item {
+        position: relative;
+        border-radius: 8rpx;
+        overflow: hidden;
+        border: 1rpx solid #f8f8f8;
+
+
+        .album-image {
+            width: 100%;
+            height: 100%;
+            border-radius: 8rpx;
+            object-fit: cover;
+            box-sizing: border-box;
+            background-color: #ccc;
+        }
+
+        .album-more {
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: rgba(0, 0, 0, 0.2);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            border-radius: 8rpx;
+
+            .more-text {
+                color: #fff;
+                font-size: 32rpx;
+                font-weight: 500;
+            }
+        }
+    }
+}
+</style>

+ 100 - 0
src/components/ut-image/ut-image.vue

@@ -0,0 +1,100 @@
+<template>
+    <template v-if="preview">
+        <view class="ut-image p-rtv ov-hd" @click.stop="previewImg" :style="{ width, height, borderRadius, backgroundColor: bgColor }">
+            <up-image :src="thumbnailUrl" :mode="mode" :width="width" :height="height" :showLoading="false" :lazyLoad="lazyLoad"></up-image>
+            <view v-if="title" class="ut-iamge-title">{{ title }}</view>
+        </view>
+    </template>
+    <template v-else>
+        <view class="ut-image p-rtv ov-hd" :style="{ width, height, borderRadius, backgroundColor: bgColor }">
+            <up-image :src="thumbnailUrl" :mode="mode" :width="width" :height="height" :showLoading="false" :lazyLoad="lazyLoad"></up-image>
+            <view class="ut-iamge-title" :style="{ fontSize: titleSize }">{{ title }}</view>
+        </view>
+    </template>
+</template>
+
+<script setup lang="ts" name="ut-image">
+interface UtImageProps {
+    width?: string;
+    height?: string;
+    value?: string;
+    src?: string;
+    url?: string;
+    defaultSrc?: string; // 无图片时的默认图片
+    preview?: boolean;
+    borderRadius?: string;
+    lazyLoad?: boolean;
+    factor?: number; // 缩略图系数
+    t?: string; // 缩略图质量/类型
+    enableThumbnail?: boolean;
+    mode?: any;
+    bgColor?: string;
+    title?: string;
+    titleSize?: string;
+}
+
+const props = withDefaults(defineProps<UtImageProps>(), {
+    width: '200rpx',
+    height: '200rpx',
+    value: '',
+    src: '',
+    url: '',
+    defaultSrc: 'https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/common/avatar.png',
+    preview: false,
+    borderRadius: '16rpx',
+    lazyLoad: true,
+    factor: 4,
+    t: '0',
+    enableThumbnail: true,
+    mode: 'aspectFit',
+    bgColor: '#f7f7f7',
+    title: '',
+    titleSize: '28rpx',
+});
+
+// 将 value/src/url 统一为图片地址(按优先级:value > src > url > defaultSrc)
+const imageUrl = computed<string>(() => props.value || props.src || props.url || props.defaultSrc || '');
+
+// 生成缩略图URL(基于统一后的 imageUrl)
+const thumbnailUrl = computed<string>(() => {
+    const base = imageUrl.value;
+    if (!base) return '';
+    if (!props.enableThumbnail) return base;
+
+    const w = Math.round(uni.upx2px(parseFloat(props.width)) * props.factor);
+    const h = Math.round(uni.upx2px(parseFloat(props.height)) * props.factor);
+    const separator = base.includes('?') ? '&' : '?';
+    return `${base}${separator}w=${w}&h=${h}&t=${props.t}`;
+});
+
+// 获取原图URL
+const getOriginalUrl = (): string => {
+    const base = imageUrl.value;
+    if (!base) return '';
+    return base.split('?')[0];
+};
+
+// 预览图片(使用原图)
+const previewImg = (): void => {
+    if (!props.preview) return;
+    const originalUrl = getOriginalUrl();
+    if (!originalUrl) return;
+    uni.previewImage({ urls: [originalUrl] });
+};
+</script>
+
+<style lang="scss" scoped>
+.ut-image {
+    overflow: hidden;
+}
+.ut-iamge-title {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    font-size: 28rpx;
+    color: #fff;
+    text-align: center;
+    background-color: rgba(0, 0, 0, 0.6);
+}
+</style>

+ 679 - 0
src/components/ut-picker-area/ut-picker-area.vue

@@ -0,0 +1,679 @@
+<template>
+    <u-popup :show="show" mode="bottom" round="30rpx" closeable @close="close">
+        <view class="up-picker-area d-flex flex-cln">
+            <!-- 标题 -->
+            <view class="area-title">{{ title }}</view>
+            <!-- 顶部链路 -->
+            <view class="area-breadcrumb">
+                <!-- 全国根:selectCodeMax 为空或 000000 时显示 -->
+                <template v-if="isNationRoot">
+                    <view class="crumb disabled" @click="onClickNationBreadcrumb">全国</view>
+                    <view class="sep" v-if="selectedCodes.length">/</view>
+                </template>
+                <!-- 基础链路(selectCodeMax 的完整链路),仅最后一个可点击以切换到第一页(市级) -->
+                <template v-for="(code, idx) in baseChain" :key="'base-' + code">
+                    <view class="crumb" :class="idx === baseChain.length - 1 ? '' : 'disabled'" @click="idx === baseChain.length - 1 && onClickBaseBreadcrumb()">
+                        {{ getNameByCode(code) }}
+                    </view>
+                    <view class="sep" v-if="idx < baseChain.length - 1 || selectedCodes.length">/</view>
+                </template>
+
+                <!-- 选中链路 -->
+                <template v-for="(code, idx) in selectedCodes" :key="'sel-' + code">
+                    <view class="crumb" :class="{ active: idx === currentSwiper }" @click="jumpToLevel(idx)">
+                        {{ getNameByCode(code) }}
+                    </view>
+                    <view class="sep" v-if="idx < selectedCodes.length - 1">/</view>
+                </template>
+            </view>
+
+            <!-- 滑动列区域 -->
+            <view class="area-body">
+                <swiper :current="currentSwiper" @change="onSwiperChange" class="area-swiper">
+                    <template v-for="(parent, colIdx) in columnParents" :key="'col-' + parent">
+                        <swiper-item>
+                            <scroll-view
+                              :id="`sv-${colIdx}`"
+                              scroll-y
+                              class="area-scroll"
+                              :scroll-into-view="scrollIntoViewArr[colIdx]"
+                            >
+                                <!-- 顶部特殊项:全国(仅全国范围且首列显示) -->
+                                <view v-if="isNationRoot && colIdx === 0" class="picker-item" :id="`area-item-0-000000`" @click="onPickNation">
+                                    <view class="name" :class="{ selected: currentValue === '000000' }">全国</view>
+                                    <up-icon v-if="currentValue === '000000'" name="checkbox-mark" color="#000" size="36rpx" />
+                                </view>
+
+                                <!-- 顶部特殊项:选择范围自身(非全国,首列显示),如 云南省 -->
+                                <view v-else-if="!isNationRoot && colIdx === 0" class="picker-item" :id="`area-item-0-${canonical6(baseRoot)}`" @click="onPickBaseRoot">
+                                    <view class="name" :class="{ selected: currentValue === baseRoot }">{{ getNameByCode(baseRoot) }}</view>
+                                    <up-icon v-if="currentValue === baseRoot" name="checkbox-mark" color="#000" size="36rpx" />
+                                </view>
+
+                                <!-- 正常子级项 -->
+                                <template v-for="item in childrenMap[parent] || []" :key="item.adcdCode">
+                                    <view class="picker-item" :id="`area-item-${colIdx}-${canonical6(item.adcdCode)}`" @click="onPick(item, colIdx)">
+                                        <view class="name" :class="{ selected: canonical6(selectedCodes[colIdx]) === canonical6(item.adcdCode) }">
+                                            {{ item.adcdName }}
+                                        </view>
+                                        <up-icon v-if="canonical6(selectedCodes[colIdx]) === canonical6(item.adcdCode)" name="checkbox-mark" color="#000" size="36rpx" />
+                                    </view>
+                                </template>
+                            </scroll-view>
+                        </swiper-item>
+                    </template>
+                </swiper>
+            </view>
+
+            <!-- 底部操作 -->
+            <view class="area-footer">
+                <up-button class="btn" @click="close">取消</up-button>
+                <up-button class="btn" color="#2A6D52" @click="confirmPick">确定</up-button>
+            </view>
+        </view>
+    </u-popup>
+    <up-toast ref="uToastRef" />
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, nextTick, getCurrentInstance } from 'vue';
+import { useClientRequest } from '@/utils/request';
+
+type MaybeStringNumber = string | number;
+interface AreaItem {
+    adcdCode: string;
+    adcdName: string;
+}
+
+interface ConfirmPayload {
+    value: string;
+    name: string;
+    fullNames: string;
+    fullName: string;
+}
+
+interface UtPickerAreaProps {
+    modelValue?: MaybeStringNumber;
+    show?: boolean;
+    title?: string;
+    maxLevel?: number;
+    selectCodeMax?: string;
+    selectedCodes?: string[];
+}
+
+const props = withDefaults(defineProps<UtPickerAreaProps>(), {
+    modelValue: '',
+    show: false,
+    title: '选择区域',
+    maxLevel: 3,
+    selectCodeMax: '',
+    selectedCodes: () => [] as string[],
+});
+
+const emits = defineEmits<{
+    (e: 'update:modelValue', value: string): void;
+    (e: 'update:show', value: boolean): void;
+    (e: 'confirm', payload: ConfirmPayload): void;
+    (e: 'update:selectedCodes', value: string[]): void;
+}>();
+
+const uToastRef = ref<{ show: (opts: any) => void } | null>(null);
+
+const close = () => emits('update:show', false);
+
+// ---------------- 工具与状态 ----------------
+// 统一为字符串,不再强制 6 位;全国仍使用 '000000'
+const normalize6 = (code: MaybeStringNumber): string => String(code ?? '');
+// 修复:全国返回空串用于层级与前缀判断,子级都视为全国的后代
+const trim00Pairs = (code: MaybeStringNumber): string => {
+    const s = normalize6(code);
+    if (s === '000000') return '';
+    if (s.length <= 6) {
+        let t = s;
+        while (t.endsWith('00')) t = t.slice(0, -2);
+        return t;
+    }
+    // 6 位之后不做去 00 处理,直接保留扩展位(如 9/12 位)
+    let first6 = s.slice(0, 6);
+    while (first6.endsWith('00')) first6 = first6.slice(0, -2);
+    return first6 + s.slice(6);
+};
+// 层级:0 全国;<=6 位时按 2 位一层;>6 位时 9/12 位按 3 位一层
+const codeLevel = (code: MaybeStringNumber): number => {
+    const t = trim00Pairs(code);
+    if (!t) return 0;
+    if (t.length <= 6) return Math.floor(t.length / 2);
+    return 3 + Math.ceil((t.length - 6) / 3);
+};
+const isDescendant = (code: MaybeStringNumber, root: MaybeStringNumber): boolean => {
+    const p = trim00Pairs(root);
+    const c = trim00Pairs(code);
+    return c.startsWith(p); // p=='' 时,任何 c 都成立(全国范围)
+};
+
+// 生成用于 nameMap 的最小 6 位标准键:
+// - 全国固定 '000000'
+// - <=6 位:去掉末尾 00 对后右补齐到 6 位
+// - >6 位:取前 6 位,再按上述规则去 00 后补齐到 6 位
+const nameKey = (code: MaybeStringNumber): string => {
+    const s = String(code ?? '');
+    if (!s || s === '000000') return '000000';
+    const base6 = s.length <= 6 ? s : s.slice(0, 6);
+    let t = base6;
+    while (t.endsWith('00') && t.length >= 2) t = t.slice(0, -2);
+    return t.padEnd(6, '0');
+};
+// 规范化用于比较和锚点:
+// - 全国返回 '000000'
+// - <=6 位:去尾部 00 对后右补齐至 6 位
+// - >6 位:保持原始完整码
+const canonical6 = (code: MaybeStringNumber): string => {
+    const s = String(code ?? '');
+    if (!s) return '';
+    if (s === '000000') return '000000';
+    if (s.length <= 6) {
+        let t = s;
+        while (t.endsWith('00')) t = t.slice(0, -2);
+        return t.padEnd(6, '0');
+    }
+    return s;
+};
+const chainFromRoot = (code: MaybeStringNumber): string[] => {
+    const s = normalize6(code);
+    if (!s || s === '000000') return [];
+    const list: string[] = [];
+    // 省/市/区:统一输出为“最少6位”,少于补0,多余去掉末尾00(canonical6 内处理)
+    for (let i = 2; i <= Math.min(6, s.length); i += 2) {
+        list.push(canonical6(s.slice(0, i)));
+    }
+    // 扩展层级:9/12 位,保持真实长度;其 6 位基底已按 canonical6 规范化
+    if (s.length > 6) {
+        const base6 = canonical6(s).slice(0, 6);
+        for (let j = 9; j <= s.length; j += 3) {
+            list.push(base6 + s.slice(6, j));
+        }
+    }
+    return list;
+};
+
+// 根(选择范围)与基础链路
+const baseRoot = computed(() => {
+    const c = normalize6(props.selectCodeMax || '');
+    // 空值或显式全国,统一为 '000000'
+    if (!c || c === '000000') return '000000';
+    return trim00Pairs(c);
+});
+const baseChain = computed(() => {
+    // 完整显示 selectCodeMax 的链路(不包含全国 '000000')
+    return chainFromRoot(baseRoot.value);
+});
+const isNationRoot = computed(() => baseRoot.value === '000000');
+
+// 缓存:父code -> 子数组
+const childrenMap = ref<Record<string, AreaItem[] | undefined>>({});
+// 新增:进行中请求去重缓存(父code -> Promise)
+const pendingMap = ref<Record<string, Promise<AreaItem[]> | undefined>>({});
+// 名称缓存:code -> name
+const nameMap = ref<Record<string, string>>({ '000000': '全国' });
+
+// 选中链路(不包含 baseRoot),例如 baseRoot=省,选中市/区... 这里存 [市, 区, ...]
+const selectedCodes = ref<string[]>(props?.selectedCodes || []);
+const currentValue = ref<string>(''); // 当前选中最终 code
+
+// 计算列:每列的父节点仅为“下一列的父节点”,避免最后一级再多出一列
+const columnParents = computed<string[]>(() => {
+    const parents: string[] = [];
+    const baseL = codeLevel(baseRoot.value); // baseRoot 的绝对层级(全国=0,省=1,市=2...)
+    const maxCols = Math.max(0, props.maxLevel - baseL); // 允许渲染的最大列数(例如全国根且 maxLevel=3 -> 3 列:省/市/区)
+    if (maxCols <= 0) return parents;
+
+    // 第 0 列父节点固定为 baseRoot(展示 baseRoot 的子级)
+    parents.push(baseRoot.value);
+
+    // 后续列的父节点是“上一列选中的 code”,最多渲染到 maxCols 数量;没有选中则不再渲染,避免空列
+    for (let k = 1; k < maxCols; k++) {
+        const parentCode = selectedCodes.value[k - 1];
+        if (!parentCode) break; // 未选择上一列时,不渲染后续空列
+        parents.push(parentCode);
+    }
+    return parents;
+});
+
+// 当前 swiper 索引(指向当前操作的列)
+const currentSwiper = ref<number>(0);
+
+// ---------------- 异步数据获取(带缓存 + 去重) ----------------
+const getChildren = async (parentCode: string): Promise<AreaItem[]> => {
+    const key = normalize6(parentCode);
+    // 命中结果缓存
+    if (childrenMap.value[key]) return childrenMap.value[key] as AreaItem[];
+    // 命中请求去重缓存
+    if (pendingMap.value[key]) return pendingMap.value[key] as Promise<AreaItem[]>;
+
+    const p = (async () => {
+        const res: any = await useClientRequest.get('/app/adcd/listChildrenByCode', {
+            adcdCode: key,
+            leval: 1,
+            pageSize: 1000,
+        });
+        if (res?.code === 200) {
+            const rows: AreaItem[] = (res.rows || []).map((x: any) => ({
+                // 不再截断为 6 位,保留完整 code
+                adcdCode: nameKey(x.adcdCode),
+                adcdName: x.adcdName,
+            }));
+            childrenMap.value[key] = rows; // 写入结果缓存
+            rows.forEach((r) => {
+                // 名称缓存:精确键必存;仅当长度<=6时,才缓存到最少6位标准键,避免覆盖区县名称
+                nameMap.value[r.adcdCode] = r.adcdName;
+                nameMap.value[nameKey(r.adcdCode)] = r.adcdName;
+            });
+            delete pendingMap.value[key]; // 清除进行中缓存
+            return rows;
+        }
+        childrenMap.value[key] = [];
+        delete pendingMap.value[key];
+        return [];
+    })();
+
+    pendingMap.value[key] = p;
+    return p;
+};
+
+// 确保能显示 baseChain 的名称:逐级拉取上级的子级来命名
+const ensureBaseNames = async () => {
+    // 若 baseChain 为空无需处理
+    if (!baseChain.value.length) return;
+    // 第一层名称来自全国的子级
+    await getChildren('000000');
+    // 逐级往下,确保每一层的 name 显示
+    for (let i = 0; i < baseChain.value.length; i++) {
+        const parent = i === 0 ? '000000' : baseChain.value[i - 1];
+        await getChildren(parent);
+    }
+};
+
+// 获取名称
+const getNameByCode = (code: string | number): string => {
+    const c = normalize6(code);
+    if (c === '000000') return '全国';
+    // 先查精确键
+    let val = nameMap.value[c];
+    if (val) return val;
+    // 对长度<=6的code再查最少6位标准键,避免用>6位的名称覆盖6位层级
+    if (c.length <= 6) {
+        const k6 = nameKey(c);
+        val = nameMap.value[k6];
+        if (val) return val;
+    }
+    // 即时回退:在已加载的 childrenMap 中查找匹配项并缓存
+    for (const rows of Object.values(childrenMap.value)) {
+        if (!rows) continue;
+        const found = rows.find((r) => r.adcdCode === c);
+        if (found) {
+            nameMap.value[c] = found.adcdName;
+            if (String(found.adcdCode).length <= 6) {
+                nameMap.value[nameKey(found.adcdCode)] = found.adcdName;
+            }
+            return found.adcdName;
+        }
+    }
+    return '';
+};
+
+// ---------------- 回显与列构造 ----------------
+const toast = (msg: string) => {
+    uToastRef.value?.show({ message: msg, type: 'error' });
+};
+
+// 将传入值回显为 selectedCodes(不含 baseRoot)
+const applyModelValue = async (val: MaybeStringNumber): Promise<void> => {
+    const v = normalize6(val);
+    // 确保基础链路名称已就绪,避免回显时中文缺失
+    await ensureBaseNames();
+    if (!v) {
+        selectedCodes.value = [];
+        currentValue.value = '';
+        currentSwiper.value = Math.min(columnParents.value.length - 1, 0);
+        return;
+    }
+    if (v === '000000') {
+        selectedCodes.value = [];
+        currentValue.value = '000000';
+        await ensureColumnsData();
+        currentSwiper.value = 0;
+        scrollToAllSelected();
+        return;
+    }
+    // 新增:等于 baseRoot 自身时
+    if (v === baseRoot.value) {
+        selectedCodes.value = [];
+        currentValue.value = baseRoot.value;
+        await ensureColumnsData();
+        currentSwiper.value = 0;
+        scrollToAllSelected();
+        return;
+    }
+    // 必须在选择范围内
+    if (!isDescendant(v, baseRoot.value)) {
+        toast('不在选择范围内');
+        selectedCodes.value = [];
+        currentValue.value = '';
+        currentSwiper.value = 0;
+        return;
+    }
+    const full = chainFromRoot(v);
+    const base = baseChain.value;
+    const baseL = codeLevel(baseRoot.value);
+    const maxCols = Math.max(0, props.maxLevel - baseL);
+    const picked = full.slice(base.length, base.length + maxCols);
+
+    
+    selectedCodes.value = picked;
+    currentValue.value = picked.length ? picked[picked.length - 1] : baseRoot.value;
+    await ensureColumnsData();
+    currentSwiper.value = Math.max(0, Math.min(selectedCodes.value.length - 1, columnParents.value.length - 1));
+    scrollToAllSelected();
+};
+
+// 确保每一列的 children 数据已拉取
+const ensureColumnsData = async (): Promise<void> => {
+    for (const parent of columnParents.value) {
+        await getChildren(parent);
+    }
+};
+
+// ---------------- 交互 ----------------
+const onSwiperChange = (e: any) => {
+    const idx = e?.detail?.current ?? 0;
+    const maxIdx = Math.max(0, columnParents.value.length - 1);
+    currentSwiper.value = Math.min(idx, maxIdx);
+};
+
+// 跳到链路的某一级进行重新选择
+const jumpToLevel = (idx: number) => {
+    // 只能跳选中链路,且 idx 为选中链路下标(不包含 baseChain)
+    currentSwiper.value = idx;
+};
+
+// 选择某列的一项(到达最后一级时自动确认)
+const onPick = async (item: AreaItem, colIdx: number) => {
+    const code = canonical6(item.adcdCode);
+    if (!isDescendant(code, baseRoot.value)) {
+        return toast('不在选择范围内');
+    }
+    // 特判:误点到全国(正常不会走到这,因为全国走 onPickNation)
+    if (code === '000000' && isNationRoot.value && colIdx === 0) {
+        return onPickNation();
+    }
+
+    selectedCodes.value = [...selectedCodes.value.slice(0, colIdx), code];
+    currentValue.value = code;
+
+    // 是否达到允许的最后一列(相对 baseRoot 的列数)
+    const baseL = codeLevel(baseRoot.value);
+    const maxCols = Math.max(0, props.maxLevel - baseL);
+    const reachedFinalColumn = (colIdx + 1) >= maxCols;
+    if (reachedFinalColumn) {
+        scrollToAllSelected();
+        confirmPick();
+        return;
+    }
+
+    await getChildren(code);
+    const nextChildren = childrenMap.value[code] || [];
+    currentSwiper.value = nextChildren.length ? colIdx + 1 : colIdx;
+    scrollToAllSelected();
+};
+
+// 选全国
+const onPickNation = (): void => {
+    if (!isNationRoot.value) return;
+    selectedCodes.value = [];
+    currentValue.value = '000000';
+    currentSwiper.value = 0;
+    scrollToAllSelected();
+};
+
+// 选“选择范围自身”(非全国时的顶部项,如:云南省)
+const onPickBaseRoot = async (): Promise<void> => {
+    if (isNationRoot.value) return; // 全国时不走这里
+    // 将当前值选为 baseRoot,自身级别
+    selectedCodes.value = [];
+    currentValue.value = baseRoot.value;
+    currentSwiper.value = 0;
+
+    // 若允许的列数仅 1(选择范围本身即最终列),直接确认
+    const baseL = codeLevel(baseRoot.value);
+    const maxCols = Math.max(0, props.maxLevel - baseL);
+    if (maxCols <= 1) {
+        scrollToAllSelected();
+        confirmPick();
+        return;
+    }
+
+    // 否则保持在第1列,滚动到该项
+    await ensureColumnsData();
+    scrollToAllSelected();
+};
+
+// 组装完整名称:包含基础链路与已选链路(全国仅返回“全国”)
+const buildFullName = (): string => {
+  if (currentValue.value === '000000') return '全国';
+  const baseNames = baseChain.value.map((code) => getNameByCode(code));
+  // 若当前值是 baseRoot 自身(选择范围自身),则不重复追加 selectedCodes
+  const selNames = selectedCodes.value.map((code) => getNameByCode(code));
+  return [...baseNames, ...selNames].filter(Boolean).join('');
+};
+
+const confirmPick = (): void => {
+    if (!currentValue.value && isNationRoot.value) currentValue.value = '000000';
+
+    const codeList = selectedCodes.value.slice();
+    const nameList = codeList.map((i) => getNameByCode(i));
+    const fullName = buildFullName();
+
+    emits('update:modelValue', currentValue.value);
+    emits('confirm', {
+        value: currentValue.value,
+        name: getNameByCode(currentValue.value),
+        fullNames: nameList.join(''),
+        fullName // 新增:完整地址名称
+    });
+    emits('update:selectedCodes', codeList);
+    close();
+};
+
+// ---------------- 滚动(H5 兼容)----------------
+// 选中项锚点
+const scrollIntoViewArr = ref<string[]>([]);
+const instance = getCurrentInstance();
+const proxy = instance && instance.proxy;
+
+const refreshScrollArrays = (): void => {
+    const len = columnParents.value.length;
+    // 初始化长度匹配列数
+    if (scrollIntoViewArr.value.length !== len) {
+        scrollIntoViewArr.value = Array(len).fill('');
+    }
+};
+
+// 仅用锚点滚动
+const updateScrollIntoView = (): void => {
+    refreshScrollArrays();
+    scrollIntoViewArr.value = scrollIntoViewArr.value.map(() => '');
+    nextTick(() => {
+        scrollIntoViewArr.value = columnParents.value.map((_, i) => {
+            if (i === 0 && isNationRoot.value && currentValue.value === '000000') return 'area-item-0-000000';
+            if (i === 0 && !isNationRoot.value && currentValue.value === baseRoot.value) return `area-item-0-${canonical6(baseRoot.value)}`;
+            const code = selectedCodes.value[i];
+            return code ? `area-item-${i}-${canonical6(code)}` : '';
+        });
+    });
+};
+
+const scrollToAllSelected = (): void => {
+    updateScrollIntoView();
+};
+
+// ---------------- 监听与初始化 ----------------
+const rebuildByInputs = async (): Promise<void> => {
+    await ensureBaseNames();
+    await ensureColumnsData();
+    await applyModelValue(String(props.modelValue || ''));
+};
+
+watch(
+    () => [props.selectCodeMax, props.maxLevel],
+    async () => {
+        // 变更范围或层级,重建链路与列
+        selectedCodes.value = [];
+        currentValue.value = '';
+        currentSwiper.value = 0;
+        await rebuildByInputs();
+    },
+    { immediate: true }
+);
+
+watch(
+    () => props.modelValue,
+    async (nv) => {
+        await applyModelValue(String(nv || ''));
+    }
+);
+
+// 每次列父集合变化时,刷新列数据并滚动
+watch(
+    () => columnParents.value.slice(),
+    async () => {
+        await ensureColumnsData();
+        // 定位到当前列并滚动选中项
+        currentSwiper.value = Math.max(0, Math.min(currentSwiper.value, columnParents.value.length - 1));
+        scrollToAllSelected();
+    }
+);
+
+// 点击顶部“全国”:切到省级并滚动到省级区域
+const onClickNationBreadcrumb = async (): Promise<void> => {
+    if (!isNationRoot.value) return;
+    // 确保省级数据已加载
+    await getChildren('000000');
+
+    // 切到第 0 列(省级)
+    currentSwiper.value = 0;
+
+    // 保证滚动数组长度正确
+    refreshScrollArrays();
+
+    // 有已选省则对齐该省;否则滚到顶部
+    nextTick(() => {
+        const first = selectedCodes.value[0];
+        scrollIntoViewArr.value.splice(0, 1, first ? `area-item-0-${canonical6(first)}` : '');
+    });
+};
+
+// 点击顶部“选择范围自身”(如云南省):切到第0列(市级)并滚动到已选市;未选则回到顶部
+const onClickBaseBreadcrumb = async (): Promise<void> => {
+    if (isNationRoot.value) return; // 全国时不走这里
+    await getChildren(baseRoot.value); // 确保市级数据已加载
+    currentSwiper.value = 0; // 切到第0列(展示 baseRoot 的子级:市)
+    refreshScrollArrays();
+    nextTick(() => {
+        const selCity = selectedCodes.value[0]; // 若已有选中的市
+        scrollIntoViewArr.value.splice(0, 1, selCity ? `area-item-0-${canonical6(selCity)}` : '');
+    });
+};
+</script>
+
+<style scoped lang="scss">
+.up-picker-area {
+    height: 70vh; /* 高度 70vh */
+    display: flex;
+    flex-direction: column;
+    background: #fff;
+}
+
+.area-title {
+    padding: 20rpx 24rpx;
+    font-size: 32rpx;
+    font-weight: 600;
+    text-align: center;
+    color: #000;
+    border-bottom: 1px solid #f2f2f2;
+}
+
+.area-breadcrumb {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    padding: 16rpx 24rpx;
+    gap: 8rpx;
+    border-bottom: 1px solid #f2f2f2;
+
+    .crumb {
+        font-size: 28rpx;
+        color: #333;
+        padding: 8rpx 12rpx;
+        border-radius: 8rpx;
+
+        &.active {
+            font-weight: 700;
+            color: #000;
+        }
+
+        &.disabled {
+            color: #999;
+        }
+    }
+
+    .sep {
+        color: #bbb;
+        padding: 0 8rpx;
+    }
+}
+
+.area-body {
+    flex: 1;
+    overflow: hidden;
+}
+
+.area-swiper {
+    height: 100%;
+}
+
+.area-scroll {
+    height: 100%;
+    /* 移除 will-change 与像素滚动相关样式,仅保留稳定布局 */
+}
+
+
+.picker-item {
+    display: flex;
+    align-items: center;
+    padding: 24rpx 32rpx;
+    border-bottom: 1px solid #f5f5f5;
+
+    .name {
+        flex: 1;
+        font-size: 30rpx;
+        color: #333;
+
+        &.selected {
+            font-weight: 700;
+            color: #000; /* 选中项黑色加粗 */
+        }
+    }
+}
+
+.area-footer {
+    display: flex;
+    gap: 20rpx;
+    padding: 20rpx 24rpx;
+    border-top: 1px solid #f2f2f2;
+    background: #fff;
+
+    .btn {
+        flex: 1;
+    }
+}
+</style>

+ 6 - 0
src/main.ts

@@ -31,6 +31,12 @@ const uviewProps: any = {
             fontSize: '30rpx',
             placeholderStyle: 'color: #ccc; font-weight: 400;',
         },
+        radio: {
+            activeColor: '#37A954',
+        },
+        checkbox: {
+            activeColor: '#37A954',
+        }
     },
 };
 export function createApp() {

+ 7 - 0
src/pages.json

@@ -116,6 +116,13 @@
                         "enablePullDownRefresh": false,
                         "navigationStyle": "default"
                     }
+                },
+                // 选择企业人员
+                {
+                    "path": "select-cpy-member/index",
+                    "style": {
+                        "navigationBarTitleText": "选择企业成员"
+                    }
                 }
             ]
         }

+ 68 - 37
src/plant/base/base-edit/models/base-info/base-info.vue

@@ -5,9 +5,9 @@
                 <up-alert type="primary" fontSize="24rpx" description="注意:基地不强制与品种挂钩!基地地址以行政村为界,可成片集中或相对集中,跨村则视为另一基地(连片跨村除外)。"></up-alert>
                 <view class="pd-10"></view>
                 <up-form class="p-rtv" labelPosition="top" :model="form" :rules="rules" labelWidth="auto" ref="upFormRef">
-                    <ut-action-sheet v-model="form.baseType" :tabs="yes_no" title="选择基地类型">
-                        <up-form-item borderBottom label="基地类型" required prop="attestationResult">
-                            <view v-if="form.baseType" class="f-s-30 c-333 f-w-5 flex1">{{ selectDictLabel(yes_no, form.baseType) }}</view>
+                    <ut-action-sheet v-model="form.baseType" :tabs="pt_base_type" title="选择基地类型">
+                        <up-form-item borderBottom label="基地类型" required prop="baseType">
+                            <view v-if="form.baseType" class="f-s-30 c-333 f-w-5 flex1">{{ selectDictLabel(pt_base_type, form.baseType) }}</view>
                             <view v-else class="f-s-30 c-ccc f-w-4 flex1">请选择基地类型</view>
                             <template #right>
                                 <up-icon size="22rpx" color="#2A6D52" name="arrow-down-fill"></up-icon>
@@ -15,22 +15,24 @@
                         </up-form-item>
                     </ut-action-sheet>
                     <!-- 基地名称 -->
+                    <view class="h-1" id="baseNamepppp"></view>
                     <up-form-item borderBottom label="基地名称" required prop="baseName">
                         <up-input v-model="form.baseName" placeholder="请输入基地名称" border="none" clearable></up-input>
                     </up-form-item>
                     <!-- 基地编号 -->
+                    <view class="h-1" id="baseCodepppp"></view>
                     <up-form-item borderBottom label="基地编号" prop="baseCode">
                         <up-input v-model="form.baseCode" placeholder="请输入基地编号" border="none" clearable></up-input>
                     </up-form-item>
-                    <ut-datetime-picker v-model="form.dateTime" mode="date" dateFields="year">
-                        <up-form-item borderBottom label="建设时间" required>
-                            <up-input v-model="form.dateTime" placeholder="请选择基地建设年份" border="none" clearable></up-input>
+                    <ut-datetime-picker v-model="form.buildDate" mode="date" dateFields="year">
+                        <up-form-item borderBottom label="建设时间" required prop="buildDate">
+                            <up-input v-model="form.buildDate" placeholder="请选择基地建设年份" border="none" clearable></up-input>
                         </up-form-item>
                     </ut-datetime-picker>
                     <!-- 选择基地组织方式 -->
-                    <ut-action-sheet v-model="form.organizationMethod" :tabs="yes_no" title="选择基地组织方式">
-                        <up-form-item borderBottom label="基地组织方式" required>
-                            <view v-if="form.organizationMethod" class="f-s-30 c-333 f-w-5 flex1">{{ selectDictLabel(yes_no, form.organizationMethod) }}</view>
+                    <ut-action-sheet v-model="form.orgType" :tabs="pt_org_type" title="选择基地组织方式">
+                        <up-form-item borderBottom label="基地组织方式" required prop="orgType">
+                            <view v-if="form.orgType" class="f-s-30 c-333 f-w-5 flex1">{{ selectDictLabel(pt_org_type, form.orgType) }}</view>
                             <view v-else class="f-s-30 c-ccc f-w-4 flex1">请选择基地组织方式</view>
                             <template #right>
                                 <up-icon size="22rpx" color="#2A6D52" name="arrow-down-fill"></up-icon>
@@ -38,18 +40,17 @@
                         </up-form-item>
                     </ut-action-sheet>
                     <!-- 选择基地负责人 -->
-                    <ut-action-sheet v-model="form.personInCharge" :tabs="yes_no" title="选择基地负责人">
-                        <up-form-item borderBottom label="基地负责人" required>
-                            <view v-if="form.personInCharge" class="f-s-30 c-333 f-w-5 flex1">{{ selectDictLabel(yes_no, form.personInCharge) }}</view>
-                            <view v-else class="f-s-30 c-ccc f-w-4 flex1">请选择基地负责人</view>
-                            <template #right>
-                                <up-icon size="22rpx" color="#2A6D52" name="arrow-down-fill"></up-icon>
-                            </template>
-                        </up-form-item>
-                    </ut-action-sheet>
+
+                    <up-form-item @click="selectCpyMember"  borderBottom label="基地负责人" required prop="contactId">
+                        <view v-if="form.contactId" class="f-s-30 c-333 f-w-5 flex1">{{ form.contactName }}</view>
+                        <view v-else class="f-s-30 c-ccc f-w-4 flex1">请选择基地负责人</view>
+                        <template #right>
+                            <up-icon size="22rpx" color="#2A6D52" name="arrow-down-fill"></up-icon>
+                        </template>
+                    </up-form-item>
                     <!-- 填写基地联系电话 -->
-                    <up-form-item borderBottom label="基地联系电话" required prop="contactNumber">
-                        <up-input v-model="form.contactNumber" placeholder="请输入基地联系电话" border="none" clearable></up-input>
+                    <up-form-item borderBottom label="基地联系电话" required prop="contactTel">
+                        <up-input v-model="form.contactTel" placeholder="请输入基地联系电话" border="none" clearable></up-input>
                     </up-form-item>
                     <!-- 是否Gap基地 -->
                     <up-form-item borderBottom label="是否为Gap基地" required prop="gapFlag">
@@ -57,43 +58,45 @@
                             <up-radio :customStyle="{ marginRight: '60rpx' }" v-for="(item, index) in yes_no" :key="index" :label="item.label" :name="item.value"></up-radio>
                         </up-radio-group>
                     </up-form-item>
-                    <up-form-item borderBottom label="基地具体地址" required prop="address">
-                        <up-input v-model="form.address" placeholder="请选择基地所在省/市/县/镇(乡)" border="none" clearable></up-input>
+                    <up-form-item @click="showArea = true" borderBottom label="基地具体地址" required prop="gapInfo.adcode">
+                        <!-- <up-input v-model="form.address" placeholder="请选择基地所在省/市/县/镇(乡)" border="none" clearable></up-input> -->
+                        <view v-if="form?.gapInfo?.adcode" class="f-s-30 c-333 f-w-5 flex1">{{ form?.gapInfo?.adcodeName }}</view>
+                        <view v-else class="f-s-30 c-ccc f-w-4 flex1">请选择基地所在省/市/县/镇(乡)</view>
                         <template #right>
                             <up-icon size="22rpx" color="#2A6D52" name="arrow-down-fill"></up-icon>
                         </template>
                     </up-form-item>
-                    <up-form-item borderBottom prop="address">
-                        <up-input v-model="form.address" placeholder="请填写村级以下的具体地址信息" border="none" clearable></up-input>
+                    <up-form-item borderBottom prop="gapInfo.address">
+                        <up-input v-model="form.gapInfo.address" placeholder="请填写村级以下的具体地址信息" border="none" clearable></up-input>
                     </up-form-item>
                     <up-form-item borderBottom>
                         <view class="flex1 ov-hd">
-                            <view class="mb-10">
-                                <span>基地范围</span>
+                              <view class="d-flex a-c mb-10" style="margin-bottom: 5px;">
+                                <view class="f-s-30 c-#666">基地范围</view>
                             </view>
                             <view class="bg-#ccc d-flex ov-hd p-rtv" @click="mapDrawArea">
-                                <image class="w-full h-380" v-if="form.mapArea?.mapImageUrl" :src="form.mapArea.mapImageUrl" mode="widthFix" />
+                                <image class="w-full h-380" v-if="form.gapInfo?.basePic" :src="form.gapInfo.basePic" mode="widthFix" />
                                 <image class="w-full h-380" v-else src="@/static/images/plant/base/select_base_gap.png" mode="widthFix" />
-                                <view class="btn-aree-center d-flex flex-cln a-c j-c">
+                                <view v-if="!form.gapInfo?.basePic" class="btn-aree-center d-flex flex-cln a-c j-c">
                                     <image class="w-52 h-52 mb-10" src="@/static/images/plant/base/draw_area_icon.png" mode="widthFix" />
                                     <view class="c-primary f-s-28 f-w-400">点击绘制基地范围</view>
                                 </view>
                             </view>
                         </view>
                     </up-form-item>
-                    <up-form-item required borderBottom label="基地面积" prop="areaSize">
-                        <up-input v-model="form.areaSize" placeholder="地块绘制后自动带出可修改" border="none" clearable></up-input>
+                    <up-form-item required borderBottom label="基地面积" prop="gapInfo.area">
+                        <up-input v-model="form.gapInfo.area" placeholder="地块绘制后自动带出可修改" border="none" clearable></up-input>
                         <template #right>
-                            <span></span>
+                            <span>{{ form.gapInfo.areaUnit }}</span>
                         </template>
                     </up-form-item>
                     <!-- 基地经纬度 -->
-                    <up-form-item label="基地经纬度" required prop="latitudeLongitude">
+                    <up-form-item label="基地经纬度" required prop="gapInfo.latitudeLongitude">
                         <view class="flex1 d-flex a-c">
                             <!-- 基地经度和基地纬度分开 -->
-                            <up-input v-model="form.latitude" border="bottom" placeholder="70-150内的经度数值" clearable></up-input>
+                            <up-input v-model="form.gapInfo.lat" border="bottom" placeholder="70-150内的经度数值" clearable></up-input>
                             <view class="pd-5"></view>
-                            <up-input v-model="form.longitude" placeholder="4-53内的纬度数值" border="bottom" clearable></up-input>
+                            <up-input v-model="form.gapInfo.lng" placeholder="4-53内的纬度数值" border="bottom" clearable></up-input>
                         </view>
                     </up-form-item>
                 </up-form>
@@ -103,18 +106,25 @@
             </template>
         </z-paging>
     </view>
+    <ut-picker-area v-model:show="showArea" v-model="form.gapInfo.adcode" @confirm="confirmArea"></ut-picker-area>
 </template>
 <script setup lang="ts" name="base-info">
+import { useClientRequest } from '@/utils/request';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { yes_no } = toRefs<any>(proxy?.useDict('yes_no'));
+const { yes_no, pt_org_type, pt_base_type } = toRefs<any>(proxy?.useDict('yes_no', 'pt_org_type', 'pt_base_type'));
 
 const paging = ref<any>(null);
 const upFormRef = ref<any>(null);
+const showArea = ref(false);
 const form = ref<any>({
     baseType: '',
     baseName: '',
     baseCode: '',
-    mapArea: {},
+    gapInfo: {
+        adcode: '',
+        adcodeName: '',
+        address: '',
+    },
 });
 const rules = reactive({
     baseType: [{ required: true, message: '请选择基地类型', trigger: 'change' }],
@@ -124,14 +134,35 @@ const mapDrawArea = () => {
     uni.$on('mapAreaData', (data: any) => {
         console.log('接收到地图绘制区域数据:', data);
         // 这里可以将 data 赋值给 form 中的相应字段
-        form.value.mapArea = data?.amapDrawData;
+        form.value.gapInfo.basePic = data.gapInfo.basePic;
+        form.value.gapInfo.area = data.gapInfo.area;
+        form.value.gapInfo.areaUnit = data.gapInfo.areaUnit;
+        form.value.coordinates = data.coordinates;
         // 删除监听,防止重复触发
         uni.$off('mapAreaData');
     });
     uni.$u.route({
+        type: 'navigateTo',
         url: '/tools/map-draw-area/index',
     });
 };
+
+const selectCpyMember = () => {
+    uni.$on('selectCpyMember', (item: any) => {
+        form.value.contactId = item.userInfo?.id;
+        form.value.contactName = item.userInfo?.name;
+        form.value.contactTel = item.userInfo?.phone;
+        uni.$off('selectCpyMember');
+    });
+    uni.$u.route({
+        type: 'navigateTo',
+        url: '/tools/select-cpy-member/index',
+    });
+};
+const confirmArea = (area: any) => {
+    form.value.gapInfo.adcodeName = area.fullName;
+};
+onLoad(() => {});
 </script>
 <style lang="scss" scoped>
 .z-paging-wrap {

+ 63 - 0
src/tools/select-cpy-member/index.vue

@@ -0,0 +1,63 @@
+<template>
+    <z-paging ref="paging" safe-area-inset-bottom v-model="list" @query="query">
+        <template #top>
+            <up-navbar title="选择企业负责人" border :fixed="false"></up-navbar>
+            <view class="pd-20">
+                <ut-search margin="0" height="68rpx" fontSize="26rpx" placeholder="请输入企业负责人姓名" border v-model="form.keyword" @search="onRefresh"></ut-search>
+            </view>
+        </template>
+        <view class="base-content pd-20">
+            <view :class="{ active: item.id === checkedId }" v-for="(item, index) in list" :key="item.id" @click="clickItem(item)" class="bg-fff mb-10 pd-20 select-item-card d-flex">
+                <view class="mr-20">
+                    <ut-image :src="item.userInfo?.avatar" width="100rpx" height="100rpx"></ut-image>
+                </view>
+                <view class="flex1">
+                    <view class="f-s-30 c-#333 f-w-600">{{ item?.userInfo?.name }}</view>
+                    <view class="f-s-28 c-#999">手机号:{{ item.userInfo?.phone }}</view>
+                </view>
+            </view>
+        </view>
+    </z-paging>
+</template>
+<script setup lang="ts">
+import { useClientRequest } from '@/utils/request';
+const paging = ref<any>(null);
+const list = ref<any[]>([]);
+const form = ref({
+    keyword: '',
+});
+const query = async (pageNum: number, pageSize: number) => {
+    const res = await useClientRequest.get('/app/company/members', {
+        ...form.value,
+        pageNum: pageNum,
+        pageSize: pageSize,
+        cpyid: '1795009980763025410',
+    });
+    if (res.code == 200) {
+        paging.value.complete(res.rows || []);
+    }
+};
+const checkedId = ref(null);
+const clickItem = (item: any) => {
+    checkedId.value = item.id;
+    uni.$emit('selectCpyMember', item);
+    uni.navigateBack();
+};
+const onRefresh = () => {
+    try {
+        paging.value?.reload();
+    } catch (e) {
+        console.error('Error refreshing address list:', e);
+    }
+};
+</script>
+<style lang="scss" scoped>
+.select-item-card {
+    box-shadow: 0rpx 2rpx 8rpx rgba(0, 0, 0, 0.1);
+    border: 1rpx solid #e8e8e8;
+
+    &.active {
+        border-color: #4456fb;
+    }
+}
+</style>

+ 44 - 5
src/types/auto-imports.d.ts

@@ -6,12 +6,18 @@
 // biome-ignore lint: disable
 export {}
 declare global {
+  const EffectScope: typeof import('vue')['EffectScope']
+  const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
   const computed: typeof import('vue')['computed']
   const createApp: typeof import('vue')['createApp']
+  const createPinia: typeof import('pinia')['createPinia']
   const customRef: typeof import('vue')['customRef']
+  const debounce: typeof import('uview-plus')['debounce']
   const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
   const defineComponent: typeof import('vue')['defineComponent']
+  const defineStore: typeof import('pinia')['defineStore']
   const effectScope: typeof import('vue')['effectScope']
+  const getActivePinia: typeof import('pinia')['getActivePinia']
   const getCurrentInstance: typeof import('vue')['getCurrentInstance']
   const getCurrentScope: typeof import('vue')['getCurrentScope']
   const h: typeof import('vue')['h']
@@ -20,21 +26,48 @@ declare global {
   const isReactive: typeof import('vue')['isReactive']
   const isReadonly: typeof import('vue')['isReadonly']
   const isRef: typeof import('vue')['isRef']
+  const mapActions: typeof import('pinia')['mapActions']
+  const mapGetters: typeof import('pinia')['mapGetters']
+  const mapState: typeof import('pinia')['mapState']
+  const mapStores: typeof import('pinia')['mapStores']
+  const mapWritableState: typeof import('pinia')['mapWritableState']
   const markRaw: typeof import('vue')['markRaw']
   const nextTick: typeof import('vue')['nextTick']
   const onActivated: typeof import('vue')['onActivated']
+  const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites']
+  const onBackPress: typeof import('@dcloudio/uni-app')['onBackPress']
   const onBeforeMount: typeof import('vue')['onBeforeMount']
-  // const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
-  // const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
   const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
   const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
   const onDeactivated: typeof import('vue')['onDeactivated']
+  const onError: typeof import('@dcloudio/uni-app')['onError']
   const onErrorCaptured: typeof import('vue')['onErrorCaptured']
+  const onHide: typeof import('@dcloudio/uni-app')['onHide']
+  const onLaunch: typeof import('@dcloudio/uni-app')['onLaunch']
+  const onLoad: typeof import('@dcloudio/uni-app')['onLoad']
   const onMounted: typeof import('vue')['onMounted']
+  const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']
+  const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']
+  const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']
+  const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']
+  const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']
+  const onPageNotFound: typeof import('@dcloudio/uni-app')['onPageNotFound']
+  const onPageScroll: typeof import('@dcloudio/uni-app')['onPageScroll']
+  const onPullDownRefresh: typeof import('@dcloudio/uni-app')['onPullDownRefresh']
+  const onReachBottom: typeof import('@dcloudio/uni-app')['onReachBottom']
+  const onReady: typeof import('@dcloudio/uni-app')['onReady']
   const onRenderTracked: typeof import('vue')['onRenderTracked']
   const onRenderTriggered: typeof import('vue')['onRenderTriggered']
+  const onResize: typeof import('@dcloudio/uni-app')['onResize']
   const onScopeDispose: typeof import('vue')['onScopeDispose']
   const onServerPrefetch: typeof import('vue')['onServerPrefetch']
+  const onShareAppMessage: typeof import('@dcloudio/uni-app')['onShareAppMessage']
+  const onShareTimeline: typeof import('@dcloudio/uni-app')['onShareTimeline']
+  const onShow: typeof import('@dcloudio/uni-app')['onShow']
+  const onTabItemTap: typeof import('@dcloudio/uni-app')['onTabItemTap']
+  const onThemeChange: typeof import('@dcloudio/uni-app')['onThemeChange']
+  const onUnhandledRejection: typeof import('@dcloudio/uni-app')['onUnhandledRejection']
+  const onUnload: typeof import('@dcloudio/uni-app')['onUnload']
   const onUnmounted: typeof import('vue')['onUnmounted']
   const onUpdated: typeof import('vue')['onUpdated']
   const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
@@ -43,9 +76,12 @@ declare global {
   const readonly: typeof import('vue')['readonly']
   const ref: typeof import('vue')['ref']
   const resolveComponent: typeof import('vue')['resolveComponent']
+  const setActivePinia: typeof import('pinia')['setActivePinia']
+  const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
   const shallowReactive: typeof import('vue')['shallowReactive']
   const shallowReadonly: typeof import('vue')['shallowReadonly']
   const shallowRef: typeof import('vue')['shallowRef']
+  const storeToRefs: typeof import('pinia')['storeToRefs']
   const toRaw: typeof import('vue')['toRaw']
   const toRef: typeof import('vue')['toRef']
   const toRefs: typeof import('vue')['toRefs']
@@ -56,10 +92,7 @@ declare global {
   const useCssModule: typeof import('vue')['useCssModule']
   const useCssVars: typeof import('vue')['useCssVars']
   const useId: typeof import('vue')['useId']
-  // const useLink: typeof import('vue-router')['useLink']
   const useModel: typeof import('vue')['useModel']
-  // const useRoute: typeof import('vue-router')['useRoute']
-  // const useRouter: typeof import('vue-router')['useRouter']
   const useSlots: typeof import('vue')['useSlots']
   const useTemplateRef: typeof import('vue')['useTemplateRef']
   const watch: typeof import('vue')['watch']
@@ -67,3 +100,9 @@ declare global {
   const watchPostEffect: typeof import('vue')['watchPostEffect']
   const watchSyncEffect: typeof import('vue')['watchSyncEffect']
 }
+// for type re-export
+declare global {
+  // @ts-ignore
+  export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
+  import('vue')
+}

+ 9 - 10
src/types/global.d.ts

@@ -90,16 +90,15 @@ declare global {
     }
 }
 
-import '@vue/runtime-core';
-
 declare module '@vue/runtime-core' {
-  export interface ComponentCustomProperties {
-    navigateBackOrHome: () => void;
-    showToast: (title: string) => void;
-    selectDictLabel: (datas: any, value: number | string) => string;
-    selectDictLabels: (datas: any, value: any, separator?: any) => string;
-    useDict: (...args: string[]) => { [key: string]: DictDataOption[] };
-    $u: any;
-  }
+    interface ComponentCustomProperties {
+        /** uview-plus 全局工具对象 */
+        $u: UviewUtils;
+    }
 }
 export {};
+
+declare interface UviewUtils {
+    getPx: (value: string | number, unit?: 'rpx' | 'px') => number;
+    [key: string]: any;
+}

Diff do ficheiro suprimidas por serem muito extensas
+ 2 - 0
stats.html


Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff