utils.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. import { store } from "@/store/store";
  2. import { PictureSlice, Task } from "@/types";
  3. // 打开cache后,会造成没有 vue devtools 时,canvas缓存错误,暂时不知道原因
  4. // 通过回看的测试,打开回看,再关闭回看,稍等一会儿再打开回看,确实可以看到该缓存时缓存了,该丢弃时丢弃了
  5. // 把store.currentTask当做 weakRef ,当它不存在时,就丢弃它所有的图片
  6. // const weakedMapImages = new WeakMap<Object, Map<string, HTMLImageElement>>();
  7. /**
  8. * 异步获取图片
  9. * @param url 完整的图片路径
  10. * @returns Promise<HTMLImageElement>
  11. */
  12. export async function loadImage(url: string): Promise<HTMLImageElement> {
  13. return new Promise((resolve, reject) => {
  14. const image = new Image();
  15. image.setAttribute("crossorigin", "anonymous");
  16. image.src = url;
  17. image.onload = () => {
  18. resolve(image);
  19. };
  20. image.onerror = () => {
  21. reject("图片载入错误");
  22. };
  23. });
  24. }
  25. // 存放裁切图的ObjectUrls
  26. let objectUrlMap = new Map<string, string>();
  27. const OBJECT_URLS_MAP_MAX_SIZE =
  28. window.APP_OPTIONS?.OBJECT_URLS_MAP_MAX_SIZE ?? 100;
  29. export async function getDataUrlForSliceConfig(
  30. image: HTMLImageElement,
  31. sliceConfig: PictureSlice,
  32. maxSliceWidth: number,
  33. urlForCache: string
  34. ) {
  35. const { i, x, y, w, h } = sliceConfig;
  36. const key = `${urlForCache}-${i}-${x}-${y}-${w}-${h}`;
  37. if (objectUrlMap.get(key)) {
  38. console.log("cached slice objectUrl");
  39. return objectUrlMap.get(key);
  40. }
  41. const canvas = document.createElement("canvas");
  42. canvas.width = Math.max(sliceConfig.w, maxSliceWidth);
  43. canvas.height = sliceConfig.h;
  44. const ctx = canvas.getContext("2d");
  45. if (!ctx) {
  46. console.log('canvas.getContext("2d") error');
  47. throw new Error("canvas ctx error");
  48. }
  49. // drawImage 画图软件透明色
  50. ctx.drawImage(
  51. image,
  52. sliceConfig.x,
  53. sliceConfig.y,
  54. sliceConfig.w,
  55. sliceConfig.h,
  56. 0,
  57. 0,
  58. sliceConfig.w,
  59. sliceConfig.h
  60. );
  61. // console.log(image, canvas.height, sliceConfig, ctx);
  62. // console.log(canvas.toDataURL());
  63. // 如果用toBlob,则产生异步,而且URL.createObjectURL还会需要手动释放
  64. // const dataurl = canvas.toDataURL();
  65. const blob: Blob = await new Promise((res) => {
  66. canvas.toBlob((b) => res(b));
  67. });
  68. const dataurl = URL.createObjectURL(blob);
  69. cacheFIFO();
  70. objectUrlMap.set(key, dataurl);
  71. return dataurl;
  72. }
  73. // 清理缓存的过时数据(清除头10张),First in first out
  74. function cacheFIFO() {
  75. if (objectUrlMap.size > OBJECT_URLS_MAP_MAX_SIZE) {
  76. const ary = [...objectUrlMap.entries()];
  77. const toRelease = ary.splice(0, 10);
  78. // 为了避免部分图片还没显示就被revoke了,这里做一个延迟revoke
  79. // 此处有个瑕疵,缩略图的显示与试卷不是同时显示,是有可能被清除了的,只能让用户刷新了。 => 见下面的fix
  80. for (const u of toRelease) {
  81. // 如果当前图片仍在引用 objectUrl , 则将其放入缓存中
  82. if (document.querySelector(`img[src="${u[1]}"]`)) {
  83. ary.push(u);
  84. } else {
  85. // console.log("revoke ", u[1]);
  86. URL.revokeObjectURL(u[1]);
  87. }
  88. }
  89. objectUrlMap = new Map(ary);
  90. }
  91. }
  92. export async function getDataUrlForSplitConfig(
  93. image: HTMLImageElement,
  94. config: [number, number],
  95. maxSliceWidth: number,
  96. urlForCache: string
  97. ) {
  98. const [start, end] = config;
  99. const key = `${urlForCache}-${start}-${end}`;
  100. if (objectUrlMap.get(key)) {
  101. console.log("cached split objectUrl");
  102. return objectUrlMap.get(key);
  103. }
  104. const width = image.naturalWidth * (end - start);
  105. const canvas = document.createElement("canvas");
  106. canvas.width = Math.max(width, maxSliceWidth);
  107. canvas.height = image.naturalHeight;
  108. const ctx = canvas.getContext("2d");
  109. if (!ctx) {
  110. console.log('canvas.getContext("2d") error');
  111. throw new Error("canvas ctx error");
  112. }
  113. // drawImage 画图软件透明色
  114. ctx.drawImage(
  115. image,
  116. image.naturalWidth * start,
  117. 0,
  118. image.naturalWidth * end,
  119. image.naturalHeight,
  120. 0,
  121. 0,
  122. image.naturalWidth * end,
  123. image.naturalHeight
  124. );
  125. // 如果用toBlob,则产生异步,而且URL.createObjectURL还会需要手动释放
  126. // const dataurl = canvas.toDataURL();
  127. const blob: Blob = await new Promise((res) => {
  128. canvas.toBlob((b) => res(b));
  129. });
  130. const dataurl = URL.createObjectURL(blob);
  131. cacheFIFO();
  132. objectUrlMap.set(key, dataurl);
  133. return dataurl;
  134. }
  135. export async function getDataUrlForCoverConfig(
  136. image: HTMLImageElement,
  137. configs: PictureSlice[]
  138. ) {
  139. const key = `${image.src}-slice`;
  140. if (objectUrlMap.get(key)) {
  141. return objectUrlMap.get(key);
  142. }
  143. const canvas = document.createElement("canvas");
  144. canvas.width = image.naturalWidth;
  145. canvas.height = image.naturalHeight;
  146. const ctx = canvas.getContext("2d");
  147. if (!ctx) {
  148. console.log('canvas.getContext("2d") error');
  149. throw new Error("canvas ctx error");
  150. }
  151. ctx.drawImage(image, 0, 0);
  152. ctx.fillStyle = "#ffffff";
  153. configs.forEach((config) => {
  154. ctx.fillRect(config.x, config.y, config.w, config.h);
  155. });
  156. const blob: Blob = await new Promise((res) => {
  157. canvas.toBlob((b) => res(b));
  158. });
  159. const dataurl = URL.createObjectURL(blob);
  160. cacheFIFO();
  161. objectUrlMap.set(key, dataurl);
  162. return dataurl;
  163. }
  164. export async function preDrawImage(_currentTask: Task | undefined) {
  165. // console.log("preDrawImage=>curTask:", _currentTask);
  166. if (!_currentTask?.taskId) return;
  167. let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
  168. // const hasSliceConfig = store.currentTask?.sliceConfig?.length;
  169. const hasSliceConfig = _currentTask?.sliceConfig?.length;
  170. const images = [];
  171. if (hasSliceConfig) {
  172. // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
  173. const sliceNum = _currentTask.sliceUrls.length;
  174. if (_currentTask.sliceConfig.some((v) => v.i > sliceNum)) {
  175. console.warn("裁切图设置的数量小于该学生的总图片数量");
  176. }
  177. _currentTask.sliceConfig = _currentTask.sliceConfig.filter(
  178. (v) => v.i <= sliceNum
  179. );
  180. for (const sliceConfig of _currentTask.sliceConfig) {
  181. const url = _currentTask.sliceUrls[sliceConfig.i - 1];
  182. const image = await loadImage(url);
  183. images[sliceConfig.i] = image;
  184. const { x, y, w, h } = sliceConfig;
  185. x < 0 && (sliceConfig.x = 0);
  186. y < 0 && (sliceConfig.y = 0);
  187. w > 1 && (sliceConfig.w = 1);
  188. h > 1 && (sliceConfig.h = 1);
  189. if (sliceConfig.w === 0 && sliceConfig.h === 0) {
  190. // 选择整图时,w/h 为0
  191. sliceConfig.w = image.naturalWidth;
  192. sliceConfig.h = image.naturalHeight;
  193. }
  194. if (x <= 1 && y <= 1 && sliceConfig.w <= 1 && sliceConfig.h <= 1) {
  195. sliceConfig.x = image.naturalWidth * x;
  196. sliceConfig.y = image.naturalHeight * y;
  197. sliceConfig.w = image.naturalWidth * w;
  198. sliceConfig.h = image.naturalHeight * h;
  199. }
  200. }
  201. maxSliceWidth = Math.max(..._currentTask.sliceConfig.map((v) => v.w));
  202. // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
  203. for (const sliceConfig of _currentTask.sliceConfig) {
  204. const url = _currentTask.sliceUrls[sliceConfig.i - 1];
  205. const image = images[sliceConfig.i];
  206. try {
  207. await getDataUrlForSliceConfig(image, sliceConfig, maxSliceWidth, url);
  208. } catch (error) {
  209. console.log("preDrawImage failed: ", error);
  210. }
  211. }
  212. } else {
  213. for (const url of _currentTask.sliceUrls) {
  214. const image = await loadImage(url);
  215. images.push(image);
  216. }
  217. const splitConfigPairs = store.setting.splitConfig.reduce<
  218. [number, number][]
  219. >((a, v, index) => {
  220. index % 2 === 0 ? a.push([v, -1]) : (a.at(-1)![1] = v);
  221. return a;
  222. }, []);
  223. const maxSplitConfig = Math.max(...store.setting.splitConfig);
  224. maxSliceWidth =
  225. Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
  226. for (const url of _currentTask.sliceUrls) {
  227. for (const config of splitConfigPairs) {
  228. const indexInSliceUrls = _currentTask.sliceUrls.indexOf(url) + 1;
  229. const image = images[indexInSliceUrls - 1];
  230. try {
  231. await getDataUrlForSplitConfig(image, config, maxSliceWidth, url);
  232. } catch (error) {
  233. console.log("preDrawImage failed: ", error);
  234. }
  235. }
  236. }
  237. }
  238. }
  239. export async function processSliceUrls(_currentTask: Task | undefined) {
  240. if (!_currentTask?.taskId) return;
  241. const getNum = (num) => Math.max(Math.min(1, num), 0);
  242. const sheetUrls = _currentTask.sheetUrls || [];
  243. const sheetConfig = (store.setting.sheetConfig || []).map((item) => {
  244. return { ...item };
  245. });
  246. const urls = [];
  247. for (let i = 0; i < sheetUrls.length; i++) {
  248. const url = sheetUrls[i];
  249. const configs = sheetConfig.filter((item) => item.i === i + 1);
  250. if (!configs.length) {
  251. urls[i] = url;
  252. continue;
  253. }
  254. const image = await loadImage(url);
  255. configs.forEach((item) => {
  256. item.x = image.naturalWidth * getNum(item.x);
  257. item.y = image.naturalHeight * getNum(item.y);
  258. item.w = image.naturalWidth * getNum(item.w);
  259. item.h = image.naturalHeight * getNum(item.h);
  260. });
  261. urls[i] = await getDataUrlForCoverConfig(image, configs);
  262. }
  263. return urls;
  264. }
  265. export async function preDrawImageHistory(_currentTask: Task | undefined) {
  266. console.log("preDrawImageHistory=>curTask:", _currentTask);
  267. if (!_currentTask?.taskId) return;
  268. let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
  269. // const hasSliceConfig = store.currentTask?.sliceConfig?.length;
  270. const hasSliceConfig = _currentTask?.sliceConfig?.length;
  271. // _currentTask.sheetUrls = ["/1-1.jpg", "/1-2.jpg"];
  272. _currentTask.sliceUrls = await processSliceUrls(_currentTask);
  273. const images = [];
  274. if (hasSliceConfig) {
  275. // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
  276. const sliceNum = _currentTask.sliceUrls.length;
  277. if (_currentTask.sliceConfig.some((v) => v.i > sliceNum)) {
  278. console.warn("裁切图设置的数量小于该学生的总图片数量");
  279. }
  280. _currentTask.sliceConfig = _currentTask.sliceConfig.filter(
  281. (v) => v.i <= sliceNum
  282. );
  283. for (const sliceConfig of _currentTask.sliceConfig) {
  284. const url = _currentTask.sliceUrls[sliceConfig.i - 1];
  285. const image = await loadImage(url);
  286. images[sliceConfig.i] = image;
  287. const { x, y, w, h } = sliceConfig;
  288. x < 0 && (sliceConfig.x = 0);
  289. y < 0 && (sliceConfig.y = 0);
  290. w > 1 && (sliceConfig.w = 1);
  291. h > 1 && (sliceConfig.h = 1);
  292. if (sliceConfig.w === 0 && sliceConfig.h === 0) {
  293. // 选择整图时,w/h 为0
  294. sliceConfig.w = image.naturalWidth;
  295. sliceConfig.h = image.naturalHeight;
  296. }
  297. if (x <= 1 && y <= 1 && sliceConfig.w <= 1 && sliceConfig.h <= 1) {
  298. sliceConfig.x = image.naturalWidth * x;
  299. sliceConfig.y = image.naturalHeight * y;
  300. sliceConfig.w = image.naturalWidth * w;
  301. sliceConfig.h = image.naturalHeight * h;
  302. }
  303. }
  304. maxSliceWidth = Math.max(..._currentTask.sliceConfig.map((v) => v.w));
  305. // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
  306. for (const sliceConfig of _currentTask.sliceConfig) {
  307. const url = _currentTask.sliceUrls[sliceConfig.i - 1];
  308. const image = images[sliceConfig.i];
  309. try {
  310. await getDataUrlForSliceConfig(image, sliceConfig, maxSliceWidth, url);
  311. } catch (error) {
  312. console.log("preDrawImage failed: ", error);
  313. }
  314. }
  315. } else {
  316. for (const url of _currentTask.sliceUrls) {
  317. const image = await loadImage(url);
  318. images.push(image);
  319. }
  320. const splitConfigPairs = store.setting.splitConfig.reduce<
  321. [number, number][]
  322. >((a, v, index) => {
  323. index % 2 === 0 ? a.push([v, -1]) : (a.at(-1)![1] = v);
  324. return a;
  325. }, []);
  326. const maxSplitConfig = Math.max(...store.setting.splitConfig);
  327. maxSliceWidth =
  328. Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
  329. for (const url of _currentTask.sliceUrls) {
  330. for (const config of splitConfigPairs) {
  331. const indexInSliceUrls = _currentTask.sliceUrls.indexOf(url) + 1;
  332. const image = images[indexInSliceUrls - 1];
  333. try {
  334. await getDataUrlForSplitConfig(image, config, maxSliceWidth, url);
  335. } catch (error) {
  336. console.log("preDrawImage failed: ", error);
  337. }
  338. }
  339. }
  340. }
  341. }
  342. export function addFileServerPrefixToTask(rawTask: Task): Task {
  343. const newTask = JSON.parse(JSON.stringify(rawTask)) as Task;
  344. return newTask;
  345. }
  346. export function addHeaderTrackColorAttr(headerTrack: any): any {
  347. return headerTrack.map((item: any) => {
  348. item.color = "green";
  349. return item;
  350. });
  351. }
  352. /**
  353. * 获取随机code,默认获取16位
  354. * @param {Number} len 推荐8的倍数
  355. *
  356. */
  357. export function randomCode(len = 16): string {
  358. if (len <= 0) return;
  359. const steps = Math.ceil(len / 8);
  360. const stepNums = [];
  361. for (let i = 0; i < steps; i++) {
  362. const ranNum = Math.random().toString(32).slice(-8);
  363. stepNums.push(ranNum);
  364. }
  365. return stepNums.join("");
  366. }
  367. /**
  368. * 解析url获取指定参数值
  369. * @param urlStr url
  370. * @param paramName 参数名
  371. * @returns 参数值
  372. */
  373. export function parseHrefParam(
  374. urlStr: string,
  375. paramName: string = null
  376. ): string | object {
  377. if (!urlStr) return;
  378. const url = new URL(urlStr);
  379. const urlParams = new URLSearchParams(url.search);
  380. if (paramName) return urlParams.get(paramName);
  381. const params = {};
  382. for (const kv of urlParams.entries()) {
  383. params[kv[0]] = kv[1];
  384. }
  385. return params;
  386. }
  387. /**
  388. * 计算总数
  389. * @param {Array} dataList 需要统计的数组
  390. */
  391. export function calcSum(dataList: number[]): number {
  392. if (!dataList.length) return 0;
  393. return dataList.reduce(function (total, item) {
  394. return total + item;
  395. }, 0);
  396. }
  397. /** 获取数组最大数 */
  398. export function maxNum(dataList: number[]): number {
  399. if (!dataList.length) return 0;
  400. return Math.max.apply(null, dataList);
  401. }