lisy 1 개월 전
부모
커밋
3e76369723

+ 4 - 4
src/assets/styles/common.scss

@@ -129,7 +129,7 @@
 // 底部样式
 .base-bottom-wrap {
     position: relative;
-    box-shadow: 6rpx 0px 27rpx 0px rgba(#2a6d52, 0.3);
+    box-shadow: 6rpx 0px 27rpx 0px rgba(#1D9C3E, 0.3);
     background-color: #fff;
 }
 
@@ -243,7 +243,7 @@
         align-items: center;
         flex: 1;
         height: 100%;
-        color: #2A6D52;
+        color: #1D9C3E;
         background-color: #D4E2DC;
     }
     .btn2 {
@@ -253,7 +253,7 @@
         flex: 1;
         height: 100%;
         color: #FBE6A9;
-        background-color: #2A6D52;
+        background-color: #1D9C3E;
     }
 }
 .w-s-no{
@@ -269,7 +269,7 @@
 
 .btm-wrap-shadow {
     position: relative;
-    box-shadow: 6rpx 0px 27rpx 0px rgba(#2A6D52, 0.3);
+    box-shadow: 6rpx 0px 27rpx 0px rgba(#1D9C3E, 0.3);
 }
 .outview {
     width: 100%;

+ 167 - 0
src/components/ut-tabs/ut-tabs.vue

@@ -0,0 +1,167 @@
+<template>
+    <template v-if="mode == 'scroll-x'">
+        <scroll-view scroll-x class="ut-tabs-scroll" show-scrollbar="false" :scroll-into-view-offset="SCROLL_OFFSET"
+            :scroll-into-view="scrollIntoView" ref="scrollViewRef" scroll-with-animation>
+            <view class="ut-tabs-row">
+                <view v-for="(tab, idx) in tabs" :key="tab.value" :id="'tab-' + idx" class="ut-tab-item p-rtv"
+                    :class="{ active: idx === activeIndex }" @click="selectTab(idx)" ref="tabRefs">
+                    <view class="tab-label" :style="{
+                        fontSize: fontSize,
+                        color: idx === activeIndex ? themeColor : '#999',
+                        fontWeight: idx === activeIndex ? 'bold' : '500'
+                    }">
+                        <span>{{ tab.label }}</span>
+                        <view v-if="tab.badge && tab.num as number > 0" class="tab-badge">{{ tab.num }}</view>
+                    </view>
+                    <view v-if="idx === activeIndex" class="tab-underline" :style="{
+                        background: themeColor,
+                        height: lineHeight
+                    }"></view>
+                </view>
+            </view>
+        </scroll-view>
+    </template>
+    <template v-if="mode == 'btw'">
+        <view class="ut-tabs-row" :style="{ justifyContent: 'space-between' }">
+            <view v-for="(tab, idx) in tabs" :key="tab.value" class="ut-tab-item p-rtv"
+                :class="{ active: idx === activeIndex }" @click="selectTab(idx)" ref="tabRefs">
+                <view class="tab-label" :style="{
+                    fontSize: fontSize,
+                    color: idx === activeIndex ? themeColor : '#999',
+                    fontWeight: idx === activeIndex ? 'bold' : '500'
+                }">
+                    <span>{{ tab.label }}</span>
+                    <view v-if="tab.badge && tab.num as number > 0" class="tab-badge">{{ tab.num }}</view>
+                </view>
+                <view v-if="idx === activeIndex" class="tab-underline" :style="{
+                    background: themeColor,
+                    height: lineHeight
+                }"></view>
+            </view>
+        </view>
+    </template>
+</template>
+
+<script setup lang="ts">
+
+const props = defineProps({
+    tabs: {
+        type: Array as () => Array<{ label: string; value: string; badge?: boolean; num?: number }>,
+        default: () => []
+    },
+    modelValue: {
+        type: [String, Number],
+        default: ''
+    },
+    themeColor: {
+        type: String,
+        default: '#37A954'
+    },
+    fontSize: {
+        type: String,
+        default: '30rpx'
+    },
+    lineHeight: {
+        type: String,
+        default: '6rpx'
+    },
+    mode: {
+        type: String,
+        default: 'scroll-x' // btw
+    }
+})
+const SCROLL_OFFSET = ref(-uni.$u?.getPx('200rpx'));
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const activeIndex = ref(0)
+const scrollIntoView = ref('')
+const scrollViewRef = ref(null)
+const tabRefs = ref([])
+
+watch(() => props.modelValue, (val) => {
+    const idx = props.tabs.findIndex(tab => tab.value === val)
+    if (idx !== -1) {
+        activeIndex.value = idx
+        scrollToTab(idx)
+    }
+})
+
+const selectTab = (idx: number) => {
+    activeIndex.value = idx
+    emit('update:modelValue', props.tabs[idx].value)
+    emit('change', props.tabs[idx])
+    scrollToTab(idx)
+}
+
+// 主动滚动到选中tab
+const scrollToTab = (idx: number) => {
+    scrollIntoView.value = 'tab-' + idx
+}
+</script>
+
+<style lang="scss" scoped>
+.ut-tabs-scroll {
+    width: 100%;
+    white-space: nowrap;
+    background: #fff;
+}
+
+.ut-tabs-row {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    min-height: 66rpx;
+}
+
+.ut-tab-item {
+    position: relative;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    height: 78rpx;
+    font-weight: 500;
+    color: #666;
+    cursor: pointer;
+    padding: 0 24rpx;
+
+    .tab-label {
+        position: relative;
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+
+        span {
+            display: inline-block;
+        }
+
+        .tab-badge {
+            position: absolute;
+            top: -10rpx;
+            left: 90%;
+            background: #fa3534;
+            color: #fff;
+            font-size: 22rpx;
+            border-radius: 20rpx;
+            padding: 0 12rpx;
+            min-width: 32rpx;
+            text-align: center;
+            line-height: 32rpx;
+            height: 32rpx;
+            box-sizing: border-box;
+            z-index: 2;
+        }
+
+
+    }
+
+    .tab-underline {
+        position: absolute;
+        left: 24rpx;
+        right: 24rpx;
+        bottom: 0;
+        border-radius: 3rpx;
+        transition: width 0.2s;
+        z-index: 1;
+    }
+}
+</style>

