Ver código fonte

Merge branch 'master' of http://192.168.0.115:3000/tangning/personalhubs

tangning 1 semana atrás
pai
commit
f5fefbcba7

+ 1 - 0
components.d.ts

@@ -23,6 +23,7 @@ declare module '@vue/runtime-core' {
     ElInput: typeof import('element-plus/es')['ElInput']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElPopover: typeof import('element-plus/es')['ElPopover']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElTable: typeof import('element-plus/es')['ElTable']

+ 32 - 2
src/api/camera/index.js

@@ -15,9 +15,39 @@ export const getlistNew = (data) => {
     });
 };
 
-export const findIncrementList = (data) => {
+export const getsceneResourceList = (data) => {
     return request({
-      url: `/ucenter/user/increment/findIncrementList`,
+      url: `/ucenter/user/scene/cooperation/sceneResourceList`,
+      method: "post",
+      data,
+      config: {
+        timeout: 10000,
+        loading: true,//隐藏进度条
+        headers: {
+          "Content-Type": "application/json;charset=UTF-8",
+        },
+      },
+    });
+};
+
+export const deleteCooperationUser = (data) => {
+    return request({
+      url: `/ucenter/user/camera/deleteCooperationUser`,
+      method: "post",
+      data,
+      config: {
+        timeout: 10000,
+        loading: true,//隐藏进度条
+        headers: {
+          "Content-Type": "application/json;charset=UTF-8",
+        },
+      },
+    });
+};
+
+export const saveCooperationUser = (data) => {
+    return request({
+      url: `/ucenter/user/camera/saveCooperationUser`,
       method: "post",
       data,
       config: {

+ 196 - 0
src/views/pc/device/components/cooperationaa.vue

@@ -0,0 +1,196 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="协作设置"
+    width="400px"
+    :before-close="handleClose"
+    @close="handleClose"
+  >
+    <div class="cooperation-dialog">
+      <div class="form-item">
+        <label class="form-label">协作用户</label>
+        <el-input
+          v-model="cooperationUserName"
+          placeholder="请输入协作用户"
+          clearable
+        />
+      </div>
+    </div>
+    
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="handleCancelCooperation" class="cancel-btn">
+          取消协作
+        </el-button>
+        <el-button type="primary" @click="handleSave" class="save-btn">
+          保存
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, watch, computed } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getsceneResourceList, saveCooperationUser, deleteCooperationUser } from '@/api/camera/index'
+
+defineOptions({
+  name: 'CooperationModal'
+})
+
+const props = defineProps({
+  modelValue: {
+    type: Boolean,
+    default: false
+  },
+  item: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+const emit = defineEmits(['update:modelValue', 'refresh'])
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+const cooperationUserName = ref('')
+
+// 监听item变化,设置初始值
+watch(() => props.item, (newItem) => {
+  if (newItem && newItem.cooperationUserName) {
+    cooperationUserName.value = newItem.cooperationUserName
+  } else {
+    cooperationUserName.value = ''
+  }
+}, { immediate: true })
+
+// 关闭弹窗
+const handleClose = () => {
+  visible.value = false
+}
+
+// 取消协作
+const handleCancelCooperation = async () => {
+  if (props.item.cooperationUserName) {
+    try {
+      await deleteCooperationUser({
+        cameraId: props.item.id
+      })
+      ElMessage.success('取消协作成功')
+      emit('refresh')
+      handleClose()
+    } catch (error) {
+      console.error('取消协作失败:', error)
+      ElMessage.error('取消协作失败')
+    }
+  } else {
+    handleClose()
+  }
+}
+
+// 保存协作
+const handleSave = async () => {
+  if (!cooperationUserName.value.trim()) {
+    ElMessage.warning('请输入协作用户')
+    return
+  }
+
+  try {
+    // 先获取场景资源列表
+    const resourceResponse = await getsceneResourceList({
+      cameraId: props.item.id,
+      sceneNum: false,
+      type: 2
+    })
+
+    // 获取所有资源ID
+    const resourceIds = resourceResponse.include ? resourceResponse.include.map((item) => item.id).join(',') : ''
+    console.log(resourceIds)
+    // 保存协作用户
+    await saveCooperationUser({
+      cameraId: props.item.id,
+      resourceIds: resourceIds,
+      userName: cooperationUserName.value.trim()
+    })
+
+    ElMessage.success('保存成功')
+    emit('refresh')
+    handleClose()
+  } catch (error) {
+    console.error('保存协作失败:', error)
+    ElMessage.error('保存协作失败')
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.cooperation-dialog {
+  .form-item {
+    margin-bottom: 20px;
+    
+    .form-label {
+      display: block;
+      margin-bottom: 8px;
+      font-size: 14px;
+      color: #333;
+      font-weight: 500;
+    }
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  
+  .cancel-btn {
+    background: #f5f5f5;
+    border-color: #d9d9d9;
+    color: #666;
+    
+    &:hover {
+      background: #e6e6e6;
+      border-color: #bfbfbf;
+    }
+  }
+  
+  .save-btn {
+    background: #15bec8;
+    border-color: #15bec8;
+    
+    &:hover {
+      background: #13a8b0;
+      border-color: #13a8b0;
+    }
+  }
+}
+
+:deep(.el-dialog__header) {
+  padding: 20px 20px 10px;
+  border-bottom: 1px solid #e5e5e5;
+  
+  .el-dialog__title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333;
+  }
+  
+  .el-dialog__headerbtn {
+    top: 20px;
+    right: 20px;
+  }
+}
+
+:deep(.el-dialog__body) {
+  padding: 20px;
+}
+
+:deep(.el-dialog__footer) {
+  padding: 10px 20px 20px;
+  border-top: 1px solid #e5e5e5;
+}
+</style>

+ 291 - 49
src/views/pc/device/index.vue

@@ -104,53 +104,57 @@
       >
         <!-- 自定义插槽内容 -->
         <template #item="{ data, subKey, type, item, canclick }">
-          <div v-if="type === 'image'" class="flex-avatar">
+          <div v-if="type === 'image'" class="flex-avatar" @click="gotoScene(item)">
             <div v-if="isMember(item)" class="vip-icon"></div>
             <div v-else-if="isExpiredMember(item)" class="vip-icon vip-expired-icon"></div>
-            <span>{{ data }}</span>
+            <span class="device-id">{{ data }}</span>
           </div>
           
-          <div v-else-if="type === 'qingkuang'">
-            <span>{{ item.usedSpaceStr || '0B' }}/{{ item.totalSpaceStr || '0B' }}</span>
+          <div v-else-if="type === 'qingkuang'" class="capacity-info">
+            <div class="capacity-text">
+              {{ item.usedSpaceStr || '0B' }}
+              {{ (isMember(item) || item.cameraType == 10 || item.cameraType == 11 || item.cameraType == 12) ? '' : `/${item.totalSpaceStr || '0B'}` }}
+            </div>
           </div>
           
           <div v-else-if="type === 'spaceEndStr'">
-            <span :class="{ 'text-danger': isExpired(item) }">
-              {{ data || '-' }}
+            {{ item.spaceEndStr || "--" }}
+            <span class="expired-icon-w">
+              <el-popover :visible="item.popoverVisible" popper-class="expired-ctrls-w" placement="bottom" :width="200">
+                <p v-if="!validExpired(item.spaceEndStr).expiredTime">权益今天到期</p>
+                <p v-else-if="validExpired(item.spaceEndStr).trueExpired"> 权益已过期,请及时续费。</p>
+                <p v-else v-html="$t('权益还有 {expired} 天到期', { expired: validExpired(item.spaceEndStr).expiredText }) "></p>
+                <div class="ctrls-w">
+                  <div class="btn cancel-btn" @click="item.popoverVisible = false">取消</div>
+                  <div class="btn submit-btn" @click="handleRepay(item)">续费</div>
+                </div>
+                <template #reference>
+                  <h-icon
+                    type="register_agreement"
+                    v-if="validExpired(item.spaceEndStr).isExpired"
+                    class="expired-icon kuoda-click-after"
+                    @click.stop="item.popoverVisible = true"
+                  ></h-icon>
+                </template>
+              </el-popover>
             </span>
           </div>
           
           <div v-else-if="canclick && subKey === 'operation'" class="operation-buttons">
-            <span 
-              v-if="tabActive === 0"
-              class="table-btn"
-              @click="handleCooperation(item)"
-            >
+            <li class="table-btn" @click="handleCooperationaa(item)">
               协作
-            </span>
-            <template v-else>
-              <span 
-                class="table-btn"
-                @click="handleCooperation(item)"
-                style="margin-right: 10px;"
-              >
-                协作
-              </span>
-              <span 
-                class="table-btn"
-                @click="unbind(item)"
-                style="margin-right: 10px;"
-              >
-                解绑
-              </span>
-              <span 
-                v-if="item.userIncrementId"
-                class="table-btn"
-                @click="handleRenew(item)"
-              >
-                续费
-              </span>
-            </template>
+            </li>
+            <el-popover trigger="hover" popper-class="info-content" placement="bottom" :width="400">
+                <div class="more-info-content">
+                  <span class="th">拍摄场景数量</span><span class="th">最后拍摄时间</span>
+                  <span class="td">{{item.sceneNum || '-'}}</span><span class="td">{{item.lastTime || '-'}}</span>
+                </div>
+                <template #reference>
+                  <li class="table-btn">
+                    拍摄信息
+                  </li> 
+                </template>
+              </el-popover>         
           </div>
           
           <span v-else>{{ data || '-' }}</span>
@@ -166,7 +170,7 @@
         </template>
       </el-empty>
     </div>
-currentPage{{ currentPage }}
+
     <!-- 分页 -->
     <Paging
       v-if="total"
@@ -192,13 +196,21 @@ currentPage{{ currentPage }}
         <el-button type="primary" @click="handleRenewSuccess">确定</el-button>
       </template>
     </el-dialog>
+
+    <!-- 协作设置弹窗 -->
+    <CooperationModal
+      v-model="showCooperationModal"
+      :item="cooperationItem"
+      @refresh="handleCooperationRefresh"
+    />
   </div>
 </template>
 
 <script setup>
-import { ref, computed, onMounted, watch } from 'vue'
+import { ref, onMounted, onUnmounted, watch } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import TableList from '@/components/tableList/index.vue'
+import CooperationModal from './components/cooperationaa.vue'
 import { getlistNew } from '@/api/camera/index'
 
 defineOptions({
@@ -222,9 +234,9 @@ const searchTypeList = ref([
 // 表格头部配置
 const tabHeader = ref([
   { key: 'snCode', name: 'S/N码', type: 'image' },
-  { key: 'status', name: '状态' },
   { key: 'qingkuang', name: '云容量', type: 'qingkuang' },
   { key: 'spaceEndStr', name: '到期时间', type: 'spaceEndStr'},
+  { key: 'cooperationUserName', name: '协作者' },
   { key: 'operation', name: '操作', canclick: true }
 ])
 
@@ -242,7 +254,10 @@ const showBinding = ref(false)
 const showRenew = ref(false)
 const reNewItem = ref({})
 const selectedType = ref({ name: 'S/N码', value: 2 })
-const cameraData = ref([])
+const showCtrls = ref(null)
+const timer = ref(null)
+const showCooperationModal = ref(false)
+const cooperationItem = ref({})
 
 // 计算属性
 const totalObj = ref({
@@ -261,6 +276,30 @@ const oldTotalObj = ref({
   12: 0
 })
 
+// 计算属性函数
+const isMember = (item) => {
+  const expiredInfo = getExpiredInfo(item)
+  return item.userIncrementId && !expiredInfo.trueExpired
+}
+
+const isExpiredMember = (item) => {
+  const expiredInfo = getExpiredInfo(item)
+  return item.userIncrementId && expiredInfo.trueExpired
+}
+
+const getExpiredInfo = (item) => {
+  if (!item.spaceEndTime) {
+    return {}
+  }
+  let expired = Math.floor((new Date(item.spaceEndTime) - new Date()) / 86400000) + 1
+  return {
+    expiredTime: `<span class="expired">${expired}</span>`,
+    isExpired: expired <= 30,
+    trueExpired: expired < 0
+  }
+}
+
+
 const cameraList = ref([])
 
 // 组件引用
@@ -282,7 +321,11 @@ const getCameraList = async (cameraType = tabActive.value, pageNum = currentPage
     
     if (cameraType === tabActive.value) {
       // 只有当前标签页的数据才更新cameraData
-      cameraList.value = response.list || []
+      // 为每个设备项添加独立的 visible 属性
+      cameraList.value = (response.list || []).map(item => ({
+        ...item,
+        popoverVisible: false,
+      }))
       total.value = response.total || 0
     }
     
@@ -389,6 +432,11 @@ const handleCooperation = (item) => {
   ElMessage.success(`设备 ${item.snCode} 协作功能`)
 }
 
+const handleCooperationaa = (item) => {
+  cooperationItem.value = item
+  showCooperationModal.value = true
+}
+
 const unbind = async (item) => {
   try {
     await ElMessageBox.confirm(`确定要解绑设备 ${item.snCode} 吗?`, '提示', {
@@ -407,6 +455,21 @@ const handleRenew = (item) => {
   showRenew.value = true
 }
 
+const setTimeoutShowCtrls = (bool, item) => {
+  clearTimeout(timer.value)
+  console.log(bool, item, 7777)
+  timer.value = setTimeout(() => {
+    showCtrls.value = bool ? item.id : null
+  }, 200)
+}
+
+const handleRepay = (item) => {
+  reNewItem.value = item
+  showRenew.value = true
+  item.popoverVisible = false
+  showCtrls.value = null
+}
+
 const addDevice = () => {
   showBinding.value = true
 }
@@ -424,6 +487,11 @@ const handleRenewSuccess = () => {
   ElMessage.success('续费成功')
 }
 
+const handleCooperationRefresh = async () => {
+  // 刷新列表
+  await getCameraList()
+}
+
 
 const pageChange = async (page) => {
   currentPage.value = page
@@ -431,18 +499,76 @@ const pageChange = async (page) => {
 }
 
 // 工具方法
-const isMember = (item) => {
-  return item.userIncrementId && !isExpired(item)
+const validExpired = (time) => {
+  if (!time) {
+    return {}
+  }
+  let expired = Math.floor((new Date(time) - new Date()) / 86400000) + 1
+
+  return {
+    expiredTime: expired,
+    expiredText: `<span class="expired">${expired}</span>`,
+    isExpired: expired <= 30,
+    trueExpired: expired < 0
+  }
 }
 
-const isExpiredMember = (item) => {
-  return item.userIncrementId && isExpired(item)
+const getBar = (a, b) => {
+  if (a === 0) {
+    return 0
+  }
+  let temp = (a / b) * 100
+  if (temp < 1) {
+    return '1%'
+  }
+  return (temp > 100 ? 100 : Math.round(temp)) + '%'
+}
+
+const getColor = (a, b) => {
+  let temp = (a / b) * 100
+  let point = 80
+  let color = ''
+  switch (true) {
+    case temp < point:
+      color = '#15BEC8'
+      break
+    default:
+      color = '#ff0000'
+      break
+  }
+  return color
+}
+
+const gotoScene = (item) => {
+  // 根据设备类型跳转到对应的场景页面
+  const tabActiveMap = {
+    10: 3,
+    9: 2, 
+    11: 4,
+    12: 6
+  }
+  
+  // 这里可以根据实际需求调整路由跳转逻辑
+  console.log('跳转到场景页面', {
+    item,
+    cameraid: item.snCode || item.childName,
+    tabActive: tabActiveMap[item.cameraType] || 1
+  })
+  
+  // 如果有路由,可以取消注释下面的代码
+  // router.push({
+  //   name: 'scenesearch',
+  //   params: { id: '4' },
+  //   query: {
+  //     cameraid: item.snCode || item.childName,
+  //     tabActive: tabActiveMap[item.cameraType] || 1
+  //   }
+  // })
 }
 
-const isExpired = (item) => {
-  if (!item.spaceEndTime) return false
-  const expired = Math.floor((new Date(item.spaceEndTime).getTime() - new Date().getTime()) / 86400000) + 1
-  return expired < 0
+// 点击外部关闭弹窗
+const handleDocumentClick = () => {
+  showCtrls.value = null
 }
 
 // 生命周期
@@ -451,6 +577,17 @@ onMounted(async () => {
   await getAllTabsTotalCount()
   // 然后获取当前标签页的数据
   await getCameraList()
+  
+  // 添加全局点击事件监听
+  document.documentElement.addEventListener('click', handleDocumentClick)
+})
+
+onUnmounted(() => {
+  // 清理定时器和事件监听
+  if (timer.value) {
+    clearTimeout(timer.value)
+  }
+  document.documentElement.removeEventListener('click', handleDocumentClick)
 })
 
 // 监听器
@@ -612,7 +749,12 @@ watch(()=>selectedType, async () => {
     }
   }
 }
-
+.expired-icon {
+  margin-left: 6px;
+  color: #ff0000;
+  font-size: 14px;
+  cursor: pointer;
+}
 .table-btn {
   color: #15bec8;
   cursor: pointer;
@@ -625,6 +767,7 @@ watch(()=>selectedType, async () => {
 .flex-avatar {
   display: flex;
   align-items: center;
+  cursor: pointer;
   
   .vip-icon {
     width: 16px;
@@ -637,6 +780,14 @@ watch(()=>selectedType, async () => {
       opacity: 0.5;
     }
   }
+  
+  .device-id {
+    color: #15bec8;
+    
+    &:hover {
+      text-decoration: underline;
+    }
+  }
 }
 
 .content-wrapper {
@@ -652,4 +803,95 @@ watch(()=>selectedType, async () => {
 .text-danger {
   color: #f56c6c;
 }
+
+// 容量相关样式
+.capacity-info {
+  .capacity-text {
+    margin-bottom: 5px;
+  }
+  
+  .capacity-bar {
+    .c-line {
+      width: 100%;
+      height: 4px;
+      background: #f0f0f0;
+      border-radius: 2px;
+      overflow: hidden;
+      
+      .active {
+        height: 100%;
+        border-radius: 2px;
+        transition: all 0.3s ease;
+      }
+    }
+  }
+}
+
+// 过期相关样式
+.expired-icon {
+  cursor: pointer;
+}
+
+.expired-ctrls-w {
+  background: #fff;
+  z-index: 1;
+  min-width: 190px;
+  height: 85px;
+  padding: 12px 16px;
+  box-shadow: 0px 2px 12px 0px rgba(50, 50, 51, 0.12);
+  border-radius: 4px;
+  font-size: 14px;
+  color: #323233;
+  line-height: 22px;
+  p {
+    white-space: nowrap;
+  }
+  &::before {
+    content: "";
+    display: block;
+    border: 8px solid transparent;
+    border-bottom-color: #fff;
+    position: absolute;
+    top: -15px;
+    right: 14px;
+    left: auto;
+    z-index: 1;
+    // transform: translateX(-50%);
+  }
+  .ctrls-w {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 13px;
+  }
+  .btn {
+    padding: 0 8px;
+    line-height: 24px;
+    border-radius: 2px;
+    background: #ebebeb;
+    display: inline-block;
+    cursor: pointer;
+  }
+  .submit-btn {
+    background: #15BEC8;
+    margin-left: 8px;
+    color: #fff;
+  }
+}
+.info-content{
+  .more-info-content {
+    span {
+      float: left;
+      width: 50%;
+      text-align: center;
+      color: #202020;
+      font-size: 14px;
+      padding: 5px;
+      box-sizing: border-box;
+
+      &.th {
+        font-weight: 600;
+      }
+    }
+  }
+}
 </style>