huangxw 2 週間 前
コミット
95da41df43

+ 1 - 0
package.json

@@ -57,6 +57,7 @@
         "clipboard": "^2.0.11",
         "dayjs": "^1.11.13",
         "pinia": "^2.0.36",
+        "pinia-plugin-persistedstate": "^4.7.1",
         "rollup-plugin-visualizer": "^6.0.3",
         "unplugin-auto-import": "^19.3.0",
         "uview-plus": "^3.6.4",

+ 24 - 0
pnpm-lock.yaml

@@ -68,6 +68,9 @@ importers:
       pinia:
         specifier: ^2.0.36
         version: 2.3.1(typescript@4.9.5)(vue@3.5.26(typescript@4.9.5))
+      pinia-plugin-persistedstate:
+        specifier: ^4.7.1
+        version: 4.7.1(pinia@2.3.1(typescript@4.9.5)(vue@3.5.26(typescript@4.9.5)))
       rollup-plugin-visualizer:
         specifier: ^6.0.3
         version: 6.0.5
@@ -3831,6 +3834,20 @@ packages:
     resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
     engines: {node: '>=0.10.0'}
 
+  pinia-plugin-persistedstate@4.7.1:
+    resolution: {integrity: sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==}
+    peerDependencies:
+      '@nuxt/kit': '>=3.0.0'
+      '@pinia/nuxt': '>=0.10.0'
+      pinia: '>=3.0.0'
+    peerDependenciesMeta:
+      '@nuxt/kit':
+        optional: true
+      '@pinia/nuxt':
+        optional: true
+      pinia:
+        optional: true
+
   pinia@2.3.1:
     resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==}
     peerDependencies:
@@ -4681,6 +4698,7 @@ packages:
 
   whatwg-encoding@1.0.5:
     resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==}
+    deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
 
   whatwg-mimetype@2.3.0:
     resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==}
@@ -9625,6 +9643,12 @@ snapshots:
 
   pify@2.3.0: {}
 
+  pinia-plugin-persistedstate@4.7.1(pinia@2.3.1(typescript@4.9.5)(vue@3.5.26(typescript@4.9.5))):
+    dependencies:
+      defu: 6.1.4
+    optionalDependencies:
+      pinia: 2.3.1(typescript@4.9.5)(vue@3.5.26(typescript@4.9.5))
+
   pinia@2.3.1(typescript@4.9.5)(vue@3.5.26(typescript@4.9.5)):
     dependencies:
       '@vue/devtools-api': 6.6.4

+ 10 - 4
src/App.vue

@@ -2,7 +2,8 @@
 import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
 import { useAuthStore } from '@/store/modules/auth';
 import { autoLogin } from '@/utils/routeGuard';
