Kaynağa Gözat

完成上架后的代码修改

jiuling 2 ay önce
ebeveyn
işleme
ce4874a5bb

+ 38 - 6
App.vue

@@ -1,5 +1,6 @@
 <script setup>
 import storage from "@/utils/storage.js";
+import privacyUtil from "@/utils/privacy.js";
 import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
 import { useStore } from 'vuex';
 import { getCurrentInstance } from 'vue';
@@ -62,18 +63,49 @@ const initPlugins = async () => {
   }
 };
 
+// 显示隐私协议弹窗
+const showPrivacyDialog = () => {
+  // 检查是否需要显示隐私协议(适配多端)
+  if (privacyUtil.shouldShowAgreement()) {
+    console.log("未同意隐私协议,跳转到隐私协议页面");
+    uni.reLaunch({
+      url: '/pages/privacy/privacy-agreement'
+    });
+    return false; // 返回false表示需要先同意隐私协议
+  }
+  
+  return true; // 已同意隐私协议
+};
+
 // uni-app 生命周期钩子 (保持原有命名)
 onLaunch(() => {
-  console.log('App Launch');
-  // 初始化插件
-  initPlugins();
-  // 初始化 store 状态
-  store.dispatch('init');
-  checkLoginStatus();
+  // console.log('App Launch');
+  
+  // 首先检查隐私协议同意状态
+  const privacyAgreed = showPrivacyDialog();
+  
+  // 只有在同意隐私协议后才初始化其他功能
+  if (privacyAgreed) {
+    // 初始化插件
+    initPlugins();
+    // 初始化 store 状态
+    store.dispatch('init');
+    // 设置登录状态检查
+    checkLoginStatus();
+  }
 });
 
 onShow(() => {
   console.log('App Show');
+  
+  // 检查隐私协议状态,如果已同意但还未初始化,则进行初始化
+  if (privacyUtil.checkAgreement()) {
+    // 确保插件和store已初始化
+    initPlugins();
+    store.dispatch('init');
+    checkLoginStatus();
+  }
+
 });
 
 onHide(() => {

+ 28 - 1
androidPrivacy.json

@@ -1,3 +1,30 @@
 {
-    "prompt" : "template"
+    "version" : "2",
+    "prompt" : "template",
+    "title" : "用户协议和隐私政策",
+    "message" : "请你务必审慎阅读、充分理解"用户协议"和"隐私政策"各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、位置信息等信息用于农事管理、设备监控等功能。\n\n你可阅读<a href=\"https://nxy.gbdfarm.com:9000/#/pages/login/terms\">《服务协议》</a>和<a href=\"https://nxy.gbdfarm.com:9000/#/pages/login/privacy\">《隐私政策》</a>了解详细信息。如果你同意,请点击"同意"开始使用我们的服务。",
+    "buttonAccept" : "同意",
+    "buttonRefuse" : "不同意",
+    "second" : {
+        "title" : "确认提示",
+        "message" : "进入应用前,你需先同意<a href=\"https://nxy.gbdfarm.com:9000/#/pages/login/terms\">《服务协议》</a>和<a href=\"https://nxy.gbdfarm.com:9000/#/pages/login/privacy\">《隐私政策》</a>,否则将退出应用。",
+        "buttonAccept" : "同意",
+        "buttonRefuse" : "退出应用"
+    },
+    "styles" : {
+        "backgroundColor" : "#ffffff",
+        "borderRadius" : "10px",
+        "title" : {
+            "color" : "#333333",
+            "fontSize" : "18px"
+        },
+        "buttonAccept" : {
+            "color" : "#ffffff",
+            "backgroundColor" : "#4CAF50"
+        },
+        "buttonRefuse" : {
+            "color" : "#666666",
+            "backgroundColor" : "#f5f5f5"
+        }
+    }
 }

+ 22 - 14
api/services/device.js

@@ -16,7 +16,7 @@ const userInfo = storage.getUserInfo()
  */
 export function loginWvp() {
   return uni.request({
-    url: `/wvp/api/user/login?username=${config.wvpUsername}&password=${config.wvpPassword}`,
+    url: `${api.wvpServer}/api/user/login?username=${config.wvpUsername}&password=${config.wvpPassword}`,
     method: Method.GET,
     needToken: false,
   });
@@ -28,19 +28,27 @@ export async function getChannels(deviceId) {
   try {
 	  // 登录获取token
     const loginRes = await loginWvp();
-    console.log("WVP登录结果:", loginRes);
-	if (loginRes[0]) {
-		  // 处理错误
-		  console.error('登录失败', loginRes[0]);
-		  throw new Error('登录失败');
-		}
-	const response = loginRes[1].data;
+    console.log("WVP登录结果 code:", loginRes.code);
+    console.log("WVP登录结果 msg:", loginRes.msg);
+    console.log("WVP登录结果 data:", loginRes.data);
+    
+    // uni.request 返回格式: [err, res]
+    if (!loginRes.data.data) {
+      // 处理错误
+      console.error('登录失败', loginRes);
+      throw new Error('登录失败');
+    }
+    
+    const response = loginRes.data;
+    console.log("WVP登录响应数据:", response);
+    
     if (response.code === 0) {
       console.log("WVP登录成功");
       storage.setWvpAccessToken(response.data.accessToken);
-	  // 查询通道列表
+      
+      // 查询通道列表
       const channelsRes = await uni.request({
-        url: `/wvp/api/device/query/devices/${deviceId}/channels`,
+        url: `${api.wvpServer}/api/device/query/devices/${deviceId}/channels`,
         method: Method.GET,
         data: {
           page: 1,
@@ -54,8 +62,8 @@ export async function getChannels(deviceId) {
       console.log("获取通道结果:", channelsRes);
       return channelsRes;
     } else {
-      console.error("WVP登录失败", loginRes);
-      throw new Error("WVP登录失败");
+      console.error("WVP登录失败,响应码:", response.code, "消息:", response.msg);
+      throw new Error(`WVP登录失败: ${response.msg || '未知错误'}`);
     }
   } catch (error) {
     console.error("获取通道失败", error);
@@ -67,7 +75,7 @@ export async function getChannels(deviceId) {
  */
 export async function playStart(deviceId, channelId) {
   return await  uni.request({
-    url: `/wvp/api/play/start/${deviceId}/${channelId}`,  
+    url: `${api.wvpServer}/api/play/start/${deviceId}/${channelId}`,  
     method: Method.GET,
     header: {
       'Access-Token': `${storage.getWvpAccessToken()}`,
@@ -79,7 +87,7 @@ export async function playStart(deviceId, channelId) {
  */
 export async function pause(deviceId, channelId) {
   return await  uni.request({
-    url: `/wvp/api/play/stop/${deviceId}/${channelId}`,  
+    url: `${api.wvpServer}/api/play/stop/${deviceId}/${channelId}`,  
     method: Method.GET,
     header: {
       'Access-Token': `${storage.getWvpAccessToken()}`,

+ 271 - 0
components/common/LocationPicker-v2.vue

@@ -0,0 +1,271 @@
+<template>
+  <!-- 编辑模式 -->
+  <view v-if="mode === 'edit'" class="location-picker-wrapper">
+    <!-- 显示输入框 -->
+    <view class="input-display" @click="showPicker = true">
+      <text class="input-text" :class="{ placeholder: !displayLabel }">
+        {{ displayLabel || placeholder }}
+      </text>
+      <view class="suffix-icons">
+        <view 
+          v-if="innerValue && displayLabel"
+          @click.stop="clearAddress"
+          class="clear-btn"
+        >
+          <text class="clear-icon">×</text>
+        </view>
+        <text class="arrow-icon">></text>
+      </view>
+    </view>
+
+    <!-- 选择器弹窗 -->
+    <uni-data-picker
+      v-model="innerValue"
+      :localdata="localData"
+      :popup-title="popupTitle"
+      :show="showPicker"
+      @change="onChange"
+      @popupopened="onPopupOpened"
+      @popupclosed="onPopupClosed"
+    />
+  </view>
+
+  <!-- 展示模式:纯文本 -->
+  <text v-else class="location-text">
+    {{ displayLabel || "无" }}
+  </text>
+</template>
+
+<script setup>
+import { ref, watch, computed } from 'vue'
+import cityRows from '@/utils/data.json'
+
+// Props definition
+const props = defineProps({
+  mode: {
+    type: String,
+    default: "edit"
+  },
+  modelValue: {
+    type: [String, Number],
+    default: ""
+  },
+  placeholder: {
+    type: String,
+    default: "请选择省市区"
+  },
+  popupTitle: {
+    type: String,
+    default: "请选择省市区"
+  }
+})
+
+// Emits definition
+const emit = defineEmits(['update:modelValue', 'clear'])
+
+// Reactive data
+const innerValue = ref(props.modelValue)
+const localData = ref([])
+const displayLabel = ref('')
+const showPicker = ref(false)
+
+// Watchers
+watch(() => props.modelValue, (newVal) => {
+  console.log('[LocationPicker] props.modelValue 变化:', newVal)
+  innerValue.value = newVal
+  updateDisplayLabel(newVal)
+})
+
+watch(innerValue, (newVal) => {
+  console.log('[LocationPicker] innerValue 变化:', newVal)
+  emit("update:modelValue", newVal)
+})
+
+// Methods
+const onPopupOpened = () => {
+  console.log('[LocationPicker] 弹窗打开')
+}
+
+const onPopupClosed = () => {
+  console.log('[LocationPicker] 弹窗关闭')
+  showPicker.value = false
+}
+
+const onChange = (e) => {
+  console.log('[LocationPicker] onChange 事件:', e)
+  
+  if (e && e.detail && e.detail.value && e.detail.value.length > 0) {
+    const selectedNodes = e.detail.value
+    const lastNode = selectedNodes[selectedNodes.length - 1]
+    
+    console.log('[LocationPicker] 选择的节点:', selectedNodes)
+    console.log('[LocationPicker] 最后节点:', lastNode)
+    
+    // 更新值
+    innerValue.value = lastNode.value
+    
+    // 构建显示文本
+    const pathLabels = selectedNodes.map(node => node.text)
+    displayLabel.value = pathLabels.join(' - ')
+    
+    console.log('[LocationPicker] 更新后 - code:', lastNode.value, 'label:', displayLabel.value)
+    
+    // 关闭弹窗
+    showPicker.value = false
+  }
+}
+
+const clearAddress = () => {
+  console.log('[LocationPicker] 清空地址')
+  innerValue.value = ""
+  displayLabel.value = ""
+  emit("update:modelValue", "")
+  emit("clear")
+}
+
+const updateDisplayLabel = (value) => {
+  if (!value) {
+    displayLabel.value = ""
+    return
+  }
+  
+  const label = getLocationLabel(value)
+  displayLabel.value = label
+  console.log('[LocationPicker] 更新显示标签:', label)
+}
+
+const getLocationLabel = (value) => {
+  if (!value) return ""
+  
+  let label = ""
+  
+  const traverse = (nodes, path = []) => {
+    for (const node of nodes) {
+      const currentPath = [...path, node.text]
+      
+      // 使用 == 而不是 === 以支持字符串和数字比较
+      if (node.value == value) {
+        label = currentPath.join(' - ')
+        return true
+      }
+      
+      if (node.children && node.children.length > 0) {
+        if (traverse(node.children, currentPath)) {
+          return true
+        }
+      }
+    }
+    return false
+  }
+  
+  traverse(localData.value)
+  return label
+}
+
+const get_city_tree = () => {
+  if (!cityRows || cityRows.length === 0) {
+    console.warn('[LocationPicker] cityRows 数据为空')
+    return []
+  }
+  return handleTree(cityRows)
+}
+
+const handleTree = (data, parent_code = null) => {
+  let res = []
+  let keys = {
+    id: "code",
+    pid: "parent_code",
+    children: "children",
+    text: "name",
+    value: "code"
+  }
+
+  for (let item of data) {
+    if (parent_code === null) {
+      if (!item.hasOwnProperty(keys.pid) || item[keys.pid] == parent_code) {
+        let node = {
+          text: item[keys.text],
+          value: item[keys.value],
+          children: handleTree(data, item[keys.id])
+        }
+        res.push(node)
+      }
+    } else {
+      if (item.hasOwnProperty(keys.pid) && item[keys.pid] == parent_code) {
+        let node = {
+          text: item[keys.text],
+          value: item[keys.value],
+          children: handleTree(data, item[keys.id])
+        }
+        res.push(node)
+      }
+    }
+  }
+  return res
+}
+
+// Initialize
+localData.value = get_city_tree()
+console.log('[LocationPicker] 初始化 - localData 长度:', localData.value.length)
+console.log('[LocationPicker] 初始化 - modelValue:', props.modelValue)
+
+updateDisplayLabel(props.modelValue)
+</script>
+
+<style scoped>
+.location-picker-wrapper {
+  width: 100%;
+}
+
+.input-display {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  min-height: 44rpx;
+  padding: 12rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.input-text {
+  flex: 1;
+  font-size: 28rpx;
+  color: #333;
+  line-height: 44rpx;
+}
+
+.input-text.placeholder {
+  color: #999;
+}
+
+.suffix-icons {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+  flex-shrink: 0;
+}
+
+.clear-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 32rpx;
+  height: 32rpx;
+}
+
+.clear-icon {
+  font-size: 32rpx;
+  color: #999;
+  line-height: 1;
+}
+
+.arrow-icon {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.location-text {
+  font-size: 28rpx;
+  color: #333;
+}
+</style>

+ 334 - 68
components/common/LocationPicker.vue

@@ -1,59 +1,82 @@
 <template>
-  <!-- <view class="form-item">	 -->
-<!--   <view class="item-label">
-      <text class="label-text">{{ label }}</text>
-      <text v-if="required" class="required">*</text>
-    </view> -->
-
-    <!-- 编辑模式:输入框 -->
-    <uni-data-picker
-      v-if="mode === 'edit'"
-      v-model="innerValue"
-      :localdata="localData"
-      :popup-title="popupTitle"
-      @change="onChange"
-    >
-      <u-input
-        :value="getLocationLabel(innerValue)"
+  <!-- 编辑模式:输入框 -->
+  <view v-if="mode === 'edit'" class="location-picker-wrapper">
+    <view class="input-wrapper" @click="openPicker">
+      <input
+        class="location-input"
+        :value="displayLabel"
         :placeholder="placeholder"
         readonly
-        suffix-icon="arrow-down"
-      >
-        <!-- 清除按钮 -->
-        <template #suffix>
-          <view 
-            v-if="innerValue"
-            @click.stop="clearAddress"
-            style="padding: 0 8rpx; display: flex; align-items: center;"
+        disabled
+      />
+      <view class="suffix-icons">
+        <view 
+          v-if="innerValue && displayLabel"
+          @click.stop="clearAddress"
+          class="clear-btn"
+        >
+          <text class="clear-icon">×</text>
+        </view>
+        <text class="arrow-icon">></text>
+      </view>
+    </view>
+    
+    <!-- 自定义弹窗选择器 -->
+    <view v-if="showPicker" class="picker-modal" @click="closePicker">
+      <view class="picker-content" @click.stop>
+        <view class="picker-header">
+          <text class="picker-cancel" @click="closePicker">取消</text>
+          <text class="picker-title">{{ popupTitle }}</text>
+          <text class="picker-confirm" @click="confirmSelection">确定</text>
+        </view>
+        <view class="picker-body">
+          <picker-view 
+            :value="pickerValue" 
+            @change="onPickerChange"
+            class="picker-view"
           >
-            <uni-icons type="close" color="#999" size="20"></uni-icons>
-          </view>
-        </template>
-      </u-input>
-    </uni-data-picker>
-
-    <!-- 展示模式:纯文本 -->
-    <text v-else class="location-text">
-      {{ getLocationLabel(innerValue) || "无" }}
-    </text>
-  <!-- </view> -->
+            <picker-view-column>
+              <view class="picker-item" v-for="(item, index) in provinceList" :key="index">
+                {{ item.text }}
+              </view>
+            </picker-view-column>
+            <picker-view-column>
+              <view class="picker-item" v-for="(item, index) in cityList" :key="index">
+                {{ item.text }}
+              </view>
+            </picker-view-column>
+            <picker-view-column>
+              <view class="picker-item" v-for="(item, index) in districtList" :key="index">
+                {{ item.text }}
+              </view>
+            </picker-view-column>
+          </picker-view>
+        </view>
+      </view>
+    </view>
+  </view>
+
+  <!-- 展示模式:纯文本 -->
+  <text v-else class="location-text">
+    {{ displayLabel || "无" }}
+  </text>
 </template>
 
 <script setup>
-import { ref, watch } from 'vue'
+import { ref, watch, computed } from 'vue'
 import cityRows from '@/utils/data.json'
 
 // Props definition
 const props = defineProps({
-  mode: { // 新增模式属性
+  mode: {
     type: String,
     default: "edit" // edit / view
   },
-  value: { // v-model
+  modelValue: {
     type: [String, Number],
     default: ""
   },
-  label: { // 左侧文字
+  label: {
     type: String,
     default: "所在地"
   },
@@ -72,52 +95,188 @@ const props = defineProps({
 })
 
 // Emits definition
-const emit = defineEmits(['input', 'clear'])
+const emit = defineEmits(['update:modelValue', 'clear'])
 
 // Reactive data
-const innerValue = ref(props.value)
-const localData = ref([]) // 省市区树数据
+const innerValue = ref(props.modelValue)
+const displayLabel = ref('')
+const showPicker = ref(false)
+const pickerValue = ref([0, 0, 0]) // picker-view 的索引值
+const tempSelection = ref({ province: null, city: null, district: null }) // 临时选择
+
+// 省市区数据
+const localData = ref([])
+const provinceList = ref([])
+const cityList = ref([])
+const districtList = ref([])
+
+// 计算属性:根据当前选择更新城市和区县列表
+const updateCityList = () => {
+  const provinceIndex = pickerValue.value[0]
+  if (provinceList.value[provinceIndex] && provinceList.value[provinceIndex].children) {
+    cityList.value = provinceList.value[provinceIndex].children
+  } else {
+    cityList.value = []
+  }
+  // 重置城市索引
+  if (pickerValue.value[1] >= cityList.value.length) {
+    pickerValue.value[1] = 0
+  }
+}
+
+const updateDistrictList = () => {
+  const cityIndex = pickerValue.value[1]
+  if (cityList.value[cityIndex] && cityList.value[cityIndex].children) {
+    districtList.value = cityList.value[cityIndex].children
+  } else {
+    districtList.value = []
+  }
+  // 重置区县索引
+  if (pickerValue.value[2] >= districtList.value.length) {
+    pickerValue.value[2] = 0
+  }
+}
 
 // Watchers
-watch(() => props.value, (newVal) => {
+watch(() => props.modelValue, (newVal) => {
+  console.log('props.modelValue 变化:', newVal)
   innerValue.value = newVal
+  const label = getLocationLabel(newVal)
+  displayLabel.value = label
+  console.log('更新后的 displayLabel:', label)
 })
 
 watch(innerValue, (newVal) => {
-  emit("input", newVal)
+  console.log('innerValue 变化:', newVal)
+  emit("update:modelValue", newVal)
 })
 
 // Methods
-/** 点击选择后的回调 */
-const onChange = (e) => {
-  const lastNode = e.detail.value[e.detail.value.length - 1]
-  innerValue.value = lastNode.value // 只存最底层的 code
+/** 打开选择器 */
+const openPicker = () => {
+  console.log('打开地区选择器')
+  showPicker.value = true
+  
+  // 如果有已选值,定位到对应位置
+  if (innerValue.value) {
+    locateSelection(innerValue.value)
+  }
+}
+
+/** 关闭选择器 */
+const closePicker = () => {
+  showPicker.value = false
+}
+
+/** picker-view 变化事件 */
+const onPickerChange = (e) => {
+  const val = e.detail.value
+  const oldValue = [...pickerValue.value]
+  pickerValue.value = val
+  
+  // 如果省份改变,更新城市列表
+  if (oldValue[0] !== val[0]) {
+    updateCityList()
+    updateDistrictList()
+  }
+  // 如果城市改变,更新区县列表
+  else if (oldValue[1] !== val[1]) {
+    updateDistrictList()
+  }
+  
+  console.log('picker 值变化:', val)
+}
+
+/** 确认选择 */
+const confirmSelection = () => {
+  const provinceIndex = pickerValue.value[0]
+  const cityIndex = pickerValue.value[1]
+  const districtIndex = pickerValue.value[2]
+  
+  const province = provinceList.value[provinceIndex]
+  const city = cityList.value[cityIndex]
+  const district = districtList.value[districtIndex]
+  
+  if (province && city && district) {
+    // 更新值为区县的 code
+    innerValue.value = district.value
+    
+    // 构建显示文本
+    displayLabel.value = `${province.text} - ${city.text} - ${district.text}`
+    
+    console.log('确认选择:', displayLabel.value, '值:', innerValue.value)
+    
+    emit("update:modelValue", innerValue.value)
+  }
+  
+  closePicker()
 }
 
 /** 清空地址 */
 const clearAddress = () => {
+  console.log('清空地址')
   innerValue.value = ""
+  displayLabel.value = ""
+  pickerValue.value = [0, 0, 0]
+  emit("update:modelValue", "")
   emit("clear")
 }
 
+/** 定位到已选择的值 */
+const locateSelection = (value) => {
+  if (!value) return
+  
+  // 在数据中查找对应的省市区
+  for (let i = 0; i < provinceList.value.length; i++) {
+    const province = provinceList.value[i]
+    if (province.children) {
+      for (let j = 0; j < province.children.length; j++) {
+        const city = province.children[j]
+        if (city.children) {
+          for (let k = 0; k < city.children.length; k++) {
+            const district = city.children[k]
+            if (district.value == value) {
+              pickerValue.value = [i, j, k]
+              updateCityList()
+              updateDistrictList()
+              console.log('定位到:', i, j, k)
+              return
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
 /** 回显文字(递归找路径) */
 const getLocationLabel = (value) => {
+  console.log('getLocationLabel 被调用, value:', value)
   if (!value) return ""
+  
   let label = ""
-  const traverse = (nodes) => {
+  
+  const traverse = (nodes, currentPath = []) => {
     for (const node of nodes) {
-      if (node.value === value) {
-        label = node.text
+      const newPath = [...currentPath, node.text]
+      
+      if (node.value == value) {
+        label = newPath.join(' - ')
+        console.log('找到匹配节点:', node, '路径:', label)
         return true
       }
-      if (node.children && traverse(node.children)) {
-        label = node.text + " - " + label
-        return true
+      
+      if (node.children && node.children.length > 0) {
+        if (traverse(node.children, newPath)) {
+          return true
+        }
       }
     }
     return false
   }
+  
   traverse(localData.value)
+  console.log('最终返回的 label:', label)
   return label
 }
 
@@ -167,32 +326,139 @@ const handleTree = (data, parent_code = null) => {
   return res
 }
 
-// Initialize data (replaces created lifecycle)
+// Initialize data
 localData.value = get_city_tree()
+provinceList.value = localData.value
+console.log('初始化 localData, 数据长度:', localData.value.length)
+
+// 初始化城市和区县列表
+if (provinceList.value.length > 0) {
+  updateCityList()
+  updateDistrictList()
+}
+
+// 初始化显示标签
+if (props.modelValue) {
+  displayLabel.value = getLocationLabel(props.modelValue)
+  console.log('初始化 displayLabel:', displayLabel.value)
+}
 </script>
 
 <style scoped>
-.form-item {
-  display: flex;
-  flex-direction: column;
-  /* margin-bottom: 20rpx; */
+.location-picker-wrapper {
+  width: 100%;
 }
-.item-label {
+
+.input-wrapper {
+  position: relative;
   display: flex;
   align-items: center;
-  margin-bottom: 10rpx;
+  width: 100%;
+  height: 44rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+  padding: 12rpx 0;
 }
-.label-text {
+
+.location-input {
+  flex: 1;
   font-size: 28rpx;
   color: #333;
+  height: 44rpx;
+  line-height: 44rpx;
 }
-.required {
-  color: red;
-  margin-left: 5rpx;
+
+.location-input:disabled {
+  background-color: transparent;
+  color: #333;
 }
+
+.suffix-icons {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+}
+
+.clear-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 32rpx;
+  height: 32rpx;
+}
+
+.clear-icon {
+  font-size: 32rpx;
+  color: #999;
+  line-height: 1;
+}
+
+.arrow-icon {
+  font-size: 24rpx;
+  color: #999;
+}
+
 .location-text {
- /* font-size: 28rpx;
-  color: #333; */
-  /* padding: 16rpx 0; */	
+  font-size: 28rpx;
+  color: #333;
+}
+
+/* 弹窗样式 */
+.picker-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  z-index: 9999;
+  display: flex;
+  align-items: flex-end;
+}
+
+.picker-content {
+  width: 100%;
+  background-color: #fff;
+  border-radius: 24rpx 24rpx 0 0;
+  overflow: hidden;
+}
+
+.picker-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24rpx 32rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.picker-cancel,
+.picker-confirm {
+  font-size: 28rpx;
+  color: #007aff;
+  padding: 8rpx 16rpx;
+}
+
+.picker-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.picker-body {
+  height: 500rpx;
+}
+
+.picker-view {
+  width: 100%;
+  height: 100%;
+}
+
+.picker-item {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+  color: #333;
+  height: 80rpx;
+  line-height: 80rpx;
 }
 </style>

+ 10 - 6
config/api.js

@@ -20,26 +20,30 @@ const dev = {
   // H5 开发环境使用代理路径,其他平台使用真实域名
   serve: isH5 ? "/base" : "https://nxy.gbdfarm.com:9000/pro-uniapp",
   // serve: "https://nxy.gbdfarm.com:9000/pro-uniapp",
-  upload: import.meta.env.VITE_UPLOAD_URL || "http://nxy.gbdfarm.com"
+  upload: import.meta.env.VITE_UPLOAD_URL || "http://nxy.gbdfarm.com",
+  // WVP 服务器地址 - 注意:WVP 可能在不同的端口或路径
+  wvpServer: isH5 ? "/wvp" : "https://nxy.gbdfarm.com:9000/wvp"
 };
 
 // 生产环境配置
 const prod = {
   serve: import.meta.env.VITE_BASE_URL || "https://nxy.gbdfarm.com:9000/pro-uniapp",
-  upload: import.meta.env.VITE_UPLOAD_URL || "https://nxy.gbdfarm.com"
+  upload: import.meta.env.VITE_UPLOAD_URL || "https://nxy.gbdfarm.com",
+  // WVP 服务器地址 - 注意:WVP 可能在不同的端口或路径
+  wvpServer: isH5 ? "/wvp" : "https://nxy.gbdfarm.com:9000/wvp"
 };
 
 // 根据环境选择配置
 let api = process.env.NODE_ENV === "development" ? dev : prod;
-
+console.log("api",api);
 // 微信小程序、App、鸿蒙始终使用生产环境配置
 // #ifdef MP-WEIXIN
 api = prod;
 // #endif
 
-// #ifdef APP-PLUS
-api = prod;
-// #endif
+// // #ifdef APP-PLUS
+// api = prod;
+// // #endif
 
 // #ifdef MP-HARMONY
 api = prod;

+ 15 - 1
config/config.js

@@ -6,7 +6,6 @@ export default {
   downloadLink: "https://pickmall.cn/download-page/index.html", //下载地址,下载app的地址
   shareLink: "https://m-b2b2c.pickmall.cn", //分享地址,也就是在h5中默认的复制地址
   appid: "wx6f10f29075dc1b0b", //小程序唯一凭证,即 AppID,可在「微信公众平台 - 设置 - 开发设置」页中获得。(需要已经成为开发者,且帐号没有异常状态)
-  aMapKey: "1f78544934b66c9fbc0104117f663973", //在高德中申请Web服务key
   scanAuthNavigation: ["https://m-b2b2c.pickmall.cn/"], //扫码认证跳转域名配置 会根据此处配置的路由进行跳转
   iosAppId: "id1564638363", //AppStore的应用地址id 具体在分享->拷贝链接中查看
   logo: "https://lilishop-oss.oss-cn-beijing.aliyuncs.com/4c864e133c2944efad1f7282ac8a3b9e.png", //logo地址
@@ -17,6 +16,21 @@ export default {
   /* wvp用户/密码(MD5) */
   wvpUsername: "admin", // 用户名
   wvpPassword: "af7b951b3a30e898e2684ffe8d20a961", // 密码(MD5加密)
+  wvpServer: "https://nxy.gbdfarm.com:9000/pro-uniapp/wvp", // WVP服务器地址
+  /**
+   * 流媒体服务器配置
+   * 用于不同平台的视频播放
+   */
+  streamServer: {
+    // WebSocket-FLV 流地址 (H5端使用)
+    wsFlvServer: "ws://121.4.16.100:6080/rtp/",
+    // RTMP 流地址 (小程序、App端优先使用)
+    rtmpServer: "rtmp://121.4.16.100:1935/live/",
+    // HLS 流地址 (小程序、App端备用)
+    hlsServer: "http://121.4.16.100:8080/hls/",
+    // HTTP-FLV 流地址 (备用)
+    httpFlvServer: "http://121.4.16.100:8080/flv/"
+  },
   /**
    * 如需更换主题请修改此处以及uni.scss中的全局颜色
    */

+ 63 - 0
harmony-configs/entry/src/main/module.json5

@@ -0,0 +1,63 @@
+{
+  "module": {
+    "name": "entry",
+    "type": "entry",
+    "description": "$string:module_desc",
+    "mainElement": "EntryAbility",
+    "deviceTypes": [
+      "phone",
+      "tablet",
+      "2in1"
+    ],
+    "deliveryWithInstall": true,
+    "installationFree": false,
+    "pages": "$profile:main_pages",
+    "abilities": [
+      {
+        "name": "EntryAbility",
+        "srcEntry": "./ets/entryability/EntryAbility.ets",
+        "description": "$string:EntryAbility_desc",
+        "icon": "$media:layered_image",
+        "label": "$string:EntryAbility_label",
+        "startWindowIcon": "$media:startIcon",
+        "startWindowBackground": "$color:start_window_background",
+        "exported": true,
+        "skills": [
+          {
+            "entities": [
+              "entity.system.home"
+            ],
+            "actions": [
+              "action.system.home"
+            ]
+          }
+        ]
+      }
+    ],
+    "requestPermissions": [
+      {
+        "name": "ohos.permission.INTERNET"
+      },
+	  {
+	    "name": "ohos.permission.APPROXIMATELY_LOCATION",
+	    "reason": "$string:location_reason",
+	    "usedScene": {
+	      "when": "inuse"
+	    }
+	  },
+	  {
+	    "name": "ohos.permission.LOCATION",
+	    "reason": "$string:location_reason",
+	    "usedScene": {
+	      "when": "inuse"
+	    }
+	  }
+    ],
+    "metadata": [
+      {
+        "name": "TENCENT_MAP_KEY",
+        "value": "2N6BZ-VX5LJ-4GCFA-DGPXT-F4ZNF-7CB5D"
+      }
+    ]
+  }
+}

+ 20 - 0
harmony-configs/entry/src/main/resources/base/element/string.json

@@ -0,0 +1,20 @@
+{
+  "string": [
+    {
+      "name": "module_desc",
+      "value": "module description"
+    },
+    {
+      "name": "EntryAbility_desc",
+      "value": "description"
+    },
+    {
+      "name": "EntryAbility_label",
+      "value": "$string:app_name"
+    },
+	{
+	  "name": "location_reason",
+	  "value": "用于提供割草机定位服务"
+	}
+  ]
+}

+ 14 - 11
manifest.json

@@ -8,6 +8,7 @@
     "app-plus" : {
         "distribute" : {
             "android" : {
+                "enableOAID" : false,
                 "abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
                 "minSdkVersion" : 21,
                 "targetSdkVersion" : 30,
@@ -27,11 +28,14 @@
                     "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />",
                     "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\" />",
                     "<uses-permission android:name=\"android.permission.INTERNET\" />",
-                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />",
                     "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />",
                     "<uses-permission android:name=\"android.permission.ACCESS_LOCATION_EXTRA_COMMANDS\" />",
+                    "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>"
+                ],
+                "excludePermissions" : [
                     "<uses-permission android:name=\"android.permission.BLUETOOTH\" />",
-                    "<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\" />"
+                    "<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\" />",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />"
                 ]
             },
             "ios" : {
@@ -50,8 +54,7 @@
                     "tencent" : {
                         "key" : "2N6BZ-VX5LJ-4GCFA-DGPXT-F4ZNF-7CB5D"
                     }
-                },
-                "ad" : {}
+                }
             },
             "icons" : {
                 "android" : {
@@ -91,7 +94,9 @@
         },
         "modules" : {
             "Geolocation" : {},
-            "Maps" : {}
+            "Maps" : {},
+            "VideoPlayer" : {},
+            "Camera" : {}
         },
         "permissions" : {
             "location" : {
@@ -125,10 +130,8 @@
         },
         "sdkConfigs" : {
             "maps" : {
-                "amap" : {
-                    "key" : "9f2cac7ea18905dd3830cf7360a43a35",
-                    "securityJsCode" : "41af52e416d1fd1b15020dac066cec86",
-                    "serviceHost" : ""
+                "tencent" : {
+                    "key" : "IY2BZ-6OLKH-TUQDT-WCKMG-W5Z6O-VUBSD"
                 }
             }
         }
@@ -169,8 +172,8 @@
                 }
             },
             "icons" : {
-                "foreground" : "E:/boxun/SoftWare/Feishu/download/nongxiaoyu_icon.png",
-                "background" : "E:/boxun/SoftWare/Feishu/download/nongxiaoyu_icon.png"
+                "foreground" : "E:/boxun/SoftWare/Feishu/download/nongxiaoyu_icon_512_clean.png/nongxiaoyu_icon_512_clean.png",
+                "background" : "E:/boxun/SoftWare/Feishu/download/nongxiaoyu_icon_512_clean.png/nongxiaoyu_icon_512_clean.png"
             },
             "splashScreens" : {
                 "startWindowIcon" : "E:/boxun/SoftWare/Feishu/download/nongxiaoyu_icon.png"

+ 8 - 0
pages.json

@@ -8,6 +8,14 @@
 				"navigationBarTitleText": "农小禹 - 首页"
 			}
 		},
+		{
+			"path": "pages/privacy/privacy-agreement",
+			"style": {
+				"navigationBarTitleText": "",
+				"navigationStyle": "custom",
+				"disableScroll": true
+			}
+		},
 		{
 			"path": "pages/login/index",
 			"style": {

+ 225 - 122
pages/activity/activity-detail.vue

@@ -941,7 +941,7 @@ const formatDateTime = (date, format = 'yyyy-MM-dd HH:mm') => {
   return format.replace(/yyyy|MM|dd|HH|mm/g, match => map[match]);
 }
 
-// 选择图片
+// 选择图片(优化跨平台兼容性)
 const chooseImage = () => {
   uni.chooseImage({
     count: 6 - formData.images.length,
@@ -949,156 +949,259 @@ const chooseImage = () => {
     sourceType: ['album', 'camera'],
     success: (res) => {
       console.log('选择图片成功:', res);
+      console.log('tempFiles:', res.tempFiles);
+      console.log('tempFilePaths:', res.tempFilePaths);
       
       // 验证文件类型和大小
       const validFiles = [];
       const invalidFiles = [];
       const maxSize = 5 * 1024 * 1024; // 5MB 最大限制
       
-      // 检查每个文件
-      res.tempFiles.forEach((file, index) => {
-        // 检查文件类型
-        const isImage = /\.(jpg|jpeg|png|gif)$/i.test(file.name);
-        // 检查文件大小
-        const isValidSize = file.size <= maxSize;
-        
-        if (isImage && isValidSize) {
-          validFiles.push(res.tempFilePaths[index]);
-        } else {
-          invalidFiles.push({
-            path: file.path,
-            size: file.size,
-            reason: !isImage ? '文件格式不支持' : '文件大于5MB'
-          });
-        }
-      });
+      // 兼容不同平台的文件信息获取方式
+      if (res.tempFiles && res.tempFiles.length > 0) {
+        // 检查每个文件
+        res.tempFiles.forEach((file, index) => {
+          console.log(`文件 ${index}:`, file);
+          
+          // 获取文件路径
+          const filePath = res.tempFilePaths[index];
+          
+          // 获取文件名(兼容不同平台)
+          let fileName = '';
+          if (file.name) {
+            fileName = file.name;
+          } else if (file.path) {
+            // 从路径中提取文件名
+            fileName = file.path.split('/').pop();
+          } else if (filePath) {
+            fileName = filePath.split('/').pop();
+          }
+          
+          console.log(`文件名: ${fileName}, 大小: ${file.size}`);
+          
+          // 检查文件类型(兼容没有扩展名的情况)
+          let isImage = true;
+          if (fileName) {
+            isImage = /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName);
+          }
+          
+          // 检查文件大小
+          const fileSize = file.size || 0;
+          const isValidSize = fileSize <= maxSize;
+          
+          if (isImage && isValidSize) {
+            validFiles.push(filePath);
+          } else {
+            invalidFiles.push({
+              path: filePath,
+              size: fileSize,
+              name: fileName,
+              reason: !isImage ? '文件格式不支持' : `文件大于5MB (${(fileSize / 1024 / 1024).toFixed(2)}MB)`
+            });
+          }
+        });
+      } else {
+        // 如果没有 tempFiles,直接使用 tempFilePaths(某些平台可能不返回详细信息)
+        console.warn('未获取到 tempFiles,直接使用 tempFilePaths');
+        res.tempFilePaths.forEach(path => {
+          validFiles.push(path);
+        });
+      }
+      
+      console.log('有效文件:', validFiles);
+      console.log('无效文件:', invalidFiles);
       
       // 显示无效文件提示
       if (invalidFiles.length > 0) {
-        uni.showToast({
-          title: `${invalidFiles.length}个文件无效,请检查格式和大小`,
-          icon: 'none',
-          duration: 2000
+        const reasons = invalidFiles.map(f => `${f.name}: ${f.reason}`).join('\n');
+        uni.showModal({
+          title: '部分文件无效',
+          content: `${invalidFiles.length}个文件无效:\n${reasons}`,
+          showCancel: false
         });
       }
       
       // 如果有有效文件,则上传
       if (validFiles.length > 0) {
         uploadImages(validFiles);
+      } else if (invalidFiles.length > 0) {
+        uni.showToast({
+          title: '没有可上传的文件',
+          icon: 'none',
+          duration: 2000
+        });
       }
     },
     fail: (err) => {
       console.error('选择图片失败:', err);
       uni.showToast({
         title: '选择图片失败',
-        icon: 'none'
+        icon: 'none',
+        duration: 2000
       });
     }
   });
 }
 
-// 上传图片到服务器
+// 上传图片到服务器(优化跨平台兼容性)
 const uploadImages = (tempFilePaths) => {
-      uni.showLoading({
-        title: '上传中...',
-        mask: true
-      });
-      
-      // 上传成功的图片计数
-      let successCount = 0;
-      let failCount = 0;
-      const totalFiles = tempFilePaths.length;
-      const newImages = [];
-      
-      // 遍历处理每张图片
-      tempFilePaths.forEach((path, index) => {
-        // 调用上传API
-        uni.uploadFile({
-          // url: api.serve + '/base/tasks/uploadTaskImage', 
-          url: api.serve + '/file/upload', 
-          filePath: path,
-          name: 'file', // 文件参数名称,需要与后端接口匹配
-          formData: {
-            type: 'task', // 标识文件类型,用于后端区分不同业务的文件
-            // directory: '/opt/app/nongxiaoyu/uploadImage' // 指定保存目录
-		},
-		header: {'Authorization': `Bearer ${storage.getAccessToken()}`},
-
-          success: (res) => {
-                          try {
-                const response = JSON.parse(res.data);
+	uni.showLoading({
+		title: '上传中...',
+		mask: true
+	});
+	
+	// 上传成功的图片计数
+	let successCount = 0;
+	let failCount = 0;
+	const totalFiles = tempFilePaths.length;
+	const newImages = [];
+	
+	// 遍历处理每张图片
+	tempFilePaths.forEach((path, index) => {
+		// 调用上传API
+		uni.uploadFile({
+			url: api.serve + '/file/upload', 
+			filePath: path,
+			name: 'file',
+			formData: {
+				type: 'task'
+			},
+			header: {
+				'Authorization': `Bearer ${storage.getAccessToken()}`
+			},
+			success: (res) => {
+				try {
+					console.log('上传原始响应:', res);
+					console.log('响应数据类型:', typeof res.data);
+					console.log('响应状态码:', res.statusCode);
+					
+					let response;
+					
+					// 兼容不同平台的响应格式
+					if (typeof res.data === 'string') {
+						// H5 端返回字符串,需要解析
+						response = JSON.parse(res.data);
+					} else if (typeof res.data === 'object') {
+						// Android/鸿蒙端可能直接返回对象
+						response = res.data;
+					} else {
+						throw new Error('未知的响应格式');
+					}
+					
+					console.log('解析后的响应:', response);
+					
+					// 检查响应是否成功
+					if (response.code === 200 || response.code === '200') {
+						// 兼容不同的数据结构
+						let imageUrl = '';
+						
+						// 情况1: response.data 是字符串(直接是URL)
+						if (typeof response.data === 'string') {
+							imageUrl = response.data;
+						}
+						// 情况2: response.data 是对象,包含 url 字段
+						else if (response.data && response.data.url) {
+							imageUrl = response.data.url;
+						}
+						// 情况3: response 直接包含 url 字段
+						else if (response.url) {
+							imageUrl = response.url;
+						}
+						// 情况4: response.data 是数组
+						else if (Array.isArray(response.data) && response.data.length > 0) {
+							imageUrl = response.data[0].url || response.data[0];
+						}
+						
+						if (imageUrl) {
+							console.log('上传成功,图片URL:', imageUrl);
+							
+							// 上传成功,将图片信息添加到数组
+							newImages.push({
+								url: imageUrl,
+								path: path,
+								status: 'success',
+								fileName: (response.data && response.data.fileName) || ''
+							});
+							successCount++;
+						} else {
+							console.error('无法从响应中提取图片URL:', response);
+							failCount++;
+							uni.showToast({
+								title: '图片URL解析失败',
+								icon: 'none',
+								duration: 2000
+							});
+						}
+					} else {
+						failCount++;
+						console.error('上传失败,错误信息:', response.msg || response.message);
+						uni.showToast({
+							title: response.msg || response.message || '上传失败',
+							icon: 'none',
+							duration: 2000
+						});
+					}
+				} catch (e) {
+					failCount++;
+					console.error('解析响应失败:', e);
+					console.error('原始响应数据:', res.data);
+					uni.showToast({
+						title: `解析失败: ${e.message}`,
+						icon: 'none',
+						duration: 2000
+					});
+				}
+			},
+			fail: (err) => {
+				failCount++;
+				console.error('上传请求失败:', err);
 				uni.showToast({
-				  title: `返回: ${response.data}`,
-				  icon: 'none',
+					title: '上传请求失败',
+					icon: 'none',
+					duration: 2000
 				});
-                if (response.code === 200) {
-                  // 获取返回的URL
-                  const imageUrl = response.data.url;
-                  console.log('上传成功,返回的图片URL:', imageUrl);
-                  
-                  // 上传成功,将图片信息添加到数组
-                  newImages.push({
-                    url: imageUrl, // 保存原始URL,在显示时会通过getImageUrl方法处理
-                    path: path, // 保存本地路径用于预览
-                    status: 'success',
-                    fileName: response.data.fileName || '' // 保存文件名,如果后端返回的话
-                  });
-                  successCount++;
-              } else {
-                failCount++;
-                console.error('上传失败:', response.msg);
-              }
-            } catch (e) {
-              failCount++;
-			  uni.showToast({
-			    title: `解析响应失败: ${e}`,
-			    icon: 'none',
-			  });
-              console.error('解析响应失败:', e);
-            }
-},
-
-          fail: (err) => {
-            failCount++;
-            console.error('上传请求失败:', err);
-			uni.showToast({
-			  title: `上传请求失败: ${err}`,
-			  icon: 'none',
-			});
-		},
-
-          complete: () => {
-            // 当所有文件都已处理完成
-            if (successCount + failCount === totalFiles) {
-              if (newImages.length > 0) {
-                // 将新上传的图片添加到已有图片列表
-                formData.images = [...formData.images, ...newImages];
-                console.log("formData.images",formData.images);
-                // 更新taskImages字段,将图片URL用逗号连接
-                formData.taskImages = formData.images.map(img => img.url).join(',');
-                
-                // 显示成功提示
-                uni.hideLoading();
-                uni.showToast({
-                  title: `成功上传${successCount}张图片`,
-                  icon: 'success'
-                });
-              } else {
-                // 全部失败
-				uni.showToast({
-				  title: `图片上传: ${newImages}`,
-				  icon: 'none',
-				});
-                uni.hideLoading();
-                // uni.showToast({
-                //   title: '图片上传失败',
-                //   icon: 'none',
-                // });
-              }
-            }
-          }
-        });
-      });
+			},
+			complete: () => {
+				// 当所有文件都已处理完成
+				if (successCount + failCount === totalFiles) {
+					uni.hideLoading();
+					
+					if (newImages.length > 0) {
+						// 将新上传的图片添加到已有图片列表
+						formData.images = [...formData.images, ...newImages];
+						// 更新taskImages字段,将图片URL用逗号连接
+						formData.taskImages = formData.images.map(img => img.url).join(',');
+						
+						console.log('所有图片上传完成:', formData.images);
+						console.log('taskImages:', formData.taskImages);
+						
+						// 显示成功提示
+						uni.showToast({
+							title: `成功上传${successCount}张图片`,
+							icon: 'success',
+							duration: 2000
+						});
+					} else {
+						// 全部失败
+						uni.showToast({
+							title: '图片上传失败,请重试',
+							icon: 'none',
+							duration: 2000
+						});
+					}
+					
+					// 如果有部分失败
+					if (failCount > 0 && successCount > 0) {
+						uni.showToast({
+							title: `成功${successCount}张,失败${failCount}张`,
+							icon: 'none',
+							duration: 2000
+						});
+					}
+				}
+			}
+		});
+	});
 }
 
     // 删除图片

+ 213 - 146
pages/dashboard/index.vue

@@ -7,10 +7,14 @@
     </view>
 
     <!-- 田间环境/天气区域 (降级为扁平状态块) -->
-    <view class="environmental-status-block" @click="showWeatherDetail">
+    <view class="environmental-status-block" @click="fetchWeatherData">
       <view class="status-main">
         <view class="main-info">
-          <text class="main-text">{{ weatherData.infos.temperature }}° · {{ weatherData.infos.weather }}</text>
+          <text :class="weatherData.infos.weather == '' ? 'main-textN' : 'main-text' "> {{
+  weatherData.infos.weather == ''
+    ? '请点击获取环境数据'
+    : `${weatherData.infos.temperature}° · ${weatherData.infos.weather}`
+}}</text>
           <text class="fixed-subtitle">田间环境概览</text>
         </view>
         <view class="refresh-btn" :class="{ 'rotating': isLoadingWeather }" @click.stop="handleRefresh">
@@ -159,6 +163,29 @@
         </view>
       </view>
     </view>
+    
+    <!-- 位置权限使用说明弹窗 -->
+    
+    <u-popup 
+      :show="showLocationPermissionPopup" 
+      mode="top" 
+      round="20"
+      :closeOnClickOverlay="false"
+      :closeable="false"
+      :safeAreaInsetTop="true"
+    >
+      <view class="location-permission-popup">
+        <view class="permission-header">
+          <view class="permission-icon">
+            <u-icon name="map" color="#666" size="20"></u-icon>
+          </view>
+          <view class="permission-title">位置权限使用说明</view>
+        </view>
+        <view class="permission-content">
+          为了向您展示当前所在地的天气信息,我们需要获取您的位置信息,用于查询并展示您当前位置的实时天气情况。
+        </view>
+      </view>
+    </u-popup>
 
     <!-- 天气详情弹窗 -->
 <!--    <u-popup 
@@ -252,11 +279,12 @@ import { ref, reactive, onMounted } from 'vue'
 import { getWeatherInfo, getLocation } from '@/api/services/weather.js'
 import { getMallList } from '@/api/services/mall.js'
 import { getTopArticles } from '@/api/services/knowledge.js'
+import privacyUtil from '@/utils/privacy.js'
 
 // 天气数据
 const weatherData = reactive({
   infos: {
-    weather: "加载中...",
+    weather: "",
     temperature: "--",
     humidity: "--",
     wind_power_v2: "--",
@@ -295,6 +323,7 @@ const articles = ref([])
 const showWeatherPopup = ref(false)
 const showArticlePopup = ref(false)
 const currentArticle = ref(null)
+const showLocationPermissionPopup = ref(false)
 
 // 方法
 const showTip = () => {
@@ -388,10 +417,37 @@ const handleRefresh = () => {
   fetchWeatherData(true);
 }
 
+/**
+ * 显示位置权限说明弹窗
+ */
+const showLocationPermissionGuide = () => {
+  showLocationPermissionPopup.value = true
+}
+
+/**
+ * 关闭位置权限说明弹窗
+ */
+const closeLocationPermissionPopup = () => {
+  showLocationPermissionPopup.value = false
+}
+
+/**
+ * 检查隐私协议同意状态
+ * @returns {boolean} 是否已同意隐私协议
+ */
+const checkPrivacyAgreement = () => {
+  return privacyUtil.checkAndRedirect()
+}
+
 /**
  * 获取实时天气数据
  */
 const fetchWeatherData = async (showToast = false) => {
+  // 首先检查隐私协议同意状态
+  if (!checkPrivacyAgreement()) {
+    return;
+  }
+  
   if (isLoadingWeather.value) {
     return;
   }
@@ -408,11 +464,17 @@ const fetchWeatherData = async (showToast = false) => {
   isLoadingWeather.value = true;
   
   try {
-    // 1. 获取当前位置
+    // 1. 显示位置权限说明弹窗
+    showLocationPermissionGuide();
+    
+    // 2. 获取当前位置(这会触发系统权限弹窗)
     const location = await getLocation();
     console.log('获取到位置信息:', location);
     
-    // 2. 根据位置查询天气
+    // 3. 位置获取成功后关闭权限说明弹窗
+    closeLocationPermissionPopup();
+    
+    // 4. 根据位置查询天气
     const weatherResult = await getWeatherInfo({
       latitude: location.latitude,
       longitude: location.longitude,
@@ -422,7 +484,7 @@ const fetchWeatherData = async (showToast = false) => {
     
     console.log('天气查询结果:', weatherResult);
     
-    // 3. 更新天气数据
+    // 5. 更新天气数据
     if (weatherResult && weatherResult.result && weatherResult.result.realtime) {
       const realtime = weatherResult.result.realtime;
       const infos = realtime[0].infos || {};
@@ -451,6 +513,10 @@ const fetchWeatherData = async (showToast = false) => {
     }
   } catch (error) {
     console.error('获取天气失败:', error);
+    
+    // 权限被拒绝或其他错误时也要关闭弹窗
+    closeLocationPermissionPopup();
+    
     uni.showToast({
         title: `${error}`,
         icon: 'none',
@@ -523,6 +589,11 @@ const formatReadCount = (count) => {
  * 获取推荐商品列表
  */
 const fetchRecommendedProducts = async () => {
+  // 首先检查隐私协议同意状态
+  if (!checkPrivacyAgreement()) {
+    return;
+  }
+  
   try {
     const result = await getMallList({
       isRecommended: 1, // 查询推荐商品
@@ -547,77 +618,60 @@ const fetchRecommendedProducts = async () => {
     } else {
       // 如果没有推荐商品,使用默认数据
       console.log('暂无推荐商品,使用默认数据');
-      products.value = [
-        {
-          title: '优质玉米种子',
-          desc: '高产抗病品种',
-          price: '68',
-          unit: '袋',
-          image: '/static/images/products/corn-seeds-new.jpg'
-        },
-        {
-          title: '有机肥料',
-          desc: '纯天然环保',
-          price: '128',
-          unit: '袋',
-          image: '/static/images/products/organic-fertilizer-new.jpg'
-        },
-        {
-          title: '杀虫剂',
-          desc: '广谱高效',
-          price: '45',
-          unit: '瓶',
-          image: '/static/images/products/insecticide.jpg'
-        },
-        {
-          title: '农用地膜',
-          desc: '保湿保温',
-          price: '89',
-          unit: '卷',
-          image: '/static/images/products/plastic-film.jpg'
-        }
-      ];
+      setDefaultProducts();
     }
   } catch (error) {
     console.error('获取推荐商品失败:', error);
     // 失败时使用默认数据
-    products.value = [
-      {
-        title: '优质玉米种子',
-        desc: '高产抗病品种',
-        price: '68',
-        unit: '袋',
-        image: '/static/images/products/corn-seeds-new.jpg'
-      },
-      {
-        title: '有机肥料',
-        desc: '纯天然环保',
-        price: '128',
-        unit: '袋',
-        image: '/static/images/products/organic-fertilizer-new.jpg'
-      },
-      {
-        title: '杀虫剂',
-        desc: '广谱高效',
-        price: '45',
-        unit: '瓶',
-        image: '/static/images/products/insecticide.jpg'
-      },
-      {
-        title: '农用地膜',
-        desc: '保湿保温',
-        price: '89',
-        unit: '卷',
-        image: '/static/images/products/plastic-film.jpg'
-      }
-    ];
+    setDefaultProducts();
   }
 }
 
+/**
+ * 设置默认商品数据
+ */
+const setDefaultProducts = () => {
+  products.value = [
+    {
+      title: '优质玉米种子',
+      desc: '高产抗病品种',
+      price: '68',
+      unit: '袋',
+      image: '/static/images/products/corn-seeds-new.jpg'
+    },
+    {
+      title: '有机肥料',
+      desc: '纯天然环保',
+      price: '128',
+      unit: '袋',
+      image: '/static/images/products/organic-fertilizer-new.jpg'
+    },
+    {
+      title: '杀虫剂',
+      desc: '广谱高效',
+      price: '45',
+      unit: '瓶',
+      image: '/static/images/products/insecticide.jpg'
+    },
+    {
+      title: '农用地膜',
+      desc: '保湿保温',
+      price: '89',
+      unit: '卷',
+      image: '/static/images/products/plastic-film.jpg'
+    }
+  ];
+}
+
 /**
  * 获取热门文章列表(根据阅读量)
  */
 const fetchTopArticles = async () => {
+  // 首先检查隐私协议同意状态
+  if (!checkPrivacyAgreement()) {
+    return;
+  }
+  
   try {
     const result = await getTopArticles();
     
@@ -640,94 +694,68 @@ const fetchTopArticles = async () => {
     } else {
       // 如果没有热门文章,使用默认数据
       console.log('暂无热门文章,使用默认数据');
-      articles.value = [
-        {
-          title: '春季水稻种植技术要点',
-          source: '农技知识',
-          readCount: '1.2k',
-          image: '/static/images/rice-farming/rice-field1.jpg',
-          time: '2024-03-15',
-          content: '春季是水稻种植的关键时期,需要掌握以下技术要点。',
-          points: []
-        },
-        {
-          title: '玉米病虫害防治指南',
-          source: '植保技术',
-          readCount: '856',
-          image: '/static/images/rice-farming/rice-field2.jpg',
-          time: '2024-03-14',
-          content: '玉米病虫害防治是确保产量的重要环节。',
-          points: []
-        },
-        {
-          title: '现代农业机械化作业规范',
-          source: '农机管理',
-          readCount: '2.1k',
-          image: '/static/images/rice-farming/rice-field3.jpg',
-          time: '2024-03-13',
-          content: '提升农业机械化作业水平是现代农业发展的必然要求。',
-          points: []
-        },
-        {
-          title: '土壤肥力提升与改良方案',
-          source: '土肥管理',
-          readCount: '1.5k',
-          image: '/static/images/rice-farming/rice-field4.jpg',
-          time: '2024-03-12',
-          content: '健康的土壤是丰收的基础。',
-          points: []
-        }
-      ];
+      setDefaultArticles();
     }
   } catch (error) {
     console.error('获取热门文章失败:', error);
     // 失败时使用默认数据
-    articles.value = [
-      {
-        title: '春季水稻种植技术要点',
-        source: '农技知识',
-        readCount: '1.2k',
-        image: '/static/images/rice-farming/rice-field1.jpg',
-        time: '2024-03-15',
-        content: '春季是水稻种植的关键时期,需要掌握以下技术要点。',
-        points: []
-      },
-      {
-        title: '玉米病虫害防治指南',
-        source: '植保技术',
-        readCount: '856',
-        image: '/static/images/rice-farming/rice-field2.jpg',
-        time: '2024-03-14',
-        content: '玉米病虫害防治是确保产量的重要环节。',
-        points: []
-      },
-      {
-        title: '现代农业机械化作业规范',
-        source: '农机管理',
-        readCount: '2.1k',
-        image: '/static/images/rice-farming/rice-field3.jpg',
-        time: '2024-03-13',
-        content: '提升农业机械化作业水平是现代农业发展的必然要求。',
-        points: []
-      },
-      {
-        title: '土壤肥力提升与改良方案',
-        source: '土肥管理',
-        readCount: '1.5k',
-        image: '/static/images/rice-farming/rice-field4.jpg',
-        time: '2024-03-12',
-        content: '健康的土壤是丰收的基础。',
-        points: []
-      }
-    ];
+    setDefaultArticles();
   }
 }
 
-// 页面加载时获取天气、推荐商品和热门文章
+/**
+ * 设置默认文章数据
+ */
+const setDefaultArticles = () => {
+  articles.value = [
+    {
+      title: '春季水稻种植技术要点',
+      source: '农技知识',
+      readCount: '1.2k',
+      image: '/static/images/rice-farming/rice-field1.jpg',
+      time: '2024-03-15',
+      content: '春季是水稻种植的关键时期,需要掌握以下技术要点。',
+      points: []
+    },
+    {
+      title: '玉米病虫害防治指南',
+      source: '植保技术',
+      readCount: '856',
+      image: '/static/images/rice-farming/rice-field2.jpg',
+      time: '2024-03-14',
+      content: '玉米病虫害防治是确保产量的重要环节。',
+      points: []
+    },
+    {
+      title: '现代农业机械化作业规范',
+      source: '农机管理',
+      readCount: '2.1k',
+      image: '/static/images/rice-farming/rice-field3.jpg',
+      time: '2024-03-13',
+      content: '提升农业机械化作业水平是现代农业发展的必然要求。',
+      points: []
+    },
+    {
+      title: '土壤肥力提升与改良方案',
+      source: '土肥管理',
+      readCount: '1.5k',
+      image: '/static/images/rice-farming/rice-field4.jpg',
+      time: '2024-03-12',
+      content: '健康的土壤是丰收的基础。',
+      points: []
+    }
+  ];
+}
+
+// 页面加载时检查隐私协议状态,只有同意后才获取数据
 onMounted(() => {
-  fetchWeatherData();
-  fetchRecommendedProducts();
-  fetchTopArticles();
+  // 检查隐私协议同意状态
+  if (checkPrivacyAgreement()) {
+    // 直接加载数据,天气数据获取时会显示权限说明弹窗(该方案鸿蒙的安卓审核不通过,需要在用户自己点击获取天气的时候显示权限说明弹窗)
+    // fetchWeatherData() 
+    fetchRecommendedProducts()
+    fetchTopArticles()
+  }
 })
 </script>
 
@@ -843,6 +871,12 @@ onMounted(() => {
         color: #1A3026;
         line-height: 1;
       }
+	  .main-textN{
+		font-size: 30rpx;
+		font-weight: 600;
+		color: #1A3026;
+		line-height: 1;
+	  }
 
       .fixed-subtitle {
         font-size: 24rpx;
@@ -1186,6 +1220,39 @@ onMounted(() => {
 }
 
 // 弹窗样式
+// 弹窗样式
+.location-permission-popup {
+  padding: 40rpx;
+  // width: 600rpx;
+  // background: white;
+  // box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
+  // max-width: 90vw;
+  .permission-header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 24rpx;
+
+    .permission-icon {
+      margin-right: 16rpx;
+      flex-shrink: 0;
+    }
+
+    .permission-title {
+      font-size: 32rpx;
+      font-weight: 600;
+      color: #333;
+      flex: 1;
+    }
+  }
+
+  .permission-content {
+    font-size: 25rpx;
+    color: #666;
+    line-height: 1.6;
+    text-align: left;
+  }
+}
+
 .popup-content {
   padding: 32rpx;
   max-height: 80vh;

+ 215 - 10
pages/device/device-list/detail-camera.vue

@@ -66,6 +66,17 @@
 					@error="onVideoError" @fullscreenchange="onFullscreenChange" class="video-player"></live-player>
 				<!-- #endif -->
 
+				<!-- #ifdef APP-PLUS || APP-HARMONY -->
+				<!-- <live-player v-if="isPlaying" id="appVideoPlayer" :src="getAppStreamUrl" mode="live"
+					:autoplay="true" :muted="isMuted" object-fit="contain" @statechange="onStateChange"
+					@error="onVideoError" @fullscreenchange="onFullscreenChange" 
+					:enable-danmu="false"
+					:show-center-play-btn="false"
+					:show-play-btn="false"
+					class="video-player"></live-player> -->
+					<video class="video-player" v-if="isPlaying" id="myVideo" :src="getAppStreamUrl" :autoplay="true" controls></video>
+				<!-- #endif -->
+
 				<!-- 视频控制层 -->
 				<view class="video-controls">
 					<view class="control-row top-controls">
@@ -174,7 +185,7 @@
 		deviceTypeId: null,
 		streamUrl: '',
 		channelId: null, // 当前通道ID
-		originalStreamUrl: 'ws://121.4.16.100:6080/rtp/34020000001110000001_34020000001320000012.live.flv',
+		originalStreamUrl: '',
 	})
 
 	const isPlaying = ref(false)
@@ -216,6 +227,7 @@
 	// 模拟告警数据
 	const alertHistory = ref([])
 	const livePlayerContext = ref(null) // 小程序视频上下文
+	const appLivePlayerContext = ref(null) // App端视频上下文
 	const isRefreshing = ref(false)
 	const jessibucaRef = ref(null)
 
@@ -227,7 +239,13 @@
 	
 	// 获取H5环境使用的流地址
 	const getH5StreamUrl = computed(() => {
-		return deviceInfo.originalStreamUrl || config.streamServer.wsFlvServer
+		// 确保使用安全的 WSS 协议
+		let url = deviceInfo.originalStreamUrl
+		if (url && url.startsWith('ws://')) {
+			console.warn('检测到不安全的 ws:// 协议,自动转换为 wss://')
+			url = url.replace('ws://', 'wss://')
+		}
+		return url
 	})
 
 	// 获取小程序环境使用的流地址
@@ -247,6 +265,38 @@
 		return streamServer.rtmpServer || streamServer.hlsServer
 	})
 
+	// 获取App(安卓/鸿蒙)环境使用的流地址
+	const getAppStreamUrl = computed(() => {
+		const {
+			streamServer
+		} = config
+
+		// // App端优先使用 RTMP 或 HLS 流
+		// if (deviceInfo.rtmpUrl) {
+		// 	return deviceInfo.rtmpUrl
+		// }
+
+		// if (deviceInfo.hlsUrl) {
+		// 	return deviceInfo.hlsUrl
+		// }
+
+		// 如果有原始流地址,尝试转换为 RTMP 或 HLS
+		if (deviceInfo.originalStreamUrl) {
+			// 如果是 ws-flv 格式,转换为 RTMP
+			// if (deviceInfo.originalStreamUrl.includes('.flv')) {
+			// 	const rtmpUrl = deviceInfo.originalStreamUrl
+			// 		.replace('ws://', 'rtmp://')
+			// 		.replace(':6080/rtp/', ':1935/live/')
+			// 		.replace('.live.flv', '')
+			// 	return rtmpUrl
+			// }
+			console.log("deviceInfo.originalStreamUrl",deviceInfo.originalStreamUrl);
+		}
+
+		// return streamServer.rtmpServer || streamServer.hlsServer
+		return deviceInfo.originalStreamUrl
+	})
+
 	// 方法
 	// 刷新数据
 	const refreshData = () => {
@@ -305,12 +355,12 @@
 		getChannels(deviceInfo.deviceId)
 			.then(response => {
 				console.log('获取通道列表:', response)
-				const res = response[1].data
+				const res = response.data
 				if (res.code === 0 && res.data.total > 0) {
 					const channels = res.data.list
 					deviceInfo.channelId = channels[0].deviceId
 					playStart(deviceInfo.deviceId, deviceInfo.channelId).then(res => {
-						if (res[1].data.code !== 0) {
+						if (res.data.code !== 0) {
 							console.error('播放开始失败:', res.message)
 							uni.showToast({
 								title: '播放失败: ' + res.message,
@@ -319,13 +369,31 @@
 							return
 						}
 						console.log('播放开始:', res)
-						deviceInfo.originalStreamUrl = res[1].data.data.ws_flv || deviceInfo.originalStreamUrl
+						// #ifdef H5
+						let streamUrl = res.data.data.wss_flv
+						
+						if (streamUrl) {
+							const urlObj = new URL(streamUrl)
+							// 替换 hostname
+							urlObj.hostname = 'nxy.gbdfarm.com'
+							// 替换端口
+							urlObj.port = '9000'
+							
+							deviceInfo.originalStreamUrl = urlObj.toString()
+							console.log("queryChannels - deviceInfo.originalStreamUrl", deviceInfo.originalStreamUrl)
+						} else {
+							console.warn('未获取到 wss_flv 流地址')
+						}
+						// #endif
+						// #ifdef APP-PLUS || APP-HARMONY
+						deviceInfo.originalStreamUrl = res.data.data.fmp4 || deviceInfo.originalStreamUrl
+						// #endif
 					}).catch(err => {
 						console.error('播放开始失败:', err)
 					})
 				} else {
 					uni.showToast({
-						title: '获取通道列表失败: ' + res.message,
+						title: '获取通道列表失败: ' + res.data.message,
 						icon: 'none'
 					})
 					return
@@ -361,10 +429,30 @@
 			}, 300)
 			// #endif
 
+			// #ifdef APP-PLUS || APP-HARMONY
+			setTimeout(() => {
+				if (appLivePlayerContext.value) {
+					appLivePlayerContext.value.play({
+						success: () => {
+							console.log('App视频播放成功')
+							uni.vibrateShort()
+						},
+						fail: (err) => {
+							console.error('App视频播放失败:', err)
+							uni.showToast({
+								title: '视频播放失败',
+								icon: 'none'
+							})
+						}
+					})
+				}
+			}, 300)
+			// #endif
+
 			// #ifdef H5
 			setTimeout(() => {
 				playStart(deviceInfo.deviceId, deviceInfo.channelId).then(res => {
-					if (res[1].data.code !== 0) {
+					if (res.data.code !== 0) {
 						console.error('播放开始失败:', res.message)
 						uni.showToast({
 							title: '播放失败: ' + res.message,
@@ -372,8 +460,17 @@
 						})
 						return
 					}
-					console.log('播放开始:', res[1])
-					deviceInfo.originalStreamUrl = res[1].data.data.ws_flv || deviceInfo.originalStreamUrl
+					console.log('播放开始:', res)
+					
+					// 使用 wss_flv 并替换域名和端口
+					let streamUrl = res.data.data.wss_flv
+					if (streamUrl) {
+						const urlObj = new URL(streamUrl)
+						urlObj.hostname = 'nxy.gbdfarm.com'
+						urlObj.port = '9000'
+						deviceInfo.originalStreamUrl = urlObj.toString()
+						console.log("togglePlayState - deviceInfo.originalStreamUrl", deviceInfo.originalStreamUrl)
+					}
 				}).catch(err => {
 					console.error('播放开始失败:', err)
 				})
@@ -393,6 +490,12 @@
 			}
 			// #endif
 
+			// #ifdef APP-PLUS || APP-HARMONY
+			if (appLivePlayerContext.value) {
+				appLivePlayerContext.value.pause()
+			}
+			// #endif
+
 			isPlaying.value = false
 
 			uni.showToast({
@@ -416,6 +519,14 @@
 			}
 		}
 		// #endif
+
+		// #ifdef APP-PLUS || APP-HARMONY
+		// App端的静音通过 live-player 的 muted 属性控制,会自动响应
+		uni.showToast({
+			title: isMuted.value ? '已静音' : '已取消静音',
+			icon: 'none'
+		})
+		// #endif
 	}
 
 	// 全屏切换
@@ -497,6 +608,31 @@
 			}
 		}
 		// #endif
+
+		// #ifdef APP-PLUS || APP-HARMONY
+		if (appLivePlayerContext.value) {
+			if (isFullscreen.value) {
+				appLivePlayerContext.value.requestFullScreen({
+					direction: 90,
+					success: () => {
+						console.log('App进入全屏模式成功')
+					},
+					fail: (err) => {
+						console.error('App进入全屏模式失败:', err)
+					}
+				})
+			} else {
+				appLivePlayerContext.value.exitFullScreen({
+					success: () => {
+						console.log('App退出全屏模式成功')
+					},
+					fail: (err) => {
+						console.error('App退出全屏模式失败:', err)
+					}
+				})
+			}
+		}
+		// #endif
 	}
 
 	// 截图
@@ -553,6 +689,44 @@
 			})
 		}
 		// #endif
+
+		// #ifdef APP-PLUS || APP-HARMONY
+		if (appLivePlayerContext.value && isPlaying.value) {
+			appLivePlayerContext.value.snapshot({
+				success: (res) => {
+					console.log('App截图成功:', res.tempImagePath)
+					uni.saveImageToPhotosAlbum({
+						filePath: res.tempImagePath,
+						success: () => {
+							uni.showToast({
+								title: '截图已保存到相册',
+								icon: 'success'
+							})
+						},
+						fail: (err) => {
+							console.error('App保存截图失败:', err)
+							uni.showToast({
+								title: '保存截图失败',
+								icon: 'none'
+							})
+						}
+					})
+				},
+				fail: (err) => {
+					console.error('App截图失败:', err)
+					uni.showToast({
+						title: '截图失败',
+						icon: 'none'
+					})
+				}
+			})
+		} else {
+			uni.showToast({
+				title: '请先播放视频',
+				icon: 'none'
+			})
+		}
+		// #endif
 	}
 
 	// 小程序播放器状态变化处理
@@ -658,6 +832,19 @@
 		}
 		// #endif
 
+		// #ifdef APP-PLUS || APP-HARMONY
+		if (streamUrls.miniProgramUrl) {
+			deviceInfo.streamUrl = streamUrls.miniProgramUrl
+		} else {
+			// App端尝试使用 RTMP 或 HLS
+			const rtmpUrl = originalUrl
+				.replace('ws://', 'rtmp://')
+				.replace(':6080/rtp/', ':1935/live/')
+				.replace('.live.flv', '')
+			deviceInfo.streamUrl = rtmpUrl || config.streamServer.rtmpServer
+		}
+		// #endif
+
 		console.log('初始化流地址:', deviceInfo.streamUrl)
 	}
 
@@ -810,6 +997,10 @@
 		livePlayerContext.value = uni.createLivePlayerContext('videoPlayer')
 		// #endif
 
+		// #ifdef APP-PLUS || APP-HARMONY
+		// appLivePlayerContext.value = uni.createLivePlayerContext('appVideoPlayer')
+		// #endif
+
 		// #ifdef H5
 		loadJessibucaScript()
 		// #endif
@@ -822,7 +1013,7 @@
 	onBeforeUnmount(() => {
 		console.log("停止点播")
 		pause(deviceInfo.deviceId, deviceInfo.channelId).then(res => {
-			if (res[1].data.code !== 0) {
+			if (res.data.code !== 0) {
 				console.error('暂停失败:', res.message)
 			} else {
 				console.log('视频已暂停')
@@ -1025,6 +1216,20 @@
 		object-fit: contain;
 	}
 
+	.h5-video-wrapper {
+		width: 100%;
+		height: 100%;
+	}
+
+	/* App端视频播放器样式优化 */
+	/* #ifdef APP-PLUS || APP-HARMONY */
+	#appVideoPlayer {
+		width: 100%;
+		height: 100%;
+		background-color: #000000;
+	}
+	/* #endif */
+
 	.video-controls {
 		position: absolute;
 		top: 0;

+ 3 - 1
pages/login/index.vue

@@ -512,7 +512,9 @@
 		display: flex;
 		justify-content: space-between;
 		align-items: center;
-		padding: 40rpx 0;
+		padding-top: calc(15rpx + constant(safe-area-inset-top));
+		padding-top: calc(15rpx + env(safe-area-inset-top));
+		/* padding: 40rpx 0; */
 	}
 
 	.back-icon {

+ 49 - 3
pages/login/privacy.vue

@@ -48,9 +48,21 @@
               为了<text class="bold">实现应用功能</text>,在获取您的同意后我们需要收集您的<text class="bold">GPS 位置、</text><text>其他大致位置信息、</text><text>账号信息、</text><text>电话号码</text>。
             </view>
           </view>
+          <view class="subsection">
+            <view class="subsection-title">1.3 设备网络信息获取说明</view>
+            <view class="paragraph">
+              在应用运行过程中,我们可能会获取当前连接的 Wi-Fi 信息(SSID、BSSID),用于识别网络连接状态及保障应用功能的正常运行,该信息仅在本地使用,不会用于其他用途。
+            </view>
+          </view>
+          <view class="subsection">
+            <view class="subsection-title">1.4 第三方SDK及信息收集说明</view>
+            <view class="paragraph">
+              为保障应用相关功能的实现和稳定运行,本应用可能接入第三方SDK。第三方SDK可能会在必要范围内收集设备相关信息。我们仅在实现相关功能所必需的范围内接入相关SDK,并要求其遵守相关法律法规及隐私保护要求。移动智能终端补充设备标识体系统一调用SDK: 用于在应用启动或运行过程中获取设备匿名标识(OAID)等设备信息,以识别设备并保障应用统计分析及运行环境适配。
+            </view> 
+          </view>
 
           <view class="subsection">
-            <view class="subsection-title">1.3 基于履行法定义务</view>
+            <view class="subsection-title">1.5 基于履行法定义务</view>
             <view class="paragraph">
               基于履行法定义务或其他法律法规规定的情形,我们可能会处理您的以下个人信息:
             </view>
@@ -137,9 +149,39 @@
             4.2 上述信息将会传输并保存至中国境内的服务器。
           </view>
         </view>
+		
+		<view class="section">
+		  <view class="section-title">5. 第三方SDK信息收集说明</view>
+		  <view class="paragraph">
+		    为保障应用相关功能的实现与安全稳定运行,本应用可能会接入由第三方提供的软件开发工具包(SDK)。这些SDK可能会收集和使用您的部分信息。具体情况如下:
+		  </view>
+		  <view class="paragraph">
+		    5.1腾讯定位SDK(Android)
+		    第三方主体:腾讯科技(深圳)有限公司;
+		  </view>
+		  <view class="paragraph">
+			SDK名称:腾讯定位SDK(Android)
+		  </view>
+		  <view class="paragraph">
+		    收集信息类型:
+		    位置信息(包括GPS定位、网络定位等)
+		  </view>
+		  <view class="paragraph">
+		    使用目的:
+		    用于实现应用中根据当前位置查询天气功能。
+		  </view>
+		  <view class="paragraph">
+		    信息使用范围:
+		    仅用于本应用提供定位服务,不会用于其他用途。  
+		  </view>
+		  <view class="paragraph">
+		    第三方隐私政策:
+		    https://privacy.qq.com/
+		  </view>
+		</view>
 
         <view class="section">
-          <view class="section-title">5. 如何联系我们</view>
+          <view class="section-title">6. 如何联系我们</view>
           <view class="paragraph">
             您可通过以下方式联系我们,并行使您的相关权利,我们会尽快回复。
           </view>
@@ -179,11 +221,15 @@ const goBack = () => {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  padding: 40rpx;
+  /* padding: 40rpx; */
+  padding-top: calc(25rpx + constant(safe-area-inset-top));
+  padding-top: calc(25rpx + env(safe-area-inset-top));
+  padding-right: 40rpx;
   border-bottom: 1rpx solid #E0E0E0;
 }
 
 .back-icon {
+  padding: 0 0 0 40rpx;
   font-size: 50rpx;
   color: #333;
   width: 60rpx;

+ 5 - 1
pages/login/terms.vue

@@ -127,11 +127,15 @@ const goBack = () => {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  padding: 40rpx;
+  /* padding: 40rpx; */
+  padding-top: calc(25rpx + constant(safe-area-inset-top));
+  padding-top: calc(25rpx + env(safe-area-inset-top));
+  padding-right: 40rpx;
   border-bottom: 1rpx solid #E0E0E0;
 }
 
 .back-icon {
+  padding: 0 0 0 40rpx;
   font-size: 50rpx;
   color: #333;
   width: 60rpx;

+ 221 - 0
pages/privacy/privacy-agreement.vue

@@ -0,0 +1,221 @@
+<template>
+  <view class="privacy-page">
+    <view class="privacy-mask">
+      <view class="privacy-dialog">
+        <view class="dialog-header">
+          <text class="dialog-title">农小禹用户协议和隐私政策</text>
+        </view>
+        
+        <view class="dialog-content">
+          <text class="content-text">
+            请你务必审慎阅读、充分理解"用户协议"和"隐私政策"各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、位置信息等信息用于农事管理、设备监控等功能。
+          </text>
+          
+          <view class="links-container">
+            <text class="content-text">你可阅读</text>
+            <text class="link-text" @click="viewTerms">《用户协议》</text>
+            <text class="content-text">和</text>
+            <text class="link-text" @click="viewPrivacy">《隐私政策》</text>
+            <text class="content-text">了解详细信息。</text>
+          </view>
+          
+          <text class="content-text">
+            如果你同意,请点击"同意"开始使用我们的服务。
+          </text>
+        </view>
+        
+        <view class="dialog-footer">
+          <view class="btn btn-cancel" @click="handleDisagree">
+            <text class="btn-text">不同意</text>
+          </view>
+          <view class="btn btn-confirm" @click="handleAgree">
+            <text class="btn-text btn-text-white">同意</text>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { onLoad, onBackPress } from '@dcloudio/uni-app'
+import privacyUtil from "@/utils/privacy.js"
+
+// 禁用返回按钮
+onBackPress(() => {
+  return true // 返回true表示阻止返回
+})
+
+// 页面加载
+onLoad(() => {
+  console.log('隐私协议页面加载')
+})
+
+// 同意协议
+const handleAgree = () => {
+  privacyUtil.setAgreement(true)
+  console.log('用户同意隐私协议')
+  
+  // 发送全局事件通知应用初始化
+  uni.$emit('privacyAgreed')
+  
+  // 返回首页
+  uni.reLaunch({
+    url: '/pages/dashboard/index'
+  })
+}
+
+// 不同意协议
+const handleDisagree = () => {
+  uni.showModal({
+    title: '确认提示',
+    content: '进入应用前,你需先同意《用户协议》和《隐私政策》,否则将无法使用应用服务。',
+    confirmText: '同意',
+    cancelText: '退出应用',
+    showCancel: true,
+    success: (res) => {
+      if (res.confirm) {
+        privacyUtil.setAgreement(true)
+        console.log('用户同意隐私协议')
+        
+        // 发送全局事件通知应用初始化
+        uni.$emit('privacyAgreed')
+        
+        // 返回首页
+        uni.reLaunch({
+          url: '/pages/dashboard/index'
+        })
+      } else {
+        // #ifdef APP-PLUS
+        plus.runtime.quit()
+        // #endif
+        
+        // #ifdef H5
+        // H5环境无法直接退出,显示提示
+        uni.showToast({
+          title: '请关闭浏览器标签页',
+          icon: 'none',
+          duration: 3000
+        })
+        // #endif
+      }
+    }
+  })
+}
+
+// 查看用户协议
+const viewTerms = () => {
+  uni.navigateTo({
+    url: '/pages/login/terms'
+  })
+}
+
+// 查看隐私政策
+const viewPrivacy = () => {
+  uni.navigateTo({
+    url: '/pages/login/privacy'
+  })
+}
+</script>
+
+<style scoped>
+.privacy-page {
+  width: 100vw;
+  height: 100vh;
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 9999;
+}
+
+.privacy-mask {
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.6);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.privacy-dialog {
+  width: 620rpx;
+  background-color: #ffffff;
+  border-radius: 20rpx;
+  overflow: hidden;
+}
+
+.dialog-header {
+  padding: 40rpx 30rpx 20rpx;
+  text-align: center;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.dialog-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333333;
+}
+
+.dialog-content {
+  padding: 30rpx;
+  max-height: 500rpx;
+  overflow-y: auto;
+}
+
+.content-text {
+  font-size: 28rpx;
+  color: #666666;
+  line-height: 1.8;
+}
+
+.links-container {
+  margin: 20rpx 0;
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+}
+
+.link-text {
+  font-size: 28rpx;
+  color: #4CAF50;
+  font-weight: bold;
+  margin: 0 4rpx;
+  text-decoration: underline;
+}
+
+.dialog-footer {
+  display: flex;
+  border-top: 1rpx solid #f0f0f0;
+}
+
+.btn {
+  flex: 1;
+  height: 100rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.btn-cancel {
+  background-color: #f5f5f5;
+  border-right: 1rpx solid #e0e0e0;
+}
+
+.btn-confirm {
+  background-color: #4CAF50;
+}
+
+.btn-text {
+  font-size: 32rpx;
+  color: #666666;
+}
+
+.btn-text-white {
+  color: #ffffff;
+  font-weight: bold;
+}
+
+.btn:active {
+  opacity: 0.8;
+}
+</style>

+ 34 - 2
pages/service/mall.vue

@@ -88,7 +88,7 @@ import { getMallList } from '@/api/services/mall.js'
 import { useDict } from '@/utils/composables/useDict'
 
 const { dictData } = useDict(['mall_product_category'])
-
+import storage from "@/utils/storage.js";
 const searchKeyword = ref('')
 const activeCategory = ref(-1)
 const scrollLeft = ref(0)
@@ -186,7 +186,7 @@ const switchCategory = (categoryId) => {
 }
 
 const scrollToActiveCategory = (categoryId) => {
-  const index = dictData.value.mall_product_category.findIndex(item => item.id == categoryId)
+  const index = dictData.mall_product_category.findIndex(item => item.id == categoryId)
   if (index > -1) {
     const itemWidth = 120
     const containerWidth = 750
@@ -196,11 +196,43 @@ const scrollToActiveCategory = (categoryId) => {
 }
 
 const handleSearch = () => {
+	if (!storage.getHasLogin()) {
+		uni.showModal({
+			title: '提示',
+			content: '您还未登录,请先登录',
+			confirmText: '去登录',
+			cancelText: '取消',
+			success: function(res) {
+				if (res.confirm) {
+					uni.navigateTo({
+						url: '/pages/login/index'
+					});
+				}
+			},
+		});
+		return;
+	}
   const keyword = searchKeyword.value.trim().toLowerCase()
   loadMallData(keyword)
 }
 
 const navigateToDetail = (goods) => {
+	if (!storage.getHasLogin()) {
+		uni.showModal({
+			title: '提示',
+			content: '您还未登录,请先登录',
+			confirmText: '去登录',
+			cancelText: '取消',
+			success: function(res) {
+				if (res.confirm) {
+					uni.navigateTo({
+						url: '/pages/login/index'
+					});
+				}
+			},
+		});
+		return;
+	}
   uni.navigateTo({
     url: `/pages/service/mall-detail?id=${goods.id}&title=${encodeURIComponent(goods.title)}`
   })

+ 7 - 4
pages/service/my-publish.vue

@@ -152,6 +152,7 @@
 
 <script setup>
 import { ref, computed, onMounted } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
 import { getProductInfoList } from '@/api/services/productInfo.js'
 import { useDict } from '@/utils/composables/useDict'
 import storage from "@/utils/storage.js"
@@ -184,9 +185,10 @@ const filteredPurchaseList = computed(() => {
 })
 
 // 页面显示时刷新数据
-const onShow = () => {
+onShow(() => {
+	console.log("加载");
   loadMyPublishInfo()
-}
+})
 
 // 页面加载时获取数据
 onMounted(() => {
@@ -217,12 +219,13 @@ const loadMyPublishInfo = (keyword) => {
   uni.showLoading({
     title: '加载中'
   })
-  
+  console.log("currentUserInfo",currentUserInfo);
   // 构建查询参数
   const params = {
     pageNum: pageNum.value,
     pageSize: pageSize.value,
-    createBy: currentUserInfo.username,
+    // createBy: currentUserInfo.username,
+	userId: currentUserInfo.userid,
     type: 0 // 默认查询出售信息
   }
   

+ 254 - 144
pages/service/purchase-publish.vue

@@ -228,7 +228,7 @@
 	})
 
 	// 计算属性:dictDataOptions
-	const dictDataOptions = computed(() => dictData.value)
+	const dictDataOptions = computed(() => dictData)
 
 	// onLoad 替换为 onMounted + getCurrentPages
 	onMounted(() => {
@@ -320,165 +320,275 @@
 
 	// 获取字典标签
 	const getDictLabel = (dictKey, value) => {
-		if (!dictData.value || !dictData.value[dictKey]) {
+		if (!dictData || !dictData[dictKey]) {
 			return ''
 		}
-		const list = dictData.value[dictKey] || []
+		const list = dictData[dictKey] || []
 		const item = list.find(u => u.dictValue == value)
 		return item ? item.dictLabel : ''
 	}
 
-	// 选择图片
+	// 选择图片(优化跨平台兼容性)
 	const chooseImage = () => {
 		uni.chooseImage({
 			count: 6 - formData.images.length,
-			    sizeType: ['original', 'compressed'],
-			    sourceType: ['album', 'camera'],
-			    success: (res) => {
-			      console.log('选择图片成功:', res);
-			      
-			      // 验证文件类型和大小
-			      const validFiles = [];
-			      const invalidFiles = [];
-			      const maxSize = 5 * 1024 * 1024; // 5MB 最大限制
-			      
-			      // 检查每个文件
-			      res.tempFiles.forEach((file, index) => {
-			        // 检查文件类型
-			        const isImage = /\.(jpg|jpeg|png|gif)$/i.test(file.name);
-			        // 检查文件大小
-			        const isValidSize = file.size <= maxSize;
-			        
-			        if (isImage && isValidSize) {
-			          validFiles.push(res.tempFilePaths[index]);
-			        } else {
-			          invalidFiles.push({
-			            path: file.path,
-			            size: file.size,
-			            reason: !isImage ? '文件格式不支持' : '文件大于5MB'
-			          });
-			        }
-			      });
-			      
-			      // 显示无效文件提示
-			      if (invalidFiles.length > 0) {
-			        uni.showToast({
-			          title: `${invalidFiles.length}个文件无效,请检查格式和大小`,
-			          icon: 'none',
-			          duration: 2000
-			        });
-			      }
-			      
-			      // 如果有有效文件,则上传
-			      if (validFiles.length > 0) {
-			        uploadImages(validFiles)
-			      }
-			    },
-			    fail: (err) => {
-			      console.error('选择图片失败:', err)
-			      uni.showToast({
-			        title: '选择图片失败',
-			        icon: 'none'
-			      })
-			    }
-		})
+			sizeType: ['original', 'compressed'],
+			sourceType: ['album', 'camera'],
+			success: (res) => {
+				console.log('选择图片成功:', res);
+				console.log('tempFiles:', res.tempFiles);
+				console.log('tempFilePaths:', res.tempFilePaths);
+				
+				// 验证文件类型和大小
+				const validFiles = [];
+				const invalidFiles = [];
+				const maxSize = 5 * 1024 * 1024; // 5MB 最大限制
+				
+				// 兼容不同平台的文件信息获取方式
+				if (res.tempFiles && res.tempFiles.length > 0) {
+					// 检查每个文件
+					res.tempFiles.forEach((file, index) => {
+						console.log(`文件 ${index}:`, file);
+						
+						// 获取文件路径
+						const filePath = res.tempFilePaths[index];
+						
+						// 获取文件名(兼容不同平台)
+						let fileName = '';
+						if (file.name) {
+							fileName = file.name;
+						} else if (file.path) {
+							// 从路径中提取文件名
+							fileName = file.path.split('/').pop();
+						} else if (filePath) {
+							fileName = filePath.split('/').pop();
+						}
+						
+						console.log(`文件名: ${fileName}, 大小: ${file.size}`);
+						
+						// 检查文件类型(兼容没有扩展名的情况)
+						let isImage = true;
+						if (fileName) {
+							isImage = /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName);
+						}
+						
+						// 检查文件大小
+						const fileSize = file.size || 0;
+						const isValidSize = fileSize <= maxSize;
+						
+						if (isImage && isValidSize) {
+							validFiles.push(filePath);
+						} else {
+							invalidFiles.push({
+								path: filePath,
+								size: fileSize,
+								name: fileName,
+								reason: !isImage ? '文件格式不支持' : `文件大于5MB (${(fileSize / 1024 / 1024).toFixed(2)}MB)`
+							});
+						}
+					});
+				} else {
+					// 如果没有 tempFiles,直接使用 tempFilePaths(某些平台可能不返回详细信息)
+					console.warn('未获取到 tempFiles,直接使用 tempFilePaths');
+					res.tempFilePaths.forEach(path => {
+						validFiles.push(path);
+					});
+				}
+				
+				console.log('有效文件:', validFiles);
+				console.log('无效文件:', invalidFiles);
+				
+				// 显示无效文件提示
+				if (invalidFiles.length > 0) {
+					const reasons = invalidFiles.map(f => `${f.name}: ${f.reason}`).join('\n');
+					uni.showModal({
+						title: '部分文件无效',
+						content: `${invalidFiles.length}个文件无效:\n${reasons}`,
+						showCancel: false
+					});
+				}
+				
+				// 如果有有效文件,则上传
+				if (validFiles.length > 0) {
+					uploadImages(validFiles);
+				} else if (invalidFiles.length > 0) {
+					uni.showToast({
+						title: '没有可上传的文件',
+						icon: 'none',
+						duration: 2000
+					});
+				}
+			},
+			fail: (err) => {
+				console.error('选择图片失败:', err);
+				uni.showToast({
+					title: '选择图片失败',
+					icon: 'none',
+					duration: 2000
+				});
+			}
+		});
 	}
 	
-	// 上传图片到服务器
+	// 上传图片到服务器(优化跨平台兼容性)
 	const uploadImages = (tempFilePaths) => {
-			  uni.showLoading({
-			    title: '上传中...',
-			    mask: true
-			  });
-			  
-			  // 上传成功的图片计数
-			  let successCount = 0;
-			  let failCount = 0;
-			  const totalFiles = tempFilePaths.length;
-			  const newImages = [];
-			  
-			  // 遍历处理每张图片
-			  tempFilePaths.forEach((path, index) => {
-			    // 调用上传API
-			    uni.uploadFile({
-			      // url: api.serve + '/base/tasks/uploadTaskImage', 
-			      url: api.serve + '/file/upload', 
-			      filePath: path,
-			      name: 'file', // 文件参数名称,需要与后端接口匹配
-			      formData: {
-			        type: 'task', // 标识文件类型,用于后端区分不同业务的文件
-			        // directory: '/opt/app/nongxiaoyu/uploadImage' // 指定保存目录
-			      },
-				  header: {
-				  	'Authorization': `Bearer ${storage.getAccessToken()}`
-				  },
-			      success: (res) => {
-			                      try {
-			            const response = JSON.parse(res.data);
+		uni.showLoading({
+			title: '上传中...',
+			mask: true
+		});
+		
+		// 上传成功的图片计数
+		let successCount = 0;
+		let failCount = 0;
+		const totalFiles = tempFilePaths.length;
+		const newImages = [];
+		
+		// 遍历处理每张图片
+		tempFilePaths.forEach((path, index) => {
+			// 调用上传API
+			uni.uploadFile({
+				url: api.serve + '/file/upload', 
+				filePath: path,
+				name: 'file',
+				formData: {
+					type: 'task'
+				},
+				header: {
+					'Authorization': `Bearer ${storage.getAccessToken()}`
+				},
+				success: (res) => {
+					try {
+						console.log('上传原始响应:', res);
+						console.log('响应数据类型:', typeof res.data);
+						console.log('响应状态码:', res.statusCode);
+						
+						let response;
+						
+						// 兼容不同平台的响应格式
+						if (typeof res.data === 'string') {
+							// H5 端返回字符串,需要解析
+							response = JSON.parse(res.data);
+						} else if (typeof res.data === 'object') {
+							// Android/鸿蒙端可能直接返回对象
+							response = res.data;
+						} else {
+							throw new Error('未知的响应格式');
+						}
+						
+						console.log('解析后的响应:', response);
+						
+						// 检查响应是否成功
+						if (response.code === 200 || response.code === '200') {
+							// 兼容不同的数据结构
+							let imageUrl = '';
+							
+							// 情况1: response.data 是字符串(直接是URL)
+							if (typeof response.data === 'string') {
+								imageUrl = response.data;
+							}
+							// 情况2: response.data 是对象,包含 url 字段
+							else if (response.data && response.data.url) {
+								imageUrl = response.data.url;
+							}
+							// 情况3: response 直接包含 url 字段
+							else if (response.url) {
+								imageUrl = response.url;
+							}
+							// 情况4: response.data 是数组
+							else if (Array.isArray(response.data) && response.data.length > 0) {
+								imageUrl = response.data[0].url || response.data[0];
+							}
+							
+							if (imageUrl) {
+								console.log('上传成功,图片URL:', imageUrl);
+								
+								// 上传成功,将图片信息添加到数组
+								newImages.push({
+									url: imageUrl,
+									path: path,
+									status: 'success',
+									fileName: (response.data && response.data.fileName) || ''
+								});
+								successCount++;
+							} else {
+								console.error('无法从响应中提取图片URL:', response);
+								failCount++;
+								uni.showToast({
+									title: '图片URL解析失败',
+									icon: 'none',
+									duration: 2000
+								});
+							}
+						} else {
+							failCount++;
+							console.error('上传失败,错误信息:', response.msg || response.message);
+							uni.showToast({
+								title: response.msg || response.message || '上传失败',
+								icon: 'none',
+								duration: 2000
+							});
+						}
+					} catch (e) {
+						failCount++;
+						console.error('解析响应失败:', e);
+						console.error('原始响应数据:', res.data);
 						uni.showToast({
-						  title: `返回: ${response.data}`,
-						  icon: 'none',
+							title: `解析失败: ${e.message}`,
+							icon: 'none',
+							duration: 2000
 						});
-			            if (response.code === 200) {
-			              // 获取返回的URL
-			              const imageUrl = response.data.url;
-			              console.log('上传成功,返回的图片URL:', imageUrl);
-			              
-			              // 上传成功,将图片信息添加到数组
-			              newImages.push({
-			                url: imageUrl, // 保存原始URL,在显示时会通过getImageUrl方法处理
-			                path: path, // 保存本地路径用于预览
-			                status: 'success',
-			                fileName: response.data.fileName || '' // 保存文件名,如果后端返回的话
-			              });
-			              successCount++;
-			          } else {
-			            failCount++;
-			            console.error('上传失败:', response.msg);
-			          }
-			        } catch (e) {
-			          failCount++;
-					  uni.showToast({
-					    title: `解析响应失败: ${e}`,
-					    icon: 'none',
-					  });
-			          console.error('解析响应失败:', e);
-			        }
-			      },
-			      fail: (err) => {
-			        failCount++;
-			        console.error('上传请求失败:', err);
+					}
+				},
+				fail: (err) => {
+					failCount++;
+					console.error('上传请求失败:', err);
 					uni.showToast({
-					  title: `上传请求失败: ${err}`,
-					  icon: 'none',
+						title: '上传请求失败',
+						icon: 'none',
+						duration: 2000
 					});
-			      },
-			      complete: () => {
-			        // 当所有文件都已处理完成
-			        if (successCount + failCount === totalFiles) {
-			          if (newImages.length > 0) {
-			            // 将新上传的图片添加到已有图片列表
-			            formData.images = [...formData.images, ...newImages]
-			            // 更新taskImages字段,将图片URL用逗号连接
-			            formData.imageUrl = formData.images.map(item => item.url).join(',')
-			            // 显示成功提示
-			            uni.hideLoading()
-			            uni.showToast({
-			              title: `成功上传${successCount}张图片`,
-			              icon: 'success'
-			            })
-			          } else {
-			            // 全部失败
-						uni.showToast({
-						  title: `图片上传: ${newImages}`,
-						  icon: 'none'
-						})
-			            uni.hideLoading()
-			          }
-			        }
-			      }
-			    })
-			  })
+				},
+				complete: () => {
+					// 当所有文件都已处理完成
+					if (successCount + failCount === totalFiles) {
+						uni.hideLoading();
+						
+						if (newImages.length > 0) {
+							// 将新上传的图片添加到已有图片列表
+							formData.images = [...formData.images, ...newImages];
+							// 更新imageUrl字段,将图片URL用逗号连接
+							formData.imageUrl = formData.images.map(item => item.url).join(',');
+							
+							console.log('所有图片上传完成:', formData.images);
+							console.log('imageUrl:', formData.imageUrl);
+							
+							// 显示成功提示
+							uni.showToast({
+								title: `成功上传${successCount}张图片`,
+								icon: 'success',
+								duration: 2000
+							});
+						} else {
+							// 全部失败
+							uni.showToast({
+								title: '图片上传失败,请重试',
+								icon: 'none',
+								duration: 2000
+							});
+						}
+						
+						// 如果有部分失败
+						if (failCount > 0 && successCount > 0) {
+							uni.showToast({
+								title: `成功${successCount}张,失败${failCount}张`,
+								icon: 'none',
+								duration: 2000
+							});
+						}
+					}
+				}
+			});
+		});
 	}
 	
 	const removeImage = (index) => {

+ 258 - 146
pages/service/sales-publish.vue

@@ -180,7 +180,7 @@
 				</view>
 				<view class="picker-options">
 					<view class="picker-option" 
-					v-for="category in dictDataOptions.agricultural_category" 
+					v-for="category in (dictDataOptions.agricultural_category || [])" 
 					:key="category.dictCode"
 					@click="selectCategory(category)"
 					:class="{ 'active': category.dictValue == formData.categoryId }">
@@ -198,7 +198,7 @@
 				</view>
 				<view class="picker-options">
 					<view class="picker-option"
-					v-for="unit in dictDataOptions.agricultural_unit"
+					v-for="unit in (dictDataOptions.agricultural_unit || [])"
 					:key="unit.dictCode"
 					@click="selectUnit(unit)"
 					:class="{ 'active': unit.dictValue == formData.unit }">
@@ -258,10 +258,12 @@
 	const currentUserInfo = storage.getUserInfo()
 
 	// 计算属性:dictDataOptions
-	const dictDataOptions = computed(() => dictData.value)
+	const dictDataOptions = computed(() => dictData)
 
 	// onLoad 替换为 onMounted + getCurrentPages
 	onMounted(() => {
+		console.log("dictDataOptions",dictDataOptions.value);
+		
 		const pages = getCurrentPages()
 		const currentPage = pages[pages.length - 1]
 		const options = currentPage.options
@@ -351,165 +353,275 @@
 	
 	// 获取字典标签
 	const getDictLabel = (dictKey, value) => {
-		if (!dictData.value || !dictData.value[dictKey]) {
+		if (!dictData || !dictData[dictKey]) {
 			return ''
 		}
-		const list = dictData.value[dictKey] || []
+		const list = dictData[dictKey] || []
 		const item = list.find(u => u.dictValue == value)
 		return item ? item.dictLabel : ''
 	}
 
-	// 选择图片
+	// 选择图片(优化跨平台兼容性)
 	const chooseImage = () => {
 		uni.chooseImage({
 			count: 6 - formData.images.length,
-			    sizeType: ['original', 'compressed'],
-			    sourceType: ['album', 'camera'],
-			    success: (res) => {
-			      console.log('选择图片成功:', res);
-			      
-			      // 验证文件类型和大小
-			      const validFiles = [];
-			      const invalidFiles = [];
-			      const maxSize = 5 * 1024 * 1024; // 5MB 最大限制
-			      
-			      // 检查每个文件
-			      res.tempFiles.forEach((file, index) => {
-			        // 检查文件类型
-			        const isImage = /\.(jpg|jpeg|png|gif)$/i.test(file.name);
-			        // 检查文件大小
-			        const isValidSize = file.size <= maxSize;
-			        
-			        if (isImage && isValidSize) {
-			          validFiles.push(res.tempFilePaths[index]);
-			        } else {
-			          invalidFiles.push({
-			            path: file.path,
-			            size: file.size,
-			            reason: !isImage ? '文件格式不支持' : '文件大于5MB'
-			          });
-			        }
-			      });
-			      
-			      // 显示无效文件提示
-			      if (invalidFiles.length > 0) {
-			        uni.showToast({
-			          title: `${invalidFiles.length}个文件无效,请检查格式和大小`,
-			          icon: 'none',
-			          duration: 2000
-			        });
-			      }
-			      
-			      // 如果有有效文件,则上传
-			      if (validFiles.length > 0) {
-			        uploadImages(validFiles)
-			      }
-			    },
-			    fail: (err) => {
-			      console.error('选择图片失败:', err)
-			      uni.showToast({
-			        title: '选择图片失败',
-			        icon: 'none'
-			      })
-			    }
-		})
+			sizeType: ['original', 'compressed'],
+			sourceType: ['album', 'camera'],
+			success: (res) => {
+				console.log('选择图片成功:', res);
+				console.log('tempFiles:', res.tempFiles);
+				console.log('tempFilePaths:', res.tempFilePaths);
+				
+				// 验证文件类型和大小
+				const validFiles = [];
+				const invalidFiles = [];
+				const maxSize = 5 * 1024 * 1024; // 5MB 最大限制
+				
+				// 兼容不同平台的文件信息获取方式
+				if (res.tempFiles && res.tempFiles.length > 0) {
+					// 检查每个文件
+					res.tempFiles.forEach((file, index) => {
+						console.log(`文件 ${index}:`, file);
+						
+						// 获取文件路径
+						const filePath = res.tempFilePaths[index];
+						
+						// 获取文件名(兼容不同平台)
+						let fileName = '';
+						if (file.name) {
+							fileName = file.name;
+						} else if (file.path) {
+							// 从路径中提取文件名
+							fileName = file.path.split('/').pop();
+						} else if (filePath) {
+							fileName = filePath.split('/').pop();
+						}
+						
+						console.log(`文件名: ${fileName}, 大小: ${file.size}`);
+						
+						// 检查文件类型(兼容没有扩展名的情况)
+						let isImage = true;
+						if (fileName) {
+							isImage = /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName);
+						}
+						
+						// 检查文件大小
+						const fileSize = file.size || 0;
+						const isValidSize = fileSize <= maxSize;
+						
+						if (isImage && isValidSize) {
+							validFiles.push(filePath);
+						} else {
+							invalidFiles.push({
+								path: filePath,
+								size: fileSize,
+								name: fileName,
+								reason: !isImage ? '文件格式不支持' : `文件大于5MB (${(fileSize / 1024 / 1024).toFixed(2)}MB)`
+							});
+						}
+					});
+				} else {
+					// 如果没有 tempFiles,直接使用 tempFilePaths(某些平台可能不返回详细信息)
+					console.warn('未获取到 tempFiles,直接使用 tempFilePaths');
+					res.tempFilePaths.forEach(path => {
+						validFiles.push(path);
+					});
+				}
+				
+				console.log('有效文件:', validFiles);
+				console.log('无效文件:', invalidFiles);
+				
+				// 显示无效文件提示
+				if (invalidFiles.length > 0) {
+					const reasons = invalidFiles.map(f => `${f.name}: ${f.reason}`).join('\n');
+					uni.showModal({
+						title: '部分文件无效',
+						content: `${invalidFiles.length}个文件无效:\n${reasons}`,
+						showCancel: false
+					});
+				}
+				
+				// 如果有有效文件,则上传
+				if (validFiles.length > 0) {
+					uploadImages(validFiles);
+				} else if (invalidFiles.length > 0) {
+					uni.showToast({
+						title: '没有可上传的文件',
+						icon: 'none',
+						duration: 2000
+					});
+				}
+			},
+			fail: (err) => {
+				console.error('选择图片失败:', err);
+				uni.showToast({
+					title: '选择图片失败',
+					icon: 'none',
+					duration: 2000
+				});
+			}
+		});
 	}
 	
-	// 上传图片到服务器
+	// 上传图片到服务器(优化跨平台兼容性)
 	const uploadImages = (tempFilePaths) => {
-			  uni.showLoading({
-			    title: '上传中...',
-			    mask: true
-			  });
-			  
-			  // 上传成功的图片计数
-			  let successCount = 0;
-			  let failCount = 0;
-			  const totalFiles = tempFilePaths.length;
-			  const newImages = [];
-			  
-			  // 遍历处理每张图片
-			  tempFilePaths.forEach((path, index) => {
-			    // 调用上传API
-			    uni.uploadFile({
-			      // url: api.serve + '/base/tasks/uploadTaskImage', 
-			      url: api.serve + '/file/upload', 
-			      filePath: path,
-			      name: 'file', // 文件参数名称,需要与后端接口匹配
-			      formData: {
-			        type: 'task', // 标识文件类型,用于后端区分不同业务的文件
-			        // directory: '/opt/app/nongxiaoyu/uploadImage' // 指定保存目录
-			      },
-				  header: {
-				  	'Authorization': `Bearer ${storage.getAccessToken()}`
-				  },
-			      success: (res) => {
-			                      try {
-			            const response = JSON.parse(res.data);
+		uni.showLoading({
+			title: '上传中...',
+			mask: true
+		});
+		
+		// 上传成功的图片计数
+		let successCount = 0;
+		let failCount = 0;
+		const totalFiles = tempFilePaths.length;
+		const newImages = [];
+		
+		// 遍历处理每张图片
+		tempFilePaths.forEach((path, index) => {
+			// 调用上传API
+			uni.uploadFile({
+				url: api.serve + '/file/upload', 
+				filePath: path,
+				name: 'file',
+				formData: {
+					type: 'task'
+				},
+				header: {
+					'Authorization': `Bearer ${storage.getAccessToken()}`
+				},
+				success: (res) => {
+					try {
+						console.log('上传原始响应:', res);
+						console.log('响应数据类型:', typeof res.data);
+						console.log('响应状态码:', res.statusCode);
+						
+						let response;
+						
+						// 兼容不同平台的响应格式
+						if (typeof res.data === 'string') {
+							// H5 端返回字符串,需要解析
+							response = JSON.parse(res.data);
+						} else if (typeof res.data === 'object') {
+							// Android/鸿蒙端可能直接返回对象
+							response = res.data;
+						} else {
+							throw new Error('未知的响应格式');
+						}
+						
+						console.log('解析后的响应:', response);
+						
+						// 检查响应是否成功
+						if (response.code === 200 || response.code === '200') {
+							// 兼容不同的数据结构
+							let imageUrl = '';
+							
+							// 情况1: response.data 是字符串(直接是URL)
+							if (typeof response.data === 'string') {
+								imageUrl = response.data;
+							}
+							// 情况2: response.data 是对象,包含 url 字段
+							else if (response.data && response.data.url) {
+								imageUrl = response.data.url;
+							}
+							// 情况3: response 直接包含 url 字段
+							else if (response.url) {
+								imageUrl = response.url;
+							}
+							// 情况4: response.data 是数组
+							else if (Array.isArray(response.data) && response.data.length > 0) {
+								imageUrl = response.data[0].url || response.data[0];
+							}
+							
+							if (imageUrl) {
+								console.log('上传成功,图片URL:', imageUrl);
+								
+								// 上传成功,将图片信息添加到数组
+								newImages.push({
+									url: imageUrl,
+									path: path,
+									status: 'success',
+									fileName: (response.data && response.data.fileName) || ''
+								});
+								successCount++;
+							} else {
+								console.error('无法从响应中提取图片URL:', response);
+								failCount++;
+								uni.showToast({
+									title: '图片URL解析失败',
+									icon: 'none',
+									duration: 2000
+								});
+							}
+						} else {
+							failCount++;
+							console.error('上传失败,错误信息:', response.msg || response.message);
+							uni.showToast({
+								title: response.msg || response.message || '上传失败',
+								icon: 'none',
+								duration: 2000
+							});
+						}
+					} catch (e) {
+						failCount++;
+						console.error('解析响应失败:', e);
+						console.error('原始响应数据:', res.data);
 						uni.showToast({
-						  title: `返回: ${response.data}`,
-						  icon: 'none',
+							title: `解析失败: ${e.message}`,
+							icon: 'none',
+							duration: 2000
 						});
-			            if (response.code === 200) {
-			              // 获取返回的URL
-			              const imageUrl = response.data.url;
-			              console.log('上传成功,返回的图片URL:', imageUrl);
-			              
-			              // 上传成功,将图片信息添加到数组
-			              newImages.push({
-			                url: imageUrl, // 保存原始URL,在显示时会通过getImageUrl方法处理
-			                path: path, // 保存本地路径用于预览
-			                status: 'success',
-			                fileName: response.data.fileName || '' // 保存文件名,如果后端返回的话
-			              });
-			              successCount++;
-			          } else {
-			            failCount++;
-			            console.error('上传失败:', response.msg);
-			          }
-			        } catch (e) {
-			          failCount++;
-					  uni.showToast({
-					    title: `解析响应失败: ${e}`,
-					    icon: 'none',
-					  });
-			          console.error('解析响应失败:', e);
-			        }
-			      },
-			      fail: (err) => {
-			        failCount++;
-			        console.error('上传请求失败:', err);
+					}
+				},
+				fail: (err) => {
+					failCount++;
+					console.error('上传请求失败:', err);
 					uni.showToast({
-					  title: `上传请求失败: ${err}`,
-					  icon: 'none',
+						title: '上传请求失败',
+						icon: 'none',
+						duration: 2000
 					});
-			      },
-			      complete: () => {
-			        // 当所有文件都已处理完成
-			        if (successCount + failCount === totalFiles) {
-			          if (newImages.length > 0) {
-			            // 将新上传的图片添加到已有图片列表
-			            formData.images = [...formData.images, ...newImages]
-			            // 更新taskImages字段,将图片URL用逗号连接
-			            formData.imageUrl = formData.images.map(item => item.url).join(',')
-			            // 显示成功提示
-			            uni.hideLoading()
-			            uni.showToast({
-			              title: `成功上传${successCount}张图片`,
-			              icon: 'success'
-			            })
-			          } else {
-			            // 全部失败
-						uni.showToast({
-						  title: `图片上传: ${newImages}`,
-						  icon: 'none'
-						})
-			            uni.hideLoading()
-			          }
-			        }
-			      }
-			    })
-			  })
+				},
+				complete: () => {
+					// 当所有文件都已处理完成
+					if (successCount + failCount === totalFiles) {
+						uni.hideLoading();
+						
+						if (newImages.length > 0) {
+							// 将新上传的图片添加到已有图片列表
+							formData.images = [...formData.images, ...newImages];
+							// 更新imageUrl字段,将图片URL用逗号连接
+							formData.imageUrl = formData.images.map(item => item.url).join(',');
+							
+							console.log('所有图片上传完成:', formData.images);
+							console.log('imageUrl:', formData.imageUrl);
+							
+							// 显示成功提示
+							uni.showToast({
+								title: `成功上传${successCount}张图片`,
+								icon: 'success',
+								duration: 2000
+							});
+						} else {
+							// 全部失败
+							uni.showToast({
+								title: '图片上传失败,请重试',
+								icon: 'none',
+								duration: 2000
+							});
+						}
+						
+						// 如果有部分失败
+						if (failCount > 0 && successCount > 0) {
+							uni.showToast({
+								title: `成功${successCount}张,失败${failCount}张`,
+								icon: 'none',
+								duration: 2000
+							});
+						}
+					}
+				}
+			});
+		});
 	}
 
 	const removeImage = (index) => {

+ 80 - 23
pages/user/index.vue

@@ -62,11 +62,24 @@
 		<view class="info-card">
 			<view class="function-list">
 				<view class="function-item" @click="handleContact">
-          <view class="left">
-            <text>联系客服</text>
-          </view>
-          <text class="arrow">></text>
-        </view>
+					<view class="left">
+						<text>联系客服</text>
+					</view>
+					<text class="arrow">></text>
+				</view>
+				<view class="function-item" @click="viewTerms">
+					<view class="left">
+						<text>用户协议</text>
+					</view>
+					<text class="arrow">></text>
+				</view>
+				<view class="function-item" @click="viewPrivacy">
+					<view class="left">
+						<text>隐私政策</text>
+					</view>
+					<text class="arrow">></text>
+				</view>
+
 <!--        <view class="function-item" @click="navigateToSettings">
           <view class="left">
             <text>系统设置</text>
@@ -91,7 +104,19 @@ import { onShow } from '@dcloudio/uni-app'
 import { logout, webLogout } from '@/api/services/connect.js'
 import { countUserPlots } from '@/api/services/field.js'
 import storage from "@/utils/storage.js"
+// 查看用户协议
+const viewTerms = () => {
+  uni.navigateTo({
+    url: '/pages/login/terms'
+  })
+}
 
+// 查看隐私政策
+const viewPrivacy = () => {
+  uni.navigateTo({
+    url: '/pages/login/privacy'
+  })
+}
 // 响应式数据
 const plotInfo = reactive({
 	total: 0,
@@ -124,24 +149,24 @@ const serviceList = ref([
 		iconSvg: '/static/icons/device-active.png',
 		path: '/pages/device/index'
 	},
-	{ 
-		name: '专家咨询',
-		iconText: '诊',
-		iconSvg: '/static/icons/expert.png',
-		path: '/pages/service/expert'
-	},
-	{ 
-		name: '绿色认证',
-		iconText: '证',
-		iconSvg: '/static/icons/certification.png',
-		path: '/pages/service/certification'
-	},
-	{ 
-		name: '保险接入',
-		iconText: '保',
-		iconSvg: '/static/icons/insurance.png',
-		path: '/pages/service/insurance'
-	}
+	// { 
+	// 	name: '专家咨询',
+	// 	iconText: '诊',
+	// 	iconSvg: '/static/icons/expert.png',
+	// 	path: '/pages/service/expert'
+	// },
+	// { 
+	// 	name: '绿色认证',
+	// 	iconText: '证',
+	// 	iconSvg: '/static/icons/certification.png',
+	// 	path: '/pages/service/certification'
+	// },
+	// { 
+	// 	name: '保险接入',
+	// 	iconText: '保',
+	// 	iconSvg: '/static/icons/insurance.png',
+	// 	path: '/pages/service/insurance'
+	// }
 ])
 
 const userInfo = reactive({
@@ -192,12 +217,44 @@ const navigateToLogin = () => {
 }
 
 const navigateToPlots = () => {
+	if (!storage.getHasLogin()) {
+		uni.showModal({
+			title: '提示',
+			content: '您还未登录,请先登录',
+			confirmText: '去登录',
+			cancelText: '取消',
+			success: function(res) {
+				if (res.confirm) {
+					uni.navigateTo({
+						url: '/pages/login/index'
+					});
+				}
+			},
+		});
+		return;
+	}
 	uni.navigateTo({
 		url: '/pages/plots/list'
 	})
 }
 
 const navigateToService = (item) => {
+	if (!storage.getHasLogin()) {
+		uni.showModal({
+			title: '提示',
+			content: '您还未登录,请先登录',
+			confirmText: '去登录',
+			cancelText: '取消',
+			success: function(res) {
+				if (res.confirm) {
+					uni.navigateTo({
+						url: '/pages/login/index'
+					});
+				}
+			},
+		});
+		return;
+	}
 	uni.navigateTo({ url: item.path })
 }
 

+ 65 - 0
utils/privacy.js

@@ -0,0 +1,65 @@
+import storage from './storage.js'
+
+/**
+ * 隐私协议管理工具
+ */
+export default {
+  /**
+   * 检查隐私协议同意状态
+   * @returns {boolean} 是否已同意隐私协议
+   */
+  checkAgreement() {
+    return !!storage.getPrivacyAgreed()
+  },
+
+  /**
+   * 检查隐私协议状态,如果未同意则跳转到协议页面
+   * @returns {boolean} 是否已同意隐私协议
+   */
+  checkAndRedirect() {
+    const agreed = this.checkAgreement()
+    if (!agreed) {
+      console.log('用户未同意隐私协议,跳转到隐私协议页面')
+      uni.reLaunch({
+        url: '/pages/privacy/privacy-agreement'
+      })
+      return false
+    }
+    return true
+  },
+
+  /**
+   * 设置隐私协议同意状态
+   * @param {boolean} agreed 是否同意
+   */
+  setAgreement(agreed) {
+    storage.setPrivacyAgreed(agreed)
+  },
+
+  /**
+   * 清除隐私协议同意状态
+   */
+  clearAgreement() {
+    storage.removePrivacyAgreed()
+  },
+
+  /**
+   * 检查是否需要显示隐私协议(适用于多端)
+   * @returns {boolean} 是否需要显示隐私协议
+   */
+  shouldShowAgreement() {
+    // #ifdef APP-PLUS || APP-HARMONY || H5
+    return !this.checkAgreement()
+    // #endif
+    
+    // #ifdef MP
+    // 小程序环境可能有不同的隐私协议要求
+    return !this.checkAgreement()
+    // #endif
+    
+    // 其他环境默认不显示
+    // #ifndef APP-PLUS || APP-HARMONY || H5 || MP
+    return false
+    // #endif
+  }
+}

+ 13 - 0
utils/storage.js

@@ -8,6 +8,7 @@ const ACCESS_TOKEN = isDev ? "access_token_key_dev" : "access_token_key";
 const REFRESH_TOKEN = isDev ? "refresh_token_key_dev" : "refresh_token_key";
 const USER_INFO = isDev ? "user_info_obj_dev" : "user_info_obj";
 const AFTERSALE_DATA = isDev ? "aftersale_data_dev" : "aftersale_data";
+const PRIVACY_AGREED = isDev ? "privacy_agreed_dev" : "privacy_agreed";
 export default {
 	setDict(type,val){
 		uni.setStorageSync(type, val);
@@ -108,4 +109,16 @@ export default {
 	getWvpAccessToken() {
 		return uni.getStorageSync('wvp_access_token');
 	},
+	// 设置隐私协议同意状态
+	setPrivacyAgreed(val) {
+		uni.setStorageSync(PRIVACY_AGREED, val);
+	},
+	// 获取隐私协议同意状态
+	getPrivacyAgreed() {
+		return uni.getStorageSync(PRIVACY_AGREED);
+	},
+	// 删除隐私协议同意状态
+	removePrivacyAgreed() {
+		uni.removeStorageSync(PRIVACY_AGREED);
+	},
 };

+ 7 - 0
vite.config.js

@@ -37,6 +37,13 @@ server: {
         secure: false,
         rewrite: (path) => path.replace(/^\/base/, ''),
       },
+      // WVP 视频平台代理配置
+      '/wvp': {
+        target: 'https://nxy.gbdfarm.com:9000',
+        changeOrigin: true,
+        secure: false,
+        rewrite: (path) => path.replace(/^\/wvp/, '/wvp'),
+      },
       // 腾讯地图API代理配置(解决CORS跨域问题)
       '/tencent-map-api': {
         target: 'https://apis.map.qq.com',