513 lines
12 KiB
Vue
513 lines
12 KiB
Vue
<template>
|
|
<div class="task-production-detail">
|
|
<div
|
|
class="task-production-detail-header flex items-center justify-between"
|
|
:class="[isMobile ? 'p-2 pt-[safe-area-inset-top] pt-[44px]' : 'p-4']"
|
|
>
|
|
<p
|
|
class="text-white font-bold one-ell"
|
|
:class="[isMobile ? 'mr-1 text-sm' : 'text-2xl ml-4']"
|
|
>
|
|
作品集
|
|
</p>
|
|
<div
|
|
class="flex items-center gap-2 bg-[#b3d5ff] rounded-lg flex-1"
|
|
:class="[isMobile ? 'w-full' : 'max-w-[300px]']"
|
|
>
|
|
<el-input
|
|
v-model="searchForm.title"
|
|
placeholder="请输入作品标题搜索对应作品"
|
|
clearable
|
|
@input="debouncedSearch"
|
|
/>
|
|
<el-icon
|
|
@click="search"
|
|
class="text-[#0074FE] font-bold"
|
|
:class="[isMobile ? 'mr-2 text-lg' : 'mr-4 ml-2 text-2xl']"
|
|
><Search
|
|
/></el-icon>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
ref="scrollContainer"
|
|
class="card-scroll-container"
|
|
:class="[isMobile ? '' : 'px-4']"
|
|
v-show="list && list.length"
|
|
>
|
|
<Waterfall
|
|
:key="waterfallKey"
|
|
:list="list"
|
|
:type="currentTaskType"
|
|
imgSelector="fileUrl"
|
|
show-loading
|
|
lazyload
|
|
:breakpoints="waterfallBreakpoints"
|
|
animation-effect="fadeInUp"
|
|
>
|
|
<template #default="{ item, url }">
|
|
<div
|
|
class="border rounded-md box-shadow-md rounded-xl overflow-hidden transition-all duration-300 card-item"
|
|
style="box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08)"
|
|
@click="handleCardClick(item)"
|
|
>
|
|
<img
|
|
:src="url"
|
|
class="w-full object-cover"
|
|
loading="lazy"
|
|
:style="
|
|
item.width && item.height
|
|
? {
|
|
height: calculateHeight(item.width, item.height) + 'px',
|
|
}
|
|
: {}
|
|
"
|
|
/>
|
|
<div
|
|
class="flex gap-2 items-center justify-start flex-wrap"
|
|
:class="[isMobile ? 'p-2' : 'p-4']"
|
|
>
|
|
<el-tag
|
|
v-for="tag in item.tags.split(',')"
|
|
:key="tag"
|
|
size="small"
|
|
plain
|
|
round
|
|
type="info"
|
|
>{{ tag }}</el-tag
|
|
>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Waterfall>
|
|
<div class="loading-tip">
|
|
<span v-if="isLoading">加载中...</span>
|
|
<span v-else-if="!hasMore">没有更多了</span>
|
|
</div>
|
|
</div>
|
|
<el-empty
|
|
v-show="!list || !list.length"
|
|
style="height: calc(100vh - 80px)"
|
|
description="没有更多了"
|
|
/>
|
|
</div>
|
|
|
|
<!-- PC端抽屉 -->
|
|
<el-drawer
|
|
v-if="!isMobile"
|
|
v-model="visible"
|
|
:size="500"
|
|
@close="handleClose"
|
|
fullscreen
|
|
>
|
|
<div v-if="currentItem" class="detail-content">
|
|
<img
|
|
v-if="[MediaType.IMAGE, MediaType.IMAGE_TEXT].includes(currentTaskType)"
|
|
:src="currentItem.fileUrl"
|
|
@error="handleImageError"
|
|
/>
|
|
<video
|
|
v-if="[MediaType.VIDEO].includes(currentTaskType)"
|
|
:src="currentItem.fileUrl"
|
|
controls
|
|
/>
|
|
<p
|
|
v-if="[MediaType.IMAGE_TEXT, MediaType.TEXT].includes(currentTaskType)"
|
|
class="text-md mt-4 align-left"
|
|
>
|
|
<el-button
|
|
type="text"
|
|
@click.stop="handleCopy(currentItem.content)"
|
|
class="!p-0"
|
|
size="small"
|
|
>
|
|
复制文案
|
|
</el-button>
|
|
{{ currentItem.content }}
|
|
</p>
|
|
</div>
|
|
<template #footer>
|
|
<el-button type="default" plain @click="handleClose">关闭</el-button>
|
|
<!-- 全环境通用保存按钮 -->
|
|
<el-button type="primary" @click="handleSave" :loading="saving">
|
|
保存
|
|
</el-button>
|
|
</template>
|
|
</el-drawer>
|
|
|
|
<!-- 移动端弹窗 -->
|
|
<!-- <el-dialog
|
|
v-if="isMobile"
|
|
v-model="visible"
|
|
:size="500"
|
|
@close="handleClose"
|
|
fullscreen
|
|
> -->
|
|
<div
|
|
v-if="isMobile && visible"
|
|
class="fixed top-0 left-0 w-full h-full bg-black bg-black/75 z-10"
|
|
@click="handleClose"
|
|
>
|
|
<img
|
|
@click.stop
|
|
:src="currentItem.fileUrl"
|
|
class="w-full h-full object-contain rounded-md mx-auto"
|
|
/>
|
|
<div
|
|
@click.stop
|
|
class="fixed bottom-3 left-0 w-full p-4 flex justify-end items-center gap-2 z-11 "
|
|
>
|
|
<el-button type="info" plain :icon="Close" @click="handleClose"
|
|
>关闭</el-button
|
|
>
|
|
<!-- circle -->
|
|
<!-- 全环境通用保存按钮 -->
|
|
<el-button
|
|
type="primary"
|
|
@click="handleSave"
|
|
:icon="Download"
|
|
color="#0074FE"
|
|
>
|
|
保存
|
|
</el-button>
|
|
</div>
|
|
<!-- </el-dialog> -->
|
|
</div>
|
|
|
|
<el-button
|
|
v-show="showScrollToTop"
|
|
type="primary"
|
|
@click="scrollToTop"
|
|
class="fixed bottom-4 right-4 z-10 transition-all duration-300"
|
|
:icon="ArrowUp"
|
|
circle
|
|
></el-button>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onBeforeUnmount, nextTick, computed } from "vue";
|
|
import { useRoute } from "vue-router";
|
|
import { debounce, throttle } from "lodash-es";
|
|
import { showLoading, hiddenLoading } from "@/utils/loading";
|
|
import { showMsg } from "@/utils/showMsg";
|
|
import { Search, ArrowUp, Close, Download } from "@element-plus/icons-vue";
|
|
import apis from "@/api/index";
|
|
import {
|
|
DictOption,
|
|
TaskInfo,
|
|
WaterfallItem,
|
|
WaterfallProps,
|
|
} from "@/typings/work";
|
|
import defaultImg from "@/assets/images/img-load-err.png";
|
|
import fallbackCopyToClipboard from "@/utils/copyText";
|
|
import { Waterfall } from "vue-waterfall-plugin-next";
|
|
import "vue-waterfall-plugin-next/dist/style.css";
|
|
// 引入通用保存工具
|
|
import { saveFile, FileType } from "@/utils/saveFile";
|
|
|
|
enum MediaType {
|
|
IMAGE = 1,
|
|
IMAGE_TEXT = 2,
|
|
VIDEO = 3,
|
|
TEXT = 4,
|
|
}
|
|
|
|
const waterfallKey = ref(0);
|
|
const waterfallBreakpoints = {
|
|
600: { rowPerView: 2 },
|
|
320: { rowPerView: 2 },
|
|
};
|
|
|
|
const searchForm = ref({
|
|
title: "",
|
|
});
|
|
|
|
const search = async () => {
|
|
currentPage.value = 1;
|
|
hasMore.value = true;
|
|
scrollContainer.value?.scrollTo({ top: 0 });
|
|
await getTaskProductionListLimit();
|
|
};
|
|
|
|
const debouncedSearch = debounce(search, 300);
|
|
const { WOEKS_LIST_TWO } = apis;
|
|
const route = useRoute();
|
|
const taskId = route.params.taskId as string;
|
|
|
|
const taskTypes = ref<DictOption[]>(
|
|
JSON.parse(sessionStorage.getItem("task_type") || "[]")
|
|
);
|
|
|
|
const taskInfo = ref<TaskInfo>(
|
|
JSON.parse(sessionStorage.getItem("currentTask") || "{}")
|
|
);
|
|
|
|
const currentTaskType = computed(() => Number(taskInfo.value.type || 1));
|
|
const list = ref<WaterfallItem[]>([]);
|
|
const visible = ref(false);
|
|
const currentItem = ref<WaterfallItem | null>(null);
|
|
const currentPage = ref(1);
|
|
const pageSize = computed(() => (isMobile.value ? 20 : 50));
|
|
const isLoading = ref(false);
|
|
const hasMore = ref(true);
|
|
const cardDom = computed(
|
|
() => document.getElementsByClassName("waterfall-item")[0]
|
|
);
|
|
const cardDomWidth = computed(() => cardDom.value?.offsetWidth ?? 200);
|
|
|
|
const calculateHeight = (width: number, height: number) => {
|
|
return (cardDomWidth.value * height) / width || 356;
|
|
};
|
|
|
|
const scrollContainer = ref<HTMLDivElement | null>(null);
|
|
const showScrollToTop = ref(false);
|
|
const saving = ref(false);
|
|
const isFirstLoad = ref(true);
|
|
|
|
const scrollToTop = () => {
|
|
scrollContainer.value?.scrollTo({
|
|
top: 0,
|
|
behavior: "smooth",
|
|
});
|
|
};
|
|
|
|
const refresh = () => {
|
|
currentPage.value = 1;
|
|
list.value = [];
|
|
hasMore.value = true;
|
|
getTaskProductionListLimit();
|
|
};
|
|
|
|
const handleImageError = (e: Event) => {
|
|
const target = e.target as HTMLImageElement;
|
|
target.src = defaultImg;
|
|
};
|
|
|
|
const getTaskProductionListLimit = async () => {
|
|
if (isLoading.value || !hasMore.value) return;
|
|
|
|
isLoading.value = true;
|
|
try {
|
|
currentPage.value == 1 && showLoading();
|
|
const res = await WOEKS_LIST_TWO({
|
|
current: currentPage.value,
|
|
size: pageSize.value,
|
|
...searchForm.value,
|
|
});
|
|
|
|
const { records = [] } = res || {};
|
|
const sizes = [
|
|
[9, 16],
|
|
[1, 1],
|
|
[16, 9],
|
|
[3, 4],
|
|
[4, 3],
|
|
[2, 3],
|
|
[3, 2],
|
|
];
|
|
|
|
records.forEach((item) => {
|
|
if (!item.width || !item.height) {
|
|
const size = sizes[Math.floor(Math.random() * sizes.length)];
|
|
item.width = size[0];
|
|
item.height = size[1];
|
|
}
|
|
});
|
|
|
|
if (currentPage.value == 1) {
|
|
list.value = records;
|
|
} else {
|
|
list.value = list.value.concat(records);
|
|
}
|
|
|
|
hasMore.value = !(records.length < pageSize.value);
|
|
} catch (error) {
|
|
console.error("列表加载失败:", error);
|
|
currentPage.value = Math.max(1, currentPage.value - 1);
|
|
} finally {
|
|
isLoading.value = false;
|
|
if (currentPage.value == 1 && !isFirstLoad.value) {
|
|
hiddenLoading();
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleScroll = throttle(() => {
|
|
const container = scrollContainer.value;
|
|
if (!container || isLoading.value || !hasMore.value) return;
|
|
|
|
const { scrollHeight, scrollTop, clientHeight } = container;
|
|
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
|
currentPage.value++;
|
|
getTaskProductionListLimit();
|
|
}
|
|
|
|
showScrollToTop.value = scrollTop > 1000;
|
|
}, 100);
|
|
|
|
const isMobile = computed(() => {
|
|
return navigator.userAgent.includes("Mobile");
|
|
});
|
|
|
|
const handleCardClick = (item: WaterfallItem) => {
|
|
visible.value = true;
|
|
currentItem.value = item;
|
|
};
|
|
|
|
const handleClose = () => {
|
|
visible.value = false;
|
|
};
|
|
|
|
const handleCopy = (text: string) => {
|
|
fallbackCopyToClipboard(text);
|
|
showMsg("复制成功", "success");
|
|
};
|
|
|
|
// ==================== 简化后的保存方法(调用通用工具) ====================
|
|
const handleSave = async () => {
|
|
if (!currentItem.value) {
|
|
showMsg("请先选择要保存的作品");
|
|
return;
|
|
}
|
|
|
|
if (saving.value) return;
|
|
saving.value = true;
|
|
|
|
try {
|
|
const item = currentItem.value;
|
|
// 生成安全的文件名(去除非法字符)
|
|
const safeTitle =
|
|
item.title?.replace(/[<>:"/\\|?*]/g, "_") || `work_${item.id}`;
|
|
|
|
// 根据作品类型调用通用保存工具
|
|
switch (currentTaskType.value) {
|
|
case MediaType.IMAGE:
|
|
await saveFile({
|
|
type: FileType.IMAGE,
|
|
url: item.fileUrl,
|
|
fileName: safeTitle,
|
|
});
|
|
break;
|
|
case MediaType.IMAGE_TEXT:
|
|
await saveFile({
|
|
type: FileType.IMAGE,
|
|
url: item.fileUrl,
|
|
fileName: safeTitle,
|
|
});
|
|
break;
|
|
case MediaType.VIDEO:
|
|
await saveFile({
|
|
type: FileType.VIDEO,
|
|
url: item.fileUrl,
|
|
fileName: safeTitle,
|
|
});
|
|
break;
|
|
case MediaType.TEXT:
|
|
await saveFile({
|
|
type: FileType.TEXT,
|
|
content: item.content,
|
|
fileName: safeTitle,
|
|
});
|
|
break;
|
|
default:
|
|
showMsg("不支持的文件类型");
|
|
}
|
|
if (!isMobile.value) {
|
|
showMsg("保存成功", "success");
|
|
}
|
|
// handleClose();
|
|
} catch (error) {
|
|
console.error("保存失败:", error);
|
|
showMsg(error.message || "保存失败,请稍后重试", "error");
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
};
|
|
|
|
// ==================== 生命周期 ====================
|
|
onMounted(async () => {
|
|
currentPage.value = 1;
|
|
hasMore.value = true;
|
|
await getTaskProductionListLimit();
|
|
|
|
await nextTick();
|
|
const container = scrollContainer.value;
|
|
if (container) {
|
|
container.addEventListener("scroll", handleScroll);
|
|
}
|
|
|
|
setTimeout(() => {
|
|
isFirstLoad.value = false;
|
|
waterfallKey.value++;
|
|
hiddenLoading();
|
|
}, 500);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
const container = scrollContainer.value;
|
|
if (container) {
|
|
container.removeEventListener("scroll", handleScroll);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.task-production-detail {
|
|
max-height: 100vh;
|
|
overflow: hidden;
|
|
|
|
.task-production-detail-header {
|
|
overflow-y: auto;
|
|
background-color: #0072fed6;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
|
> div {
|
|
// max-width: 300px;
|
|
:deep(.el-input) {
|
|
flex: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
.card-scroll-container {
|
|
max-height: calc(100vh - 60px);
|
|
overflow-y: auto;
|
|
}
|
|
}
|
|
|
|
.loading-tip {
|
|
text-align: center;
|
|
padding: 0 0 20px;
|
|
font-size: 14px;
|
|
color: #999;
|
|
}
|
|
|
|
.detail-content {
|
|
img,
|
|
video {
|
|
width: 100%;
|
|
height: auto;
|
|
display: block;
|
|
object-fit: contain;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
}
|
|
|
|
:deep(.el-drawer__title) {
|
|
margin-bottom: 0 !important;
|
|
}
|
|
|
|
.card-item {
|
|
&:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
img {
|
|
transition: transform 0.3s ease;
|
|
&:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
}
|
|
}
|
|
</style> |