add:新增保存功能
This commit is contained in:
@@ -137,6 +137,7 @@
|
|||||||
class="h-[80vh] object-contain rounded-md mx-auto"
|
class="h-[80vh] object-contain rounded-md mx-auto"
|
||||||
/>
|
/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<span class="text-sm text-gray-500">长按保存图片</span>
|
||||||
<el-button type="info" plain @click="handleClose">关闭</el-button>
|
<el-button type="info" plain @click="handleClose">关闭</el-button>
|
||||||
<!-- <el-button type="primary" @click="handleCopy(currentItem.content)">复制文案</el-button> -->
|
<!-- <el-button type="primary" @click="handleCopy(currentItem.content)">复制文案</el-button> -->
|
||||||
<!-- 保存 -->
|
<!-- 保存 -->
|
||||||
|
|||||||
+135
-250
@@ -1,9 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- <div v-if="taskInfo && taskTypes.length"> -->
|
|
||||||
<div class="task-production-detail">
|
<div class="task-production-detail">
|
||||||
<div
|
<div
|
||||||
class="task-production-detail-header flex items-center justify-between"
|
class="task-production-detail-header flex items-center justify-between"
|
||||||
:class="[isMobile ? 'p-2' : 'p-4']"
|
:class="[isMobile ? 'p-2 pt-[safe-area-inset-top] pt-[44px]' : 'p-4']"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
class="text-white font-bold one-ell"
|
class="text-white font-bold one-ell"
|
||||||
@@ -11,7 +10,10 @@
|
|||||||
>
|
>
|
||||||
作品集
|
作品集
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-2 bg-[#b3d5ff] rounded-lg flex-1">
|
<div
|
||||||
|
class="flex items-center gap-2 bg-[#b3d5ff] rounded-lg flex-1"
|
||||||
|
:class="[isMobile ? 'w-full' : 'max-w-[300px]']"
|
||||||
|
>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchForm.title"
|
v-model="searchForm.title"
|
||||||
placeholder="请输入作品标题搜索对应作品"
|
placeholder="请输入作品标题搜索对应作品"
|
||||||
@@ -26,7 +28,7 @@
|
|||||||
/></el-icon>
|
/></el-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 滚动容器 ref 绑定 -->
|
|
||||||
<div
|
<div
|
||||||
ref="scrollContainer"
|
ref="scrollContainer"
|
||||||
class="card-scroll-container"
|
class="card-scroll-container"
|
||||||
@@ -62,7 +64,7 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex gap-2 items-center justify-start"
|
class="flex gap-2 items-center justify-start flex-wrap"
|
||||||
:class="[isMobile ? 'p-2' : 'p-4']"
|
:class="[isMobile ? 'p-2' : 'p-4']"
|
||||||
>
|
>
|
||||||
<el-tag
|
<el-tag
|
||||||
@@ -78,7 +80,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Waterfall>
|
</Waterfall>
|
||||||
<!-- 加载状态提示 -->
|
|
||||||
<div class="loading-tip">
|
<div class="loading-tip">
|
||||||
<span v-if="isLoading">加载中...</span>
|
<span v-if="isLoading">加载中...</span>
|
||||||
<span v-else-if="!hasMore">没有更多了</span>
|
<span v-else-if="!hasMore">没有更多了</span>
|
||||||
@@ -91,6 +92,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- PC端抽屉 -->
|
||||||
<el-drawer
|
<el-drawer
|
||||||
v-if="!isMobile"
|
v-if="!isMobile"
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
@@ -125,30 +127,53 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button type="info" plain @click="handleClose">关闭</el-button>
|
<el-button type="default" plain @click="handleClose">关闭</el-button>
|
||||||
<!-- <el-button type="primary" @click="handleCopy(currentItem.content)">复制文案</el-button> -->
|
<!-- 全环境通用保存按钮 -->
|
||||||
<!-- 保存 -->
|
<el-button type="primary" @click="handleSave" :loading="saving">
|
||||||
<!-- <el-button type="primary" @click="handleSave">保存</el-button> -->
|
保存
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
<el-dialog
|
|
||||||
|
<!-- 移动端弹窗 -->
|
||||||
|
<!-- <el-dialog
|
||||||
v-if="isMobile"
|
v-if="isMobile"
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:size="500"
|
:size="500"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
fullscreen
|
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
|
<img
|
||||||
|
@click.stop
|
||||||
:src="currentItem.fileUrl"
|
:src="currentItem.fileUrl"
|
||||||
class="h-[80vh] object-contain rounded-md mx-auto"
|
class="w-full h-full object-contain rounded-md mx-auto"
|
||||||
/>
|
/>
|
||||||
<template #footer>
|
<div
|
||||||
<el-button type="info" plain @click="handleClose">关闭</el-button>
|
@click.stop
|
||||||
<!-- <el-button type="primary" @click="handleCopy(currentItem.content)">复制文案</el-button> -->
|
class="fixed bottom-3 left-0 w-full p-4 flex justify-end items-center gap-2 z-11 "
|
||||||
<!-- 保存 -->
|
>
|
||||||
<!-- <el-button type="primary" @click="handleSave">保存</el-button> -->
|
<el-button type="info" plain :icon="Close" @click="handleClose"
|
||||||
</template>
|
>关闭</el-button
|
||||||
</el-dialog>
|
>
|
||||||
|
<!-- circle -->
|
||||||
|
<!-- 全环境通用保存按钮 -->
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSave"
|
||||||
|
:icon="Download"
|
||||||
|
color="#0074FE"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<!-- </el-dialog> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
v-show="showScrollToTop"
|
v-show="showScrollToTop"
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -160,90 +185,67 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { ref, onMounted, onBeforeUnmount, nextTick, computed } from "vue";
|
||||||
ref,
|
|
||||||
reactive,
|
|
||||||
onMounted,
|
|
||||||
onBeforeUnmount,
|
|
||||||
nextTick,
|
|
||||||
computed,
|
|
||||||
} from "vue";
|
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { debounce, throttle } from "lodash-es"; // 记得安装lodash-es
|
import { debounce, throttle } from "lodash-es";
|
||||||
import { showLoading, hiddenLoading } from "@/utils/loading";
|
import { showLoading, hiddenLoading } from "@/utils/loading";
|
||||||
import { showMsg } from "@/utils/showMsg";
|
import { showMsg } from "@/utils/showMsg";
|
||||||
// Element Plus 图标 + 组件
|
import { Search, ArrowUp, Close, Download } from "@element-plus/icons-vue";
|
||||||
import { Refresh, ArrowLeft, Search, ArrowUp } from "@element-plus/icons-vue";
|
|
||||||
// 接口请求
|
|
||||||
import apis from "@/api/index";
|
import apis from "@/api/index";
|
||||||
// 子组件
|
|
||||||
import WaterfallFlow from "./waterfallFlow.vue";
|
|
||||||
// import DictTag from "@/components/DictTag/index.vue";
|
|
||||||
// 静态资源 & 工具函数
|
|
||||||
import {
|
import {
|
||||||
DictOption,
|
DictOption,
|
||||||
TaskInfo,
|
TaskInfo,
|
||||||
WaterfallItem,
|
WaterfallItem,
|
||||||
// MediaType,
|
|
||||||
WaterfallProps,
|
WaterfallProps,
|
||||||
} from "@/typings/work";
|
} from "@/typings/work";
|
||||||
import defaultImg from "@/assets/images/img-load-err.png";
|
import defaultImg from "@/assets/images/img-load-err.png";
|
||||||
import fallbackCopyToClipboard from "@/utils/copyText";
|
import fallbackCopyToClipboard from "@/utils/copyText";
|
||||||
import { LazyImg, Waterfall } from "vue-waterfall-plugin-next";
|
import { Waterfall } from "vue-waterfall-plugin-next";
|
||||||
import "vue-waterfall-plugin-next/dist/style.css";
|
import "vue-waterfall-plugin-next/dist/style.css";
|
||||||
|
// 引入通用保存工具
|
||||||
|
import { saveFile, FileType } from "@/utils/saveFile";
|
||||||
|
|
||||||
enum MediaType {
|
enum MediaType {
|
||||||
IMAGE = 1, // 纯图片
|
IMAGE = 1,
|
||||||
IMAGE_TEXT = 2, // 图文
|
IMAGE_TEXT = 2,
|
||||||
VIDEO = 3, // 视频
|
VIDEO = 3,
|
||||||
TEXT = 4, // 纯文字
|
TEXT = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
const waterfallKey = ref(0);
|
const waterfallKey = ref(0);
|
||||||
// ✅ 自定义响应式断点配置
|
|
||||||
const waterfallBreakpoints = {
|
const waterfallBreakpoints = {
|
||||||
// 1200: { rowPerView: 4 }, // 宽度 < 1200px:4列
|
600: { rowPerView: 2 },
|
||||||
// 900: { rowPerView: 3 }, // 宽度 < 900px:3列
|
320: { rowPerView: 2 },
|
||||||
600: { rowPerView: 2 }, // 宽度 < 600px:2列(平板横屏)
|
|
||||||
320: { rowPerView: 2 }, // 宽度 < 320px:2列(所有手机竖屏)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchForm = ref({
|
const searchForm = ref({
|
||||||
title: "",
|
title: "",
|
||||||
// type: 1, //默认图片
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
hasMore.value = true;
|
hasMore.value = true;
|
||||||
scrollContainer.value.scrollTop = 0;
|
scrollContainer.value?.scrollTo({ top: 0 });
|
||||||
await getTaskProductionListLimit();
|
await getTaskProductionListLimit();
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedSearch = debounce(search, 300);
|
const debouncedSearch = debounce(search, 300);
|
||||||
const { WOEKS_LIST_TWO } = apis;
|
const { WOEKS_LIST_TWO } = apis;
|
||||||
// ===================== 1. 路由实例(Vue3 替代 this.$route) =====================
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const taskId = route.params.taskId as string;
|
const taskId = route.params.taskId as string;
|
||||||
// 复用枚举:1图片 2图文 3视频 4文字
|
|
||||||
|
|
||||||
// ===================== 2. TS 类型定义 =====================
|
|
||||||
/** 任务基础信息 */
|
|
||||||
|
|
||||||
// ===================== 3. 全局响应式数据 =====================
|
|
||||||
// 字典类型列表
|
|
||||||
const taskTypes = ref<DictOption[]>(
|
const taskTypes = ref<DictOption[]>(
|
||||||
JSON.parse(sessionStorage.getItem("task_type") || "[]")
|
JSON.parse(sessionStorage.getItem("task_type") || "[]")
|
||||||
);
|
);
|
||||||
// 任务详情
|
|
||||||
const taskInfo = ref<TaskInfo>(
|
const taskInfo = ref<TaskInfo>(
|
||||||
JSON.parse(sessionStorage.getItem("currentTask") || "{}")
|
JSON.parse(sessionStorage.getItem("currentTask") || "{}")
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentTaskType = computed(() => Number(taskInfo.value.type || 1));
|
const currentTaskType = computed(() => Number(taskInfo.value.type || 1));
|
||||||
|
|
||||||
// 瀑布流列表
|
|
||||||
const list = ref<WaterfallItem[]>([]);
|
const list = ref<WaterfallItem[]>([]);
|
||||||
// 抽屉显隐
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
// 当前选中的卡片项
|
|
||||||
const currentItem = ref<WaterfallItem | null>(null);
|
const currentItem = ref<WaterfallItem | null>(null);
|
||||||
|
|
||||||
// 分页 & 加载状态
|
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const pageSize = computed(() => (isMobile.value ? 20 : 50));
|
const pageSize = computed(() => (isMobile.value ? 20 : 50));
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
@@ -252,84 +254,75 @@ const cardDom = computed(
|
|||||||
() => document.getElementsByClassName("waterfall-item")[0]
|
() => document.getElementsByClassName("waterfall-item")[0]
|
||||||
);
|
);
|
||||||
const cardDomWidth = computed(() => cardDom.value?.offsetWidth ?? 200);
|
const cardDomWidth = computed(() => cardDom.value?.offsetWidth ?? 200);
|
||||||
// 计算高度方法
|
|
||||||
const calculateHeight = (width: number, height: number) => {
|
const calculateHeight = (width: number, height: number) => {
|
||||||
// console.log(11111111111111, cardDom, cardDomWidth.value, width, height);
|
|
||||||
// 16*200/9=
|
|
||||||
return (cardDomWidth.value * height) / width || 356;
|
return (cardDomWidth.value * height) / width || 356;
|
||||||
};
|
};
|
||||||
// 滚动容器 DOM 引用(标注 TS 类型)
|
|
||||||
const scrollContainer = ref<HTMLDivElement | null>(null);
|
const scrollContainer = ref<HTMLDivElement | null>(null);
|
||||||
|
const showScrollToTop = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const isFirstLoad = ref(true);
|
||||||
|
|
||||||
// ===================== 4. 工具/业务方法 =====================
|
|
||||||
/** 返回上一页 */
|
|
||||||
const goBack = () => {
|
|
||||||
sessionStorage.clear();
|
|
||||||
window.history.go(-1);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 返回顶部 */
|
|
||||||
const scrollToTop = () => {
|
const scrollToTop = () => {
|
||||||
scrollContainer.value?.scrollTo({
|
scrollContainer.value?.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
/** 刷新列表 */
|
|
||||||
const refresh = () => {
|
const refresh = () => {
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
list.value = [];
|
list.value = [];
|
||||||
hasMore.value = true;
|
hasMore.value = true;
|
||||||
getTaskProductionListLimit();
|
getTaskProductionListLimit();
|
||||||
};
|
};
|
||||||
/** 图片加载错误兜底 */
|
|
||||||
const handleImageError = (e: Event) => {
|
const handleImageError = (e: Event) => {
|
||||||
const target = e.target as HTMLImageElement;
|
const target = e.target as HTMLImageElement;
|
||||||
target.src = defaultImg;
|
target.src = defaultImg;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 分页加载作品列表(无限滚动核心) */
|
|
||||||
const getTaskProductionListLimit = async () => {
|
const getTaskProductionListLimit = async () => {
|
||||||
// 防重复请求
|
|
||||||
if (isLoading.value || !hasMore.value) return;
|
if (isLoading.value || !hasMore.value) return;
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
currentPage.value == 1 && showLoading();
|
currentPage.value == 1 && showLoading();
|
||||||
const res = await WOEKS_LIST_TWO({
|
const res = await WOEKS_LIST_TWO({
|
||||||
// taskId,
|
|
||||||
current: currentPage.value,
|
current: currentPage.value,
|
||||||
size: pageSize.value,
|
size: pageSize.value,
|
||||||
...searchForm.value,
|
...searchForm.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { records = [] } = res || {};
|
const { records = [] } = res || {};
|
||||||
|
const sizes = [
|
||||||
|
[9, 16],
|
||||||
|
[1, 1],
|
||||||
|
[16, 9],
|
||||||
|
[3, 4],
|
||||||
|
[4, 3],
|
||||||
|
[2, 3],
|
||||||
|
[3, 2],
|
||||||
|
];
|
||||||
|
|
||||||
records.forEach((item) => {
|
records.forEach((item) => {
|
||||||
let sizes = [
|
|
||||||
[9, 16],
|
|
||||||
[1, 1],
|
|
||||||
[16, 9],
|
|
||||||
[3, 4],
|
|
||||||
[4, 3],
|
|
||||||
[2, 3],
|
|
||||||
[3, 2],
|
|
||||||
];
|
|
||||||
if (!item.width || !item.height) {
|
if (!item.width || !item.height) {
|
||||||
let size = sizes[Math.floor(Math.random() * sizes.length)];
|
const size = sizes[Math.floor(Math.random() * sizes.length)];
|
||||||
item.width = size[0];
|
item.width = size[0];
|
||||||
item.height = size[1];
|
item.height = size[1];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentPage.value == 1) {
|
if (currentPage.value == 1) {
|
||||||
list.value = records;
|
list.value = records;
|
||||||
} else {
|
} else {
|
||||||
list.value = list.value.concat(records);
|
list.value = list.value.concat(records);
|
||||||
}
|
}
|
||||||
// 判断是否还有更多数据
|
|
||||||
hasMore.value = !(records.length < pageSize.value);
|
hasMore.value = !(records.length < pageSize.value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("列表加载失败:", error);
|
console.error("列表加载失败:", error);
|
||||||
// 加载失败,页码回退
|
|
||||||
currentPage.value = Math.max(1, currentPage.value - 1);
|
currentPage.value = Math.max(1, currentPage.value - 1);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
@@ -338,206 +331,111 @@ const getTaskProductionListLimit = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const showScrollToTop = ref(false);
|
|
||||||
/** 滚动触底加载 */
|
|
||||||
const handleScroll = throttle(() => {
|
const handleScroll = throttle(() => {
|
||||||
const container = scrollContainer.value;
|
const container = scrollContainer.value;
|
||||||
if (!container || isLoading.value || !hasMore.value) return;
|
if (!container || isLoading.value || !hasMore.value) return;
|
||||||
|
|
||||||
const { scrollHeight, scrollTop, clientHeight } = container;
|
const { scrollHeight, scrollTop, clientHeight } = container;
|
||||||
// 距离底部 50px 预加载
|
|
||||||
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
||||||
currentPage.value++;
|
currentPage.value++;
|
||||||
getTaskProductionListLimit();
|
getTaskProductionListLimit();
|
||||||
}
|
}
|
||||||
if (scrollTop > 1000) {
|
|
||||||
showScrollToTop.value = true;
|
showScrollToTop.value = scrollTop > 1000;
|
||||||
} else {
|
|
||||||
showScrollToTop.value = false;
|
|
||||||
}
|
|
||||||
}, 100);
|
}, 100);
|
||||||
// 检测师手机端还是pc端,不要宽度判断,用userAgent判断
|
|
||||||
const isMobile = computed(() => {
|
const isMobile = computed(() => {
|
||||||
return navigator.userAgent.includes("Mobile");
|
return navigator.userAgent.includes("Mobile");
|
||||||
});
|
});
|
||||||
/** 瀑布流卡片点击 */
|
|
||||||
const handleCardClick = (item: WaterfallItem) => {
|
const handleCardClick = (item: WaterfallItem) => {
|
||||||
// if (isMobile.value) {
|
|
||||||
// } else {
|
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
currentItem.value = item;
|
currentItem.value = item;
|
||||||
console.log("点击作品:", item);
|
|
||||||
// }
|
|
||||||
};
|
};
|
||||||
const saving = ref(false);
|
|
||||||
/** 抽屉关闭 */
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
// currentItem.value = null;
|
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* ✅ 纯前端保存功能(无后端接口)
|
const handleCopy = (text: string) => {
|
||||||
* 支持:图片/视频/纯文字三种类型
|
fallbackCopyToClipboard(text);
|
||||||
*/
|
showMsg("复制成功", "success");
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== 简化后的保存方法(调用通用工具) ====================
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!currentItem.value) {
|
if (!currentItem.value) {
|
||||||
showMsg("请先选择要保存的作品");
|
showMsg("请先选择要保存的作品");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saving.value) return; // 防止重复点击
|
if (saving.value) return;
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const item = currentItem.value;
|
const item = currentItem.value;
|
||||||
// 生成文件名:标题_时间戳.后缀(避免重复)
|
// 生成安全的文件名(去除非法字符)
|
||||||
const timestamp = Date.now();
|
|
||||||
const safeTitle =
|
const safeTitle =
|
||||||
item.title?.replace(/[<>:"/\\|?*]/g, "_") || `work_${item.id}`;
|
item.title?.replace(/[<>:"/\\|?*]/g, "_") || `work_${item.id}`;
|
||||||
|
|
||||||
|
// 根据作品类型调用通用保存工具
|
||||||
switch (currentTaskType.value) {
|
switch (currentTaskType.value) {
|
||||||
// 图片类型:Canvas转Blob下载(解决跨域问题)
|
|
||||||
case MediaType.IMAGE:
|
case MediaType.IMAGE:
|
||||||
|
await saveFile({
|
||||||
|
type: FileType.IMAGE,
|
||||||
|
url: item.fileUrl,
|
||||||
|
fileName: safeTitle,
|
||||||
|
});
|
||||||
|
break;
|
||||||
case MediaType.IMAGE_TEXT:
|
case MediaType.IMAGE_TEXT:
|
||||||
await saveImage(item.fileUrl, `${safeTitle}_${timestamp}.png`);
|
await saveFile({
|
||||||
|
type: FileType.IMAGE,
|
||||||
|
url: item.fileUrl,
|
||||||
|
fileName: safeTitle,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// 视频类型:Fetch获取Blob下载
|
|
||||||
case MediaType.VIDEO:
|
case MediaType.VIDEO:
|
||||||
await saveVideo(item.fileUrl, `${safeTitle}_${timestamp}.mp4`);
|
await saveFile({
|
||||||
|
type: FileType.VIDEO,
|
||||||
|
url: item.fileUrl,
|
||||||
|
fileName: safeTitle,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// 纯文字类型:生成TXT文件下载
|
|
||||||
case MediaType.TEXT:
|
case MediaType.TEXT:
|
||||||
saveText(item.content, `${safeTitle}_${timestamp}.txt`);
|
await saveFile({
|
||||||
|
type: FileType.TEXT,
|
||||||
|
content: item.content,
|
||||||
|
fileName: safeTitle,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
showMsg("不支持的文件类型");
|
showMsg("不支持的文件类型");
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
if (!isMobile.value) {
|
||||||
showMsg("保存成功!", "success");
|
showMsg("保存成功", "success");
|
||||||
handleClose();
|
}
|
||||||
|
// handleClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("保存失败:", error);
|
console.error("保存失败:", error);
|
||||||
showMsg(error.message || "保存失败,请检查网络连接或稍后重试", "error");
|
showMsg(error.message || "保存失败,请稍后重试", "error");
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false;
|
saving.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// ==================== 生命周期 ====================
|
||||||
* 保存图片(解决跨域问题)
|
|
||||||
* @param url 图片URL
|
|
||||||
* @param fileName 保存的文件名
|
|
||||||
*/
|
|
||||||
const saveImage = (url: string, fileName: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.crossOrigin = "anonymous"; // 关键:允许跨域
|
|
||||||
img.onload = () => {
|
|
||||||
try {
|
|
||||||
// 创建Canvas
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = img.naturalWidth;
|
|
||||||
canvas.height = img.naturalHeight;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
|
|
||||||
if (!ctx) {
|
|
||||||
reject(new Error("Canvas不支持"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制图片
|
|
||||||
ctx.drawImage(img, 0, 0);
|
|
||||||
|
|
||||||
// 转Blob并下载
|
|
||||||
canvas.toBlob((blob) => {
|
|
||||||
if (!blob) {
|
|
||||||
reject(new Error("图片转换失败"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
downloadBlob(blob, fileName);
|
|
||||||
resolve();
|
|
||||||
}, "image/png");
|
|
||||||
} catch (e) {
|
|
||||||
reject(new Error("图片处理失败,可能是跨域限制"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
img.onerror = () => {
|
|
||||||
reject(new Error("图片加载失败"));
|
|
||||||
};
|
|
||||||
|
|
||||||
img.src = url;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存视频
|
|
||||||
* @param url 视频URL
|
|
||||||
* @param fileName 保存的文件名
|
|
||||||
*/
|
|
||||||
const saveVideo = async (url: string, fileName: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`视频加载失败:${response.status}`);
|
|
||||||
}
|
|
||||||
const blob = await response.blob();
|
|
||||||
downloadBlob(blob, fileName);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error("视频下载失败,可能是文件过大或网络问题");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存纯文字为TXT文件
|
|
||||||
* @param text 文字内容
|
|
||||||
* @param fileName 保存的文件名
|
|
||||||
*/
|
|
||||||
const saveText = (text: string, fileName: string) => {
|
|
||||||
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
|
|
||||||
downloadBlob(blob, fileName);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通用Blob下载方法
|
|
||||||
* @param blob 要下载的Blob对象
|
|
||||||
* @param fileName 保存的文件名
|
|
||||||
*/
|
|
||||||
const downloadBlob = (blob: Blob, fileName: string) => {
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = url;
|
|
||||||
a.download = fileName;
|
|
||||||
a.style.display = "none";
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url); // 释放内存
|
|
||||||
};
|
|
||||||
/** 复制文案 */
|
|
||||||
const handleCopy = (text: string) => {
|
|
||||||
fallbackCopyToClipboard(text);
|
|
||||||
};
|
|
||||||
// 是否首次加载
|
|
||||||
const isFirstLoad = ref(true);
|
|
||||||
// ===================== 5. 生命周期 =====================
|
|
||||||
/** 组件挂载:初始化所有数据 + 绑定滚动事件 */
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 纯文字类型,修改每页条数
|
|
||||||
// 重置分页 + 加载第一页
|
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
hasMore.value = true;
|
hasMore.value = true;
|
||||||
await getTaskProductionListLimit();
|
await getTaskProductionListLimit();
|
||||||
// 绑定滚动事件
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
const container = scrollContainer.value;
|
const container = scrollContainer.value;
|
||||||
if (container) {
|
if (container) {
|
||||||
container.addEventListener("scroll", handleScroll);
|
container.addEventListener("scroll", handleScroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isFirstLoad.value = false;
|
isFirstLoad.value = false;
|
||||||
waterfallKey.value++;
|
waterfallKey.value++;
|
||||||
@@ -545,7 +443,6 @@ onMounted(async () => {
|
|||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 组件卸载:解绑滚动事件,防止内存泄漏 */
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
const container = scrollContainer.value;
|
const container = scrollContainer.value;
|
||||||
if (container) {
|
if (container) {
|
||||||
@@ -562,14 +459,11 @@ onBeforeUnmount(() => {
|
|||||||
.task-production-detail-header {
|
.task-production-detail-header {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: #0072fed6;
|
background-color: #0072fed6;
|
||||||
// border-bottom: 1px solid #e5e5e5;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
// text-align: center;
|
|
||||||
// background-color: #0072fe;
|
|
||||||
> div {
|
> div {
|
||||||
max-width: 300px;
|
// max-width: 300px;
|
||||||
:deep(.el-input) {
|
:deep(.el-input) {
|
||||||
// width: 100%;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -589,13 +483,6 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detail-content {
|
.detail-content {
|
||||||
h3 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
img,
|
img,
|
||||||
video {
|
video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -606,22 +493,20 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Vue3 推荐使用 :deep() 替代 ::v-deep,两种都兼容 */
|
|
||||||
:deep(.el-drawer__title) {
|
:deep(.el-drawer__title) {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
span {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.card-item {
|
.card-item {
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.829);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,632 @@
|
|||||||
|
<template>
|
||||||
|
<!-- <div v-if="taskInfo && taskTypes.length"> -->
|
||||||
|
<div class="task-production-detail">
|
||||||
|
<div
|
||||||
|
class="task-production-detail-header flex items-center justify-between"
|
||||||
|
:class="[isMobile ? 'p-2' : '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">
|
||||||
|
<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>
|
||||||
|
<!-- 滚动容器 ref 绑定 -->
|
||||||
|
<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"
|
||||||
|
: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>
|
||||||
|
|
||||||
|
<el-drawer
|
||||||
|
v-if="!isMobile"
|
||||||
|
v-model="visible"
|
||||||
|
:size="500"
|
||||||
|
@close="handleClose"
|
||||||
|
fullscreen
|
||||||
|
title="右键保存图片"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<span class="">右键保存图片</span>
|
||||||
|
<el-button type="info" plain @click="handleClose">关闭</el-button>
|
||||||
|
<!-- <el-button type="primary" @click="handleCopy(currentItem.content)">复制文案</el-button> -->
|
||||||
|
<!-- 保存 -->
|
||||||
|
<!-- <el-button type="primary" @click="handleSave">保存</el-button> -->
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
<el-dialog
|
||||||
|
v-if="isMobile"
|
||||||
|
v-model="visible"
|
||||||
|
:size="500"
|
||||||
|
@close="handleClose"
|
||||||
|
fullscreen
|
||||||
|
title="长按保存图片"
|
||||||
|
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="currentItem.fileUrl"
|
||||||
|
class="h-[80vh] object-contain rounded-md mx-auto"
|
||||||
|
/>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="info" plain @click="handleClose">关闭</el-button>
|
||||||
|
<!-- <el-button type="primary" @click="handleCopy(currentItem.content)">复制文案</el-button> -->
|
||||||
|
<!-- 保存 -->
|
||||||
|
<!-- <el-button type="primary" @click="handleSave">保存</el-button> -->
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
<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,
|
||||||
|
reactive,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
nextTick,
|
||||||
|
computed,
|
||||||
|
} from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { debounce, throttle } from "lodash-es"; // 记得安装lodash-es
|
||||||
|
import { showLoading, hiddenLoading } from "@/utils/loading";
|
||||||
|
import { showMsg } from "@/utils/showMsg";
|
||||||
|
// Element Plus 图标 + 组件
|
||||||
|
import { Refresh, ArrowLeft, Search, ArrowUp } from "@element-plus/icons-vue";
|
||||||
|
// 接口请求
|
||||||
|
import apis from "@/api/index";
|
||||||
|
// 子组件
|
||||||
|
import WaterfallFlow from "./waterfallFlow.vue";
|
||||||
|
// import DictTag from "@/components/DictTag/index.vue";
|
||||||
|
// 静态资源 & 工具函数
|
||||||
|
import {
|
||||||
|
DictOption,
|
||||||
|
TaskInfo,
|
||||||
|
WaterfallItem,
|
||||||
|
// MediaType,
|
||||||
|
WaterfallProps,
|
||||||
|
} from "@/typings/work";
|
||||||
|
import defaultImg from "@/assets/images/img-load-err.png";
|
||||||
|
import fallbackCopyToClipboard from "@/utils/copyText";
|
||||||
|
import { LazyImg, Waterfall } from "vue-waterfall-plugin-next";
|
||||||
|
import "vue-waterfall-plugin-next/dist/style.css";
|
||||||
|
enum MediaType {
|
||||||
|
IMAGE = 1, // 纯图片
|
||||||
|
IMAGE_TEXT = 2, // 图文
|
||||||
|
VIDEO = 3, // 视频
|
||||||
|
TEXT = 4, // 纯文字
|
||||||
|
}
|
||||||
|
const waterfallKey = ref(0);
|
||||||
|
// ✅ 自定义响应式断点配置
|
||||||
|
const waterfallBreakpoints = {
|
||||||
|
// 1200: { rowPerView: 4 }, // 宽度 < 1200px:4列
|
||||||
|
// 900: { rowPerView: 3 }, // 宽度 < 900px:3列
|
||||||
|
600: { rowPerView: 2 }, // 宽度 < 600px:2列(平板横屏)
|
||||||
|
320: { rowPerView: 2 }, // 宽度 < 320px:2列(所有手机竖屏)
|
||||||
|
};
|
||||||
|
const searchForm = ref({
|
||||||
|
title: "",
|
||||||
|
// type: 1, //默认图片
|
||||||
|
});
|
||||||
|
const search = async () => {
|
||||||
|
currentPage.value = 1;
|
||||||
|
hasMore.value = true;
|
||||||
|
scrollContainer.value.scrollTop = 0;
|
||||||
|
await getTaskProductionListLimit();
|
||||||
|
};
|
||||||
|
const debouncedSearch = debounce(search, 300);
|
||||||
|
const { WOEKS_LIST_TWO } = apis;
|
||||||
|
// ===================== 1. 路由实例(Vue3 替代 this.$route) =====================
|
||||||
|
const route = useRoute();
|
||||||
|
const taskId = route.params.taskId as string;
|
||||||
|
// 复用枚举:1图片 2图文 3视频 4文字
|
||||||
|
|
||||||
|
// ===================== 2. TS 类型定义 =====================
|
||||||
|
/** 任务基础信息 */
|
||||||
|
|
||||||
|
// ===================== 3. 全局响应式数据 =====================
|
||||||
|
// 字典类型列表
|
||||||
|
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) => {
|
||||||
|
// console.log(11111111111111, cardDom, cardDomWidth.value, width, height);
|
||||||
|
// 16*200/9=
|
||||||
|
return (cardDomWidth.value * height) / width || 356;
|
||||||
|
};
|
||||||
|
// 滚动容器 DOM 引用(标注 TS 类型)
|
||||||
|
const scrollContainer = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
// ===================== 4. 工具/业务方法 =====================
|
||||||
|
/** 返回上一页 */
|
||||||
|
const goBack = () => {
|
||||||
|
sessionStorage.clear();
|
||||||
|
window.history.go(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 返回顶部 */
|
||||||
|
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({
|
||||||
|
// taskId,
|
||||||
|
current: currentPage.value,
|
||||||
|
size: pageSize.value,
|
||||||
|
...searchForm.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { records = [] } = res || {};
|
||||||
|
records.forEach((item) => {
|
||||||
|
let sizes = [
|
||||||
|
[9, 16],
|
||||||
|
[1, 1],
|
||||||
|
[16, 9],
|
||||||
|
[3, 4],
|
||||||
|
[4, 3],
|
||||||
|
[2, 3],
|
||||||
|
[3, 2],
|
||||||
|
];
|
||||||
|
if (!item.width || !item.height) {
|
||||||
|
let 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 showScrollToTop = ref(false);
|
||||||
|
/** 滚动触底加载 */
|
||||||
|
const handleScroll = throttle(() => {
|
||||||
|
const container = scrollContainer.value;
|
||||||
|
if (!container || isLoading.value || !hasMore.value) return;
|
||||||
|
|
||||||
|
const { scrollHeight, scrollTop, clientHeight } = container;
|
||||||
|
// 距离底部 50px 预加载
|
||||||
|
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
||||||
|
currentPage.value++;
|
||||||
|
getTaskProductionListLimit();
|
||||||
|
}
|
||||||
|
if (scrollTop > 1000) {
|
||||||
|
showScrollToTop.value = true;
|
||||||
|
} else {
|
||||||
|
showScrollToTop.value = false;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
// 检测师手机端还是pc端,不要宽度判断,用userAgent判断
|
||||||
|
const isMobile = computed(() => {
|
||||||
|
return navigator.userAgent.includes("Mobile");
|
||||||
|
});
|
||||||
|
/** 瀑布流卡片点击 */
|
||||||
|
const handleCardClick = (item: WaterfallItem) => {
|
||||||
|
// if (isMobile.value) {
|
||||||
|
// } else {
|
||||||
|
visible.value = true;
|
||||||
|
currentItem.value = item;
|
||||||
|
console.log("点击作品:", item);
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
const saving = ref(false);
|
||||||
|
/** 抽屉关闭 */
|
||||||
|
const handleClose = () => {
|
||||||
|
visible.value = false;
|
||||||
|
// currentItem.value = null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* ✅ 纯前端保存功能(无后端接口)
|
||||||
|
* 支持:图片/视频/纯文字三种类型
|
||||||
|
*/
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!currentItem.value) {
|
||||||
|
showMsg("请先选择要保存的作品");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saving.value) return; // 防止重复点击
|
||||||
|
saving.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item = currentItem.value;
|
||||||
|
// 生成文件名:标题_时间戳.后缀(避免重复)
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const safeTitle =
|
||||||
|
item.title?.replace(/[<>:"/\\|?*]/g, "_") || `work_${item.id}`;
|
||||||
|
|
||||||
|
switch (currentTaskType.value) {
|
||||||
|
// 图片类型:Canvas转Blob下载(解决跨域问题)
|
||||||
|
case MediaType.IMAGE:
|
||||||
|
case MediaType.IMAGE_TEXT:
|
||||||
|
await saveImage(item.fileUrl, `${safeTitle}_${timestamp}.png`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// 视频类型:Fetch获取Blob下载
|
||||||
|
case MediaType.VIDEO:
|
||||||
|
await saveVideo(item.fileUrl, `${safeTitle}_${timestamp}.mp4`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// 纯文字类型:生成TXT文件下载
|
||||||
|
case MediaType.TEXT:
|
||||||
|
saveText(item.content, `${safeTitle}_${timestamp}.txt`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
showMsg("不支持的文件类型");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
showMsg("保存成功!", "success");
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("保存失败:", error);
|
||||||
|
showMsg(error.message || "保存失败,请检查网络连接或稍后重试", "error");
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存图片(解决跨域问题)
|
||||||
|
* @param url 图片URL
|
||||||
|
* @param fileName 保存的文件名
|
||||||
|
*/
|
||||||
|
const saveImage = (url: string, fileName: string): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = "anonymous"; // 关键:允许跨域
|
||||||
|
img.onload = () => {
|
||||||
|
try {
|
||||||
|
// 创建Canvas
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = img.naturalWidth;
|
||||||
|
canvas.height = img.naturalHeight;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
reject(new Error("Canvas不支持"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制图片
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
// 转Blob并下载
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (!blob) {
|
||||||
|
reject(new Error("图片转换失败"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloadBlob(blob, fileName);
|
||||||
|
resolve();
|
||||||
|
}, "image/png");
|
||||||
|
} catch (e) {
|
||||||
|
reject(new Error("图片处理失败,可能是跨域限制"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
reject(new Error("图片加载失败"));
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存视频
|
||||||
|
* @param url 视频URL
|
||||||
|
* @param fileName 保存的文件名
|
||||||
|
*/
|
||||||
|
const saveVideo = async (url: string, fileName: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`视频加载失败:${response.status}`);
|
||||||
|
}
|
||||||
|
const blob = await response.blob();
|
||||||
|
downloadBlob(blob, fileName);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("视频下载失败,可能是文件过大或网络问题");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存纯文字为TXT文件
|
||||||
|
* @param text 文字内容
|
||||||
|
* @param fileName 保存的文件名
|
||||||
|
*/
|
||||||
|
const saveText = (text: string, fileName: string) => {
|
||||||
|
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
|
||||||
|
downloadBlob(blob, fileName);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用Blob下载方法
|
||||||
|
* @param blob 要下载的Blob对象
|
||||||
|
* @param fileName 保存的文件名
|
||||||
|
*/
|
||||||
|
const downloadBlob = (blob: Blob, fileName: string) => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = fileName;
|
||||||
|
a.style.display = "none";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url); // 释放内存
|
||||||
|
};
|
||||||
|
/** 复制文案 */
|
||||||
|
const handleCopy = (text: string) => {
|
||||||
|
fallbackCopyToClipboard(text);
|
||||||
|
};
|
||||||
|
// 是否首次加载
|
||||||
|
const isFirstLoad = ref(true);
|
||||||
|
// ===================== 5. 生命周期 =====================
|
||||||
|
/** 组件挂载:初始化所有数据 + 绑定滚动事件 */
|
||||||
|
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;
|
||||||
|
// border-bottom: 1px solid #e5e5e5;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
// text-align: center;
|
||||||
|
// background-color: #0072fe;
|
||||||
|
> div {
|
||||||
|
max-width: 300px;
|
||||||
|
:deep(.el-input) {
|
||||||
|
// width: 100%;
|
||||||
|
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 {
|
||||||
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vue3 推荐使用 :deep() 替代 ::v-deep,两种都兼容 */
|
||||||
|
:deep(.el-drawer__title) {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.card-item {
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.829);
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import { showMsg } from "@/utils/showMsg";
|
||||||
|
/**
|
||||||
|
* 全环境通用文件保存工具
|
||||||
|
* 支持:PC浏览器、手机浏览器、uniapp webview
|
||||||
|
* 支持文件类型:图片、视频、纯文字
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 文件类型枚举
|
||||||
|
export const FileType = {
|
||||||
|
IMAGE: 'image',
|
||||||
|
VIDEO: 'video',
|
||||||
|
TEXT: 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否在uniapp webview环境中
|
||||||
|
*/
|
||||||
|
const isUniAppWebview = () => {
|
||||||
|
return typeof uni !== 'undefined'
|
||||||
|
&& typeof uni.downloadFile === 'function'
|
||||||
|
&& typeof uni.saveImageToPhotosAlbum === 'function'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否是iOS设备
|
||||||
|
*/
|
||||||
|
const isIOS = () => {
|
||||||
|
return /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用保存文件入口(所有环境通用)
|
||||||
|
* @param {Object} options 保存选项
|
||||||
|
* @param {string} options.type 文件类型:FileType.IMAGE / FileType.VIDEO / FileType.TEXT
|
||||||
|
* @param {string} options.url 文件URL(图片/视频必填)
|
||||||
|
* @param {string} options.content 文字内容(文字类型必填)
|
||||||
|
* @param {string} options.fileName 保存的文件名(可选,自动生成)
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export const saveFile = async (options) => {
|
||||||
|
const { type, url, content, fileName } = options
|
||||||
|
|
||||||
|
// 参数校验
|
||||||
|
if (!type) {
|
||||||
|
throw new Error('请指定文件类型')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((type === FileType.IMAGE || type === FileType.VIDEO) && !url) {
|
||||||
|
throw new Error('请提供文件URL')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === FileType.TEXT && !content) {
|
||||||
|
throw new Error('请提供文字内容')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成默认文件名
|
||||||
|
const timestamp = Date.now()
|
||||||
|
const defaultFileName = `file_${timestamp}`
|
||||||
|
const finalFileName = fileName || defaultFileName
|
||||||
|
|
||||||
|
// 自动选择保存方式
|
||||||
|
if (isUniAppWebview()) {
|
||||||
|
await saveInUniApp(type, url, content, finalFileName)
|
||||||
|
} else {
|
||||||
|
await saveInBrowser(type, url, content, finalFileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* uniapp webview环境保存
|
||||||
|
*/
|
||||||
|
const saveInUniApp = async (type, url, content, fileName) => {
|
||||||
|
switch (type) {
|
||||||
|
case FileType.IMAGE:
|
||||||
|
await uniSaveImage(url)
|
||||||
|
break
|
||||||
|
case FileType.VIDEO:
|
||||||
|
await uniSaveVideo(url)
|
||||||
|
break
|
||||||
|
case FileType.TEXT:
|
||||||
|
// webview无法直接保存文件,降级为复制到剪贴板
|
||||||
|
await uniCopyText(content)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error('不支持的文件类型')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 浏览器环境保存
|
||||||
|
*/
|
||||||
|
const saveInBrowser = async (type, url, content, fileName) => {
|
||||||
|
switch (type) {
|
||||||
|
case FileType.IMAGE:
|
||||||
|
await browserSaveImage(url, fileName)
|
||||||
|
break
|
||||||
|
case FileType.VIDEO:
|
||||||
|
await browserSaveVideo(url, fileName)
|
||||||
|
break
|
||||||
|
case FileType.TEXT:
|
||||||
|
browserSaveText(content, fileName)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error('不支持的文件类型')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== uniapp保存方法 ====================
|
||||||
|
const uniSaveImage = (url) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.showLoading({ title: '保存中...' })
|
||||||
|
|
||||||
|
uni.downloadFile({
|
||||||
|
url,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
reject(new Error('图片下载失败'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.saveImageToPhotosAlbum({
|
||||||
|
filePath: res.tempFilePath,
|
||||||
|
success: () => resolve(),
|
||||||
|
fail: (err) => {
|
||||||
|
if (err.errMsg.includes('auth')) {
|
||||||
|
reject(new Error('请在设置中开启相册权限后重试'))
|
||||||
|
} else if (err.errMsg.includes('cancel')) {
|
||||||
|
reject(new Error('已取消保存'))
|
||||||
|
} else {
|
||||||
|
reject(new Error('保存图片失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: () => reject(new Error('网络请求失败,请检查网络连接')),
|
||||||
|
complete: () => uni.hideLoading()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniSaveVideo = (url) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.showLoading({ title: '保存中...' })
|
||||||
|
|
||||||
|
uni.downloadFile({
|
||||||
|
url,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
reject(new Error('视频下载失败'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.saveVideoToPhotosAlbum({
|
||||||
|
filePath: res.tempFilePath,
|
||||||
|
success: () => resolve(),
|
||||||
|
fail: (err) => {
|
||||||
|
if (err.errMsg.includes('auth')) {
|
||||||
|
reject(new Error('请在设置中开启相册权限后重试'))
|
||||||
|
} else if (err.errMsg.includes('cancel')) {
|
||||||
|
reject(new Error('已取消保存'))
|
||||||
|
} else {
|
||||||
|
reject(new Error('保存视频失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: () => reject(new Error('网络请求失败,请检查网络连接')),
|
||||||
|
complete: () => uni.hideLoading()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniCopyText = (text) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: text,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({ title: '文案已复制到剪贴板', icon: 'success' })
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
fail: () => reject(new Error('复制失败'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 浏览器保存方法 ====================
|
||||||
|
const browserSaveImage = (url, fileName) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.crossOrigin = 'anonymous'
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
try {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = img.naturalWidth
|
||||||
|
canvas.height = img.naturalHeight
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
// Canvas不支持,降级为打开新标签页
|
||||||
|
openInNewTab(url)
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.drawImage(img, 0, 0)
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (!blob) {
|
||||||
|
openInNewTab(url)
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
downloadBlob(blob, `${fileName}.png`)
|
||||||
|
resolve()
|
||||||
|
}, 'image/png')
|
||||||
|
} catch (e) {
|
||||||
|
// 发生错误,降级为打开新标签页
|
||||||
|
openInNewTab(url)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
openInNewTab(url)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
img.src = url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const browserSaveVideo = async (url, fileName) => {
|
||||||
|
try {
|
||||||
|
// iOS Safari不支持Blob下载视频,直接打开新标签页
|
||||||
|
if (isIOS()) {
|
||||||
|
openInNewTab(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`视频加载失败:${response.status}`)
|
||||||
|
}
|
||||||
|
const blob = await response.blob()
|
||||||
|
downloadBlob(blob, `${fileName}.mp4`)
|
||||||
|
} catch (error) {
|
||||||
|
// 发生错误,降级为打开新标签页
|
||||||
|
openInNewTab(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const browserSaveText = (text, fileName) => {
|
||||||
|
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
|
||||||
|
downloadBlob(blob, `${fileName}.txt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 通用工具方法 ====================
|
||||||
|
const downloadBlob = (blob, fileName) => {
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = fileName
|
||||||
|
a.style.display = 'none'
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
showMsg("保存成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
const openInNewTab = (url) => {
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.target = '_blank'
|
||||||
|
a.rel = 'noopener noreferrer'
|
||||||
|
a.style.display = 'none'
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出默认对象,方便使用
|
||||||
|
export default {
|
||||||
|
saveFile,
|
||||||
|
FileType
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user