-import { getToken, setToken, removeToken } from '@/utils/auth';
+import { useInfoStore } from '@/store';
+const infoStore = useInfoStore();
 onLaunch(async () => {
     console.log('App Launch');
     // 应用启动时检查登录状态
@@ -12,7 +13,12 @@ onLaunch(async () => {
         console.error('Auto login check failed:', error);
     }
 });
-setToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJhcHBfdXNlcjoxOTQwMjQ1NzU5Mjk2ODY0MjU3Iiwicm5TdHIiOiJiVllYa0QwVnY2T3JLNnc1d1ZOTlpiV0hFUTlhU1J2RyIsImNsaWVudGlkIjoiMjAyNTAyMTQiLCJvcGVuaWQiOiJva0NHMjRrU29nT2VWTGFkc1JVYm8tS2JqR2Y4IiwidXNlcklkIjoxOTQwMjQ1NzU5Mjk2ODY0MjU3fQ.-gNoaCFtXKHYtiGLdd5mpEyhBf9AZ9BTw9udNA_1feM')
+// 设置token并指定过期时间(例如24小时)
+console.log('设定token');
+//infoStore.setToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJhcHBfdXNlcjoxOTQwMjQ1NzU5Mjk2ODY0MjU3Iiwicm5TdHIiOiJiVllYa0QwVnY2T3JLNnc1d1ZOTlpiV0hFUTlhU1J2RyIsImNsaWVudGlkIjoiMjAyNTAyMTQiLCJvcGVuaWQiOiJva0NHMjRrU29nT2VWTGFkc1JVYm8tS2JqR2Y4IiwidXNlcklkIjoxOTQwMjQ1NzU5Mjk2ODY0MjU3fQ.-gNoaCFtXKHYtiGLdd5mpEyhBf9AZ9BTw9udNA_1feM', 24 * 60 * 60 * 1000)
+console.log('获取用户信息,app');
+// infoStore.getUserInfo()
+// infoStore.getCompanyInfo()
 onShow(() => {
     console.log('App Show');
 });
@@ -22,12 +28,12 @@ onHide(() => {
 });
 </script>
 <style lang="scss">
-@import "uview-plus/index.scss";
+@import 'uview-plus/index.scss';
 @import '@/assets/styles/public.scss';
 @import '@/assets/styles/uview-plus.scss';
 
 // 设置背景色
 page {
-	background-color: #f7f7f7;
+    background-color: #f7f7f7;
 }
 </style>

+ 7 - 0
src/assets/styles/public.scss

@@ -142,3 +142,10 @@ $colors: (
 .w-s-no {
     white-space: nowrap;
 }
+.radius-10 {
+    border-radius: 10rpx;
+}
+.home_icon{
+    width: 40rpx;
+    height: 40rpx;
+}

+ 4 - 1
src/components/ut-confirm-dialog/ut-confirm-dialog.vue

@@ -1,7 +1,7 @@
 // 如果up-popup弹框的遮罩层被覆盖或者是变为白色,需要查看unocss配置,unocss把u-popup__content—transition转化为两个变量了,需要过滤
 <template>
     <up-popup :show="internalShow" mode="center" :round="16" :closeable="false" @close="handleClose" bgColor="transparent">
-        <view class="confirm-dialog">
+        <view class="confirm-dialog" :style="{width}">
             <!-- 标题区域 -->
             <view class="dialog-header pd-32">
                 <text class="dialog-title f-s-32 f-w-6 c-#333">{{ title }}</text>
@@ -47,6 +47,8 @@ interface Props {
     confirmColor?: string;
     // 是否显示取消按钮
     showCancel?: boolean;
+    //弹框宽度
+    width?: string;
 }
 
 const props = withDefaults(defineProps<Props>(), {
@@ -60,6 +62,7 @@ const props = withDefaults(defineProps<Props>(), {
     confirmBgColor: '#37A954',
     confirmColor: '#fff',
     showCancel: true,
+    width: 'auto'
 });
 
 const emit = defineEmits<{

+ 9 - 7
src/main.ts

@@ -1,12 +1,13 @@
 import { createSSRApp } from 'vue';
 import App from './App.vue';
-import * as Pinia from 'pinia';
+import pinia from './store';
 import 'uno.css';
 import { selectDictLabel, selectDictLabels } from './utils/ruoyi';
 import { useDict } from '@/utils/dict';
 import uviewPlus, { setConfig } from 'uview-plus';
 import { navigateBackOrHome, showToast } from '@/utils/common';
 import routerGuard from '@/uni_modules/hh-router-guard/src/index';
+import { useInfoStore } from '@/store';
 const uviewProps: any = {
     config: {
         loadFontOnce: true,
@@ -36,12 +37,12 @@ const uviewProps: any = {
         },
         checkbox: {
             activeColor: '#37A954',
-        }
+        },
     },
 };
 export function createApp() {
     const app = createSSRApp(App);
-    app.use(Pinia.createPinia());
+    app.use(pinia);
     // setConfig(uviewProps)
     app.use(uviewPlus, () => {
         return {
@@ -59,12 +60,14 @@ export function createApp() {
             '/tools/map-draw-area/index',
             '/plant/species/config/index',
             '/plant/base/gap-base-info/index',
+            '/plant/base/base-edit/index',
         ],
 
         // 自定义登录检查函数(返回 true 表示已登录)
         checkLogin: () => {
-            const token = uni.getStorageSync('token');
-            return !!token;
+            //检查token是否过期 true代表过期了
+            const token = useInfoStore().isTokenExpired();
+            return !token;
         },
 
         // 登录页面路径 === 需要替换为实际项目中登录页面的路径
@@ -72,7 +75,6 @@ export function createApp() {
 
         // 未登录时的处理逻辑
         loginHandler: (to: string) => {
-            console.log(to);
             uni.navigateTo({
                 url: `/pages/login/login?redirect=${encodeURIComponent(to)}`,
             });
@@ -85,6 +87,6 @@ export function createApp() {
     app.config.globalProperties.useDict = useDict;
     return {
         app,
-        Pinia,
+        Pinia: { pinia },
     };
 }

+ 1 - 4
src/pages.json

@@ -20,10 +20,7 @@
         {
             "path": "pages/login/login",
             "style": {
-                "navigationBarTitleText": "用户登录",
-                "navigationStyle": "default",
-                "navigationBarBackgroundColor": "#667eea",
-                "navigationBarTextStyle": "white"
+                "navigationBarTitleText": "用户登录"
             }
         },
         {

+ 238 - 474
src/pages/login/login.vue

@@ -1,508 +1,272 @@
 <template>
-  <view class="login-container">
-    <view class="login-wrapper">
-      <!-- Logo 区域 -->
-      <view class="logo-section">
-        <image class="logo" src="/static/logo.png" mode="aspectFit" />
-        <text class="app-name">系统登录</text>
-      </view>
-
-      <!-- 登录表单 -->
-      <view class="form-section">
-        <uni-forms 
-          ref="loginFormRef" 
-          :model="loginForm" 
-          :rules="loginRules"
-          label-position="top"
-        >
-          <!-- 用户名输入框 -->
-          <uni-forms-item label="用户名" name="username" required>
-            <uni-easyinput
-              v-model="loginForm.username"
-              placeholder="请输入用户名"
-              :clearable="true"
-              :focus="false"
-              @input="clearError"
-            >
-              <template #left>
-                <uni-icons type="person" size="20" color="#999" />
-              </template>
-            </uni-easyinput>
-          </uni-forms-item>
-
-          <!-- 密码输入框 -->
-          <uni-forms-item label="密码" name="password" required>
-            <uni-easyinput
-              v-model="loginForm.password"
-              type="password"
-              placeholder="请输入密码"
-              :clearable="true"
-              @input="clearError"
-            >
-              <template #left>
-                <uni-icons type="locked" size="20" color="#999" />
-              </template>
-            </uni-easyinput>
-          </uni-forms-item>
-
-          <!-- 验证码输入框 -->
-          <uni-forms-item 
-            v-if="needCaptcha" 
-            label="验证码" 
-            name="code" 
-            required
-          >
-            <view class="captcha-container">
-              <uni-easyinput
-                v-model="loginForm.code"
-                placeholder="请输入验证码"
-                :clearable="true"
-                class="captcha-input"
-                @input="clearError"
-              >
+    <z-paging ref="paging" bgColor="rgba(0,0,0,0)">
+        <template #top>
+            <up-navbar :fixed="false" title="登录">
                 <template #left>
-                  <uni-icons type="checkmarkempty" size="20" color="#999" />
+                    <!-- <image @click="$u.route({ type: 'switchTab', url: '/pages/list/index' })" class="home_icon" src="https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/common/home.png" mode="widthFix" /> -->
+                    <up-icon @click="homeBack()" name="arrow-left" color="#333" size="40rpx"></up-icon>
                 </template>
-              </uni-easyinput>
-              <image 
-                v-if="captchaImage"
-                :src="captchaImage" 
-                class="captcha-image"
-                @click="refreshCaptcha"
-              />
+            </up-navbar>
+        </template>
+        <view class="login-centent">
+            <view class="login-logo-wrap p-rtv d-flex j-c a-c">
+                <image class="bg-circle" src="https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/login/bg_circle.png" mode="aspectFit" />
+                <image class="login-logo" src="https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/login/logo.png" mode="aspectFit" />
             </view>
-          </uni-forms-item>
-
-          <!-- 记住我 -->
-          <view class="remember-section">
-            <uni-data-checkbox
-              v-model="loginForm.rememberMe"
-              :localdata="rememberOptions"
-              mode="tag"
-            />
-          </view>
-
-          <!-- 错误提示 -->
-          <view v-if="errorMessage" class="error-message">
-            <uni-icons type="info" color="#f56c6c" size="16" />
-            <text class="error-text">{{ errorMessage }}</text>
-          </view>
-
-          <!-- 登录按钮 -->
-          <button 
-            class="login-btn"
-            :class="{ 'login-btn--loading': loading }"
-            :disabled="loading"
-            @click="handleLogin"
-          >
-            <uni-icons 
-              v-if="loading" 
-              type="spinner-cycle" 
-              class="loading-icon" 
-              size="18" 
-              color="#fff" 
-            />
-            {{ loading ? '登录中...' : '登录' }}
-          </button>
-        </uni-forms>
-      </view>
-
-      <!-- 底部链接 -->
-      <view class="footer-section">
-        <text class="footer-link" @click="handleForgotPassword">忘记密码?</text>
-        <text class="footer-divider">|</text>
-        <text class="footer-link" @click="handleRegister">注册账号</text>
-      </view>
-    </view>
-  </view>
+            <view class="login-form pl-20 pd-25">
+                <up-button type="primary" shape="circle" @click="$u.route({ url: '/pages/login/phone/phone', params: { redirect: encodeURIComponent(redirect) } })">
+                    <view class="d-flex a-c j-c">
+                        <!-- <image class="base-icon mr-20"
+                            src="https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/login-phone/phone.png"
+                            mode="widthFix" /> -->
+                        手机号验证码登录
+                    </view>
+                </up-button>
+                <view class="pd-20"></view>
+                <template v-if="isBindPhone">
+                    <up-button class="mb-40" @click="handleLogin" color="#28A94B" type="primary" shape="circle">
+                        <view class="d-flex a-c j-c">
+                            <!-- <image class="base-icon mr-20"
+                                src="https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/login/weixin_logo.png"
+                                mode="widthFix" /> -->
+                            手机号快捷登录
+                        </view>
+                    </up-button>
+                </template>
+                <template v-else>
+                    <up-button class="mb-40" color="#28A94B" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber" type="primary" shape="circle">
+                        <view class="d-flex a-c j-c">
+                            <!-- <image class="base-icon mr-20"
+                                src="https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/login/weixin_logo.png"
+                                mode="widthFix" /> -->
+                            手机号快捷登录
+                        </view>
+                    </up-button>
+                </template>
+                <view class="d-flex a-c pd-10 mb-20" @click="clickAgree">
+                    <up-checkbox activeColor="#2A6D52" size="32rpx" name="agree" usedAlone v-model:checked="form.aloneChecked"></up-checkbox>
+                    <view class="d-flex a-c f-s-24">
+                        <text class="c-999">我已阅读并同意</text>
+                        <text class="c-primary" @click="$u.route({ url: '/views/tool/agreement/agreement' })">《用户协议》</text>
+                        <text class="c-666">、</text>
+                        <text class="c-primary" @click="$u.route({ url: '/views/tool/privacy/privacy' })">《隐私政策》</text>
+                    </view>
+                </view>
+                <official-account></official-account>
+            </view>
+        </view>
+        <image class="login-bttom-bg" src="https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/login/bg_bottom.png" mode="widthFix" />
+    </z-paging>
 </template>
 
-<script setup lang="ts">
-import { reactive, ref, onMounted } from 'vue';
-import { useAuthStore } from '@/store/modules/auth';
-import type { LoginForm } from '@/store/modules/auth';
-
-// Store
-// const authStore = useAuthStore();
-
-// 响应式数据
-const loginFormRef = ref();
-const loading = ref<boolean>(false);
-const errorMessage = ref<string>('');
-const needCaptcha = ref<boolean>(false);
-const captchaImage = ref<string>('');
-
-// 登录表单
-const loginForm = reactive<LoginForm>({
-  username: '',
-  password: '',
-  code: '',
-  uuid: '',
-  rememberMe: false
+<script setup>
+import { ref, getCurrentInstance } from 'vue';
+import { useInfoStore } from '@/store';
+import config from '@/config';
+import { useRouter } from 'vue-router';
+import { getCurrentPage } from '@/utils/public';
+import { onLoad } from '@dcloudio/uni-app';
+import { updateUserInfo } from '@/utils/common';
+import { recursiveDecodeURIComponent } from '@/utils/ruoyi';
+import { useClientRequest } from '@/utils/request';
+const pages = ref(getCurrentPages());
+const infoStore = useInfoStore();
+const paging = ref(null);
+const redirect = ref('');
+// 判断是否绑定手机号
+const isBindPhone = ref(0);
+const form = ref({
+    aloneChecked: false,
+    phoneCode: '',
+    xcxcode: '',
+    grantType: 'xcx',
+    clientId: config.clientId,
+    // tenantId: config.tenantId,
 });
-
-// 记住我选项
-const rememberOptions = [
-  {
-    text: '记住我',
-    value: true
-  }
-];
-
-// 表单验证规则
-const loginRules = {
-  username: [
-    {
-      required: true,
-      errorMessage: '请输入用户名'
-    },
-    {
-      minLength: 2,
-      maxLength: 20,
-      errorMessage: '用户名长度在 2 到 20 个字符'
-    }
-  ],
-  password: [
-    {
-      required: true,
-      errorMessage: '请输入密码'
-    },
-    {
-      minLength: 6,
-      maxLength: 20,
-      errorMessage: '密码长度在 6 到 20 个字符'
-    }
-  ],
-  code: [
-    {
-      required: true,
-      errorMessage: '请输入验证码'
-    },
-    {
-      minLength: 4,
-      maxLength: 6,
-      errorMessage: '验证码长度在 4 到 6 个字符'
-    }
-  ]
-};
-
-/**
- * 清除错误信息
- */
-const clearError = (): void => {
-  errorMessage.value = '';
+const clickAgree = () => {
+    form.value.aloneChecked = !form.value.aloneChecked;
 };
 
-/**
- * 获取验证码
- */
-const getCaptcha = async (): Promise<void> => {
-  try {
-    // 这里应该调用获取验证码的API
-    // const response = await useClientRequest.get('/auth/captcha');
-    // if (response.code === 200) {
-    //   captchaImage.value = response.data.image;
-    //   loginForm.uuid = response.data.uuid;
-    //   needCaptcha.value = true;
-    // }
-    
-    // 临时模拟
-    needCaptcha.value = false;
-  } catch (error) {
-    console.error('获取验证码失败:', error);
-  }
+const checkUserRegist = (data) => {
+    return useClientRequest.post('/auth/checkUserRegist', data, false);
 };
-
-/**
- * 刷新验证码
- */
-const refreshCaptcha = (): void => {
-  getCaptcha();
-};
-
-/**
- * 处理登录
- */
-const handleLogin = async (): Promise<void> => {
-  try {
-    // 表单验证
-    const valid = await loginFormRef.value?.validate();
-    if (!valid) {
-      return;
+const getPhoneNumber = (e) => {
+    console.log(e);
+    // 判读是否获取成功
+    if (!e.detail.code) {
+        uni.showToast({
+            title: '获取手机号失败',
+            icon: 'none',
+        });
+        return;
     }
-
-    loading.value = true;
-    errorMessage.value = '';
-
-    // 调用登录
-    const success = await authStore.login(loginForm);
-    
-    if (success) {
-      uni.showToast({
-        title: '登录成功',
-        icon: 'success'
-      });
-
-      // 登录成功后跳转
-      setTimeout(() => {
-        const pages = getCurrentPages();
-        const currentPage = pages[pages.length - 1];
-        const options = (currentPage as any).options || {};
-        const redirectUrl = options.redirect || '/pages/index/index';
-        
-        uni.reLaunch({
-          url: redirectUrl
+    form.value.phoneCode = e.detail.code;
+    // 判断是否勾选隐私协议
+    if (!form.value.aloneChecked) {
+        uni.showToast({
+            title: '请阅读并同意用户协议和隐私政策',
+            icon: 'none',
         });
-      }, 1500);
-    } else {
-      errorMessage.value = authStore.loginError || '登录失败,请重试';
-      // 如果是验证码错误,刷新验证码
-      if (needCaptcha.value) {
-        refreshCaptcha();
-        loginForm.code = '';
-      }
+        return;
     }
-  } catch (error: any) {
-    console.error('登录错误:', error);
-    errorMessage.value = error.message || '网络错误,请稍后重试';
-  } finally {
-    loading.value = false;
-  }
+    // 手机号登录方法
+    handleLogin();
 };
-
-/**
- * 忘记密码
- */
-const handleForgotPassword = (): void => {
-  uni.showToast({
-    title: '功能开发中',
-    icon: 'none'
-  });
+const handleLogin = () => {
+    // 判断是否勾选隐私协议
+    if (!form.value.aloneChecked) {
+        uni.showToast({
+            title: '请阅读并同意用户协议和隐私政策',
+            icon: 'none',
+        });
+        return;
+    }
+    // 微信小程序登录获取的code
+    uni.login({
+        provider: 'weixin',
+        success: function (loginRes) {
+            // 获取用户信息
+            if (loginRes.errMsg === 'login:ok') {
+                form.value.xcxcode = loginRes.code;
+                // 请求登录接口
+                weixinLogin();
+            }
+        },
+    });
 };
-
-/**
- * 注册账号
- */
-const handleRegister = (): void => {
-  uni.showToast({
-    title: '功能开发中',
-    icon: 'none'
-  });
+const weixinLogin = async () => {
+    try {
+        // 请求登录接口
+        uni.showLoading({
+            title: '登录中...',
+            mask: true,
+        });
+        await infoStore.wxLogin(form.value);
+        // 调用获取用户信息接口
+        infoStore.getUserInfo();
+        console.log('调用获取用户信息接口');
+
+        infoStore.getCompanyInfo();
+        uni.hideLoading();
+        // 重定向或跳转首页
+        // 获取redirect参数
+        const redirectUrl = redirect.value || '/pages/plant/index';
+        // switchTab 页面
+        const switchTabs = ['/pages/plant/index'];
+        if (switchTabs.includes(redirectUrl)) {
+            uni.$u.route({
+                type: 'switchTab',
+                url: redirectUrl,
+            });
+            return;
+        }
+        uni.$u.route({
+            type: 'redirect',
+            url: redirectUrl,
+        });
+    } catch (error) {
+        uni.showToast({
+            title: error.message || '登录失败,请稍后重试',
+            icon: 'none',
+        });
+    }
 };
-
-/**
- * 页面加载
- */
-onMounted(() => {
-  // 检查是否需要验证码
-  getCaptcha();
-  
-  // 如果已经登录,直接跳转
-  if (authStore.isLoggedIn) {
-    uni.reLaunch({
-      url: '/pages/index/index'
+// 判断用户是否绑定手机号
+const hasBindPhone = async () => {
+    // 微信小程序登录获取的code
+    uni.login({
+        provider: 'weixin',
+        success: function (loginRes) {
+            // 获取用户信息
+            if (loginRes.errMsg === 'login:ok') {
+                console.log(loginRes, 'loginRes');
+                checkUserRegist({
+                    code: loginRes.code,
+                    clientId: config.clientId,
+                }).then((res) => {
+                    if (res.code === 200) {
+                        isBindPhone.value = +res.data;
+                    }
+                });
+            }
+        },
     });
-  }
+};
+const homeBack = () => {
+    const pages = getCurrentPages();
+    if (pages.length > 1) {
+        uni.navigateBack();
+    } else {
+        const redirectUrl = redirect.value || '/pages/list/index';
+        const switchTabs = ['/pages/index/index', '/pages/find/index/index', '/pages/cart/index', '/pages/list/index', '/pages/user/index'];
+        if (switchTabs.includes(redirectUrl)) {
+            uni.$u.route({
+                type: 'switchTab',
+                url: redirectUrl,
+            });
+            return;
+        } else {
+            uni.$u.route({
+                type: 'switchTab',
+                url: '/pages/list/index',
+            });
+        }
+    }
+};
+onLoad((options) => {
+    // 获取redirect参数
+    const redirectStr = options.redirect ? recursiveDecodeURIComponent(options.redirect) : '';
+    console.log(redirectStr);
+    redirect.value = redirectStr;
+    // 判断用户是否绑定手机号
+    hasBindPhone();
 });
 </script>
 
 <style lang="scss" scoped>
-.login-container {
-  min-height: 100vh;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 40rpx;
-}
-
-.login-wrapper {
-  width: 100%;
-  max-width: 600rpx;
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 60rpx 40rpx;
-  box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.1);
+.login-logo-wrap {
+    width: 750rpx;
+    height: 750rpx;
+    margin: auto;
+    box-sizing: border-box;
 }
 
-.logo-section {
-  text-align: center;
-  margin-bottom: 60rpx;
-  
-  .logo {
-    width: 120rpx;
-    height: 120rpx;
-    margin-bottom: 20rpx;
-  }
-  
-  .app-name {
-    font-size: 32rpx;
-    font-weight: 600;
-    color: #333;
-  }
+.bg-circle {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    margin: auto;
+    width: 770rpx;
+    height: 770rpx;
+    // 360度旋转
+    animation: rotate 10s linear infinite;
 }
 
-.form-section {
-  .captcha-container {
-    display: flex;
-    align-items: center;
-    gap: 20rpx;
-    
-    .captcha-input {
-      flex: 1;
+@keyframes rotate {
+    from {
+        transform: rotate(0deg);
     }
-    
-    .captcha-image {
-      width: 160rpx;
-      height: 80rpx;
-      border: 1rpx solid #ddd;
-      border-radius: 8rpx;
-      cursor: pointer;
-    }
-  }
-  
-  .remember-section {
-    margin: 30rpx 0;
-  }
-  
-  .error-message {
-    display: flex;
-    align-items: center;
-    gap: 10rpx;
-    margin-bottom: 30rpx;
-    padding: 20rpx;
-    background: #fef0f0;
-    border: 1rpx solid #fbc4c4;
-    border-radius: 8rpx;
-    
-    .error-text {
-      font-size: 28rpx;
-      color: #f56c6c;
-    }
-  }
-  
-  .login-btn {
-    width: 100%;
-    height: 88rpx;
-    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    color: #fff;
-    border: none;
-    border-radius: 12rpx;
-    font-size: 32rpx;
-    font-weight: 600;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    gap: 10rpx;
-    transition: all 0.3s ease;
-    
-    &:not([disabled]):active {
-      transform: translateY(2rpx);
-      opacity: 0.9;
-    }
-    
-    &[disabled] {
-      opacity: 0.7;
-    }
-    
-    &--loading {
-      .loading-icon {
-        animation: spin 1s linear infinite;
-      }
-    }
-  }
-}
 
-.footer-section {
-  text-align: center;
-  margin-top: 40rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 20rpx;
-  
-  .footer-link {
-    font-size: 28rpx;
-    color: #666;
-    cursor: pointer;
-    
-    &:hover {
-      color: #667eea;
+    to {
+        transform: rotate(360deg);
     }
-  }
-  
-  .footer-divider {
-    font-size: 28rpx;
-    color: #ddd;
-  }
-}
-
-@keyframes spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
 }
 
-// uni-app 组件样式重写
-::v-deep .uni-forms-item {
-  margin-bottom: 30rpx;
+.login-logo {
+    width: 493rpx;
+    height: 284rpx;
 }
 
-::v-deep .uni-forms-item__label {
-  font-size: 28rpx;
-  color: #666;
-  margin-bottom: 10rpx;
-}
-
-::v-deep .uni-easyinput {
-  .uni-easyinput__content {
-    height: 88rpx;
-    border: 1rpx solid #ddd;
-    border-radius: 12rpx;
-    background: #fafafa;
-    
-    &.uni-easyinput__content-focus {
-      border-color: #667eea;
-      background: #fff;
-    }
-  }
-  
-  .uni-easyinput__content-input {
-    font-size: 30rpx;
-    color: #333;
-    padding-left: 20rpx;
-  }
-  
-  .uni-easyinput__placeholder-class {
-    font-size: 30rpx;
-    color: #999;
-  }
-}
-
-::v-deep .uni-data-checklist {
-  .checklist-group {
-    .checklist-box {
-      border: none;
-      padding: 0;
-      margin: 0;
-      
-      .checklist-content {
-        font-size: 28rpx;
-        color: #666;
-      }
-      
-      .checkbox__inner {
-        border-color: #667eea;
-        
-        &.checkbox__inner--checked {
-          background-color: #667eea;
-          border-color: #667eea;
-        }
-      }
-    }
-  }
+.login-bttom-bg {
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    width: 687rpx;
+    margin: auto;
+    // 不能有事件
+    pointer-events: none;
 }
 </style>

+ 147 - 82
src/pages/plant/index.vue

@@ -4,18 +4,14 @@
             <up-navbar :fixed="true" :bgColor="navBarBgColor">
                 <template #left>
                     <view class="d-flex a-c pb-5" id="topup-navbar" :style="{ width: `${bubble.left - 30}px` }">
-                        <image class="home_icon mr-20"
-                            src="https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/common/home.png"
-                            mode="widthFix" />
+                        <image class="home_icon mr-20" src="https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/common/home.png" mode="widthFix" />
                         <text class="f-s-40 c-333 f-w-5 w-s-no">中药材种植全链条追溯</text>
                         <view class="flex1"></view>
                     </view>
                 </template>
             </up-navbar>
         </template>
-        <view class="h-500 w-100%"
-            style="background: linear-gradient(to left, #d2f7d5, #eafad8); position: absolute; top: 0; left: 0; z-index: -1">
-        </view>
+        <view class="h-500 w-100%" style="background: linear-gradient(to left, #d2f7d5, #eafad8); position: absolute; top: 0; left: 0; z-index: -1"> </view>
         <template>
             <up-navbar :fixed="false" bgColor="transparent">
                 <template #left>
@@ -24,19 +20,17 @@
             </up-navbar>
             <view class="user-page-header pd-10 d-flex a-c mg-14 p-rtv">
                 <view class="user-page-header-avatar mr-20 p-rtv">
-                    <up-avatar size="116rpx"
-                        :src="avatar || 'https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/common/avatar.png'"></up-avatar>
+                    <up-avatar size="116rpx" :src="avatar || 'https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/common/avatar.png'"></up-avatar>
                 </view>
                 <view class="flex1 ov-hd mr-40">
                     <view class="p-rtv d-flex a-c mb-6">
                         <view class="flex1 ov-hd f-s-32 c-333 d-flex a-ed">
                             <text class="mr-12 up-line-1 f-w-5">{{ name }}</text>
-                            <text class="c-999 f-s-24">{{ setCipByNum(phone, 3, 4) || '-' }}</text>
+                            <text class="c-999 f-s-24">{{ setCipByNum(phone ?? null, 3, 4) || '-' }}</text>
                         </view>
                     </view>
-                    <view class="f-s-22 mr-10 b-radius pt-4 pb-4 pl-10 pr-10 c-primary"
-                        style="width: max-content; background-color: #b7e8bc">
-                        {{ cpyname }}
+                    <view class="f-s-22 mr-10 radius-30 pt-4 pb-4 pl-10 pr-10 c-primary bg-#b7e8bc" style="width: max-content">
+                        {{ currentCpyName }}
                     </view>
                 </view>
             </view>
@@ -44,138 +38,198 @@
         <template>
             <view class="p-rtv">
                 <view class="pd-10 mg-14">
-                    <view class="b-radius pd-6"
-                        style="border: 1rpx solid #fff; background: linear-gradient(90deg, #c1f3c5 0%, rgba(193, 243, 197, 0.5) 20%, rgba(255, 255, 255, 0.5) 35%, rgba(255, 255, 255, 0.5) 50%, rgba(232, 255, 234, 0.5) 100%, #e8ffea 100%), linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 30%, rgba(255, 255, 255, 0.3) 80%, transparent 100%)">
+                    <view class="b-radius pd-6" style="border: 1rpx solid #fff; background: linear-gradient(90deg, #c1f3c5 0%, rgba(193, 243, 197, 0.5) 20%, rgba(255, 255, 255, 0.5) 35%, rgba(255, 255, 255, 0.5) 50%, rgba(232, 255, 234, 0.5) 100%, #e8ffea 100%), linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 30%, rgba(255, 255, 255, 0.3) 80%, transparent 100%)">
                         <view class="b-radius pd-10 p-rtv" style="border: 1rpx solid #baedbf">
-                            <image class="w-200" src="/static/images/plant/typeofBusiness.png" mode="widthFix"
-                                style="position: absolute; top: 10rpx; left: 10rpx" />
+                            <image class="w-200" src="/static/images/plant/typeofBusiness.png" mode="widthFix" style="position: absolute; top: 10rpx; left: 10rpx" />
                             <view v-if="!speciesArray.length" class="pd-20"></view>
                             <view v-if="speciesArray.length" class="d-flex pr-15">
                                 <view class="flex1"></view>
-                                <view class="f-s-22 c-primary"
-                                    @click="$u.route({ url: '/plant/species/config/index' })">去修改{{ '>' }}</view>
+                                <view class="f-s-22 c-primary" @click="$u.route({ url: '/plant/species/config/index' })">去修改{{ '>' }}</view>
                             </view>
                             <view v-if="speciesArray.length" class="c-#333 f-s-24 d-flex pl-40 pr-15 pb-15 pt-15">
                                 <view class="ov-hd tx-ov w-s-no">{{ speciesArray.join('、') }}</view>
-                                <view v-if="speciesArray.length > 4" class="flex1 w-s-no">等{{ speciesArray.length }}个品种
-                                </view>
+                                <view v-if="speciesArray.length > 4" class="flex1 w-s-no">等{{ speciesArray.length }}个品种 </view>
                             </view>
-                            <view v-if="!speciesArray.length" @click="$u.route({ url: '/plant/species/config/index' })"
-                                class="c-primary bg-#E3F6E7 f-s-22 mg-at radius-10 w-250 h-50 d-flex a-c j-c">
-                                暂未配置品种,去配置{{ '>' }}</view>
+                            <view v-if="!speciesArray.length" @click="$u.route({ url: '/plant/species/config/index' })" class="c-primary bg-#E3F6E7 f-s-22 mg-at radius-10 w-250 h-50 d-flex a-c j-c"> 暂未配置品种,去配置{{ '>' }}</view>
                             <view v-if="!speciesArray.length" class="pd-7"></view>
                         </view>
                     </view>
                 </view>
-                <view class="b-radius pt-0 bg-#f7f7f7"
-                    style="border: 1rpx solid #fff; border-bottom-color: transparent; margin-top: -40rpx">
+                <view class="b-radius pt-0 bg-#f7f7f7" style="border: 1rpx solid #fff; border-bottom-color: transparent; margin-top: -40rpx">
                     <up-sticky :offset-top="stickyTop">
                         <view class="d-flex a-c pd-16 p-rtv">
                             <view class="c-333 f-s-32 f-w-5 z-index-1">基地与地块管理</view>
+                            <image class="w-230" src="/static/images/plant/BasePlotManagement.png" mode="widthFix" style="position: absolute; top: 44rpx; left: 16rpx; z-index: 0" />
                             <view class="flex1"></view>
-                            <view @click="$u.route({ url: '/plant/base/gap-base-info/index' })"
-                                class="c-primary f-s-22 z-index-1">GAP基地获评信息管理{{ '>' }}</view>
-                            <image src="/static/images/plant/basePlotBG.png" class="w-100%" mode="widthFix"
-                                style="position: absolute; top: 0; left: 0; z-index: -1"></image>
+                            <view @click="$u.route({ url: '/plant/base/gap-base-info/index' })" class="c-primary f-s-22 z-index-1">GAP基地获评信息管理{{ '>' }}</view>
+                            <image src="/static/images/plant/basePlotBG.png" class="w-100%" mode="widthFix" style="position: absolute; top: 0; left: 0; z-index: -1"></image>
                         </view>
                         <view class="d-flex a-c pt-20 pb-20 pl-16 pr-16 bg-#f7f7f7">
                             <view class="min-w-170 flex1">
-                                <ut-action-sheet v-model="form.type" :tabs="[{ label: '全部', value: '' }]"
-                                    @change="onRefresh" title="选择原料类型">
+                                <ut-action-sheet v-model="form.queryType" :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 w-s-no">{{ '全部' }}
-                                        </view>
-                                        <up-icon size="24rpx" color="#333" name="arrow-down-fill"
-                                            class="mr-5"></up-icon>
+                                        <view class="flex1 ov-hd f-s-28 c-333 text-center f-w-5 w-s-no">{{ '全部' }} </view>
+                                        <up-icon size="24rpx" color="#333" name="arrow-down-fill" class="mr-5"></up-icon>
                                     </view>
                                 </ut-action-sheet>
                             </view>
                             <view class="h-86 pl-20 w-100%">
-                                <ut-search ref="searchRef" v-model="form.keyword" @search="changeSeach" margin="0"
-                                    :border="false" :placeholder="form.placeholder" bgColor="#fff" height="86rpx"
-                                    borderRadius="10rpx"></ut-search>
+                                <ut-search ref="searchRef" v-model="form.keyword" @search="changeSeach" margin="0" :border="false" placeholder="搜基地名称、编号、地址、负责人" bgColor="#fff" height="86rpx" borderRadius="10rpx"></ut-search>
                             </view>
                         </view>
                     </up-sticky>
                     <view class="pd-16">
                         <template>
                             <view v-for="(item, index) in list" :key="index" class="b-radius bg-#fff pd-20 mb-20">
-                                <view class="c-333 f-s-34 pd-5 f-w-5">德钦县拖顶乡洛沙村各加尼拉基地</view>
-                                <view class="c-ccc f-s-24 pd-5 pt-0">DQTDLSC00156768951001</view>
+                                <view class="c-333 f-s-34 pd-5 f-w-5">{{ item?.baseName }}</view>
+                                <view class="c-ccc f-s-24 pd-5 pt-0">{{ item?.baseCode }}</view>
                                 <view class="d-flex a-c">
                                     <view class="c-333 f-s-28 pd-5">
                                         <text class="c-#666">基地面积:</text>
-                                        <text class="f-w-5">12000亩</text>
+                                        <text class="f-w-5">{{ item?.gapInfo?.area || '-' }}</text>
                                     </view>
                                     <view class="flex1"></view>
                                     <view class="c-333 f-s-28 pd-5">
                                         <text class="c-#666">建设时间:</text>
-                                        <text class="f-w-5">2003年</text>
+                                        <text class="f-w-5">{{ item?.buildDate || '-' }}</text>
                                     </view>
                                 </view>
                                 <view class="c-333 f-s-28 pd-5">
                                     <text class="c-#666">基地地址:</text>
-                                    <text class="f-w-5">云南省红河州个旧市卡房镇田心村小田心村268号</text>
+                                    <text class="f-w-5">{{ item?.gapInfo?.address || '-' }}</text>
                                 </view>
                                 <view class="c-333 f-s-28 pd-5 d-flex">
                                     <text class="c-#666 w-s-no">当前在地品种:</text>
-                                    <text
-                                        class="ov-hd tx-ov w-s-no f-w-5">三七、天麻、徐长卿、白及、徐长卿、白及、三七、天麻、徐长卿、白及、徐长卿、白及</text>
-                                    <text class="flex1 w-s-no f-w-5">等120个品种</text>
+                                    <text class="ov-hd tx-ov w-s-no f-w-5">{{ item.plantingVarieties?.map((items) => items.variety).join('、') || '-' }}</text>
+                                    <text v-if="item.plantingVarieties?.length" class="flex1 w-s-no f-w-5">等{{ item.plantingVarieties?.length }}个品种</text>
                                 </view>
                                 <view class="pd-10"></view>
                                 <view class="p-rtv">
-                                    <up-image width="100%"
-                                        src="https://img1.baidu.com/it/u=436536502,2810995452&fm=253&fmt=auto&app=120&f=JPEG?w=820&h=500"
-                                        mode="widthFix"> </up-image>
-                                    <view class="pl-20 pr-20 pt-10 pb-10 bg-#00000080 c-#ccc f-s-20"
-                                        style="position: absolute; bottom: 140rpx; right: 0; border-radius: 10rpx 0 0 10rpx">
-                                        李思思负责</view>
-                                    <view class="pl-20 pr-20 pt-10 pb-10 bg-#00000080 c-#ccc f-s-20"
-                                        style="position: absolute; bottom: 80rpx; right: 0; border-radius: 10rpx 0 0 10rpx">
-                                        公司+合作社</view>
-                                    <view class="pl-20 pr-20 pt-10 pb-10 bg-#00000080 c-#ccc f-s-20"
-                                        style="position: absolute; bottom: 20rpx; right: 0; border-radius: 10rpx 0 0 10rpx">
-                                        经度:E64.63 纬度:N27.7385</view>
+                                    <up-image width="100%" :src="item.gapInfo?.basePic" mode="widthFix"> </up-image>
+                                    <view class="pl-20 pr-20 pt-10 pb-10 bg-#00000080 c-#ccc f-s-20" style="position: absolute; bottom: 140rpx; right: 0; border-radius: 10rpx 0 0 10rpx"> {{ item?.contactName }}负责</view>
+                                    <view class="pl-20 pr-20 pt-10 pb-10 bg-#00000080 c-#ccc f-s-20" style="position: absolute; bottom: 80rpx; right: 0; border-radius: 10rpx 0 0 10rpx"> 公司+合作社</view>
+                                    <view class="pl-20 pr-20 pt-10 pb-10 bg-#00000080 c-#ccc f-s-20" style="position: absolute; bottom: 20rpx; right: 0; border-radius: 10rpx 0 0 10rpx"> 经度:E{{ item?.gapInfo?.lng }} 纬度:N{{ item?.gapInfo?.lat }}</view>
                                 </view>
                             </view>
                         </template>
-
                     </view>
                 </view>
             </view>
         </template>
         <template #empty>
             <ut-empty class="mg-at" color="#ccc" size="28rpx" image="/static/images/plant/noEmptyBase.png">尚未添加绘制种养殖基地信息~</ut-empty>
-            <view class="b-radius c-#fff f-s-36 bg-#37A954 h-78 w-382 d-flex a-c j-c mg-at">
+            <view class="b-radius c-#fff f-s-36 bg-#37A954 h-78 w-382 d-flex a-c j-c mg-at" @click="showDeleteDialog = true">
                 <img class="w-38 h-36 mr-10" src="/static/images/plant/chooseGAP.png" alt="" mode="widthFix" />
                 <text>去添加基地</text>
             </view>
         </template>
     </z-paging>
+    <ut-confirm-dialog v-model:show="showDeleteDialog" width="80vw" title="请选择要添加到基地类型" :confirmText="'确认选择'" :cancelText="'取消'" @confirm="handlechoseConfirm" @cancel="handleDeleteCancel">
+        <view class="" v-for="item in pt_base_type" :key="item?.value">
+            <view style="border: 1rpx solid" :style="{ backgroundColor: item?.value == basetype ? '#EBF6EE' : '#f7f7f7', borderColor: item?.value == basetype ? '#37A954' : 'transparent' }" class="pr-30 d-flex a-c mb-20 radius-100" @click="handlechose(item.value)">
+                <view class="radius-50% mg-8">
+                    <up-avatar size="90rpx" :src="item?.avatar || 'https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/common/avatar.png'" class="mr-20"></up-avatar>
+                </view>
+                <view class="c-#333 f-s-34">
+                    {{ item?.label }}
+                </view>
+                <view class="flex1"></view>
+                <view class="d-flex">
+                    <img v-if="basetype === item.value" class="w-30 h-30" src="/static/images/plant/chooseSuccessfully.png" mode="widthFix" alt="" />
+                </view>
+            </view>
+        </view>
+    </ut-confirm-dialog>
 </template>
 <script setup lang="ts">
 import { useClientRequest } from '@/utils/request';
 import { setCipByNum } from '@/utils/public';
-interface navbar {
-    bottom: number;
-    height: number;
-    left: number;
-    right: number;
-    top: number;
-    width: number;
-    id: string;
-    dataset?: any
+import { useInfoStore } from '@/store';
+import { computed, ref } from 'vue';
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { pt_base_type } = toRefs<any>(proxy?.useDict('pt_base_type'));
+const infoStore = useInfoStore();
+// 获取用户信息
+const avatar = computed(() => infoStore.userInfo?.avatar || '');
+const name = computed(() => infoStore.userInfo?.name || '');
+const phone = computed(() => infoStore.userInfo?.phone || '');
+const currentCpyName = computed(() => infoStore.userInfo?.currentCpyName || '');
+// 证书文件类型
+interface CertFile {
+    fileName: string;
+    url: string;
+    fileSize: number;
+}
+// 差异信息类型
+interface GapInfo {
+    id: number;
+    sourceType: string;
+    gapBaseName: string;
+    sn: string;
+    medicineName: string;
+    medicineId: number;
+    area: number;
+    basePic: string;
+    lng: number;
+    lat: number;
+    adcode: string;
+    address: string;
+    ratedDate: string; // 格式: "YYYY-MM-DD"
+    certFile: CertFile[];
+    res: string;
+    auditor: number;
+    msg: string;
+}
+
+// 种植品种类型
+interface PlantingVariety {
+    baseId: number;
+    varietyId: string;
+    variety: string;
+}
+
+// 坐标点类型
+interface Coordinate {
+    lng: number;
+    lat: number;
+}
+
+// 主数据类型
+interface BaseData {
+    id: number;
+    baseName: string;
+    baseCode?: string;
+    baseType?: string;
+    adcode?: string;
+    buildDate?: string; // 格式: "YYYY-MM-DD"
+    orgType: string;
+    contactId: number;
+    contactTel: string;
+    contactName?: string;
+    lng: number;
+    lat: number;
+    basePic: string;
+    address: string;
+    area: number;
+    gapFlag: string;
+    gapInfo: GapInfo;
+    cpyid: number;
+    appid: number;
+    partnerId: number;
+    createBy: number;
+    updateBy: number;
+    createTime: string; // ISO 8601 格式
+    updateTime: string; // ISO 8601 格式
+    hide: string;
+    plantingVarieties: PlantingVariety[];
+    coordinates: Coordinate[][]; // 二维坐标数组
 }
 const instance = getCurrentInstance();
-const list = ref([]);
+const list = ref<BaseData[]>();
 const paging = ref();
 const bubble = ref(uni.getMenuButtonBoundingClientRect());
-const avatar = ref();
-const name = ref('神奇大侠');
-const phone = ref('17708862791');
-const cpyname = ref('智慧溯源有限公司');
-const form = ref({ type: '', placeholder: '搜基地名称、编号、地址、负责人', keyword: '' });
+
+const form = ref({ queryType: '', keyword: '' });
 const speciesArray = ref([]);
 const navBarBgColor = ref('transparent');
 const stickyTop = ref(0);
@@ -185,7 +239,7 @@ const changeSeach = () => {
 const onRefresh = () => {
     paging.value.reload();
 };
-const onPageScroll = (e) => {
+const onPageScroll = (e: any) => {
     const { scrollTop } = e.detail;
     if (scrollTop > 20) {
         navBarBgColor.value = '#d9f8d6';
@@ -200,7 +254,6 @@ const query = async (pageNum: number, pageSize: number) => {
         ...form.value,
     };
     const res = await useClientRequest.get('/plt-api/app/base/pageList', params);
-    console.log(res);
     const { rows } = res;
     paging.value.complete(rows);
 };
@@ -208,21 +261,33 @@ const query = async (pageNum: number, pageSize: number) => {
 const getSpecies = async () => {
     const res = await useClientRequest.get('/plt-api/app/cpyVariety/list');
     if (res.code === 200) {
-        console.log(res);
-        speciesArray.value = res.data.map((item) => item.varietyName)
+        speciesArray.value = res.data.map((item: any) => item.medicineName);
     }
 };
+const showDeleteDialog = ref(false);
+const basetype = ref();
+const handlechose = (item: string) => {
+    basetype.value = item;
+};
+
+// 处理删除取消
+const handleDeleteCancel = () => {
+    showDeleteDialog.value = false;
+    basetype.value = null;
+};
+const handlechoseConfirm = () => {
+    uni.$u.route({ type: 'navigateTo', url: '/plant/base/base-edit/index', params: { basetype: basetype.value } });
+};
 
 onMounted(() => {
-    const querys = uni.createSelectorQuery().in(instance.proxy);
+    const querys = uni.createSelectorQuery().in(instance?.proxy);
     querys
         .select('#topup-navbar')
         .boundingClientRect((data: any) => {
             stickyTop.value = data.top + data.height;
         })
         .exec();
-    getSpecies()
-    query(1, 10)
+    getSpecies();
 });
 
 // setTimeout(() => {

+ 52 - 24
src/plant/base/gap-base-info/index.vue

@@ -22,25 +22,25 @@
                     <up-swipe-action>
                         <up-swipe-action-item v-for="item in list" :name="item?.id" :key="item?.id" :options="optionsAction" @click="clickSwipe" class="mb-20">
                             <view class="b-radius bg-#fff pd-20">
-                                <view class="c-#333 f-s-34 f-w-5 pb-5">德钦县拖顶乡洛沙村各尼拉三七GAP基地</view>
-                                <view class="c-#999 f-s-24 pb-20">2025年-05-12获评</view>
+                                <view class="c-#333 f-s-34 f-w-5 pb-5">{{ item?.gapBaseName }}</view>
+                                <view class="c-#999 f-s-24 pb-20">{{ item?.ratedDate }}获评</view>
                                 <view class="d-flex pb-5">
                                     <view class="w-50%">
                                         <text class="c-#666 f-s-28">种养殖品种:</text>
-                                        <text class="c-#333 f-s-28 f-w-5">三七</text>
+                                        <text class="c-#333 f-s-28 f-w-5">{{ item?.medicineName }}</text>
                                     </view>
                                     <view class="w-50%">
                                         <text class="c-#666 f-s-28">基地面积:</text>
-                                        <text class="c-#333 f-s-28 f-w-5">120亩</text>
+                                        <text class="c-#333 f-s-28 f-w-5">{{ item?.area }}</text>
                                     </view>
                                 </view>
                                 <view class="border-bottom-#f7f7f7 pb-20">
                                     <text class="c-#666 f-s-28">基地地址:</text>
-                                    <text class="c-#333 f-s-28 f-w-5">云南省红河州个旧市卡房镇田心村小田心村268号工业园</text>
+                                    <text class="c-#333 f-s-28 f-w-5">{{ item?.address }}</text>
                                 </view>
                                 <view class="pl-5 pr-5 pt-20 pb-20 border-top-#f7f7f7 c-#FC333F">
                                     <text class="f-s-28">审核不通过原因:</text>
-                                    <text class="f-s-28">云南省红河州个旧市卡房镇田心村小田心村268号工业园</text>
+                                    <text class="f-s-28">{{ item?.msg }}</text>
                                 </view>
                             </view>
                         </up-swipe-action-item>
@@ -48,11 +48,11 @@
                 </view>
             </template>
             <!-- 空数据处理 -->
-            <template v-if="!address && list.length === 0">
+            <template #empty v-if="!address && list.length === 0">
                 <ut-empty class="mg-at" size="28rpx" color="#999" padding="10rpx" image="/static/images/plant/noEmptyBase.png">非云南省内企业,无法获取已获评的GAP基地信息~</ut-empty>
                 <view class="d-flex j-c f-s-28 c-#ccc">可点击底部按钮上传佐证材料添加获评信息</view>
             </template>
-            <template v-if="address && list.length === 0">
+            <template #empty v-if="address && list.length === 0">
                 <ut-empty class="mg-at" size="28rpx" color="#999" padding="10rpx" image="/static/images/plant/noEmptyBase.png">暂无获评GAP基地信息~</ut-empty>
                 <view class="d-flex j-c f-s-28 c-#ccc">如需认定GAP基地,可前往数字云药官网进行申报</view>
                 <view class="d-flex j-c f-s-28 c-primary pd-15">https://www.shuziyunyao.com/</view>
@@ -71,12 +71,45 @@
 </template>
 <script setup lang="ts">
 import { copyText } from '@/utils/public';
+import { useClientRequest } from '@/utils/request';
+import { useInfoStore } from '@/store';
 interface ListItem {
+    rows: MedicineBase[];
+    code: number;
+    msg: string;
+    total: number;
+}
+interface MedicineBase {
     id: number;
-    name: string;
+    sourceType: string;
+    gapBaseName: string;
+    sn: string;
+    medicineName: string;
+    medicineId: number;
+    area: number;
+    basePic: string;
+    lng: number;
+    lat: number;
+    adcode: string;
+    address: string;
+    ratedDate: string;
+    certFile: Array<{
+        fileName: string;
+        url: string;
+        fileSize: number;
+    }>;
+    res: string;
+    auditor: number;
+    msg: string;
+    coordinates: Array<
+        Array<{
+            lng: number;
+            lat: number;
+        }>
+    >;
 }
 const paging = ref();
-const list = ref<ListItem[]>([]);
+const list = ref<MedicineBase[]>([]);
 const placeholder = ref('搜基地名称、品种、基地地址');
 const tabs = ref([
     { label: '全部', value: '' },
@@ -85,14 +118,15 @@ const tabs = ref([
     { label: '待审核', value: '2' },
 ]);
 // 判断是否是云南的企业
-const address = ref(false);
+const isYunnanCompany = (): boolean => {
+    const adcdCode = useInfoStore().companyInfo?.adcdCode;
+    return adcdCode?.startsWith('53') || false;
+};
+const address = computed(() => isYunnanCompany());
 const form = ref({
     keywords: '',
     type: '',
 });
-setTimeout(() => {
-    address.value = true;
-}, 2000);
 const query = async (pageNum: number, pageSize: number) => {
     const params = {
         pageNum,
@@ -100,16 +134,10 @@ const query = async (pageNum: number, pageSize: number) => {
         ...form.value,
     };
     // const res = await cpyList(params);
-    let res: ListItem[] = [];
-    //循环push10条数据
-    for (let i = 0; i < 10; i++) {
-        res.push({
-            id: i + 1,
-            name: `${i + 1}`,
-        });
-    }
-    // const { rows } = res;
-    paging.value.complete(res);
+    ///app/gapCertificationInfo/pageList
+    const res = await useClientRequest.get<ListItem>('/plt-api/app/gapCertificationInfo/pageList', params);
+    const { rows } = res;
+    paging.value.complete(rows);
 };
 const onRefresh = () => {
     paging.value.reload();

+ 27 - 24
src/plant/species/config/index.vue

@@ -13,7 +13,7 @@
                 <view class="f-s-32 c-333 f-w-5 mb-20">已配置的主营品种</view>
                 <!-- speLable 组件使用示例 -->
                 <view class="d-flex a-c f-w-w">
-                    <spe-lable class="mr-20 mb-20" v-for="(item, index) in speArray" :key="index" :text="item?.varietyName" size="30rpx" :id="item?.id" @close="() => handleLabelClose(item?.id)" />
+                    <spe-lable class="mr-20 mb-20" v-for="(item, index) in speArray" :key="index" :text="item?.medicineName" size="30rpx" :id="item?.id" @close="() => handleLabelClose(item?.id)" />
                 </view>
                 <ut-empty class="mg-at" v-if="speArray.length == 0">
                     <view class="d-flex a-c j-c f-s-28 c-ccc">暂未配置单位主营品种 </view>
@@ -22,7 +22,7 @@
             </view>
         </template>
         <template v-for="(item, index) in list" :key="index">
-            <spe-list :text="item?.medicineName" :searchText="form.medicineName" :check="item?.isChoice" @update:check="(value) => handleCheckChange(value, item.id)"> </spe-list>
+            <spe-list :text="item?.medicineName" :searchText="form.medicineName" :check="item?.isChoice" @update:check="(value) => handleCheckChange(value, item?.medicineCode)"> </spe-list>
         </template>
         <template #empty>
             <ut-empty class="mg-at">
@@ -45,10 +45,10 @@ import speLable from '../models/speLable.vue';
 import SpeList from '../models/speList.vue';
 
 interface ListItem {
-    rows:SpeArrayItem[]
-    code:number;
+    rows: SpeArrayItem[];
+    code: number;
     msg: string;
-    total: number;    
+    total: number;
 }
 //定义speArray
 interface SpeArrayItem {
@@ -65,6 +65,7 @@ interface SpeArrayItem {
     modifier: string | null;
     sourceType: number;
     updateTime: string | null;
+    medicineCode?: string | null;
 }
 
 const list = ref<SpeArrayItem[]>([]);
@@ -92,15 +93,14 @@ const handleLabelClose = (id: string | number) => {
     currentDeleteId.value = id as number;
     showDeleteDialog.value = true;
 };
-const handleCheckChange = (newCheckValue: string, id: number) => {
+const handleCheckChange = (newCheckValue: string, medicineCode: string | null) => {
     // 根据ID找到对应的列表项并更新
-    const item = list.value.find((item) => item.id === id);
+    const item = list.value.find((item) => item.medicineCode === medicineCode);
     if (item) {
         item.isChoice = newCheckValue;
     }
-    speArray.value = speArray.value.filter((item) => item.varietyId !== id);
-    console.log(speArray.value,'speArray.value');
-    
+    speArray.value = speArray.value.filter((item) => item.medicineCode !== medicineCode);
+    console.log(speArray.value, 'speArray.value');
 };
 const query = async (pageNum: number, pageSize: number) => {
     const params = {
@@ -113,10 +113,10 @@ const query = async (pageNum: number, pageSize: number) => {
         return;
     }
     // const res = await cpyList(params);
-    const res: ListItem = await useClientRequest.get<ListItem>('/plt-api/app/medicine/pageList',params)   
+    const res: ListItem = await useClientRequest.get<ListItem>('/plt-api/app/medicine/pageList', params);
     const { rows } = res;
     // 给列表项添加 isChoice 属性
-    paging.value.complete(rows);    
+    paging.value.complete(rows);
 };
 const changeSeach = () => {
     paging.value.reload();
@@ -124,29 +124,32 @@ const changeSeach = () => {
 const onRefresh = () => {
     paging.value.reload();
 };
-const subMit = async() => {
+const subMit = async () => {
     // 方法1:使用 Set 避免重复,更高效
-    const existingIds = new Set(speArray.value.map(speItem => speItem.varietyId));
+    const existingIds = new Set(speArray.value.map((speItem) => speItem.medicineCode));
     list.value.forEach((item) => {
-        if (item.isChoice === '1' && !existingIds.has(item.id)) {
-            speArray.value.push({varietyId:item.id,varietyName:item?.medicineName, isChoice: '1'});
+        console.log(item);
+        if (item.isChoice === '1' && !existingIds.has(item.medicineCode)) {
+            speArray.value.push({ medicineName: item?.medicineName, medicineCode: item?.medicineCode, isChoice: '1' });
         }
     });
+    console.log(speArray.value, 'speArray.value');
+
     // 直接使用过滤后的 speArray 中的所有 id
     await useClientRequest.post('/plt-api/app/cpyVariety/add', {
-        varietyIds: speArray.value.map(item => item.varietyId),
+        medicineCodes: speArray.value.map((item) => item.medicineCode),
     });
-    
+
     form.value.medicineName = '';
     onRefresh();
-    getSpecies()
+    getSpecies();
 };
 
 // 处理删除确认
-const handleDeleteConfirm = async() => {
+const handleDeleteConfirm = async () => {
     if (currentDeleteId.value !== null) {
-       await useClientRequest.get(`/plt-api/app/cpyVariety/delById/${currentDeleteId.value}`);
-       getSpecies()
+        await useClientRequest.get(`/plt-api/app/cpyVariety/delById/${currentDeleteId.value}`);
+        getSpecies();
     }
     showDeleteDialog.value = false;
     currentDeleteId.value = null;
@@ -158,6 +161,6 @@ const handleDeleteCancel = () => {
     currentDeleteId.value = null;
 };
 onMounted(() => {
-    getSpecies()
-})
+    getSpecies();
+});
 </script>

+ 0 - 0
src/static/images/plant/基地与地块管理.png → src/static/images/plant/BasePlotManagement.png


+ 9 - 6
src/store/index.ts

@@ -1,9 +1,12 @@
 import { createPinia } from 'pinia';
+import { createPersistedState } from 'pinia-plugin-persistedstate';
+const pinia = createPinia();
+pinia.use(createPersistedState());
 
-const store = createPinia();
+export default pinia;
 
-export { store };
-
-export * from './modules/dict';
-export * from './modules/user';
-export * from './modules/auth';
+// 显式导出以避免命名冲突
+export { useDictStore } from './modules/dict';
+export { useUserStore, type UserInfo as UserInfoType, type UserProfile } from './modules/user';
+export { useAuthStore, type LoginForm, type LoginResponse } from './modules/auth';
+export { useInfoStore } from './modules/info';

+ 179 - 0
src/store/modules/info.ts

@@ -0,0 +1,179 @@
+import { useClientRequest } from '@/utils/request';
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import { UserInfoOptional, CompanyInfoData } from '../type/infoType';
+import type { StorageLike } from 'pinia-plugin-persistedstate';
+// 为 UniApp 创建适配的 storage(与 store/index.ts 保持一致)
+const uniStorage: StorageLike = {
+    getItem: (key: string): string | null => {
+        try {
+            return uni.getStorageSync(key);
+        } catch (error) {
+            console.error('读取存储失败:', error);
+            return null;
+        }
+    },
+    setItem: (key: string, value: string): void => {
+        try {
+            uni.setStorageSync(key, value);
+        } catch (error) {
+            console.error('写入存储失败:', error);
+        }
+    },
+};
+//判断当前是测试版还是正式版
+const accountInfo = uni.getAccountInfoSync();
+const prefixMap = {
+    release: 'prod_', // production的缩写
+    trial: 'beta_', // beta测试版
+    develop: 'dev_', // development的缩写
+};
+// Token存储键和默认过期时间(24小时)
+const TOKEN_STORAGE_KEY = `${prefixMap[accountInfo?.miniProgram?.envVersion]}_token`;
+const INFOSTORE_KEY = `${prefixMap[accountInfo?.miniProgram?.envVersion]}info_store`;
+const DEFAULT_TOKEN_EXPIRE = 24 * 60 * 60 * 1000; // 24小时(毫秒)
+export const useInfoStore = defineStore(
+    'infoStore',
+    () => {
+        // token
+        const token = ref('');
+        //从存储中读取token并检查是否过期
+        const getTokenFromStorage = (): string => {
+            try {
+                const stored = uni.getStorageSync(TOKEN_STORAGE_KEY);
+                console.log(stored, 'stored');
+                if (!stored) return '';
+                const tokenData = JSON.parse(stored);
+                const { value, timestamp, expire } = tokenData;
+                // 检查是否过期
+                if (expire && Date.now() - timestamp > expire) {
+                    uni.removeStorageSync(TOKEN_STORAGE_KEY);
+                    return '';
+                }
+                return value;
+            } catch (error) {
+                console.error('读取token失败:', error);
+                return '';
+            }
+        };
+        /**
+         * 设置token,可指定过期时间(毫秒)
+         * @param value token值
+         * @param expireMs 过期时间(毫秒),默认24小时
+         */
+        const setToken = (value: string, expireMs: number = DEFAULT_TOKEN_EXPIRE) => {
+            token.value = value;
+            // 存储带过期时间的token
+            const tokenData = {
+                value,
+                timestamp: Date.now(),
+                expire: expireMs,
+            };
+            uni.setStorageSync(TOKEN_STORAGE_KEY, JSON.stringify(tokenData));
+        };
+        //移除token
+        const removeToken = () => {
+            token.value = '';
+            uni.removeStorageSync(TOKEN_STORAGE_KEY);
+            // 移除token,并删除其他缓存数据
+            uni.removeStorageSync(INFOSTORE_KEY);
+        };
+        // 检查token是否过期
+        const isTokenExpired = (): boolean => {
+            try {
+                const stored = uni.getStorageSync(TOKEN_STORAGE_KEY);
+                if (!stored) return true;
+                const tokenData = JSON.parse(stored);
+                const { timestamp, expire } = tokenData;
+                // 如果没有设置过期时间,则永不过期
+                if (!expire) return false;
+                return Date.now() - timestamp > expire;
+            } catch {
+                return true;
+            }
+        };
+
+        // 获取token剩余有效时间(毫秒)
+        const getTokenRemainingTime = (): number => {
+            try {
+                const stored = uni.getStorageSync(TOKEN_STORAGE_KEY);
+                if (!stored) return 0;
+
+                const tokenData = JSON.parse(stored);
+                const { timestamp, expire } = tokenData;
+
+                if (!expire) return Infinity;
+
+                const elapsed = Date.now() - timestamp;
+                const remaining = expire - elapsed;
+
+                return remaining > 0 ? remaining : 0;
+            } catch {
+                return 0;
+            }
+        };
+        // 初始化时从存储读取token
+        token.value = getTokenFromStorage();
+        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+        // 用户信息
+        const userInfo = ref<UserInfoOptional | null>(null);
+        // 获取用户信息
+        const getUserInfo = async (): Promise<UserInfoOptional | null> => {
+            try {
+                if (!token.value) return null;
+                const { data } = await useClientRequest.get('/app/auth/getUserInfo');
+                userInfo.value = data;
+                return data;
+            } catch (error) {
+                console.error('获取用户信息失败:', error);
+                return null;
+            }
+        };
+        // 清除用户信息
+        const clearUserInfo = (): void => {
+            userInfo.value = null;
+        };
+        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+        //公司信息
+        const companyInfo = ref<CompanyInfoData | null>(null);
+        // 获取公司信息
+        const getCompanyInfo = async (): Promise<CompanyInfoData | null> => {
+            try {
+                const { data } = await useClientRequest.get(`/app/company/currentCpyDetail`);
+                companyInfo.value = data;
+                return data;
+            } catch (error) {
+                console.error('获取公司信息失败:', error);
+                return null;
+            }
+        };
+        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+        // 微信小程序登录获取的code
+
+        const wxLogin = async (data: unknown) => {
+            const res = await useClientRequest.post(`/auth/login`, data, false);
+            setToken(res.data?.access_token);
+        };
+        return {
+            token,
+            setToken,
+            removeToken,
+            isTokenExpired,
+            getTokenRemainingTime,
+            userInfo,
+            getUserInfo,
+            clearUserInfo,
+            companyInfo,
+            getCompanyInfo,
+            wxLogin,
+        };
+    },
+    {
+        // 启用持久化,但只持久化 userInfo,其中token使用自定义的带过期时间的存储逻辑
+        persist: {
+            key: INFOSTORE_KEY,
+            storage: uniStorage,
+            pick: ['userInfo', 'companyInfo'],
+        },
+    }
+);

+ 94 - 0
src/store/type/infoType.ts

@@ -0,0 +1,94 @@
+export interface UserInfoOptional {
+  adcode?: string | null;
+  adcodeName?: string | null;
+  avatar?: string | null;
+  bindCpyRes?: string | null;
+  certCount?: number | null;
+  cpyNames?: string[] | null;
+  createBy?: string | null;
+  createTime?: string | null;
+  currentCardId?: string | null;
+  currentCpyName?: string | null;
+  currentCpyid?: string | null;
+  currentCreditCode?: string | null;
+  email?: string | null;
+  expertPersonId?: string | null;
+  functions?: string[] | null;
+  id?: string | null;
+  idcard?: string | null;
+  isExpertPerson?: string | null;
+  name?: string | null;
+  nickName?: string | null;
+  openid?: string | null;
+  phone?: string | null;
+  postName?: string | null;
+  pwdStatus?: string | null;
+  roles?: string[] | null;
+  status?: string | null;
+  tenantId?: string | null;
+  unionid?: string | null;
+  updateTime?: string | null;
+  vipEnd?: string | null;
+  vipFlag?: string | null;
+  vipStart?: string | null;
+  wxBindStauts?: string | null;
+  wxbind?: string | null;
+}
+interface LicenseInfo {
+  url: string;
+  fileName: string;
+  fileType: string | null;
+  fileSize: string | null;
+  pureFileName: string;
+}
+export interface CompanyInfoData{
+    id: string | null;
+  cpySn: string | null;
+  cpyName: string | null;
+  tel: string | null;
+  creditCode: string | null;
+  status: string | null;
+  authStatus: string | null;
+  adcdCode: string | null;
+  storePhoto: string | null;
+  address: string | null;
+  contactPerson: string | null;
+  currentTel: string | null;
+  currentPerson: string | null;
+  adminId: string | null;
+  adminName: string | null;
+  res: string | null;
+  bindMsg: string | null;
+  joinPartner: string | null;
+  partnerId: number | null;
+  partnerName: string | null;
+  userid: string | null;
+  createBy: string | null;
+  license: LicenseInfo | null;
+  cpyType: string | null;
+  purposeApp: string | null;
+  fieldApplyId: string | null;
+  userType: string | null;
+  operatorName: string | null;
+  operateType: string | null;
+  allStatus: string | null;
+  application: any[] | null;
+  certSpecial: any[] | null;
+  adcdCodeName: string | null;
+  adminPhone: string | null;
+  adminAvatar: string | null;
+  certBase: any[] | null;
+  createTime: string | null;
+  updateTime: string | null;
+  postCode: string | null;
+  bindStatus: string | null;
+  cpyResMsg: string | null;
+  integrate: string | null;
+  extraInfo: ExtraInfo | null;
+  hasAdmin: string | null;
+  lastLoginInfo: any | null;
+  supervisionType: string | null;
+  supervisionArea: string | null;
+  supervisionAreaName: string | null;
+  integrateApp: any[] | null;
+}

+ 2 - 2
src/utils/public.ts

@@ -60,9 +60,9 @@ export function goLogin(): void {
     });
 }
 
-export const setCipByNum = (val: string, startNum: number, num: number, isCip: boolean = false): string => {
+export const setCipByNum = (val: string | null, startNum: number, num: number, isCip: boolean = false): string => {
     if (isCip) {
-        return val;
+        return val || '-';
     }
     if (!val) return '-';
     const a = val.slice(0, startNum);

+ 109 - 15
src/utils/request.ts

@@ -1,4 +1,8 @@
 import config from '@/config';
+import { useInfoStore } from '@/store';
+import { getCurrentPage } from '@/utils/public';
+import { recursiveDecodeURIComponent } from '@/utils/ruoyi';
+import errorCode from '@/utils/errorCode';
 const { clientId, appid } = config;
 // uniapp封装的请求方法
 let timeout = 60 * 1000;
@@ -7,16 +11,38 @@ let timeout = 60 * 1000;
 const getHeader = () => {
     let header = {
         'Content-Type': 'application/json',
-        Authorization: 'Bearer ' + uni.getStorageSync('token') || '',
-        'xid':config?.appid || '',
-        'clientId':config?.clientId || '',
+        Authorization: 'Bearer ' + useInfoStore().token || '',
+        xid: config?.appid || '',
+        clientId: config?.clientId || '',
     };
     return header;
 };
 
 // 获取host地址
-export const request = ({ url, method = 'GET', data = {}, header = null }: any) => {
+export const request = ({ url, method = 'GET', data = {}, isToken = true, header = null }: any) => {
     const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
+    if (isToken && useInfoStore().isTokenExpired()) {
+        uni.hideLoading();
+        useInfoStore().removeToken();
+        let fullPath = recursiveDecodeURIComponent(getCurrentPage()?.$page?.fullPath);
+        const isLoginPage = recursiveDecodeURIComponent(fullPath).indexOf('/pages/login/login') !== -1;
+        if (isLoginPage) {
+            return;
+        }
+        const fulllpathParams = fullPath.split('?');
+        const fullpathstr = fulllpathParams.length > 1 ? `${fulllpathParams[0]}?${fulllpathParams[fulllpathParams.length - 1]}` : fulllpathParams[0];
+        // 获取当前页面路径
+        uni.$u.route({
+            type: 'redirect',
+            url: '/pages/login/login',
+            params: {
+                // 转成浏览器可识别的字符串
+                // 这里可以传递当前页面的路径作为参数,方便登录后重定向回原页面
+                redirect: encodeURIComponent(fullpathstr),
+            },
+        });
+        return;
+    }
     return new Promise((resolve, reject) => {
         uni.request({
             url: baseUrl + url,
@@ -24,42 +50,110 @@ export const request = ({ url, method = 'GET', data = {}, header = null }: any)
             data,
             timeout: timeout,
             header: header || getHeader(),
-            success: (res: any) => {
-                resolve(res.data);
-            },
-            fail: (err) => {
-                reject(err);
-            },
-        });
+        })
+            .then((response) => {
+                let { data, statusCode } = response;
+                if (statusCode !== 200) {
+                    uni.showToast({
+                        icon: 'none',
+                        title: '后端接口连接异常',
+                    });
+                    reject('后端接口连接异常');
+                    return;
+                }
+                const code = (data as any).code || 200;
+                const msg = errorCode[code] || (data as any).msg || errorCode['default'];
+                // 处理业务错误
+                if (code === 401) {
+                    uni.hideLoading();
+                    useInfoStore().removeToken();
+                    let fullPath = recursiveDecodeURIComponent(getCurrentPage()?.$page?.fullPath);
+                    const isLoginPage = recursiveDecodeURIComponent(fullPath).indexOf('/pages/login/login') !== -1;
+                    if (isLoginPage) {
+                        return;
+                    }
+                    const fulllpathParams = fullPath.split('?');
+                    const fullpathstr = fulllpathParams.length > 1 ? `${fulllpathParams[0]}?${fulllpathParams[fulllpathParams.length - 1]}` : fulllpathParams[0];
+                    // 获取当前页面路径
+                    uni.$u.route({
+                        type: 'redirect',
+                        url: '/pages/login/login',
+                        params: {
+                            // 转成浏览器可识别的字符串
+                            // 这里可以传递当前页面的路径作为参数,方便登录后重定向回原页面
+                            redirect: encodeURIComponent(fullpathstr),
+                        },
+                    });
+                    return reject('无效的会话,或者会话已过期,请重新登录。');
+                } else if (code === 500) {
+                    uni.hideLoading();
+                    if (msg) {
+                        uni.showToast({
+                            title: msg,
+                            icon: 'none',
+                        });
+                    }
+                    return reject('500');
+                } else if (code !== 200) {
+                    uni.hideLoading();
+                    uni.showToast({
+                        title: msg,
+                        icon: 'none',
+                    });
+                    reject(code);
+                }
+                // 成功情况
+                resolve(data);
+            })
+            .catch((error) => {
+                let { message } = error;
+                if (message === 'Network Error') {
+                    message = '后端接口连接异常';
+                } else if (message.includes('timeout')) {
+                    message = '系统接口请求超时';
+                } else if (message.includes('Request failed with status code')) {
+                    message = '系统接口' + message.substr(message.length - 3) + '异常';
+                }
+                uni.hideLoading();
+                uni.showToast({
+                    title: message,
+                    icon: 'none',
+                });
+                return reject(error);
+            });
     });
 };
 export const useClientRequest = {
-    post: <T = any>(url: string, data?: any): Promise<T> => {
+    post: <T = any>(url: string, data?: any, isToken?: boolean): Promise<T> => {
         return request({
             url,
             method: 'POST',
             data,
+            isToken,
         }) as Promise<T>;
     },
-    get: <T = any>(url: string, data?: any): Promise<T> => {
+    get: <T = any>(url: string, data?: any, isToken?: boolean): Promise<T> => {
         return request({
             url,
             method: 'GET',
             data,
+            isToken,
         }) as Promise<T>;
     },
-    put: <T = any>(url: string, data?: any): Promise<T> => {
+    put: <T = any>(url: string, data?: any, isToken?: boolean): Promise<T> => {
         return request({
             url,
             method: 'PUT',
             data,
+            isToken,
         }) as Promise<T>;
     },
-    delete: <T = any>(url: string, data?: any): Promise<T> => {
+    delete: <T = any>(url: string, data?: any, isToken?: boolean): Promise<T> => {
         return request({
             url,
             method: 'DELETE',
             data,
+            isToken,
         }) as Promise<T>;
     },
 };

+ 64 - 0
src/utils/ruoyi.ts

@@ -347,3 +347,67 @@ export const getFileSuffix = (fileName: string): string => {
     const lastDotIndex = fileName.lastIndexOf('.');
     return lastDotIndex !== -1 ? fileName.substring(lastDotIndex + 1) : '';
 };
+export const recursiveDecode = (input: string, maxDepth: number = 10): string => {
+    if (typeof input !== 'string' || input.length === 0) return input;
+    
+    let prev: string = input;
+    
+    for (let i = 0; i < maxDepth; i++) {
+        let decoded: string;
+        try {
+            // 先替换 '+' 为 ' '(常见 form 编码场景),再解码
+            decoded = decodeURIComponent(prev.replace(/\+/g, ' '));
+        } catch (e) {
+            // 非法序列或无法解码,停止
+            break;
+        }
+        if (decoded === prev) break;
+        prev = decoded;
+    }
+    
+    return prev;
+};
+
+// 递归解码对象的类型
+type RecursiveDecodeResult<T> = T extends string 
+    ? string 
+    : T extends Array<infer U> 
+        ? Array<RecursiveDecodeResult<U>> 
+        : T extends object 
+            ? { [K in keyof T]: RecursiveDecodeResult<T[K]> } 
+            : T;
+
+export const recursiveDecodeURIComponent = <T>(obj: T): RecursiveDecodeResult<T> => {
+    if (typeof obj === 'string') {
+        return recursiveDecode(obj) as RecursiveDecodeResult<T>;
+    } else if (Array.isArray(obj)) {
+        return obj.map((item) => recursiveDecodeURIComponent(item)) as RecursiveDecodeResult<T>;
+    } else if (typeof obj === 'object' && obj !== null) {
+        const decodedObj: Record<string, any> = {};
+        for (const key in obj) {
+            if (Object.prototype.hasOwnProperty.call(obj, key)) {
+                decodedObj[key] = recursiveDecodeURIComponent((obj as Record<string, any>)[key]);
+            }
+        }
+        return decodedObj as RecursiveDecodeResult<T>;
+    }
+    return obj as RecursiveDecodeResult<T>;
+};
+
+// 或者使用更简单的类型定义,保持函数简洁
+export const recursiveDecodeURIComponentSimple = (obj: any): any => {
+    if (typeof obj === 'string') {
+        return recursiveDecode(obj);
+    } else if (Array.isArray(obj)) {
+        return obj.map((item) => recursiveDecodeURIComponentSimple(item));
+    } else if (typeof obj === 'object' && obj !== null) {
+        const decodedObj: Record<string, any> = {};
+        for (const key in obj) {
+            if (Object.prototype.hasOwnProperty.call(obj, key)) {
+                decodedObj[key] = recursiveDecodeURIComponentSimple(obj[key]);
+            }
+        }
+        return decodedObj;
+    }
+    return obj;
+};

ファイルの差分が大きいため隠しています
+ 1 - 0
stats.html


+ 3 - 7
unocss.config.js

@@ -5,6 +5,7 @@ export default defineConfig({
     shortcuts: [
         {
             center: 'flex justify-center items-center',
+            'b-radius': 'radius-16',
         },
     ],
     rules: [
@@ -91,6 +92,8 @@ export default defineConfig({
                 'background-color': `rgba(0, 0, 0, ${num})`,
             }),
         ],
+        [/^radius-([\.\d]+)$/, ([_, num]) => ({ 'border-radius': `${num}rpx` })],
+        [/^radius-([\.\d]+)%$/, ([_, num]) => ({ 'border-radius': `${num}%` })],
         // 透明度
         [
             /^opacity-([\.\d]+)$/,
@@ -127,13 +130,6 @@ export default defineConfig({
         [/^min-h-([\.\d]+)$/, ([_, num]) => ({ 'min-height': `${num}rpx` })],
         // 栅格 30份
         [/^hcol-([\.\d]+)$/, ([_, num]) => ({ width: `${(+num / 30) * 100}%` })],
-        // border-radius-8
-        [
-            /^border-radius-([\.\d]+)$/,
-            ([_, num]) => ({
-                'border-radius': `${num}rpx`,
-            }),
-        ],
         // border-w-4
         [
             /^border-w-([\.\d]+)$/,

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません