瀏覽代碼

Merge branch 'fm-dev'

wangfumin 1 周之前
父節點
當前提交
63da2de1c4

+ 1 - 0
components.d.ts

@@ -24,6 +24,7 @@ declare module '@vue/runtime-core' {
     Popup: typeof import('./src/components/popup/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    TableList: typeof import('./src/components/tableList/index.vue')['default']
     TheWelcome: typeof import('./src/components/TheWelcome.vue')['default']
     Toast: typeof import('./src/components/Toast/Toast.vue')['default']
     WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']

文件差異過大導致無法顯示
+ 5398 - 0
pnpm-lock.yaml


二進制
src/assets/images/information/avatar_vip.png


二進制
src/assets/images/information/member-tag.png


文件差異過大導致無法顯示
+ 745 - 0
src/components/tableList/index.vue


+ 688 - 21
src/views/pc/device/index.vue

@@ -1,27 +1,694 @@
-<script setup lang="ts">
-import { showConfirm } from '@/components/Toast'
-import { ref, computed, onMounted } from 'vue'
-import { useUserStore } from '@/stores/user'
-import { openPay, getOrderInfo, wxLogin } from '@/api/api'
-import { useRoute } from 'vue-router'
-import { GetRequest, getWeChatCode, getRemark } from '@/utils/index'
-import { useI18n } from 'vue-i18n'
-const route = useRoute()
-const { locale: language, t } = useI18n()
-const userStore = useUserStore();
-const isEur = userStore.isEur
-</script>
 <template>
-  <div class="pcPage">
-      <h-icon class="icon" type="sousuo" />
+  <div class="camera-page">
+    <!-- 顶部标签页 -->
+    <div class="camera-header" v-if="Object.values(oldTotalObj).reduce((t,c) => t+c, 0)">
+      <el-tabs v-model="tabActive" @tab-click="handleTabClick">
+        <el-tab-pane 
+          v-for="item in tabList" 
+          :key="item.id"
+          :label="`${item.name}(${totalObj[item.id]})`"
+          :name="item.id"
+          v-show="oldTotalObj[item.id]"
+        />
+      </el-tabs>
+      
+      <!-- 操作栏 -->
+      <div class="main-list">
+        <div class="btns">
+          <!-- 全选 -->
+          <div v-show="isImgType" class="all-select" :class="{disable: !cameraList.length}">
+            <el-checkbox 
+              v-model="selectAll" 
+              @change="handleSelectAll"
+              :disabled="!cameraList.length"
+            >
+              全选
+            </el-checkbox>
+          </div>
+          
+          <!-- 协作按钮 -->
+          <el-button 
+            v-if="tabActive !== 0 && selectedArr.length > 0"
+            type="primary" 
+            size="small"
+            @click="multCop"
+          >
+            协作
+          </el-button>
+          
+          <!-- 解绑按钮 -->
+          <el-button 
+            v-if="tabActive !== 0 && selectedArr.length > 0"
+            type="danger" 
+            size="small"
+            @click="multDel"
+          >
+            解绑
+          </el-button>
+        </div>
+
+        <div class="rig-con">
+          <!-- 添加设备 -->
+          <el-button type="primary" size="small" @click="addDevice">
+            添加设备
+          </el-button>
+          
+          <!-- 视图切换 -->
+          <template v-if="tabActive !== 0">
+            <el-button-group class="view-toggle">
+              <el-button 
+                :type="isImgType ? 'primary' : 'default'"
+                size="small"
+                @click="changeType(true)"
+              >
+                卡片
+              </el-button>
+              <el-button 
+                :type="!isImgType ? 'primary' : 'default'"
+                size="small"
+                @click="changeType(false)"
+              >
+                列表
+              </el-button>
+            </el-button-group>
+          </template>
+          
+          <!-- 搜索框 -->
+          <el-input
+            v-model="searchKey"
+            placeholder="搜索设备ID"
+            class="search-input"
+            @keyup.enter="handleSearch"
+            clearable
+          >
+            <template #prepend>
+              <el-select v-model="selectedType" placeholder="选择类型" style="width: 100px">
+                <el-option
+                  v-for="item in searchTypeList"
+                  :key="item.value"
+                  :label="item.name"
+                  :value="item"
+                />
+              </el-select>
+            </template>
+            <template #append>
+              <el-button @click="handleSearch">
+                搜索
+              </el-button>
+            </template>
+          </el-input>
+        </div>
+      </div>
+    </div>
+
+    <!-- 内容区域 -->
+    <template>
+      <!-- 卡片视图 -->
+      <el-row :gutter="20" class="camera-cards" v-show="isImgType || tabActive === 0">
+        <template v-if="!loading">
+          <el-col
+            :span="8"
+            v-for="(item, index) in cameraList"
+            :key="index"
+            class="camera-item"
+          >
+            <div class="card-wrapper">
+              <el-checkbox 
+                v-model="item.selected" 
+                @change="handleItemSelect(item)"
+                class="item-checkbox"
+              />
+              <camera-item
+                :item="item"
+                :tabActive="tabActive"
+                @handleCooperation="handleCooperation"
+                @unbind="unbind"
+                @renew="handleRenew"
+              />
+            </div>
+          </el-col>
+        </template>
+      </el-row>
+
+      <!-- 列表视图 -->
+      <table-list
+        v-show="!(isImgType || tabActive === 0)"
+        ref="tableRef"
+        @selection-change="selectHandle"
+        @unbind="unbind"
+        @cooperation="handleCooperation"
+        @renew="handleRenew"
+        :header="tabHeader"
+        :selection="cameraList.length > 0"
+        :data="cameraList"
+        :show-view-toggle="false"
+        class="table-list"
+      >
+        <template #item="{ data, type, canclick, item }">
+          <template v-if="canclick">
+            <span
+              class="table-btn"
+              @click="handleCooperation(item)"
+              v-if="item.status !== 0"
+            >
+              协作列表
+            </span>
+
+            <span class="info-wrapper">
+              <span class="table-btn" @mouseover="showInfo = item.id" @mouseout="showInfo = null">
+                详细信息
+              </span>
+              <div
+                v-show="showInfo === item.id"
+                class="info-tooltip"
+              >
+                <div class="info-content">
+                  <span class="th">场景数量</span><span class="th">最后时间</span>
+                  <span class="td">{{ item.sceneNum || '-' }}</span><span class="td">{{ item.lastTime || '-' }}</span>
+                </div>
+              </div>
+            </span>
+          </template>
+          
+          <span v-else-if="type === 'image'" class="flex-avatar">
+            <span v-if="isMember(item)" class="vip-icon"></span>
+            <span v-else-if="isExpiredMember(item)" class="vip-icon vip-expired-icon"></span>
+            {{ item.snCode }}
+          </span>
+          
+          <span v-else-if="type === 'qingkuang'">
+            <span v-if="item.usedSpaceStr && item.totalSpaceStr && item.totalSpaceStr != '0B'">
+              {{ item.usedSpaceStr }}{{ isMember(item) ? "" : ` / ${item.totalSpaceStr}` }}
+            </span>
+            <span v-else>--</span>
+          </span>
+          
+          <span v-else-if="type === 'spaceEndStr'">
+            {{ item.spaceEndStr || "--" }}
+            <span v-if="isExpired(item)" class="expired-icon" @mouseover="showCtrls = item.id" @mouseout="showCtrls = null">
+              ⚠️
+              <div v-show="showCtrls === item.id" class="expired-tooltip">
+                <p>{{ isExpired(item) ? '会员已过期' : '会员即将过期' }}</p>
+                <div class="ctrls-w">
+                  <el-button size="small" @click="showCtrls = null">取消</el-button>
+                  <el-button size="small" type="primary" @click="handleRenew(item)">续费</el-button>
+                </div>
+              </div>
+            </span>
+          </span>
+          
+          <span v-else>{{ data || "-" }}</span>
+        </template>
+      </table-list>
+    </template>
+
+    <!-- 空状态 -->
+    <div class="empty-state" v-if="!loading && !total">
+      <el-empty description="暂无数据">
+        <template v-if="!Object.values(oldTotalObj).reduce((t,c) => t+c, 0)">
+          <el-button type="primary" @click="addDevice">添加设备</el-button>
+        </template>
+      </el-empty>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination-wrapper" v-if="total">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[9, 18, 36, 72]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 绑定设备弹窗 -->
+    <el-dialog v-model="showBinding" title="绑定设备" width="600px">
+      <div>绑定设备功能</div>
+      <template #footer>
+        <el-button @click="showBinding = false">取消</el-button>
+        <el-button type="primary" @click="handleBindingSuccess">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 续费弹窗 -->
+    <el-dialog v-model="showRenew" title="会员续费" width="600px">
+      <div>会员续费功能</div>
+      <template #footer>
+        <el-button @click="showRenew = false">取消</el-button>
+        <el-button type="primary" @click="handleRenewSuccess">确定</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
+<script setup lang="ts">
+import { ref, computed, onMounted, watch, nextTick } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import TableList from '@/components/tableList/index.vue'
+
+// 静态数据
+const tabList = ref([
+  { id: 0, name: '协作设备' },
+  { id: 4, name: '我的设备' },
+  { id: 10, name: '共享设备' },
+  { id: 11, name: '租赁设备' }
+])
+
+const searchTypeList = ref([
+  { name: '设备编号', value: 2 },
+  { name: '用户名', value: 1 }
+])
+
+// 表格头部配置
+const tabHeader = ref([
+  { key: 'snCode', name: '设备编号', type: 'image', width: 200 },
+  { key: 'status', name: '状态', width: 100 },
+  { key: 'qingkuang', name: '使用情况', type: 'qingkuang', width: 150 },
+  { key: 'spaceEndStr', name: '到期时间', type: 'spaceEndStr', width: 150 },
+  { key: 'operation', name: '操作', canclick: true, width: 200 }
+])
+
+// 静态设备数据
+const mockCameraData = ref([
+  {
+    id: 1,
+    snCode: 'CAM001',
+    status: 1,
+    usedSpaceStr: '2.5GB',
+    totalSpaceStr: '10GB',
+    spaceEndStr: '2024-12-31',
+    spaceEndTime: '2024-12-31',
+    userIncrementId: 'inc_001',
+    sceneNum: 5,
+    lastTime: '2024-01-15 10:30:00',
+    selected: false
+  },
+  {
+    id: 2,
+    snCode: 'CAM002', 
+    status: 1,
+    usedSpaceStr: '5.2GB',
+    totalSpaceStr: '20GB',
+    spaceEndStr: '2024-11-30',
+    spaceEndTime: '2024-11-30',
+    userIncrementId: 'inc_002',
+    sceneNum: 8,
+    lastTime: '2024-01-14 15:20:00',
+    selected: false
+  },
+  {
+    id: 3,
+    snCode: 'CAM003',
+    status: 0,
+    usedSpaceStr: '1.8GB',
+    totalSpaceStr: '5GB', 
+    spaceEndStr: '2024-10-15',
+    spaceEndTime: '2024-10-15',
+    userIncrementId: null,
+    sceneNum: 3,
+    lastTime: '2024-01-13 09:15:00',
+    selected: false
+  }
+])
+
+// 响应式数据
+const tabActive = ref(4)
+const currentPage = ref(1)
+const pageSize = ref(9)
+const total = ref(0)
+const isImgType = ref(localStorage.getItem("isImgTypeForDevice") !== "false")
+const searchKey = ref("")
+const loading = ref(false)
+const selectedArr = ref([])
+const selectAll = ref(false)
+const showBinding = ref(false)
+const showRenew = ref(false)
+const reNewItem = ref({})
+const showCtrls = ref(null)
+const showInfo = ref(null)
+const selectedType = ref({ name: '设备编号', value: 2 })
+
+// 计算属性
+const totalObj = ref({
+  0: 0,
+  4: 0, 
+  10: 0,
+  11: 0
+})
+
+const oldTotalObj = ref({
+  0: 0,
+  4: 0,
+  10: 0, 
+  11: 0
+})
+
+const cameraList = computed(() => {
+  let filteredData = mockCameraData.value
+  
+  // 根据当前标签页过滤
+  if (tabActive.value !== 4) {
+    // 这里可以根据不同的标签页显示不同的数据
+    filteredData = mockCameraData.value.filter(item => {
+      if (tabActive.value === 0) return item.status === 0 // 协作设备
+      if (tabActive.value === 10) return item.id % 2 === 0 // 共享设备
+      if (tabActive.value === 11) return item.id % 3 === 0 // 租赁设备
+      return true
+    })
+  }
+  
+  // 搜索过滤
+  if (searchKey.value) {
+    filteredData = filteredData.filter(item => {
+      if (selectedType.value.value === 2) {
+        return item.snCode.toLowerCase().includes(searchKey.value.toLowerCase())
+      }
+      return true
+    })
+  }
+  
+  // 分页
+  const start = (currentPage.value - 1) * pageSize.value
+  const end = start + pageSize.value
+  total.value = filteredData.length
+  
+  return filteredData.slice(start, end)
+})
+
+// 组件引用
+const tableRef = ref()
+
+// 方法
+const handleTabClick = (tab: any) => {
+  tabActive.value = parseInt(tab.name)
+  currentPage.value = 1
+  updateTotalCount()
+}
+
+const updateTotalCount = () => {
+  // 更新各个标签的数量
+  totalObj.value[0] = mockCameraData.value.filter(item => item.status === 0).length
+  totalObj.value[4] = mockCameraData.value.length
+  totalObj.value[10] = mockCameraData.value.filter(item => item.id % 2 === 0).length
+  totalObj.value[11] = mockCameraData.value.filter(item => item.id % 3 === 0).length
+  
+  oldTotalObj.value = { ...totalObj.value }
+}
+
+const changeType = (status: boolean) => {
+  isImgType.value = status
+  localStorage.setItem("isImgTypeForDevice", status.toString())
+}
+
+const handleSearch = () => {
+  currentPage.value = 1
+}
+
+const handleSelectAll = (val: boolean) => {
+  cameraList.value.forEach(item => {
+    item.selected = val
+  })
+  updateSelectedArr()
+}
+
+const handleItemSelect = (item: any) => {
+  updateSelectedArr()
+  
+  // 更新全选状态
+  selectAll.value = cameraList.value.every(item => item.selected)
+}
+
+const updateSelectedArr = () => {
+  selectedArr.value = cameraList.value.filter(item => item.selected)
+}
+
+const selectHandle = (selection: any[]) => {
+  selectedArr.value = selection
+}
+
+const multCop = () => {
+  if (selectedArr.value.length === 0) {
+    ElMessage.warning('请至少选择一个设备')
+    return
+  }
+  ElMessage.success('批量协作功能')
+}
+
+const multDel = async () => {
+  if (selectedArr.value.length === 0) {
+    ElMessage.warning('请至少选择一个设备')
+    return
+  }
+  
+  try {
+    await ElMessageBox.confirm('确定要解绑选中的设备吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    ElMessage.success('解绑成功')
+    selectedArr.value = []
+  } catch {
+    // 用户取消
+  }
+}
+
+const handleCooperation = (item: any) => {
+  ElMessage.success(`设备 ${item.snCode} 协作功能`)
+}
+
+const unbind = async (item: any) => {
+  try {
+    await ElMessageBox.confirm(`确定要解绑设备 ${item.snCode} 吗?`, '提示', {
+      confirmButtonText: '确定', 
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    ElMessage.success('解绑成功')
+  } catch {
+    // 用户取消
+  }
+}
+
+const handleRenew = (item: any) => {
+  reNewItem.value = item
+  showRenew.value = true
+}
+
+const addDevice = () => {
+  showBinding.value = true
+}
+
+const handleBindingSuccess = () => {
+  showBinding.value = false
+  ElMessage.success('绑定成功')
+  updateTotalCount()
+}
+
+const handleRenewSuccess = () => {
+  showRenew.value = false
+  ElMessage.success('续费成功')
+}
+
+const handleSizeChange = (val: number) => {
+  pageSize.value = val
+  currentPage.value = 1
+}
+
+const handleCurrentChange = (val: number) => {
+  currentPage.value = val
+}
+
+// 工具方法
+const isMember = (item: any) => {
+  return item.userIncrementId && !isExpired(item)
+}
+
+const isExpiredMember = (item: any) => {
+  return item.userIncrementId && isExpired(item)
+}
+
+const isExpired = (item: any) => {
+  if (!item.spaceEndTime) return false
+  const expired = Math.floor((new Date(item.spaceEndTime).getTime() - new Date().getTime()) / 86400000) + 1
+  return expired < 0
+}
+
+// 生命周期
+onMounted(() => {
+  updateTotalCount()
+})
+
+// 监听器
+watch(tabActive, () => {
+  selectedArr.value = []
+  selectAll.value = false
+})
+</script>
+
 <style lang="less" scoped>
-.pcPage {
-  background: #f7f7f7;
-  max-width: 100vw;
-  display: block;
-  color:#202020;
+.camera-page {
+  padding: 30px;
+  background: #fff;
+  min-height: calc(100vh - 60px);
+}
+
+.camera-header {
+  margin-bottom: 20px;
+  
+  :deep(.el-tabs__header) {
+    margin-bottom: 20px;
+  }
+  
+  .main-list {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px 0;
+    border-bottom: 1px solid #e5e5e5;
+    
+    .btns {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      
+      .all-select {
+        &.disable {
+          opacity: 0.5;
+          pointer-events: none;
+        }
+      }
+    }
+    
+    .rig-con {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      
+      .view-toggle {
+        margin: 0 10px;
+      }
+      
+      .search-input {
+        width: 300px;
+        
+        :deep(.el-input-group__prepend) {
+          padding: 0;
+          
+          .el-select {
+            border: none;
+            
+            .el-input__wrapper {
+              box-shadow: none;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+.camera-cards {
+  padding: 20px 0;
+  
+  .camera-item {
+    margin-bottom: 20px;
+    
+    .card-wrapper {
+      position: relative;
+      background: #f7f7f7;
+      padding: 20px;
+      border-radius: 8px;
+      min-height: 180px;
+      
+      .item-checkbox {
+        position: absolute;
+        top: 10px;
+        left: 10px;
+        z-index: 1;
+      }
+    }
+  }
+}
+
+.table-list {
+  margin: 20px 0;
+}
+
+.empty-state {
+  padding: 60px 0;
+  text-align: center;
+}
+
+.pagination-wrapper {
+  margin-top: 40px;
+  text-align: center;
+}
+
+// 工具提示样式
+.info-wrapper, .expired-icon {
+  position: relative;
+  
+  .info-tooltip, .expired-tooltip {
+    position: absolute;
+    background: #fff;
+    border: 1px solid #e5e5e5;
+    border-radius: 4px;
+    padding: 10px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+    z-index: 1000;
+    min-width: 200px;
+    top: 100%;
+    left: 0;
+    
+    .info-content {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 5px;
+      
+      .th {
+        font-weight: 600;
+        color: #323233;
+      }
+      
+      .td {
+        color: #666;
+      }
+    }
+    
+    .ctrls-w {
+      margin-top: 10px;
+      text-align: right;
+    }
+  }
+}
+
+.table-btn {
+  color: #15bec8;
+  cursor: pointer;
+  
+  &:hover {
+    text-decoration: underline;
+  }
+}
+
+.flex-avatar {
+  display: flex;
+  align-items: center;
+  
+  .vip-icon {
+    width: 16px;
+    height: 16px;
+    background: url('@/assets/images/information/avatar_vip.png') no-repeat center center;
+    background-size: cover;
+    margin-right: 5px;
+    
+    &.vip-expired-icon {
+      opacity: 0.5;
+    }
+  }
 }
-</style>
+</style>

+ 699 - 0
src/views/pc/device/index_BACKUP_382.vue

@@ -0,0 +1,699 @@
+<template>
+<<<<<<< HEAD
+  <div class="camera-page">
+    <!-- 顶部标签页 -->
+    <div class="camera-header" v-if="Object.values(oldTotalObj).reduce((t,c) => t+c, 0)">
+      <el-tabs v-model="tabActive" @tab-click="handleTabClick">
+        <el-tab-pane 
+          v-for="item in tabList" 
+          :key="item.id"
+          :label="`${item.name}(${totalObj[item.id]})`"
+          :name="item.id"
+          v-show="oldTotalObj[item.id]"
+        />
+      </el-tabs>
+      
+      <!-- 操作栏 -->
+      <div class="main-list">
+        <div class="btns">
+          <!-- 全选 -->
+          <div v-show="isImgType" class="all-select" :class="{disable: !cameraList.length}">
+            <el-checkbox 
+              v-model="selectAll" 
+              @change="handleSelectAll"
+              :disabled="!cameraList.length"
+            >
+              全选
+            </el-checkbox>
+          </div>
+          
+          <!-- 协作按钮 -->
+          <el-button 
+            v-if="tabActive !== 0 && selectedArr.length > 0"
+            type="primary" 
+            size="small"
+            @click="multCop"
+          >
+            协作
+          </el-button>
+          
+          <!-- 解绑按钮 -->
+          <el-button 
+            v-if="tabActive !== 0 && selectedArr.length > 0"
+            type="danger" 
+            size="small"
+            @click="multDel"
+          >
+            解绑
+          </el-button>
+        </div>
+
+        <div class="rig-con">
+          <!-- 添加设备 -->
+          <el-button type="primary" size="small" @click="addDevice">
+            添加设备
+          </el-button>
+          
+          <!-- 视图切换 -->
+          <template v-if="tabActive !== 0">
+            <el-button-group class="view-toggle">
+              <el-button 
+                :type="isImgType ? 'primary' : 'default'"
+                size="small"
+                @click="changeType(true)"
+              >
+                卡片
+              </el-button>
+              <el-button 
+                :type="!isImgType ? 'primary' : 'default'"
+                size="small"
+                @click="changeType(false)"
+              >
+                列表
+              </el-button>
+            </el-button-group>
+          </template>
+          
+          <!-- 搜索框 -->
+          <el-input
+            v-model="searchKey"
+            placeholder="搜索设备ID"
+            class="search-input"
+            @keyup.enter="handleSearch"
+            clearable
+          >
+            <template #prepend>
+              <el-select v-model="selectedType" placeholder="选择类型" style="width: 100px">
+                <el-option
+                  v-for="item in searchTypeList"
+                  :key="item.value"
+                  :label="item.name"
+                  :value="item"
+                />
+              </el-select>
+            </template>
+            <template #append>
+              <el-button @click="handleSearch">
+                搜索
+              </el-button>
+            </template>
+          </el-input>
+        </div>
+      </div>
+    </div>
+
+    <!-- 内容区域 -->
+    <template>
+      <!-- 卡片视图 -->
+      <el-row :gutter="20" class="camera-cards" v-show="isImgType || tabActive === 0">
+        <template v-if="!loading">
+          <el-col
+            :span="8"
+            v-for="(item, index) in cameraList"
+            :key="index"
+            class="camera-item"
+          >
+            <div class="card-wrapper">
+              <el-checkbox 
+                v-model="item.selected" 
+                @change="handleItemSelect(item)"
+                class="item-checkbox"
+              />
+              <camera-item
+                :item="item"
+                :tabActive="tabActive"
+                @handleCooperation="handleCooperation"
+                @unbind="unbind"
+                @renew="handleRenew"
+              />
+            </div>
+          </el-col>
+        </template>
+      </el-row>
+
+      <!-- 列表视图 -->
+      <table-list
+        v-show="!(isImgType || tabActive === 0)"
+        ref="tableRef"
+        @selection-change="selectHandle"
+        @unbind="unbind"
+        @cooperation="handleCooperation"
+        @renew="handleRenew"
+        :header="tabHeader"
+        :selection="cameraList.length > 0"
+        :data="cameraList"
+        :show-view-toggle="false"
+        class="table-list"
+      >
+        <template #item="{ data, type, canclick, item }">
+          <template v-if="canclick">
+            <span
+              class="table-btn"
+              @click="handleCooperation(item)"
+              v-if="item.status !== 0"
+            >
+              协作列表
+            </span>
+
+            <span class="info-wrapper">
+              <span class="table-btn" @mouseover="showInfo = item.id" @mouseout="showInfo = null">
+                详细信息
+              </span>
+              <div
+                v-show="showInfo === item.id"
+                class="info-tooltip"
+              >
+                <div class="info-content">
+                  <span class="th">场景数量</span><span class="th">最后时间</span>
+                  <span class="td">{{ item.sceneNum || '-' }}</span><span class="td">{{ item.lastTime || '-' }}</span>
+                </div>
+              </div>
+            </span>
+          </template>
+          
+          <span v-else-if="type === 'image'" class="flex-avatar">
+            <span v-if="isMember(item)" class="vip-icon"></span>
+            <span v-else-if="isExpiredMember(item)" class="vip-icon vip-expired-icon"></span>
+            {{ item.snCode }}
+          </span>
+          
+          <span v-else-if="type === 'qingkuang'">
+            <span v-if="item.usedSpaceStr && item.totalSpaceStr && item.totalSpaceStr != '0B'">
+              {{ item.usedSpaceStr }}{{ isMember(item) ? "" : ` / ${item.totalSpaceStr}` }}
+            </span>
+            <span v-else>--</span>
+          </span>
+          
+          <span v-else-if="type === 'spaceEndStr'">
+            {{ item.spaceEndStr || "--" }}
+            <span v-if="isExpired(item)" class="expired-icon" @mouseover="showCtrls = item.id" @mouseout="showCtrls = null">
+              ⚠️
+              <div v-show="showCtrls === item.id" class="expired-tooltip">
+                <p>{{ isExpired(item) ? '会员已过期' : '会员即将过期' }}</p>
+                <div class="ctrls-w">
+                  <el-button size="small" @click="showCtrls = null">取消</el-button>
+                  <el-button size="small" type="primary" @click="handleRenew(item)">续费</el-button>
+                </div>
+              </div>
+            </span>
+          </span>
+          
+          <span v-else>{{ data || "-" }}</span>
+        </template>
+      </table-list>
+    </template>
+
+    <!-- 空状态 -->
+    <div class="empty-state" v-if="!loading && !total">
+      <el-empty description="暂无数据">
+        <template v-if="!Object.values(oldTotalObj).reduce((t,c) => t+c, 0)">
+          <el-button type="primary" @click="addDevice">添加设备</el-button>
+        </template>
+      </el-empty>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination-wrapper" v-if="total">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[9, 18, 36, 72]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 绑定设备弹窗 -->
+    <el-dialog v-model="showBinding" title="绑定设备" width="600px">
+      <div>绑定设备功能</div>
+      <template #footer>
+        <el-button @click="showBinding = false">取消</el-button>
+        <el-button type="primary" @click="handleBindingSuccess">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 续费弹窗 -->
+    <el-dialog v-model="showRenew" title="会员续费" width="600px">
+      <div>会员续费功能</div>
+      <template #footer>
+        <el-button @click="showRenew = false">取消</el-button>
+        <el-button type="primary" @click="handleRenewSuccess">确定</el-button>
+      </template>
+    </el-dialog>
+=======
+  <div class="pcPage">
+      <h-icon class="icon" type="sousuo" />
+>>>>>>> master
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, watch, nextTick } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import TableList from '@/components/tableList/index.vue'
+
+// 静态数据
+const tabList = ref([
+  { id: 0, name: '协作设备' },
+  { id: 4, name: '我的设备' },
+  { id: 10, name: '共享设备' },
+  { id: 11, name: '租赁设备' }
+])
+
+const searchTypeList = ref([
+  { name: '设备编号', value: 2 },
+  { name: '用户名', value: 1 }
+])
+
+// 表格头部配置
+const tabHeader = ref([
+  { key: 'snCode', name: '设备编号', type: 'image', width: 200 },
+  { key: 'status', name: '状态', width: 100 },
+  { key: 'qingkuang', name: '使用情况', type: 'qingkuang', width: 150 },
+  { key: 'spaceEndStr', name: '到期时间', type: 'spaceEndStr', width: 150 },
+  { key: 'operation', name: '操作', canclick: true, width: 200 }
+])
+
+// 静态设备数据
+const mockCameraData = ref([
+  {
+    id: 1,
+    snCode: 'CAM001',
+    status: 1,
+    usedSpaceStr: '2.5GB',
+    totalSpaceStr: '10GB',
+    spaceEndStr: '2024-12-31',
+    spaceEndTime: '2024-12-31',
+    userIncrementId: 'inc_001',
+    sceneNum: 5,
+    lastTime: '2024-01-15 10:30:00',
+    selected: false
+  },
+  {
+    id: 2,
+    snCode: 'CAM002', 
+    status: 1,
+    usedSpaceStr: '5.2GB',
+    totalSpaceStr: '20GB',
+    spaceEndStr: '2024-11-30',
+    spaceEndTime: '2024-11-30',
+    userIncrementId: 'inc_002',
+    sceneNum: 8,
+    lastTime: '2024-01-14 15:20:00',
+    selected: false
+  },
+  {
+    id: 3,
+    snCode: 'CAM003',
+    status: 0,
+    usedSpaceStr: '1.8GB',
+    totalSpaceStr: '5GB', 
+    spaceEndStr: '2024-10-15',
+    spaceEndTime: '2024-10-15',
+    userIncrementId: null,
+    sceneNum: 3,
+    lastTime: '2024-01-13 09:15:00',
+    selected: false
+  }
+])
+
+// 响应式数据
+const tabActive = ref(4)
+const currentPage = ref(1)
+const pageSize = ref(9)
+const total = ref(0)
+const isImgType = ref(localStorage.getItem("isImgTypeForDevice") !== "false")
+const searchKey = ref("")
+const loading = ref(false)
+const selectedArr = ref([])
+const selectAll = ref(false)
+const showBinding = ref(false)
+const showRenew = ref(false)
+const reNewItem = ref({})
+const showCtrls = ref(null)
+const showInfo = ref(null)
+const selectedType = ref({ name: '设备编号', value: 2 })
+
+// 计算属性
+const totalObj = ref({
+  0: 0,
+  4: 0, 
+  10: 0,
+  11: 0
+})
+
+const oldTotalObj = ref({
+  0: 0,
+  4: 0,
+  10: 0, 
+  11: 0
+})
+
+const cameraList = computed(() => {
+  let filteredData = mockCameraData.value
+  
+  // 根据当前标签页过滤
+  if (tabActive.value !== 4) {
+    // 这里可以根据不同的标签页显示不同的数据
+    filteredData = mockCameraData.value.filter(item => {
+      if (tabActive.value === 0) return item.status === 0 // 协作设备
+      if (tabActive.value === 10) return item.id % 2 === 0 // 共享设备
+      if (tabActive.value === 11) return item.id % 3 === 0 // 租赁设备
+      return true
+    })
+  }
+  
+  // 搜索过滤
+  if (searchKey.value) {
+    filteredData = filteredData.filter(item => {
+      if (selectedType.value.value === 2) {
+        return item.snCode.toLowerCase().includes(searchKey.value.toLowerCase())
+      }
+      return true
+    })
+  }
+  
+  // 分页
+  const start = (currentPage.value - 1) * pageSize.value
+  const end = start + pageSize.value
+  total.value = filteredData.length
+  
+  return filteredData.slice(start, end)
+})
+
+// 组件引用
+const tableRef = ref()
+
+// 方法
+const handleTabClick = (tab: any) => {
+  tabActive.value = parseInt(tab.name)
+  currentPage.value = 1
+  updateTotalCount()
+}
+
+const updateTotalCount = () => {
+  // 更新各个标签的数量
+  totalObj.value[0] = mockCameraData.value.filter(item => item.status === 0).length
+  totalObj.value[4] = mockCameraData.value.length
+  totalObj.value[10] = mockCameraData.value.filter(item => item.id % 2 === 0).length
+  totalObj.value[11] = mockCameraData.value.filter(item => item.id % 3 === 0).length
+  
+  oldTotalObj.value = { ...totalObj.value }
+}
+
+const changeType = (status: boolean) => {
+  isImgType.value = status
+  localStorage.setItem("isImgTypeForDevice", status.toString())
+}
+
+const handleSearch = () => {
+  currentPage.value = 1
+}
+
+const handleSelectAll = (val: boolean) => {
+  cameraList.value.forEach(item => {
+    item.selected = val
+  })
+  updateSelectedArr()
+}
+
+const handleItemSelect = (item: any) => {
+  updateSelectedArr()
+  
+  // 更新全选状态
+  selectAll.value = cameraList.value.every(item => item.selected)
+}
+
+const updateSelectedArr = () => {
+  selectedArr.value = cameraList.value.filter(item => item.selected)
+}
+
+const selectHandle = (selection: any[]) => {
+  selectedArr.value = selection
+}
+
+const multCop = () => {
+  if (selectedArr.value.length === 0) {
+    ElMessage.warning('请至少选择一个设备')
+    return
+  }
+  ElMessage.success('批量协作功能')
+}
+
+const multDel = async () => {
+  if (selectedArr.value.length === 0) {
+    ElMessage.warning('请至少选择一个设备')
+    return
+  }
+  
+  try {
+    await ElMessageBox.confirm('确定要解绑选中的设备吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    ElMessage.success('解绑成功')
+    selectedArr.value = []
+  } catch {
+    // 用户取消
+  }
+}
+
+const handleCooperation = (item: any) => {
+  ElMessage.success(`设备 ${item.snCode} 协作功能`)
+}
+
+const unbind = async (item: any) => {
+  try {
+    await ElMessageBox.confirm(`确定要解绑设备 ${item.snCode} 吗?`, '提示', {
+      confirmButtonText: '确定', 
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    ElMessage.success('解绑成功')
+  } catch {
+    // 用户取消
+  }
+}
+
+const handleRenew = (item: any) => {
+  reNewItem.value = item
+  showRenew.value = true
+}
+
+const addDevice = () => {
+  showBinding.value = true
+}
+
+const handleBindingSuccess = () => {
+  showBinding.value = false
+  ElMessage.success('绑定成功')
+  updateTotalCount()
+}
+
+const handleRenewSuccess = () => {
+  showRenew.value = false
+  ElMessage.success('续费成功')
+}
+
+const handleSizeChange = (val: number) => {
+  pageSize.value = val
+  currentPage.value = 1
+}
+
+const handleCurrentChange = (val: number) => {
+  currentPage.value = val
+}
+
+// 工具方法
+const isMember = (item: any) => {
+  return item.userIncrementId && !isExpired(item)
+}
+
+const isExpiredMember = (item: any) => {
+  return item.userIncrementId && isExpired(item)
+}
+
+const isExpired = (item: any) => {
+  if (!item.spaceEndTime) return false
+  const expired = Math.floor((new Date(item.spaceEndTime).getTime() - new Date().getTime()) / 86400000) + 1
+  return expired < 0
+}
+
+// 生命周期
+onMounted(() => {
+  updateTotalCount()
+})
+
+// 监听器
+watch(tabActive, () => {
+  selectedArr.value = []
+  selectAll.value = false
+})
+</script>
+
+<style lang="less" scoped>
+.camera-page {
+  padding: 30px;
+  background: #fff;
+  min-height: calc(100vh - 60px);
+}
+
+.camera-header {
+  margin-bottom: 20px;
+  
+  :deep(.el-tabs__header) {
+    margin-bottom: 20px;
+  }
+  
+  .main-list {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px 0;
+    border-bottom: 1px solid #e5e5e5;
+    
+    .btns {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      
+      .all-select {
+        &.disable {
+          opacity: 0.5;
+          pointer-events: none;
+        }
+      }
+    }
+    
+    .rig-con {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      
+      .view-toggle {
+        margin: 0 10px;
+      }
+      
+      .search-input {
+        width: 300px;
+        
+        :deep(.el-input-group__prepend) {
+          padding: 0;
+          
+          .el-select {
+            border: none;
+            
+            .el-input__wrapper {
+              box-shadow: none;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+.camera-cards {
+  padding: 20px 0;
+  
+  .camera-item {
+    margin-bottom: 20px;
+    
+    .card-wrapper {
+      position: relative;
+      background: #f7f7f7;
+      padding: 20px;
+      border-radius: 8px;
+      min-height: 180px;
+      
+      .item-checkbox {
+        position: absolute;
+        top: 10px;
+        left: 10px;
+        z-index: 1;
+      }
+    }
+  }
+}
+
+.table-list {
+  margin: 20px 0;
+}
+
+.empty-state {
+  padding: 60px 0;
+  text-align: center;
+}
+
+.pagination-wrapper {
+  margin-top: 40px;
+  text-align: center;
+}
+
+// 工具提示样式
+.info-wrapper, .expired-icon {
+  position: relative;
+  
+  .info-tooltip, .expired-tooltip {
+    position: absolute;
+    background: #fff;
+    border: 1px solid #e5e5e5;
+    border-radius: 4px;
+    padding: 10px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+    z-index: 1000;
+    min-width: 200px;
+    top: 100%;
+    left: 0;
+    
+    .info-content {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 5px;
+      
+      .th {
+        font-weight: 600;
+        color: #323233;
+      }
+      
+      .td {
+        color: #666;
+      }
+    }
+    
+    .ctrls-w {
+      margin-top: 10px;
+      text-align: right;
+    }
+  }
+}
+
+.table-btn {
+  color: #15bec8;
+  cursor: pointer;
+  
+  &:hover {
+    text-decoration: underline;
+  }
+}
+
+.flex-avatar {
+  display: flex;
+  align-items: center;
+  
+  .vip-icon {
+    width: 16px;
+    height: 16px;
+    background: url('@/assets/images/information/avatar_vip.png') no-repeat center center;
+    background-size: cover;
+    margin-right: 5px;
+    
+    &.vip-expired-icon {
+      opacity: 0.5;
+    }
+  }
+}
+</style>

+ 26 - 0
src/views/pc/device/index_BASE_382.vue

@@ -0,0 +1,26 @@
+<script setup lang="ts">
+import { showConfirm } from '@/components/Toast'
+import { ref, computed, onMounted } from 'vue'
+import { useUserStore } from '@/stores/user'
+import { openPay, getOrderInfo, wxLogin } from '@/api/api'
+import { useRoute } from 'vue-router'
+import { GetRequest, getWeChatCode, getRemark } from '@/utils/index'
+import { useI18n } from 'vue-i18n'
+const route = useRoute()
+const { locale: language, t } = useI18n()
+const userStore = useUserStore();
+const isEur = userStore.isEur
+</script>
+<template>
+  <div class="pcPage">mobilePage
+  </div>
+</template>
+
+<style lang="less" scoped>
+.pcPage {
+  background: #f7f7f7;
+  max-width: 100vw;
+  display: block;
+  color:#202020;
+}
+</style>

+ 694 - 0
src/views/pc/device/index_LOCAL_382.vue

@@ -0,0 +1,694 @@
+<template>
+  <div class="camera-page">
+    <!-- 顶部标签页 -->
+    <div class="camera-header" v-if="Object.values(oldTotalObj).reduce((t,c) => t+c, 0)">
+      <el-tabs v-model="tabActive" @tab-click="handleTabClick">
+        <el-tab-pane 
+          v-for="item in tabList" 
+          :key="item.id"
+          :label="`${item.name}(${totalObj[item.id]})`"
+          :name="item.id"
+          v-show="oldTotalObj[item.id]"
+        />
+      </el-tabs>
+      
+      <!-- 操作栏 -->
+      <div class="main-list">
+        <div class="btns">
+          <!-- 全选 -->
+          <div v-show="isImgType" class="all-select" :class="{disable: !cameraList.length}">
+            <el-checkbox 
+              v-model="selectAll" 
+              @change="handleSelectAll"
+              :disabled="!cameraList.length"
+            >
+              全选
+            </el-checkbox>
+          </div>
+          
+          <!-- 协作按钮 -->
+          <el-button 
+            v-if="tabActive !== 0 && selectedArr.length > 0"
+            type="primary" 
+            size="small"
+            @click="multCop"
+          >
+            协作
+          </el-button>
+          
+          <!-- 解绑按钮 -->
+          <el-button 
+            v-if="tabActive !== 0 && selectedArr.length > 0"
+            type="danger" 
+            size="small"
+            @click="multDel"
+          >
+            解绑
+          </el-button>
+        </div>
+
+        <div class="rig-con">
+          <!-- 添加设备 -->
+          <el-button type="primary" size="small" @click="addDevice">
+            添加设备
+          </el-button>
+          
+          <!-- 视图切换 -->
+          <template v-if="tabActive !== 0">
+            <el-button-group class="view-toggle">
+              <el-button 
+                :type="isImgType ? 'primary' : 'default'"
+                size="small"
+                @click="changeType(true)"
+              >
+                卡片
+              </el-button>
+              <el-button 
+                :type="!isImgType ? 'primary' : 'default'"
+                size="small"
+                @click="changeType(false)"
+              >
+                列表
+              </el-button>
+            </el-button-group>
+          </template>
+          
+          <!-- 搜索框 -->
+          <el-input
+            v-model="searchKey"
+            placeholder="搜索设备ID"
+            class="search-input"
+            @keyup.enter="handleSearch"
+            clearable
+          >
+            <template #prepend>
+              <el-select v-model="selectedType" placeholder="选择类型" style="width: 100px">
+                <el-option
+                  v-for="item in searchTypeList"
+                  :key="item.value"
+                  :label="item.name"
+                  :value="item"
+                />
+              </el-select>
+            </template>
+            <template #append>
+              <el-button @click="handleSearch">
+                搜索
+              </el-button>
+            </template>
+          </el-input>
+        </div>
+      </div>
+    </div>
+
+    <!-- 内容区域 -->
+    <template>
+      <!-- 卡片视图 -->
+      <el-row :gutter="20" class="camera-cards" v-show="isImgType || tabActive === 0">
+        <template v-if="!loading">
+          <el-col
+            :span="8"
+            v-for="(item, index) in cameraList"
+            :key="index"
+            class="camera-item"
+          >
+            <div class="card-wrapper">
+              <el-checkbox 
+                v-model="item.selected" 
+                @change="handleItemSelect(item)"
+                class="item-checkbox"
+              />
+              <camera-item
+                :item="item"
+                :tabActive="tabActive"
+                @handleCooperation="handleCooperation"
+                @unbind="unbind"
+                @renew="handleRenew"
+              />
+            </div>
+          </el-col>
+        </template>
+      </el-row>
+
+      <!-- 列表视图 -->
+      <table-list
+        v-show="!(isImgType || tabActive === 0)"
+        ref="tableRef"
+        @selection-change="selectHandle"
+        @unbind="unbind"
+        @cooperation="handleCooperation"
+        @renew="handleRenew"
+        :header="tabHeader"
+        :selection="cameraList.length > 0"
+        :data="cameraList"
+        :show-view-toggle="false"
+        class="table-list"
+      >
+        <template #item="{ data, type, canclick, item }">
+          <template v-if="canclick">
+            <span
+              class="table-btn"
+              @click="handleCooperation(item)"
+              v-if="item.status !== 0"
+            >
+              协作列表
+            </span>
+
+            <span class="info-wrapper">
+              <span class="table-btn" @mouseover="showInfo = item.id" @mouseout="showInfo = null">
+                详细信息
+              </span>
+              <div
+                v-show="showInfo === item.id"
+                class="info-tooltip"
+              >
+                <div class="info-content">
+                  <span class="th">场景数量</span><span class="th">最后时间</span>
+                  <span class="td">{{ item.sceneNum || '-' }}</span><span class="td">{{ item.lastTime || '-' }}</span>
+                </div>
+              </div>
+            </span>
+          </template>
+          
+          <span v-else-if="type === 'image'" class="flex-avatar">
+            <span v-if="isMember(item)" class="vip-icon"></span>
+            <span v-else-if="isExpiredMember(item)" class="vip-icon vip-expired-icon"></span>
+            {{ item.snCode }}
+          </span>
+          
+          <span v-else-if="type === 'qingkuang'">
+            <span v-if="item.usedSpaceStr && item.totalSpaceStr && item.totalSpaceStr != '0B'">
+              {{ item.usedSpaceStr }}{{ isMember(item) ? "" : ` / ${item.totalSpaceStr}` }}
+            </span>
+            <span v-else>--</span>
+          </span>
+          
+          <span v-else-if="type === 'spaceEndStr'">
+            {{ item.spaceEndStr || "--" }}
+            <span v-if="isExpired(item)" class="expired-icon" @mouseover="showCtrls = item.id" @mouseout="showCtrls = null">
+              ⚠️
+              <div v-show="showCtrls === item.id" class="expired-tooltip">
+                <p>{{ isExpired(item) ? '会员已过期' : '会员即将过期' }}</p>
+                <div class="ctrls-w">
+                  <el-button size="small" @click="showCtrls = null">取消</el-button>
+                  <el-button size="small" type="primary" @click="handleRenew(item)">续费</el-button>
+                </div>
+              </div>
+            </span>
+          </span>
+          
+          <span v-else>{{ data || "-" }}</span>
+        </template>
+      </table-list>
+    </template>
+
+    <!-- 空状态 -->
+    <div class="empty-state" v-if="!loading && !total">
+      <el-empty description="暂无数据">
+        <template v-if="!Object.values(oldTotalObj).reduce((t,c) => t+c, 0)">
+          <el-button type="primary" @click="addDevice">添加设备</el-button>
+        </template>
+      </el-empty>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination-wrapper" v-if="total">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[9, 18, 36, 72]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 绑定设备弹窗 -->
+    <el-dialog v-model="showBinding" title="绑定设备" width="600px">
+      <div>绑定设备功能</div>
+      <template #footer>
+        <el-button @click="showBinding = false">取消</el-button>
+        <el-button type="primary" @click="handleBindingSuccess">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 续费弹窗 -->
+    <el-dialog v-model="showRenew" title="会员续费" width="600px">
+      <div>会员续费功能</div>
+      <template #footer>
+        <el-button @click="showRenew = false">取消</el-button>
+        <el-button type="primary" @click="handleRenewSuccess">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, watch, nextTick } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import TableList from '@/components/tableList/index.vue'
+
+// 静态数据
+const tabList = ref([
+  { id: 0, name: '协作设备' },
+  { id: 4, name: '我的设备' },
+  { id: 10, name: '共享设备' },
+  { id: 11, name: '租赁设备' }
+])
+
+const searchTypeList = ref([
+  { name: '设备编号', value: 2 },
+  { name: '用户名', value: 1 }
+])
+
+// 表格头部配置
+const tabHeader = ref([
+  { key: 'snCode', name: '设备编号', type: 'image', width: 200 },
+  { key: 'status', name: '状态', width: 100 },
+  { key: 'qingkuang', name: '使用情况', type: 'qingkuang', width: 150 },
+  { key: 'spaceEndStr', name: '到期时间', type: 'spaceEndStr', width: 150 },
+  { key: 'operation', name: '操作', canclick: true, width: 200 }
+])
+
+// 静态设备数据
+const mockCameraData = ref([
+  {
+    id: 1,
+    snCode: 'CAM001',
+    status: 1,
+    usedSpaceStr: '2.5GB',
+    totalSpaceStr: '10GB',
+    spaceEndStr: '2024-12-31',
+    spaceEndTime: '2024-12-31',
+    userIncrementId: 'inc_001',
+    sceneNum: 5,
+    lastTime: '2024-01-15 10:30:00',
+    selected: false
+  },
+  {
+    id: 2,
+    snCode: 'CAM002', 
+    status: 1,
+    usedSpaceStr: '5.2GB',
+    totalSpaceStr: '20GB',
+    spaceEndStr: '2024-11-30',
+    spaceEndTime: '2024-11-30',
+    userIncrementId: 'inc_002',
+    sceneNum: 8,
+    lastTime: '2024-01-14 15:20:00',
+    selected: false
+  },
+  {
+    id: 3,
+    snCode: 'CAM003',
+    status: 0,
+    usedSpaceStr: '1.8GB',
+    totalSpaceStr: '5GB', 
+    spaceEndStr: '2024-10-15',
+    spaceEndTime: '2024-10-15',
+    userIncrementId: null,
+    sceneNum: 3,
+    lastTime: '2024-01-13 09:15:00',
+    selected: false
+  }
+])
+
+// 响应式数据
+const tabActive = ref(4)
+const currentPage = ref(1)
+const pageSize = ref(9)
+const total = ref(0)
+const isImgType = ref(localStorage.getItem("isImgTypeForDevice") !== "false")
+const searchKey = ref("")
+const loading = ref(false)
+const selectedArr = ref([])
+const selectAll = ref(false)
+const showBinding = ref(false)
+const showRenew = ref(false)
+const reNewItem = ref({})
+const showCtrls = ref(null)
+const showInfo = ref(null)
+const selectedType = ref({ name: '设备编号', value: 2 })
+
+// 计算属性
+const totalObj = ref({
+  0: 0,
+  4: 0, 
+  10: 0,
+  11: 0
+})
+
+const oldTotalObj = ref({
+  0: 0,
+  4: 0,
+  10: 0, 
+  11: 0
+})
+
+const cameraList = computed(() => {
+  let filteredData = mockCameraData.value
+  
+  // 根据当前标签页过滤
+  if (tabActive.value !== 4) {
+    // 这里可以根据不同的标签页显示不同的数据
+    filteredData = mockCameraData.value.filter(item => {
+      if (tabActive.value === 0) return item.status === 0 // 协作设备
+      if (tabActive.value === 10) return item.id % 2 === 0 // 共享设备
+      if (tabActive.value === 11) return item.id % 3 === 0 // 租赁设备
+      return true
+    })
+  }
+  
+  // 搜索过滤
+  if (searchKey.value) {
+    filteredData = filteredData.filter(item => {
+      if (selectedType.value.value === 2) {
+        return item.snCode.toLowerCase().includes(searchKey.value.toLowerCase())
+      }
+      return true
+    })
+  }
+  
+  // 分页
+  const start = (currentPage.value - 1) * pageSize.value
+  const end = start + pageSize.value
+  total.value = filteredData.length
+  
+  return filteredData.slice(start, end)
+})
+
+// 组件引用
+const tableRef = ref()
+
+// 方法
+const handleTabClick = (tab: any) => {
+  tabActive.value = parseInt(tab.name)
+  currentPage.value = 1
+  updateTotalCount()
+}
+
+const updateTotalCount = () => {
+  // 更新各个标签的数量
+  totalObj.value[0] = mockCameraData.value.filter(item => item.status === 0).length
+  totalObj.value[4] = mockCameraData.value.length
+  totalObj.value[10] = mockCameraData.value.filter(item => item.id % 2 === 0).length
+  totalObj.value[11] = mockCameraData.value.filter(item => item.id % 3 === 0).length
+  
+  oldTotalObj.value = { ...totalObj.value }
+}
+
+const changeType = (status: boolean) => {
+  isImgType.value = status
+  localStorage.setItem("isImgTypeForDevice", status.toString())
+}
+
+const handleSearch = () => {
+  currentPage.value = 1
+}
+
+const handleSelectAll = (val: boolean) => {
+  cameraList.value.forEach(item => {
+    item.selected = val
+  })
+  updateSelectedArr()
+}
+
+const handleItemSelect = (item: any) => {
+  updateSelectedArr()
+  
+  // 更新全选状态
+  selectAll.value = cameraList.value.every(item => item.selected)
+}
+
+const updateSelectedArr = () => {
+  selectedArr.value = cameraList.value.filter(item => item.selected)
+}
+
+const selectHandle = (selection: any[]) => {
+  selectedArr.value = selection
+}
+
+const multCop = () => {
+  if (selectedArr.value.length === 0) {
+    ElMessage.warning('请至少选择一个设备')
+    return
+  }
+  ElMessage.success('批量协作功能')
+}
+
+const multDel = async () => {
+  if (selectedArr.value.length === 0) {
+    ElMessage.warning('请至少选择一个设备')
+    return
+  }
+  
+  try {
+    await ElMessageBox.confirm('确定要解绑选中的设备吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    ElMessage.success('解绑成功')
+    selectedArr.value = []
+  } catch {
+    // 用户取消
+  }
+}
+
+const handleCooperation = (item: any) => {
+  ElMessage.success(`设备 ${item.snCode} 协作功能`)
+}
+
+const unbind = async (item: any) => {
+  try {
+    await ElMessageBox.confirm(`确定要解绑设备 ${item.snCode} 吗?`, '提示', {
+      confirmButtonText: '确定', 
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+    ElMessage.success('解绑成功')
+  } catch {
+    // 用户取消
+  }
+}
+
+const handleRenew = (item: any) => {
+  reNewItem.value = item
+  showRenew.value = true
+}
+
+const addDevice = () => {
+  showBinding.value = true
+}
+
+const handleBindingSuccess = () => {
+  showBinding.value = false
+  ElMessage.success('绑定成功')
+  updateTotalCount()
+}
+
+const handleRenewSuccess = () => {
+  showRenew.value = false
+  ElMessage.success('续费成功')
+}
+
+const handleSizeChange = (val: number) => {
+  pageSize.value = val
+  currentPage.value = 1
+}
+
+const handleCurrentChange = (val: number) => {
+  currentPage.value = val
+}
+
+// 工具方法
+const isMember = (item: any) => {
+  return item.userIncrementId && !isExpired(item)
+}
+
+const isExpiredMember = (item: any) => {
+  return item.userIncrementId && isExpired(item)
+}
+
+const isExpired = (item: any) => {
+  if (!item.spaceEndTime) return false
+  const expired = Math.floor((new Date(item.spaceEndTime).getTime() - new Date().getTime()) / 86400000) + 1
+  return expired < 0
+}
+
+// 生命周期
+onMounted(() => {
+  updateTotalCount()
+})
+
+// 监听器
+watch(tabActive, () => {
+  selectedArr.value = []
+  selectAll.value = false
+})
+</script>
+
+<style lang="less" scoped>
+.camera-page {
+  padding: 30px;
+  background: #fff;
+  min-height: calc(100vh - 60px);
+}
+
+.camera-header {
+  margin-bottom: 20px;
+  
+  :deep(.el-tabs__header) {
+    margin-bottom: 20px;
+  }
+  
+  .main-list {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px 0;
+    border-bottom: 1px solid #e5e5e5;
+    
+    .btns {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      
+      .all-select {
+        &.disable {
+          opacity: 0.5;
+          pointer-events: none;
+        }
+      }
+    }
+    
+    .rig-con {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      
+      .view-toggle {
+        margin: 0 10px;
+      }
+      
+      .search-input {
+        width: 300px;
+        
+        :deep(.el-input-group__prepend) {
+          padding: 0;
+          
+          .el-select {
+            border: none;
+            
+            .el-input__wrapper {
+              box-shadow: none;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+.camera-cards {
+  padding: 20px 0;
+  
+  .camera-item {
+    margin-bottom: 20px;
+    
+    .card-wrapper {
+      position: relative;
+      background: #f7f7f7;
+      padding: 20px;
+      border-radius: 8px;
+      min-height: 180px;
+      
+      .item-checkbox {
+        position: absolute;
+        top: 10px;
+        left: 10px;
+        z-index: 1;
+      }
+    }
+  }
+}
+
+.table-list {
+  margin: 20px 0;
+}
+
+.empty-state {
+  padding: 60px 0;
+  text-align: center;
+}
+
+.pagination-wrapper {
+  margin-top: 40px;
+  text-align: center;
+}
+
+// 工具提示样式
+.info-wrapper, .expired-icon {
+  position: relative;
+  
+  .info-tooltip, .expired-tooltip {
+    position: absolute;
+    background: #fff;
+    border: 1px solid #e5e5e5;
+    border-radius: 4px;
+    padding: 10px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+    z-index: 1000;
+    min-width: 200px;
+    top: 100%;
+    left: 0;
+    
+    .info-content {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 5px;
+      
+      .th {
+        font-weight: 600;
+        color: #323233;
+      }
+      
+      .td {
+        color: #666;
+      }
+    }
+    
+    .ctrls-w {
+      margin-top: 10px;
+      text-align: right;
+    }
+  }
+}
+
+.table-btn {
+  color: #15bec8;
+  cursor: pointer;
+  
+  &:hover {
+    text-decoration: underline;
+  }
+}
+
+.flex-avatar {
+  display: flex;
+  align-items: center;
+  
+  .vip-icon {
+    width: 16px;
+    height: 16px;
+    background: url('@/assets/images/information/avatar_vip.png') no-repeat center center;
+    background-size: cover;
+    margin-right: 5px;
+    
+    &.vip-expired-icon {
+      opacity: 0.5;
+    }
+  }
+}
+</style>

+ 27 - 0
src/views/pc/device/index_REMOTE_382.vue

@@ -0,0 +1,27 @@
+<script setup lang="ts">
+import { showConfirm } from '@/components/Toast'
+import { ref, computed, onMounted } from 'vue'
+import { useUserStore } from '@/stores/user'
+import { openPay, getOrderInfo, wxLogin } from '@/api/api'
+import { useRoute } from 'vue-router'
+import { GetRequest, getWeChatCode, getRemark } from '@/utils/index'
+import { useI18n } from 'vue-i18n'
+const route = useRoute()
+const { locale: language, t } = useI18n()
+const userStore = useUserStore();
+const isEur = userStore.isEur
+</script>
+<template>
+  <div class="pcPage">
+      <h-icon class="icon" type="sousuo" />
+  </div>
+</template>
+
+<style lang="less" scoped>
+.pcPage {
+  background: #f7f7f7;
+  max-width: 100vw;
+  display: block;
+  color:#202020;
+}
+</style>

文件差異過大導致無法顯示
+ 441 - 0
src/views/pc/information/components/MemberTable.vue


+ 297 - 19
src/views/pc/information/index.vue

@@ -1,26 +1,304 @@
-<script setup lang="ts">
-import { showConfirm } from '@/components/Toast'
-import { ref, computed, onMounted } from 'vue'
-import { useUserStore } from '@/stores/user'
-import { openPay, getOrderInfo, wxLogin } from '@/api/api'
-import { useRoute } from 'vue-router'
-import { GetRequest, getWeChatCode, getRemark } from '@/utils/index'
-import { useI18n } from 'vue-i18n'
-const route = useRoute()
-const { locale: language, t } = useI18n()
-const userStore = useUserStore();
-const isEur = userStore.isEur
-</script>
 <template>
-  <div class="pcPage">mobilePage
+  <div class="info-layout">
+    <div class="info">
+      <div class="s-tx">
+        <div class="card-img avatar" :class="info.incrementNum ? 'vip' : 'pub'" :style="{backgroundImage: info.head? `url(${info.head})`:`#e6e6e6`}"></div>
+        <div class="member-icon" v-if="info.incrementNum"></div>
+      </div>
+      <div class="top-right-card">
+        <p class="nickname"><span>{{info.nickName||'--'}}</span></p>
+        <div class="contact-info">
+          <p><h-icon type="phone" class="info-icon" />{{info.userName||'--'}}</p>
+          <p><h-icon type="mail"  class="info-icon" />{{info.email||'--'}}</p>
+        </div>
+        <div class="ctrls-w">
+          <a href="javascript:;" @click="changeShowStatus('informationEdit')">编辑资料></a>
+          <a href="javascript:;" @click="changeShowStatus('editAddress')">编辑地址></a>
+          <a href="javascript:;" @click="changeShowStatus('changePassword')">修改密码></a>
+        </div>
+        <div class="pay-btn">
+          <span class="member-tag"></span>
+          去充值
+        </div>
+      </div>
+    </div>
+    
+    <div class="bottom-info">
+      <div class="bottom-info-right">
+        <div class="card info-right-card">
+          <div class="card-header">账户概览</div>
+          <div class="account-info">
+            <div class="info-item">
+              <div class="value">
+                <p>{{ info.incrementNum || 0 }}</p><span></span>
+              </div>
+              <p class="label">权益数量(个)</p>
+            </div>
+            <div class="info-item">
+              <div class="value">
+                <p>{{ info.downloadNumTotal - info.downloadNum }}</p><span></span>
+              </div>
+              <p class="label">剩余下载(次)</p>
+            </div>
+            <div class="info-item">
+              <div class="value">
+                <p>{{ totalScene }}</p><span></span>
+              </div>
+              <p class="label">拍摄场景(个)</p>
+            </div>
+            <div class="info-item">
+              <div class="value">
+                <p>{{ info.cameraCount }}</p><span></span>
+              </div>
+              <p class="label">相机数量(台)</p>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 会员表格 -->
+    <MemberTable />
   </div>
 </template>
 
+<script setup lang="ts">
+import { ref, onMounted, defineOptions } from 'vue'
+import MemberTable from '@/views/pc/information/components/MemberTable.vue'
+
+defineOptions({
+  name: 'InformationPage'
+})
+
+// 模拟数据结构
+const info = ref({
+  head: '',
+  nickName: '用户昵称',
+  userName: '手机号码',
+  email: '邮箱地址',
+  incrementNum: 1,
+  downloadNumTotal: 100,
+  downloadNum: 20,
+  cameraCount: 5
+})
+
+const showStatus = ref('')
+const totalScene = ref(10)
+
+const changeShowStatus = (component: string) => {
+  showStatus.value = component
+}
+
+onMounted(() => {
+  // 页面初始化逻辑
+})
+</script>
+
 <style lang="less" scoped>
-.pcPage {
-  background: #f7f7f7;
-  max-width: 100vw;
-  display: block;
-  color:#202020;
+.info-layout {
+  background: #f4f4f6!important;
+  min-height: 100vh;
+  padding-top: 0!important;
+}
+
+.card {
+  background: #fff;
+  padding: 30px;
+  border-radius: 4px;
+  
+  .card-header {
+    border-bottom: 1px solid #ebebeb;
+    font-size: 16px;
+    font-weight: 400;
+    margin-bottom: 20px;
+    color: #333;
+  }
+}
+
+.info {
+  display: flex;
+  background: #fff;
+  padding: 30px;
+  font-size: 14px;
+  position: relative;
+  border-radius: 4px;
+  
+  & > div {
+    display: inline-block;
+    vertical-align: middle;
+  }
+  
+  .s-tx, .top-right-card {
+    position: relative;
+  }
+  
+  .top-right-card {
+    flex: 1;
+    position: relative;
+  }
+  
+  .avatar {
+    width: 100px;
+    height: 100px;
+    background-size: cover;
+    background-position: center center;
+    border-radius: 50%;
+    margin-right: 20px;
+    border: 2px solid;
+
+    &.vip {
+      border-color: #D8AF7C;
+    }
+
+    &.pub {
+      border-color: rgb(222,228,228);
+    }
+  }
+  
+  .member-icon {
+    width: 32px;
+    height: 32px;
+    background: url('@/assets/images/information/avatar_vip.png') no-repeat center center;
+    background-size: cover;
+    position: absolute;
+    right: 20px;
+    bottom: 0;
+  }
+  
+  .nickname {
+    width: calc(100% - 40px);
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    font-weight: 600;
+    font-size: 16px;
+    line-height: 24px;
+  }
+  
+  .contact-info {
+    margin: 13px 0;
+    display: flex;
+    
+    & > p {
+      width: 150px;
+      display: flex;
+      align-items: center;
+      
+      .info-icon {
+        font-size: 16px;
+        margin-right: 6px;
+      }
+    }
+  }
+  
+  .ctrls-w {
+    a {
+      display: inline-block;
+      width: 150px;
+      color: #15bec8;
+      text-decoration: none;
+      
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+  }
+}
+
+.bottom-info {
+  margin: 30px 0;
+  display: flex;
+  
+  .bottom-info-right {
+    flex: 1;
+    background: #fff;
+    border-radius: 4px;
+    // box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+    
+    .strong {
+      font-size: 24px;
+      margin: 0 5px;
+      font-weight: bold;
+    }
+    
+    .account-info {
+      display: flex;
+    }
+    
+    .info-item {
+      padding-top: 40px;
+      flex: 1;
+      position: relative;
+      
+      &::after {
+        content: '';
+        display: block;
+        width: 1px;
+        height: 45px;
+        background: #ebebeb;
+        position: absolute;
+        top: 48px;
+        right: 0;
+      }
+      
+      &:last-child {
+        &::after {
+          display: none;
+        }
+      }
+      
+      .value {
+        font-size: 30px;
+        line-height: 36px;
+        text-align: center;
+        
+        p {
+          display: inline-block;
+        }
+        
+        span {
+          font-size: 14px;
+          line-height: 20px;
+          display: inline-block;
+          margin-left: 2px;
+        }
+      }
+      
+      .label {
+        font-size: 14px;
+        color: #909090;
+        line-height: 20px;
+        margin-top: 20px;
+        text-align: center;
+      }
+    }
+  }
+}
+
+.pay-btn {
+  position: absolute;
+  width: 140px;
+  line-height: 40px;
+  background: linear-gradient(90deg, #FEE2B0 0%, #C89756 100%);
+  margin: 0 auto;
+  cursor: pointer;
+  font-weight: 400;
+  right: 30px;
+  color: #572D06;
+  text-align: center;
+  border-radius: 4px;
+  font-size: 16px;
+  top: 50%;
+  transform: translateY(-50%);
+  .member-tag {
+    display: block;
+    width: 16px;
+    height: 16px;
+    background: url(@/assets/images/information/member-tag.png) no-repeat center center;
+    background-size: cover;
+    position: absolute;
+    right: 16px;
+    top: 0;
+  }
 }
 </style>