|
|
@@ -0,0 +1,405 @@
|
|
|
+<template>
|
|
|
+ <view class="webtopo-page">
|
|
|
+ <view class="search-panel">
|
|
|
+ <view class="search-box">
|
|
|
+ <uni-icons type="search" size="18" color="#94a3b8" />
|
|
|
+ <input
|
|
|
+ v-model.trim="projectName"
|
|
|
+ class="search-input"
|
|
|
+ confirm-type="search"
|
|
|
+ placeholder="请输入组态名称"
|
|
|
+ @confirm="search"
|
|
|
+ />
|
|
|
+ <uni-icons
|
|
|
+ v-if="projectName"
|
|
|
+ type="clear"
|
|
|
+ size="18"
|
|
|
+ color="#94a3b8"
|
|
|
+ @click="clearSearch"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ <button class="search-button" @click="search">查询</button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="list-header">
|
|
|
+ <view>
|
|
|
+ <view class="list-title">组态项目</view>
|
|
|
+ <view class="list-subtitle">共 {{ total }} 个项目</view>
|
|
|
+ </view>
|
|
|
+ <view class="refresh-button" @click="refreshList">
|
|
|
+ <uni-icons type="refreshempty" size="18" color="#2563eb" />
|
|
|
+ <text>刷新</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="loading" class="state-panel">
|
|
|
+ <uni-load-more status="loading" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-else-if="!list.length" class="state-panel empty-panel">
|
|
|
+ <view class="empty-icon">
|
|
|
+ <uni-icons type="image" size="34" color="#94a3b8" />
|
|
|
+ </view>
|
|
|
+ <view class="empty-title">暂无组态项目</view>
|
|
|
+ <view class="empty-description">请尝试调整搜索条件后重新查询</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-else class="project-list">
|
|
|
+ <view
|
|
|
+ v-for="item in list"
|
|
|
+ :key="item.id"
|
|
|
+ class="project-card"
|
|
|
+ @click="openPreview(item)"
|
|
|
+ >
|
|
|
+ <view class="thumbnail-wrap">
|
|
|
+ <image
|
|
|
+ v-if="item.thumbnail && !thumbnailErrors[item.id]"
|
|
|
+ class="thumbnail"
|
|
|
+ :src="item.thumbnail"
|
|
|
+ mode="aspectFill"
|
|
|
+ @error="markThumbnailError(item.id)"
|
|
|
+ />
|
|
|
+ <view v-else class="thumbnail-placeholder">
|
|
|
+ <uni-icons type="image" size="38" color="#94a3b8" />
|
|
|
+ <text>暂无缩略图</text>
|
|
|
+ </view>
|
|
|
+ <view class="device-badge">
|
|
|
+ <uni-icons type="gear" size="14" color="#ffffff" />
|
|
|
+ <text>{{ getLinkedDeviceCount(item) }} 台设备</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="project-content">
|
|
|
+ <view class="project-name">{{
|
|
|
+ item.projectName || "未命名组态"
|
|
|
+ }}</view>
|
|
|
+ <view class="project-meta">
|
|
|
+ <uni-icons type="calendar" size="15" color="#94a3b8" />
|
|
|
+ <text>{{ formatCreateTime(item.createTime) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="project-remark">{{ item.remark || "暂无备注" }}</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="card-footer">
|
|
|
+ <text>查看组态</text>
|
|
|
+ <uni-icons type="right" size="16" color="#2563eb" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <uni-load-more
|
|
|
+ v-if="list.length"
|
|
|
+ :status="loadStatus"
|
|
|
+ :content-text="loadText"
|
|
|
+ @clickLoadMore="loadMore"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { computed, reactive, ref } from "vue";
|
|
|
+import { onLoad, onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app";
|
|
|
+import dayjs from "dayjs";
|
|
|
+import { getWebtopoProjectPage } from "@/api/webtopo";
|
|
|
+import { getDeptId } from "@/utils/auth";
|
|
|
+
|
|
|
+const projectName = ref("");
|
|
|
+const list = ref([]);
|
|
|
+const total = ref(0);
|
|
|
+const pageNo = ref(1);
|
|
|
+const pageSize = 10;
|
|
|
+const loading = ref(false);
|
|
|
+const loadingMore = ref(false);
|
|
|
+const thumbnailErrors = reactive({});
|
|
|
+
|
|
|
+const hasMore = computed(() => list.value.length < total.value);
|
|
|
+const loadStatus = computed(() =>
|
|
|
+ loadingMore.value ? "loading" : hasMore.value ? "more" : "noMore",
|
|
|
+);
|
|
|
+const loadText = {
|
|
|
+ contentdown: "上拉加载更多",
|
|
|
+ contentrefresh: "加载中",
|
|
|
+ contentnomore: "没有更多数据了",
|
|
|
+};
|
|
|
+
|
|
|
+const fetchList = async (append = false) => {
|
|
|
+ append ? (loadingMore.value = true) : (loading.value = true);
|
|
|
+ try {
|
|
|
+ const response = await getWebtopoProjectPage({
|
|
|
+ pageNo: pageNo.value,
|
|
|
+ pageSize,
|
|
|
+ deptId: getDeptId() || undefined,
|
|
|
+ projectName: projectName.value || undefined,
|
|
|
+ });
|
|
|
+ const pageData = response?.data;
|
|
|
+ const rows = Array.isArray(pageData) ? pageData : pageData?.list || [];
|
|
|
+ list.value = append ? [...list.value, ...rows] : rows;
|
|
|
+ total.value = Array.isArray(pageData)
|
|
|
+ ? pageData.length
|
|
|
+ : Number(pageData?.total || 0);
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ loadingMore.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const search = () => {
|
|
|
+ pageNo.value = 1;
|
|
|
+ fetchList();
|
|
|
+};
|
|
|
+
|
|
|
+const clearSearch = () => {
|
|
|
+ projectName.value = "";
|
|
|
+ search();
|
|
|
+};
|
|
|
+
|
|
|
+const refreshList = async () => {
|
|
|
+ pageNo.value = 1;
|
|
|
+ await fetchList();
|
|
|
+};
|
|
|
+
|
|
|
+const loadMore = () => {
|
|
|
+ if (!hasMore.value || loadingMore.value) return;
|
|
|
+ pageNo.value += 1;
|
|
|
+ fetchList(true);
|
|
|
+};
|
|
|
+
|
|
|
+const getLinkedDeviceCount = (item) =>
|
|
|
+ item.linkedDeviceIds?.length || item.linkedDevices?.length || 0;
|
|
|
+
|
|
|
+const formatCreateTime = (value) => {
|
|
|
+ if (!value) return "创建时间未知";
|
|
|
+ const time =
|
|
|
+ typeof value === "number" && String(value).length === 10
|
|
|
+ ? value * 1000
|
|
|
+ : value;
|
|
|
+ return dayjs(time).format("YYYY-MM-DD HH:mm");
|
|
|
+};
|
|
|
+
|
|
|
+const markThumbnailError = (id) => {
|
|
|
+ thumbnailErrors[id] = true;
|
|
|
+};
|
|
|
+
|
|
|
+const openPreview = (item) => {
|
|
|
+ if (!item?.id) return;
|
|
|
+ uni.navigateTo({
|
|
|
+ url: `/pages/zutai/preview?id=${encodeURIComponent(item.id)}&name=${encodeURIComponent(
|
|
|
+ item.projectName || "组态预览",
|
|
|
+ )}`,
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+onLoad(() => fetchList());
|
|
|
+onReachBottom(loadMore);
|
|
|
+onPullDownRefresh(async () => {
|
|
|
+ await refreshList();
|
|
|
+ uni.stopPullDownRefresh();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.webtopo-page {
|
|
|
+ min-height: 100vh;
|
|
|
+ padding: 24rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ background: #f4f7fb;
|
|
|
+}
|
|
|
+
|
|
|
+.search-panel {
|
|
|
+ display: flex;
|
|
|
+ padding: 22rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ box-shadow: 0 8rpx 28rpx rgba(30, 64, 175, 0.05);
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.search-box {
|
|
|
+ display: flex;
|
|
|
+ height: 72rpx;
|
|
|
+ padding: 0 20rpx;
|
|
|
+ background: #f4f7fb;
|
|
|
+ border: 1rpx solid #e2e8f0;
|
|
|
+ border-radius: 14rpx;
|
|
|
+ flex: 1;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.search-input {
|
|
|
+ height: 72rpx;
|
|
|
+ margin: 0 14rpx;
|
|
|
+ color: #172033;
|
|
|
+ font-size: 28rpx;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.search-button {
|
|
|
+ width: 128rpx;
|
|
|
+ height: 72rpx;
|
|
|
+ margin: 0 0 0 16rpx;
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 28rpx;
|
|
|
+ line-height: 72rpx;
|
|
|
+ background: #2563eb;
|
|
|
+ border-radius: 14rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.list-header {
|
|
|
+ display: flex;
|
|
|
+ margin: 34rpx 6rpx 20rpx;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+
|
|
|
+.list-title {
|
|
|
+ color: #172033;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+
|
|
|
+.list-subtitle {
|
|
|
+ margin-top: 5rpx;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 23rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.refresh-button {
|
|
|
+ display: flex;
|
|
|
+ padding: 14rpx 20rpx;
|
|
|
+ color: #2563eb;
|
|
|
+ font-size: 25rpx;
|
|
|
+ background: #eaf2ff;
|
|
|
+ border-radius: 28rpx;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.state-panel {
|
|
|
+ padding: 120rpx 0;
|
|
|
+ color: #94a3b8;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-icon {
|
|
|
+ display: flex;
|
|
|
+ width: 104rpx;
|
|
|
+ height: 104rpx;
|
|
|
+ margin: 0 auto 24rpx;
|
|
|
+ background: #eaf2ff;
|
|
|
+ border-radius: 50%;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-title {
|
|
|
+ color: #475569;
|
|
|
+ font-size: 29rpx;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-description {
|
|
|
+ margin-top: 12rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.project-card {
|
|
|
+ overflow: hidden;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1rpx solid #edf2f7;
|
|
|
+ border-radius: 22rpx;
|
|
|
+ box-shadow: 0 9rpx 30rpx rgba(30, 64, 175, 0.06);
|
|
|
+}
|
|
|
+
|
|
|
+.project-card + .project-card {
|
|
|
+ margin-top: 22rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.thumbnail-wrap {
|
|
|
+ position: relative;
|
|
|
+ height: 340rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ background: linear-gradient(135deg, #eef3f8 0%, #e2e8f0 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.thumbnail {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.thumbnail-placeholder {
|
|
|
+ display: flex;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 24rpx;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.device-badge {
|
|
|
+ position: absolute;
|
|
|
+ top: 20rpx;
|
|
|
+ left: 20rpx;
|
|
|
+ display: flex;
|
|
|
+ padding: 10rpx 16rpx;
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 23rpx;
|
|
|
+ background: rgba(15, 23, 42, 0.7);
|
|
|
+ border-radius: 24rpx;
|
|
|
+ align-items: center;
|
|
|
+ gap: 7rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.project-content {
|
|
|
+ padding: 26rpx 26rpx 22rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.project-name {
|
|
|
+ overflow: hidden;
|
|
|
+ color: #172033;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.project-meta {
|
|
|
+ display: flex;
|
|
|
+ margin-top: 18rpx;
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 24rpx;
|
|
|
+ align-items: center;
|
|
|
+ gap: 9rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.project-remark {
|
|
|
+ display: -webkit-box;
|
|
|
+ overflow: hidden;
|
|
|
+ min-height: 68rpx;
|
|
|
+ margin-top: 17rpx;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 25rpx;
|
|
|
+ line-height: 34rpx;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
+}
|
|
|
+
|
|
|
+.card-footer {
|
|
|
+ display: flex;
|
|
|
+ padding: 20rpx 26rpx;
|
|
|
+ color: #2563eb;
|
|
|
+ font-size: 26rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ background: #f8fbff;
|
|
|
+ border-top: 1rpx solid #edf2f7;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 5rpx;
|
|
|
+}
|
|
|
+
|
|
|
+button::after {
|
|
|
+ border: 0;
|
|
|
+}
|
|
|
+</style>
|