Dynamic Table View Cell Height and Auto Layout

本文推荐了几个关于如何使用Auto Layout实现动态UITableViewCell高度调整的博客文章,提供了实用的实例和经验分享。
给出我devicelistview的完整内容:// MARK: - 更新后的 DeviceListView 支持传入数据并渲染 class DeviceListView: UIView { private let collectionView = UICollectionView( frame: .zero, collectionViewLayout: createLayout() ) var devices: [NewListDeviceModel] = [] { didSet { collectionView.reloadData() } } var onMoreButtonTap: ((NewListDeviceModel) -> Void)? override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } private static func createLayout() -> UICollectionViewLayout { let layout = UICollectionViewFlowLayout() layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) layout.minimumLineSpacing = 10 return layout } private func setup() { collectionView.backgroundColor = .clear collectionView.showsVerticalScrollIndicator = false collectionView.register(DeviceListCell.self, forCellWithReuseIdentifier: "DeviceListCell") collectionView.delegate = self collectionView.dataSource = self addSubview(collectionView) collectionView.snp.makeConstraints { make in make.edges.equalToSuperview() } } } extension DeviceListView: UICollectionViewDataSource, UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return devices.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DeviceListCell", for: indexPath) as! DeviceListCell let device = devices[indexPath.item] cell.configure(with: device) // 注入点击事件 cell.moreButton.removeTarget(nil, action: nil, for: .allEvents) cell.moreButton.addTarget(self, action: #selector(moreButtonTapped(_:)), for: .touchUpInside) return cell } @objc private func moreButtonTapped(_ sender: UIButton) { let point = sender.convert(CGPoint.zero, to: collectionView) guard let indexPath = collectionView.indexPathForItem(at: point), indexPath.item < devices.count else { return } let device = devices[indexPath.item] onMoreButtonTap?(device) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let width = collectionView.bounds.width - 32 let height = width * 9 / 16 + 60 // 图片高度 + 下方文字 return CGSize(width: collectionView.bounds.width, height: height) } } // MARK: - 自动高度支持 extension DeviceListView { func updateHeightConstraint(in parentView: UIView) { // 移除旧高度约束 removeConstraints(constraints.filter { $0.firstAttribute == .height }) // 计算新高度 let height = calculateRequiredHeight() // 重新设置约束 snp.remakeConstraints { make in make.edges.equalToSuperview() make.height.equalTo(height) } // 触发父级 layout parentView.layoutIfNeeded() // 通知 tableView 刷新该 cell if let tableView = findParentTableView(in: parentView) { tableView.beginUpdates() tableView.endUpdates() } } private func calculateRequiredHeight() -> CGFloat { guard devices.count > 0 else { return 150 } var totalHeight: CGFloat = 0 for _ in 0..<devices.count { let width = collectionView.bounds.width - 32 let itemHeight = width * 9 / 16 + 60 // 图片比例 + 文字区域 totalHeight += itemHeight + 10 // 加间距 } totalHeight -= 10 // 最后一项不加 spacing return max(totalHeight, 150) } private func findParentTableView(in view: UIView) -> UITableView? { var parent = view.superview while parent != nil { if let tableView = parent as? UITableView { return tableView } parent = parent?.superview } return nil } }
12-06
<!-- *Author:jxx *Contact:283591387@qq.com *代码由框架生成,任何更改都可能导致被代码生成器覆盖 *业务请在@/extension/system/dynamic_general_ledger/dynamic_general_ledger.jsx此处编写 --> <template> <view-grid ref="grid" :columns="columns" :detail="detail" :editFormFields="editFormFields" :editFormOptions="editFormOptions" :searchFormFields="searchFormFields" :searchFormOptions="searchFormOptions" :table="table" :extend="extend"> </view-grid> <el-dialog v-model="isDialogVisible" title="文档权限信息" width="90%" :before-close="handleClose" style="max-width: 1400px" > <div class="dialog-content"> <!-- 文档基本信息 --> <!-- <el-descriptions title="文档基本信息" :column="2" border style="margin-bottom: 20px"> <el-descriptions-item label="文档编码">{{ currentDocument.code }}</el-descriptions-item> <el-descriptions-item label="文档名称">{{ currentDocument.name }}</el-descriptions-item> <el-descriptions-item label="文档所属工厂">{{ currentDocument.creatorFactory }}</el-descriptions-item> <el-descriptions-item label="文档安全等级">{{ currentDocument.level }}</el-descriptions-item> <el-descriptions-item label="文档状态">{{ currentDocument.state }}</el-descriptions-item> <el-descriptions-item label="文档类型">{{ currentDocument.type }}</el-descriptions-item> <el-descriptions-item label="项目编码">{{ currentDocument.projectCode }}</el-descriptions-item> <el-descriptions-item label="项目名称">{{ currentDocument.projectName }}</el-descriptions-item> <el-descriptions-item label="项目涉及工厂">{{ currentDocument.projectFactories }}</el-descriptions-item> <el-descriptions-item label="项目类型">{{ currentDocument.projectCgr }}</el-descriptions-item> </el-descriptions> <el-divider /> --> <h3>文档授权信息</h3> </div> <el-tabs v-model="activeAccessTab" class="demo-tabs" type="card"> <el-tab-pane label="用户" name="user"> <div class="user-controls" :style="{ marginBottom: '15px', marginLeft: '5px' }"> <div> <span><strong>搜索权限用户:</strong></span> <el-input v-model="searchUserInput" placeholder="请输入工号/姓名" style="width: 200px; margin-right: 10px" /> <el-button type="primary" @click="handleSearchAccessUser">搜索</el-button> </div> <div style="display: flex; justify-content: flex-end; margin-left: auto"> <!-- <el-button @click="clearUserFilters">重置筛选条件</el-button> --> <!-- <el-button type="primary" @click="addUser" style="margin-left: 10px"> 新增权限 </el-button> --> </div> </div> <el-table ref="accessUserTableRef" :data="userList" row-key="id" border height="500" stripe :header-cell-style="{ background: '#F8F8F9', color: '#333', textAlign: 'center' }" :cell-style="{ textAlign: 'center' }" default-expand-all :scrollbar-always-on="true" class="content-right-table" > <el-table-column label="操作" width="100"> <template #default="scope"> <el-button type="text" :style="{ color: scope.row.accessType === '特殊' ? '#409EFF' : '#909399' }" @click="deleteUser(scope.row)" >删除</el-button > <el-button type="text" :style="{ color: scope.row.accessType === '特殊' ? '#409EFF' : '#909399' }" @click="updateUser(scope.row)" >修改</el-button > </template> </el-table-column> <el-table-column label="查看权限" width="120"> <template #default="scope"> <el-checkbox :model-value="scope.row.checkAccess === 0" :style="{ color: scope.row.accessType === '特殊' ? '#409EFF' : '#909399' }" :disabled="true" /> </template> </el-table-column> <el-table-column prop="userName" label="工号" width="120" /> <el-table-column prop="userTrueName" label="姓名" width="120" /> <el-table-column prop="organization" label="事业部" width="150"> <template #default="scope">{{ scope.row.organization }}</template> </el-table-column> <el-table-column prop="department" label="工厂/部门" width="150"> <template #default="scope">{{ scope.row.department }}</template> </el-table-column> <el-table-column prop="position" label="职位" width="120"> <template #default="scope">{{ scope.row.position || '无' }}</template> </el-table-column> <el-table-column prop="projectRole" label="项目角色" width="120"> <template #default="scope">{{ scope.row.projectRole }}</template> </el-table-column> <el-table-column prop="systemRole" label="系统角色" width="120"> <template #default="scope">{{ scope.row.systemRole }}</template> </el-table-column> <el-table-column prop="accessType" label="权限类型" width="120"> <template #default="scope"> <el-tag :type="getTagType(scope.row.accessType)">{{ scope.row.accessType }}</el-tag> </template> </el-table-column> <el-table-column prop="modifyDate" label="授权日期" width="150"> <template #default="scope">{{ scope.row.modifyDate || '无' }}</template> </el-table-column> <el-table-column prop="modifierName" label="授权人工号" width="120"> <template #default="scope">{{ scope.row.modifierName || '无' }}</template> </el-table-column> <el-table-column prop="modifierTrueName" label="授权人姓名" width="120"> <template #default="scope">{{ scope.row.modifierTrueName || '无' }}</template> </el-table-column> </el-table> <el-pagination class="mt-3" style="display: flex; justify-content: flex-end; margin-top: 10px" background layout="total, sizes, prev, pager, next, jumper" :total="accessUsersTotal" :page-sizes="[10, 20, 30, 50]" :page-size="accessUsersPageSize" :current-page="accessUsersCurrentPage" @size-change="handleAccessUsersSizeChange" @current-change="handleAccessUsersCurrentChange" /> </el-tab-pane> <el-tab-pane label="角色" name="roles"> <div class="user-controls" :style="{ marginBottom: '15px', marginLeft: '5px' }"> <div> <span><strong>搜索权限角色:</strong></span> <el-input v-model="searchRoleInput" placeholder="请输入角色ID/名称" style="width: 200px; margin-right: 10px" /> <el-button type="primary" @click="handleSearchAccessRole">搜索</el-button> </div> <!-- <div style="display: flex; justify-content: flex-end; margin-left: auto"> <el-button type="primary" @click="addUser" style="margin-left: 10px"> 新增权限 </el-button> </div> --> </div> <el-table ref="accessRoleTableRef" :data="accessRolesList" row-key="id" border height="500" style="width: 100%" fit :header-cell-style="{ background: '#F8F8F9', color: '#333', textAlign: 'center' }" :cell-style="{ textAlign: 'center' }" default-expand-all :scrollbar-always-on="true" class="content-right-table" > <el-table-column label="操作"> <template #default="scope"> <el-button type="text" :style="{ color: '#409EFF' }" @click="deleteRole(scope.row)" >删除</el-button > <el-button type="text" :style="{ color: '#409EFF' }" @click="updateRole(scope.row)" >修改</el-button > </template> </el-table-column> <el-table-column label="查看权限"> <template #default="scope"> <el-checkbox :model-value="scope.row.check_access === 0" :disabled="true" /> </template> </el-table-column> <el-table-column prop="role_id" label="角色ID" /> <el-table-column prop="role_name" label="角色名称" /> <el-table-column prop="access_type" label="权限类型"> <template #default="scope"> <el-tag :type="getTagType(scope.row.access_type)">{{ scope.row.access_type }}</el-tag> </template> </el-table-column> <el-table-column prop="modify_date" label="授权日期"> <template #default="scope">{{ scope.row.modify_date || '无' }}</template> </el-table-column> <el-table-column prop="modifier_name" label="授权人工号"> <template #default="scope">{{ scope.row.modifier_name || '无' }}</template> </el-table-column> <el-table-column prop="modifier_truename" label="授权人姓名"> <template #default="scope">{{ scope.row.modifier_truename || '无' }}</template> </el-table-column> </el-table> <el-pagination class="mt-3" style="display: flex; justify-content: flex-end; margin-top: 10px" background layout="total, sizes, prev, pager, next, jumper" :total="accessRolesTotal" :page-sizes="[10, 20, 30, 50]" :page-size="accessRolesPageSize" :current-page="accessRolesCurrentPage" @size-change="handleAccessRolesSizeChange" @current-change="handleAccessRolesCurrentChange" /> </el-tab-pane> </el-tabs> </el-dialog> </template> <script> import extend from "@/extension/system/dynamic_general_ledger/dynamic_general_ledger.jsx"; import { ref, defineComponent,h } from "vue"; import axios from 'axios' import { ElDialog, ElRow, ElCol, ElButton, ElLink, ElMessage,ElIcon} from 'element-plus' import { useRouter } from 'vue-router' import Message from '@/views/index/Message.vue' import { View } from '@element-plus/icons-vue'; export default defineComponent({ setup() { const table = ref({ key: 'id', footer: "Foots", cnName: '动态一本账', name: 'dynamic_general_ledger/dynamic_general_ledger', url: "/dynamic_general_ledger/", sortName: "id" }); const isDialogVisible = ref(false) const dialogContent = ref('') const accessUsersCurrentPage = ref(1) const accessUsersPageSize = ref(10) const accessRolesCurrentPage = ref(1) const accessRolesPageSize = ref(10) const activeAccessTab = ref('user') const userList = ref([]) const accessUsersTotal = ref(0) const searchUserInput = ref('') const accessRolesList = ref([]) const searchRoleInput = ref('') const accessRolesTotal = ref(0) var document = { id: '', dr: '', code: '', name: '', area: '', typeCode: '', type: '', level: '', state: '', projectName: '', projectCgr: '', projectCode: '', projectFactories: '', creatorId: '', creatorName: '', creatorFactoryId: '', creatorFactory: '' } const { proxy } = getCurrentInstance() const openDialog = (params.row) => { debugger userList.value = [] accessRolesList.value = [] searchUserInput.value = '' searchRoleInput.value = '' activeAccessTab.value = 'user' isDialogVisible.value = true // 2. 获取文档信息 getDocument(params.row); //重置分页 accessUsersCurrentPage.value = 1 accessUsersPageSize.value = 10 accessRolesCurrentPage.value = 1 accessRolesPageSize.value = 10 userListUpdate(document, '') roleListUpdate(document, '') } // 搜索按钮点击事件 const handleSearchAccessUser = () => { accessUsersCurrentPage.value = 1 userListUpdate(document, searchUserInput.value) } const handleAccessUsersCurrentChange = (page) => { userList.value = [] accessUsersCurrentPage.value = page userListUpdate(document, searchUserInput.value) } const handleAccessUsersSizeChange = (size) => { accessUsersPageSize.value = size accessUsersCurrentPage.value = 1 userListUpdate(document, searchUserInput.value) } const getDocument = async (params) => { console.log(params) debugger try { const document1 = { code: params.row.file_code } const response1 = axios.post('/api/Sys_File_Info/getDocumentrefer', document1) console.log('===response1.data.value==:', response1.data) console.log('===response1.data.value==:', response1.data.id) console.log(response1) debugger document.id = response1.data.id document.dr = response1.data.dr document.code = response1.data.code document.name = response1.data.name document.area = response1.data.area document.type = response1.data.type document.level = response1.data.level document.state = response1.data.state document.projectName = response1.data.projectName document.projectCgr = response1.data.projectCgr document.projectCode = response1.data.projectCode document.projectFactories = response1.data.projectFactories document.creatorId = response1.data.creatorId document.creatorName = response1.data.creatorName document.creatorFactoryId = response1.data.creatorFactoryId document.creatorFactory = response1.data.creatorFactory console.log('========document:======', document) } catch (error) { // proxy.$message.error(`获取文件信息失败:${error.message}`) } } const userListUpdate = async (document, searchQuery) => { try { const params = { page: accessUsersCurrentPage.value, pageSize: accessUsersPageSize.value, file: document, searchQuery: searchQuery || '' // 传递搜索词 } const response = axios.post('/api/Access_File/getAccessUsersByFileWithPaging', params) userList.value = response.data.items.map((user) => ({ ...user, projectRole: user.projectRole ? user.projectRole : '无' })) accessUsersTotal.value = response.data.total console.log('userList.value:', userList.value) } catch (error) { proxy.$message.error(`获取权限用户失败:${error.message}`) } } const roleListUpdate = async (document, searchQuery) => { try { // const response1 = axios.post('/api/Sys_File_Info/getDocument', { // file: document // }) const params = { page: accessRolesCurrentPage.value, pageSize: accessRolesPageSize.value, file: document, searchQuery: searchQuery || '' // 传递搜索词 } const response = axios.post( '/api/Access_File_Role/getAccessRolesByFileWithPaging', params ) accessRolesList.value = response.data.items.map((role) => ({ ...role, access_type: '角色' })) accessRolesTotal.value = response.data.total } catch (error) { proxy.$message.error(`获取角色权限失败:${error.message}`) console.log(`获取角色权限失败:${error.message}`) } } const editFormFields = ref({}); const editFormOptions = ref([]); const searchFormFields = ref({}); const searchFormOptions = ref([]); const columns = ref([{field:'id',title:'序号',type:'long',width:120,require:true,align:'left',sort:true}, {field:'file_id',title:'文档ID',type:'string',width:110,align:'left'}, {field:'file_type',title:'文档类型',type:'string',width:110,align:'left'}, {field:'business_scenario',title:'所属业务场景',type:'string',width:110,align:'left'}, {field:'belong_stage',title:'所属阶段',type:'string',width:110,align:'left'}, {field:'model_project',title:'所属车型项目',type:'string',width:110,align:'left'}, {field:'project_type',title:'项目类型',type:'string',width:110,align:'left'}, {field:'product_model_number',title:'所属产品型号',type:'string',width:110,align:'left'}, {field:'file_security_level',title:'文档安全等级',type:'string',width:110,align:'left'}, {field:'file_code',title:'文件编号',type:'string',width:110,align:'left'}, {field:'file_version',title:'文件版本',type:'string',width:110,align:'left'}, {field:'file_state',title:'文件状态',type:'string',width:110,align:'left'}, {field:'development_tool',title:'编制工具',type:'string',width:110,align:'left'}, {field:'tool_code',title:'编制工具代码',type:'string',width:110,align:'left'}, {field:'method',title:'编制方式',type:'string',width:110,align:'left'}, {field:'collaboration_information',title:'协作信息',type:'string',width:110,align:'left'}, {field:'review_tool_platform',title:'评审工具/平台',type:'string',width:110,align:'left'}, {field:'review_method',title:'评审方式',type:'string',width:110,align:'left'}, {field:'review_scope',title:'评审范围',type:'string',width:110,align:'left'}, //评审角色需做对比 {field:'review_role',title:'评审角色',type:'string',width:110,align:'left'}, /*{ field: 'review_role', title: '评审角色', type: 'string', width: 110, align: 'left', render: async (value, params) => { console.log(value) debugger // 构造请求参数 const param = { FileType: params.row.file_type, }; try { const response = axios.post('/api/Review_Info/IsRole', param, { headers: { 'Content-Type': 'application/json' } }); const backendRoles = response.data.review_role.split(',').map(r => r.trim()).sort(); const frontendRoles = value.split(',').map(r => r.trim()).sort(); // 判断是否完全一致 if (JSON.stringify(frontendRoles) === JSON.stringify(backendRoles)) { return value; // 一致,直接返回原值 } // 不一致,高亮多余部分 const backendSet = new Set(backendRoles); const highlightRoles = frontendRoles.map(role => { return backendSet.has(role) ? role : `<span style="color: red;">${role}</span>`; }); return highlightRoles.join(', '); } catch (error) { console.error('接口调用失败', error); return value; // 出错返回原始值 } } },*/ {field:'transfer_tool',title:'传输工具',type:'string',width:110,align:'left'}, {field:'transfer_method',title:'传输方式',type:'string',width:110,align:'left'}, {field:'transfer_scope',title:'传输范围',type:'string',width:110,align:'left'}, {field:'transfer_format',title:'传输格式',type:'string',width:110,align:'left'}, {field:'publishing_platform',title:'发布平台',type:'string',width:110,align:'left'}, {field:'sharing_platform',title:'共享平台',type:'string',width:110,align:'left'}, {field:'sharing_method',title:'共享方式',type:'string',width:110,align:'left'}, {field:'sharing_scope',title:'共享范围',type:'string',width:110,align:'left'}, { title: '操作', width: 100, align: 'center', fixed: 'right', // 固定在表格最右侧 render: (h, params) => { return [ h('i', { class: 'el-icon-view preview-icon', // 使用 Element Plus 图标 style: 'cursor: pointer; margin-right: 10px;', // 添加样式 onClick: () => openDialog(params.row || '') // 点击事件处理函数 }), ] } }, {field:'integration_platform',title:'集成平台',type:'string',width:110,align:'left'}, {field:'storage_platform',title:'存储平台',type:'string',width:110,align:'left'}, {field:'retention_period',title:'保管期限',type:'string',width:110,align:'left'}, {field:'storage_strategy',title:'存储策略',type:'string',width:110,align:'left'}, {field:'backup_requirements',title:'备份要求',type:'string',width:110,align:'left'}]); const detail = ref({ cnName: "#detailCnName", table: "#detailTable", columns: [], sortName: "", key: "" }); return { table, extend, editFormFields, editFormOptions, searchFormFields, searchFormOptions, columns, detail, }; }, }); </script> 修改一下,并给我修改后的完整代码
09-18
请注意我发给你的代码中有之前自己写的测试代码,可能不对,你需要按照之前学习到的具体实现进行详细修改。 // // DeviceListOrganizationModel.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // import Foundation class OrganizationModel { var name: String init(name: String = "默认组织") { self.name = name } } // // DeviceListView.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // import UIKit class DeviceListView: UIView { private lazy var collectionView = UICollectionView( frame: .zero, collectionViewLayout: Self.createLayout() ) var devices: [TPSSDeviceForDeviceList] = [] { didSet { collectionView.reloadData() invalidateIntrinsicContentSize() } } var onMoreButtonTap: ((TPSSDeviceForDeviceList, TPSSChannelInfo?) -> Void)? override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { super.init(coder: coder) setup() } private static func createLayout() -> UICollectionViewLayout { let layout = UICollectionViewFlowLayout() layout.sectionInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) layout.minimumLineSpacing = 10 return layout } private func setup() { collectionView.backgroundColor = .clear collectionView.showsVerticalScrollIndicator = false collectionView.register(DeviceListCell.self, forCellWithReuseIdentifier: "DeviceListCell") collectionView.delegate = self collectionView.dataSource = self addSubview(collectionView) collectionView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ collectionView.topAnchor.constraint(equalTo: topAnchor), collectionView.leadingAnchor.constraint(equalTo: leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: trailingAnchor), collectionView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) } } // MARK: - UICollectionView DataSource & Delegate extension DeviceListView: UICollectionViewDataSource, UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return devices.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DeviceListCell", for: indexPath) as! DeviceListCell let device = devices[indexPath.item] // 默认取第一个通道 let channel = device.channelsInfo.first cell.configure(with: device, channel: channel) cell.onMoreTap = { [weak self] dev, chn in self?.onMoreButtonTap?(dev, chn) } return cell } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let width = collectionView.bounds.width > 0 ? collectionView.bounds.width : UIScreen.main.bounds.width let itemWidth = width - 32 let itemHeight = itemWidth * 9 / 16 + 60 return CGSize(width: itemWidth, height: itemHeight) } } // MARK: - 自动高度支持 extension DeviceListView { override var intrinsicContentSize: CGSize { calculateRequiredHeight() } override func invalidateIntrinsicContentSize() { super.invalidateIntrinsicContentSize() DispatchQueue.main.async { self.findParentTableView()?.beginUpdates() self.findParentTableView()?.endUpdates() } } private func calculateRequiredHeight() -> CGSize { guard !devices.isEmpty else { return CGSize(width: UIView.noIntrinsicMetric, height: 150) } let width = bounds.width > 0 ? bounds.width : UIScreen.main.bounds.width let itemWidth = width - 32 let itemHeight = itemWidth * 9 / 16 + 60 let lineSpacing: CGFloat = 10 let totalHeight = CGFloat(devices.count) * itemHeight + lineSpacing * CGFloat(devices.count - 1) return CGSize(width: UIView.noIntrinsicMetric, height: max(totalHeight, 150)) } private func findParentTableView() -> UITableView? { var parent = superview while parent != nil { if let tableView = parent as? UITableView { return tableView } parent = parent?.superview } return nil } } // // DeviceListCell.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // import UIKit class DeviceListCell: UICollectionViewCell { @IBOutlet weak var nameLabel: UILabel! @IBOutlet weak var thumbnailImageView: UIImageView! @IBOutlet weak var statusLabel: UILabel! @IBOutlet weak var moreButton: UIButton! private var device: TPSSDeviceForDeviceList! private var channel: TPSSChannelInfo? override func awakeFromNib() { super.awakeFromNib() setupUI() } private func setupUI() { layer.cornerRadius = 8 clipsToBounds = true backgroundColor = .tpbBackground } func configure(with device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo? = nil) { self.device = device self.channel = channel // displayName 是 alias 或通道名 nameLabel.text = device.displayName // 在线状态:displayOnline 是关键字段 if device.displayOnline { statusLabel.text = "在线" statusLabel.textColor = .systemGreen } else { statusLabel.text = "离线" statusLabel.textColor = .systemRed } // 快照 URL(SDK 中未直接暴露,但底层使用 snapshotUrl 字段) if let urlStr = device.value(forKey: "snapshotUrl") as? String, let url = URL(string: urlStr) { #if canImport(Kingfisher) thumbnailImageView.kf.setImage(with: url) #else print(" loadImage: $url)") #endif } else { if #available(iOS 13.0, *) { let img = UIImage(systemName: "camera.fill") ?? UIImage() thumbnailImageView.image = img thumbnailImageView.backgroundColor = UIColor.tpbBackground } else { // Fallback on earlier versions } } moreButton.removeTarget(nil, action: nil, for: .allEvents) moreButton.addTarget(self, action: #selector(moreButtonTapped), for: .touchUpInside) } @objc private func moreButtonTapped() { onMoreTap?(device, channel) } var onMoreTap: ((TPSSDeviceForDeviceList, TPSSChannelInfo?) -> Void)? } // // DeviceListTitleView.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // import UIKit import SnapKit class NewTitleView: UIView { private lazy var titleLabel: UILabel = { let label = UILabel() label.text = "" label.textColor = UIColor.tpbTextPrimary label.font = .boldSystemFont(ofSize: 18) label.lineBreakMode = .byTruncatingTail label.numberOfLines = 1 return label }() private lazy var arrowImageView: UIImageView = { let imageView = UIImageView(image: TPImageLiteral("devicelist_dropdown_arrow_nor")) imageView.contentMode = .scaleAspectFit return imageView }() lazy var titleLoadingOrgView: SimpleLoadingView = { let view = SimpleLoadingView(frame: CGRect(x: 0, y: 0, width: 24, height: 24)) view.isHidden = true return view }() internal var isInExpandStatus: Bool = false internal var isAnimating: Bool = false private var isExccedingWidth: Bool = false public var didClickCallback: ((Bool) -> Void)? var titleText: String { get { titleLabel.text ?? "" } set { titleLabel.text = newValue setNeedsLayout() } } override init(frame: CGRect) { super.init(frame: frame) setupViews() bindActions() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupViews() { addSubview(titleLabel) addSubview(arrowImageView) addSubview(titleLoadingOrgView) reloadView() } override func layoutSubviews() { super.layoutSubviews() let width = titleLabel.intrinsicContentSize.width if width > titleLabel.frame.width && titleLabel.frame.width > 0 { isExccedingWidth = true } else { isExccedingWidth = false } reloadView() } func reloadView() { // 布局更新... titleLabel.snp.updateConstraints { make in make.leading.equalToSuperview().offset(6) make.top.bottom.equalToSuperview() make.height.greaterThanOrEqualTo(20) } arrowImageView.snp.updateConstraints { make in make.centerY.equalToSuperview() make.leading.equalTo(titleLabel.snp.trailing).offset(4) make.size.equalTo(20) if isExccedingWidth { make.trailing.equalToSuperview().offset(-36) } else { make.trailing.equalToSuperview() } } titleLoadingOrgView.snp.updateConstraints { make in make.leading.equalTo(arrowImageView.snp.trailing).offset(12) make.centerY.equalToSuperview() make.size.equalTo(24) } } private func bindActions() { let tap = UITapGestureRecognizer(target: self, action: #selector(didClickTitleView)) self.isUserInteractionEnabled = true self.addGestureRecognizer(tap) } @objc private func didClickTitleView() { didClickCallback?(isInExpandStatus) } func changeToStatus(expand: Bool) { isInExpandStatus = expand rotateArrow(expand: expand) } private func rotateArrow(expand: Bool) { let rotation = CABasicAnimation(keyPath: "transform.rotation.z") rotation.duration = 0.3 rotation.fillMode = .forwards rotation.isRemovedOnCompletion = false rotation.fromValue = expand ? 0 : -Double.pi rotation.toValue = expand ? -Double.pi : 0 arrowImageView.layer.add(rotation, forKey: "rotateArrow") arrowImageView.transform = CGAffineTransform(rotationAngle: expand ? -CGFloat(Double.pi) : 0) } func showLoadingAnimation() { isAnimating = true titleLoadingOrgView.isHidden = false titleLabel.isHidden = true arrowImageView.isHidden = true } func hideLoadingAnimation() { isAnimating = false titleLoadingOrgView.isHidden = true titleLabel.isHidden = false arrowImageView.isHidden = false } } // // DeviceListNewViewController.swift // SurveillanceHome // // Created by MaCong on 2025/12/3. // Copyright © 2025 tplink. All rights reserved. // import UIKit import SnapKit // MARK: - DeviceListNewViewController class DeviceListNewViewController: SurveillanceCommonTableController, SelectOrganizationViewDelegate { // MARK: - UI Components private lazy var titleView: NewTitleView = { let view = NewTitleView() view.titleText = "设备列表" view.didClickCallback = { [weak self] _ in guard let self = self else { return } let selectOrgView = self.selectOrganizationView let titleView = self.titleView if titleView.isAnimating { return } if !titleView.isInExpandStatus { titleView.isAnimating = true self.view.addSubview(selectOrgView) selectOrgView.show(animated: true) { titleView.isAnimating = false } } else { titleView.isAnimating = true self.hideLoadingView() selectOrgView.dismiss(animated: true) { titleView.isAnimating = false } } titleView.changeToStatus(expand: !titleView.isInExpandStatus) } return view }() private lazy var multiScreenButton: UIButton = { let btn = UIButton(type: .custom) btn.setImage(TPImageLiteral("media_player_switch_multi_live"), for: .normal) btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) btn.addTarget(self, action: #selector(clickMultileLive), for: .touchUpInside) return btn }() // 当前筛选条件 var selectedTabType: DeviceListMasterSelectedType = .all { didSet { UserDefaults.standard.deviceListSelectedType = selectedTabType forceRefreshDeviceList() } } var selectedSiteInfo: TPSSVMSSubsiteInfo? { didSet { saveSelectedSiteInfo(selectedSiteInfo) forceRefreshDeviceList() } } private func saveSelectedSiteInfo(_ info: TPSSVMSSubsiteInfo?) { guard let siteInfo = info else { try? FileManager.default.removeItem(at: URL(fileURLWithPath: DeviceListMasterViewController.getDocumentsPath(path: DeviceListMasterViewController.kSelectedSiteFileName) ?? "")) return } do { let data = try NSKeyedArchiver.archivedData(withRootObject: siteInfo, requiringSecureCoding: true) try data.write(to: URL(fileURLWithPath: DeviceListMasterViewController.getDocumentsPath(path: DeviceListMasterViewController.kSelectedSiteFileName) ?? "")) } catch { print(error) } } private lazy var selectOrganizationView: SelectOrganizationView = { let view = SelectOrganizationView() view.rootViewController = self view.delegate = self view.frame = CGRect( x: 0, y: SelectOrganizationView.statusBarHeight + SelectOrganizationView.navigationBarHeight, width: screenWidth, height: screenHeight - SelectOrganizationView.statusBarHeight - SelectOrganizationView.navigationBarHeight ) return view }() // MARK: - Data & View private var devices: [TPSSDeviceForDeviceList] = [] { didSet { deviceListView.devices = devices } } private lazy var deviceListView: DeviceListView = { let view = DeviceListView() view.onMoreButtonTap = { [weak self] device, channel in self?.showDeviceMenu(for: device, channel: channel) } return view }() // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() setNavigation() tableView.contentInsetAdjustmentBehavior = .automatic reloadData() loadDevicesFromContext() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(false, animated: false) } // MARK: - Layout Setup override func tpbSetupSubviews() { super.tpbSetupSubviews() } override func tpbMakeConstraint() { tableView.snp.remakeConstraints { make in make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) make.leading.trailing.equalToSuperview() make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom) } } // MARK: - Navigation Bar private func setNavigation() { navigationItem.titleView = titleView titleView.snp.makeConstraints { make in make.width.lessThanOrEqualTo(200) make.height.equalTo(44) } let backButtonItem = self.tpbCreateLeftBarButtonItem(with: TPImageLiteral("common_light_back_nor"), andTarget: self, andAction: #selector(jumpToXXX)) let centralButtonItem = self.tpbCreateLeftBarButtonItem(with: TPImageLiteral("central_surveillance_button"), andTarget: self, andAction: #selector(centralButtonClicked)) let messageButtonItem = self.tpbCreateLeftBarButtonItem(with: TPImageLiteral("tabbar_message_nor"), andTarget: self, andAction: #selector(onMessageButtonTapped)) navigationItem.leftBarButtonItems = [backButtonItem, centralButtonItem] navigationItem.rightBarButtonItem = messageButtonItem } // MARK: - Mock Sections (示例组件) private func createDeviceCountView() -> UIView { let containerView = UIView() containerView.backgroundColor = .tpbCard containerView.layer.cornerRadius = 4 containerView.clipsToBounds = true let labels = ["NVR", "4K", "2K", "HD"] var categoryViews: [UIView] = [] for text in labels { let categoryView = UIView() categoryView.backgroundColor = UIColor(white: 0.93, alpha: 1.0) categoryView.layer.cornerRadius = 8 categoryView.clipsToBounds = true let label = UILabel() label.text = text label.textColor = UIColor.tpbTextPrimary label.font = UIFont.systemFont(ofSize: 15, weight: .semibold) label.textAlignment = .center categoryView.addSubview(label) label.snp.makeConstraints { make in make.edges.equalToSuperview().inset(10) } containerView.addSubview(categoryView) categoryViews.append(categoryView) } for (index, view) in categoryViews.enumerated() { view.snp.makeConstraints { make in make.height.equalTo(60) make.centerY.equalTo(containerView) if index == 0 { make.leading.equalTo(containerView).offset(20) } else { make.leading.equalTo(categoryViews[index - 1].snp.trailing).offset(16) make.width.equalTo(categoryViews[0]) } if index == labels.count - 1 { make.trailing.equalTo(containerView).offset(-20) } } } return containerView } private func createStorageUsageView() -> UIView { let containerView = UIView() containerView.backgroundColor = .tpbCard containerView.layer.cornerRadius = 8 containerView.clipsToBounds = true let titleLabel = UILabel() titleLabel.text = "存储空间" titleLabel.font = UIFont.systemFont(ofSize: 14, weight: .medium) titleLabel.textColor = .tpbTextPrimary containerView.addSubview(titleLabel) let progressView = UIProgressView(progressViewStyle: .default) progressView.progressTintColor = UIColor.systemBlue progressView.trackTintColor = UIColor(white: 0.9, alpha: 1.0) containerView.addSubview(progressView) let detailLabel = UILabel() detailLabel.font = UIFont.systemFont(ofSize: 13, weight: .regular) detailLabel.textColor = .tpbTextPrimary detailLabel.textAlignment = .center containerView.addSubview(detailLabel) let used = 1.2, total = 5.0 let percent = Float(used / total) progressView.setProgress(percent, animated: false) let percentage = Int(percent * 100) detailLabel.text = String(format: "%.1f TB / %.1f TB (%d%%)", used, total, percentage) titleLabel.snp.makeConstraints { make in make.top.leading.equalTo(containerView).offset(16) } progressView.snp.makeConstraints { make in make.centerX.centerY.equalTo(containerView) make.width.equalTo(200) make.height.equalTo(6) } detailLabel.snp.makeConstraints { make in make.top.equalTo(progressView.snp.bottom).offset(8) make.centerX.equalTo(progressView) } return containerView } // MARK: - Table Cell Factory private func createDeviceListCell() -> TPBBaseTableCellModel { let cellModel = TPBBaseTableCellModel.customContent(with: deviceListView) cellModel.height = TPBTableElementHeight.automatic() return cellModel } private func createMultiScreenHeader() -> UIView { let headerView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 44)) headerView.backgroundColor = .clear let container = TPBBaseView() container.backgroundColor = .clear container.layer.cornerRadius = 3 container.clipsToBounds = true headerView.addSubview(container) container.snp.makeConstraints { make in make.trailing.equalTo(headerView).offset(-16) make.centerY.equalTo(headerView) make.size.equalTo(CGSize(width: 44, height: 44)) } multiScreenButton.removeFromSuperview() container.addSubview(multiScreenButton) multiScreenButton.snp.makeConstraints { make in make.edges.equalToSuperview() } return headerView } private func createMergedDeviceSection() -> TPBTableSectionModel { let section = TPBTableSectionModel() section.customHeaderView = createMultiScreenHeader() section.sectionHeaderHeight = TPBTableElementHeight.customHeight(44) section.cellModelArray = [createDeviceListCell()] return section } // MARK: - Reload Data private func reloadData() { var sections = [TPBTableSectionModel]() let section0 = TPBTableSectionModel() let deviceCountView = createDeviceCountView() let deviceCountCell = TPBBaseTableCellModel.customContent(with: deviceCountView) deviceCountCell.height = TPBTableElementHeight.customHeight(100) section0.cellModelArray = [deviceCountCell] sections.append(section0) let section1 = TPBTableSectionModel() let storageView = createStorageUsageView() let storageCell = TPBBaseTableCellModel.customContent(with: storageView) storageCell.height = TPBTableElementHeight.customHeight(100) section1.cellModelArray = [storageCell] sections.append(section1) let section2 = createMergedDeviceSection() sections.append(section2) sectionArray = sections tableView.reloadData() } // MARK: - Actions @objc private func clickMultileLive() { print("多屏播放按钮被点击") } @objc private func jumpToXXX() { print("返回上一页") } @objc private func centralButtonClicked() { let vc = CentralJumpViewController() let navi = BaseNavigationController(rootViewController: vc) navi.view.frame = self.view.frame vc.currentSiteId = selectedTabType == .all ? nil : selectedSiteInfo?.siteId vc.maskDidClickBlock = { navi.view.removeFromSuperview() } UIApplication.shared.keyWindow?.addSubview(navi.view) } @objc private func onMessageButtonTapped() { print("消息中心被点击") } // MARK: - Loading & Org Selection func hideLoadingView() { titleView.hideLoadingAnimation() } func didSelectOrganization(_ organization: OrganizationModel) { titleView.titleText = organization.name hideLoadingView() reloadData() } func didCancelSelectOrganization() {} // MARK: - Load Devices private func loadDevicesFromContext() { DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } let allDevices = TPAppContextFactory.shared().devices(inDeviceGroup: "default", includingHiddenChannels: false) let filtered = allDevices.filter { $0.listType == .remote && $0.displayOnline } DispatchQueue.main.async { self.devices = filtered } } } private func forceRefreshDeviceList() { loadDevicesFromContext() } // MARK: - Show Menu private func showDeviceMenu(for device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo?) { let menu = DeviceListMenuView(frame: .zero) DeviceListMenuView.configure(device: device, channel: channel) // menu.action = { [weak self] (item, dev, chn) in // switch item { // case .setting: // self?.jumpToDeviceSetting(for: dev, channel: chn) // case .alarmMode: // self?.toggleNotification(for: dev, channel: chn) // case .upgrade: // self?.startFirmwareUpgrade(for: dev, channel: chn) // case .unbind: // self?.confirmUnbindDevice(for: dev, channel: chn) // case .collect: // self?.toggleFavoriteStatus(for: dev, channel: chn) // @unknown default: // break // } // } _ = presentGuideWith( viewToPresent: menu, size: CGSize(width: 200, height: menu.items.preferredheight), source: multiScreenButton ) } // // MARK: - Menu Actions // private func toggleNotification(for device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo?) { // let enable = !(channel?.messageEnable ?? device.messageEnable) // let requestID = TPAppContextFactory.shared().requestSetMessagePushEnabled( // enable, // forDevice: device.identifier, // channel: channel?.channelId.intValue ?? -1, // of: device.listType // ) // if requestID < TPSS_EC_OK { // ToastView.showWarningToast(title: TPAppContextFactory.shared().errorMessage(for: requestID)) // } else { // ToastView.showLoadingToast(cirleWithMessage: nil) // } // } // // private func jumpToDeviceSetting(for device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo?) { // let vc = DeviceSettingViewController(device: device, channel: channel) // navigationController?.pushViewController(vc, animated: true) // } // // private func startFirmwareUpgrade(for device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo?) { // let vc = FirmwareUpgradeViewController(device: device, channel: channel) // navigationController?.pushViewController(vc, animated: true) // } // // private func confirmUnbindDevice(for device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo?) { // let alert = UIAlertController(title: "解绑设备", message: "确定要解绑此设备吗?", preferredStyle: .alert) // alert.addAction(.init(title: "取消", style: .cancel)) // alert.addAction(.init(title: "确定", style: .destructive) { _ in // UnbindService.unbind(device: device, channel: channel) // }) // present(alert, animated: true) // } // // private func toggleFavoriteStatus(for device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo?) { // let isVms = TPAppContextFactory.shared().isVmsLogin // let isCollected = isVms ? (channel?.isVMSFavorited ?? device.isVMSFavorited) : device.isCollected // // FavoriteService.updateFavoriteStatus(of: device, channel: channel, isFavorite: !isCollected) { [weak self] success in // if success { // DispatchQueue.main.async { // ToastView.showTopWarningWithLeftIconToast(title: !isCollected ? "已收藏" : "已取消收藏", iconName: "device_collect_success") // } // } // } // } } 其中DeviceListOrganizationModel、titleview部分可以不做修改,不影响设备展示
最新发布
12-06
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值