+ 34 - 1
src/main.ts

@@ -7,10 +7,43 @@ import { useDict } from '@/utils/dict';
 import uviewPlus from 'uview-plus';
 import { navigateBackOrHome, showToast } from '@/utils/common';
 import routerGuard from '@/uni_modules/hh-router-guard/src/index';
+const uviewProps = {
+    config: {
+        loadFontOnce: true,
+        color: {
+            primary: '#714B22',
+        },
+    },
+    props: {
+        form: {
+            labelStyle: {
+                color: '#666',
+                fontSize: '30rpx',
+            },
+        },
+        input: {
+            color: '#333',
+            fontSize: '30rpx',
+            placeholderStyle: 'color: #ccc; font-weight: 400;',
+        },
+        textarea: {
+            color: '#333',
+            fontSize: '30rpx',
+            placeholderStyle: 'color: #ccc; font-weight: 400;',
+        }
+    },
+};
 export function createApp() {
     const app = createSSRApp(App);
     app.use(Pinia.createPinia());
-    app.use(uviewPlus);
+    app.use(uviewPlus, () => {
+        return {
+            options: {
+                // 这里可以配置uView的全局属性
+                ...uviewProps,
+            },
+        };
+    });
     app.use(routerGuard, {
         // 白名单:无需登录即可访问的页面路径
         whiteList: [

+ 68 - 4
src/pages.json

@@ -41,16 +41,80 @@
     ],
     "subPackages": [
         {
-            "root": "plant",
+            // 基地分包
+            "root": "plant/base",
             "pages": [
                 {
-                    "path": "index/index",
+                    "path": "base-list/index",
                     "style": {
-                        "navigationBarTitleText": "企业信息",
+                        "navigationBarTitleText": "基地列表"
+                    }
+                },
+                {
+                    "path": "base-detail/index",
+                    "style": {
+                        "navigationBarTitleText": "基地详情"
+                    }
+                },
+                {
+                    "path": "base-edit/index",
+                    "style": {
+                        "navigationBarTitleText": "编辑基地"
+                    }
+                },
+                {
+                    "path": "base-map/index",
+                    "style": {
+                        "navigationBarTitleText": "基地地图"
+                    }
+                },
+                // GAP基地信息
+                {
+                    "path": "gap-base-info/index",
+                    "style": {
+                        "navigationBarTitleText": "GAP基地信息"
+                    }
+                },
+                // 添加Gap基地信息添加
+                {
+                    "path": "gap-base-info-edit/index",
+                    "style": {
+                        "navigationBarTitleText": "添加GAP基地信息"
+                    }
+                }
+            ]
+        },
+        // 主营物种
+        {
+            "root": "plant/species",
+            "pages": [
+                {
+                    "path": "species-list/index",
+                    "style": {
+                        "navigationBarTitleText": "主营物种"
+                    }
+                }
+            ]
+        },
+        {
+            "root": "tools",
+            "pages": [
+               {
+                    "path": "map-draw-area/index",
+                    "style": {
+                        "navigationBarTitleText": "地图绘制面积图",
                         "disableScroll": true,
                         "enablePullDownRefresh": false
                     }
-                }
+               },
+               {
+                    "path": "map-gd/index",
+                    "style": {
+                        "navigationBarTitleText": "地图绘制面积图",
+                        "disableScroll": true,
+                        "enablePullDownRefresh": false
+                    }
+               }
             ]
         }
     ],

+ 1 - 1
src/pages/plant/index.vue

@@ -57,7 +57,7 @@
                             <ut-action-sheet v-model="form.type" :tabs="[{ label: '全部', value: '' }]" @change="onRefresh" title="选择原料类型">
                                 <view class="d-flex search-select-item a-c">
                                     <view class="flex1 ov-hd f-s-28 c-333 text-center f-w-5">{{ '全部' }}</view>
-                                    <up-icon size="20rpx" color="#2A6D52" name="arrow-down-fill" class="mr-10"></up-icon>
+                                    <up-icon size="20rpx" color="#1D9C3E" name="arrow-down-fill" class="mr-10"></up-icon>
                                 </view>
                             </ut-action-sheet>
                         </view>

+ 3 - 0
src/plant/base/base-detail/index.vue

@@ -0,0 +1,3 @@
+<template>
+    <view>页面内容</view>
+</template>

+ 17 - 0
src/plant/base/base-edit/index.vue

@@ -0,0 +1,17 @@
+<template>
+    <view class="">
+        <up-navbar title="添加基地" :fixed="false"></up-navbar>
+        <view class="pd-5"></view>
+        <ut-tabs :tabs="tabs" mode="btw"></ut-tabs>
+    </view>
+</template>
+<script setup lang="ts">
+import { useUserStore } from "@/store";
+import { useClientRequest } from "@/utils/request";
+const tabs = reactive([
+    { label: '基本信息', value: 'base' },
+    { label: '地块信息', value: 'plot' },
+    { label: '选址依据及环境信息', value: 'environment' },
+])
+</script>
+<style lang="scss" scoped></style>

+ 3 - 0
src/plant/base/base-list/index.vue

@@ -0,0 +1,3 @@
+<template>
+    <view>页面内容</view>
+</template>

+ 3 - 0
src/plant/base/base-map/index.vue

@@ -0,0 +1,3 @@
+<template>
+    <view>页面内容</view>
+</template>

+ 3 - 0
src/plant/base/gap-base-info-edit/index.vue

@@ -0,0 +1,3 @@
+<template>
+    <view>页面内容</view>
+</template>

+ 3 - 0
src/plant/base/gap-base-info/index.vue

@@ -0,0 +1,3 @@
+<template>
+    <view>页面内容</view>
+</template>

+ 0 - 10
src/plant/index/index.vue

@@ -1,10 +0,0 @@
-<template>
-    <view class="c-primary">种植端</view>
-</template>
-<script setup lang="ts">
-import { useClientRequest } from '@/utils/request';
-setTimeout(() => {
-    useClientRequest.get('/time');
-}, 2000);
-</script>
-<style></style>

+ 3 - 0
src/plant/species/species-list/index.vue

@@ -0,0 +1,3 @@
+<template>
+    <view>页面内容</view>
+</template>

+ 188 - 0
src/tools/map-draw-area/index.vue

@@ -0,0 +1,188 @@
+<template>
+    <!-- 仅微信小程序端 -->
+    <!-- #ifdef MP-WEIXIN -->
+    <view class="wx-map-draw-area">
+        <up-navbar title="地图绘制" @leftClick="navigateBackOrHome()" :fixed="false"></up-navbar>
+        <view class="flex1 ov-hd">
+            <map class="wx-map" :latitude="center[0]" :longitude="center[1]"  enable-rotate :scale="zoomToScale(zoom)"
+                :polygons="wxPolygons" :polyline="wxPolylines" :show-location="false" @tap="onWxMapTap"
+                enable-satellite />
+
+        </view>
+        <view class="wx-toolbar">
+            <u-button size="small" type="primary" @click="startOrFinish">
+                {{ isDrawing ? '完成' : '开始绘制' }}
+            </u-button>
+            <u-button size="small" class="mt-8" @click="undoPoint" :disabled="wxPoints.length === 0">撤销</u-button>
+            <u-button size="small" class="mt-8" type="warning" @click="clearAll"
+                :disabled="wxPoints.length === 0">清除</u-button>
+        </view>
+
+        <view class="wx-info" v-if="showInfo">
+            <text>点数:{{ wxPoints.length }}</text>
+            <text class="ml-12">面积:{{ formattedWxArea }}</text>
+            <text class="ml-12" v-if="isClosed">(已闭合)</text>
+        </view>
+    </view>
+    <!-- #endif -->
+
+    <!-- 非微信端兜底提示(可选) -->
+    <!-- #ifndef MP-WEIXIN -->
+    <view class="map-draw-area__unsupported">
+        <text>该组件仅支持微信小程序端绘制。</text>
+    </view>
+    <!-- #endif -->
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+
+type LatLng = [number, number]
+type WxPoint = { latitude: number; longitude: number }
+
+const props = defineProps({
+    center: { type: Array as () => LatLng, default: () => [23.1291, 113.2644] },
+    zoom: { type: Number, default: 13 },
+    showInfo: { type: Boolean, default: true },
+})
+
+const emits = defineEmits<{
+    (e: 'change', payload: { area: number; latlngs: LatLng[]; closed: boolean }): void
+}>()
+
+// 绘制状态
+const wxPoints = ref<WxPoint[]>([])
+const isDrawing = ref(false)
+const isClosed = ref(false)
+const wxArea = ref(0)
+
+// 可视化:多边形(填充)、折线(路径)
+const wxPolygons = computed<any[]>(() => {
+    if (wxPoints.value.length < 3) return []
+    return [
+        {
+            points: wxPoints.value,
+            strokeColor: '#18A058',
+            strokeWidth: 2,
+            fillColor: '#18A05833',
+            zIndex: 2,
+        },
+    ]
+})
+
+const wxPolylines = computed<any[]>(() => {
+    if (wxPoints.value.length < 2) return []
+    const lines: any[] = [
+        { points: wxPoints.value, color: '#18A058', width: 2, dottedLine: false, zIndex: 3 },
+    ]
+    if (wxPoints.value.length >= 2) {
+        const first = wxPoints.value[0]
+        const last = wxPoints.value[wxPoints.value.length - 1]
+        lines.push({ points: [last, first], color: '#18A058', width: 2, dottedLine: true, zIndex: 3 })
+    }
+    return lines
+})
+
+const formattedWxArea = computed(() => formatArea(wxArea.value))
+
+function onWxMapTap(e: any) {
+    if (!isDrawing.value) return
+    const { latitude, longitude } = e?.detail || {}
+    if (typeof latitude === 'number' && typeof longitude === 'number') {
+        wxPoints.value = [...wxPoints.value, { latitude, longitude }]
+        computeWxArea()
+        emitChange()
+    }
+}
+
+function startOrFinish() {
+    if (!isDrawing.value) {
+        isDrawing.value = true
+        isClosed.value = false
+        wxPoints.value = []
+        wxArea.value = 0
+    } else {
+        isDrawing.value = false
+        isClosed.value = wxPoints.value.length >= 3
+        computeWxArea()
+        emitChange()
+    }
+}
+
+function undoPoint() {
+    if (wxPoints.value.length === 0) return
+    wxPoints.value = wxPoints.value.slice(0, -1)
+    computeWxArea()
+    emitChange()
+}
+
+function clearAll() {
+    wxPoints.value = []
+    wxArea.value = 0
+    isDrawing.value = false
+    isClosed.value = false
+    emitChange()
+}
+
+function emitChange() {
+    emits('change', {
+        area: wxArea.value,
+        latlngs: wxPoints.value.map((p) => [p.latitude, p.longitude]) as LatLng[],
+        closed: isClosed.value,
+    })
+}
+
+function computeWxArea() {
+    if (wxPoints.value.length < 3) {
+        wxArea.value = 0
+        return
+    }
+    wxArea.value = calcSphericalPolygonAreaLngLat(
+        wxPoints.value.map((p) => [p.longitude, p.latitude])
+    )
+}
+
+// 球面多边形面积(近似,WGS84)输入 [lng, lat]
+function calcSphericalPolygonAreaLngLat(coords: [number, number][]): number {
+    const n = coords.length
+    if (n < 3) return 0
+    const rad = (d: number) => (d * Math.PI) / 180
+    const R = 6378137
+    let area = 0
+    for (let i = 0; i < n; i++) {
+        const lower = coords[(i + n - 1) % n]
+        const middle = coords[i]
+        const upper = coords[(i + 1) % n]
+        area += (rad(upper[0]) - rad(lower[0])) * Math.sin(rad(middle[1]))
+    }
+    area = (area * R * R) / 2
+    return Math.abs(area)
+}
+
+function zoomToScale(zoom: number) {
+    const z = Math.max(3, Math.min(20, Math.round(zoom)))
+    return z
+}
+
+function formatArea(m2: number): string {
+    if (!m2) return '0 m²'
+    if (m2 < 100000) return `${m2.toFixed(2)} m²`
+    const km2 = m2 / 1_000_000
+    return `${km2.toFixed(4)} km²`
+}
+
+defineExpose({ startOrFinish, undoPoint, clearAll })
+</script>
+
+<style scoped lang="scss">
+.wx-map-draw-area {
+    width: 100%;
+    height: 100vh;
+    display: flex;
+    flex-direction: column;
+}
+.wx-map {
+    width: 750rpx;
+    height: 100%;
+}
+</style>

+ 5 - 0
src/tools/map-gd/index.vue

@@ -0,0 +1,5 @@
+<template>
+    <div>
+        <web-view src="https://www.shuziyunyao.com/admin/client/report-list?tlk=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJzeXNfdXNlcjoxOTIxMDQzNjEwNjY5MTk5MzYxIiwicm5TdHIiOiJHNzZNSlgxbjZUOFBiVEdYQnk4RnBXU3FyRDFTZkRsYSIsImNsaWVudGlkIjoiZTVjZDdlNDg5MWJmOTVkMWQxOTIwNmNlMjRhN2IzMmUiLCJjcHlpZCI6IjkxNTMwMTAyTUFDQTk2TUo5SCIsImNweU5hbWUiOiLmvJTnpLrkvIHkuJoiLCJ0ZW5hbnRJZCI6IjAwMDAwMCIsInVzZXJJZCI6MTkyMTA0MzYxMDY2OTE5OTM2MSwiZGVwdElkIjoxNzQ2Nzg0MjA3MjM5MDczNzk0fQ.Fq6ebfuf_galuCRUEVbZyj0lcXuqp9a0e7vBvSjlQKk&state=1766373817974"></web-view>
+    </div>
+</template>

+ 0 - 29
src/utils/request.ts

@@ -1,35 +1,6 @@
 // uniapp封装的请求方法
 let timeout = 60 * 1000;
 
-// 获取当前应该使用的 base URL
-const getBaseUrl = (): string => {
-    try {
-        // 获取当前页面栈
-        const pages = getCurrentPages();
-        if (pages.length === 0) {
-            // 默认使用 VITE_API_BASE_URL
-            return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
-        }
-        // 获取当前页面路由
-        const currentPage = pages[pages.length - 1];
-        const route = currentPage.route || '';
-        console.log(route, 'route');
-
-        // 判断路由路径
-        if (route.includes('plant/')) {
-            return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
-        } else if (route.includes('production/')) {
-            return import.meta.env.VITE_API_OTHER_BASE_URL || 'http://localhost:3000';
-        }
-
-        // 默认情况
-        return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
-    } catch (error) {
-        // 出错时使用默认值
-        return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
-    }
-};
-
 // 获取全局请求头方法
 const getHeader = () => {
     let header = {

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
stats.html


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.