JS工具函数整理

2023-07-12 143 次阅读
JavaScripttypeScript

前端工具函数库

基于原文档整理优化:统一代码风格、补齐类型标注、修复已知问题、规范 JSDoc 示例。

依赖说明:除 TransformDate(dayjs)、MakeDir(Node.js fs/path)外,其余均为零依赖的纯 TypeScript 函数。

目录

  1. URL 参数处理
  2. 字符串操作
  3. 数字操作
  4. 数组操作
  5. 时间处理
  6. 网络操作
  7. 枚举与常量
  8. Vite + Vue3
  9. 杂项

优化说明

问题 处理方式
JsonToQuery 参数顺序与文档示例矛盾(示例第 2 个参数传布尔值,代码实际为 excludeKeys 参数调整为 (json, includeEmpty, excludeKeys),与示例保持一致
QueryToJson 无法解析纯 query 字符串(如 "a=1&b=2" 增加兼容逻辑,无 ? 时整体按 query 解析
QueryToJson 对值为 "0" 等 falsy 字符串会误返回 "" 改用 ?? 判断,仅对缺失字段返回 ""
FixFlatArr 同名方法重复定义互相覆盖,且方法 1 逻辑错误 保留并优化方法 2(Set + filter 分组)
makeArrByNum 未导出、未处理 start > end 导出并支持降序生成
SortArr 使用 sort 直接修改原数组,未匹配项排在最前 返回新数组,未匹配项置于末尾
MakeDir 递归实现复杂易错 改用 path.dirname + fs.mkdirSync({ recursive: true })
getTimeLong 秒数出现小数、倒计时结束不清除定时器 重写:整秒输出、归零后自动 clearInterval
函数命名不统一(小驼峰未导出与 PascalCase 导出混用) 统一为 PascalCase 并全部导出,见下表

函数重命名对照

原函数名 优化后函数名
filterArrByStartStrMax FilterArrByStartStrMax
flatArr FlatArr
makeArrByNum MakeArrByNum
getTimeLong GetTimeLong

1. URL 参数处理

query 转 json

typescript 复制代码
/**
 * query 字符串转 JSON 对象
 *
 * @param key        要获取的字段名;不传则返回整个解析后的对象
 * @param url        目标 URL 或纯 query 字符串,如 "/aaa?a=1&b=2" 或 "a=1&b=2";默认读取 location.href
 * @param joinSymbol 连接符数组 [path与query的连接符, query之间的连接符, kv连接符],默认 ["?", "&", "="]
 * @returns 传入 key 时返回对应值(不存在返回 ""),否则返回对象(值为字符串)
 *
 * @example
 * QueryToJson("", "/aaa?a=1&b=2&c=3") // => { a: "1", b: "2", c: "3" }
 * QueryToJson("a", "/aaa?a=1&b=2")    // => "1"
 * QueryToJson("a", "a=1&b=2")         // => "1"(纯 query 字符串也可解析)
 */
export const QueryToJson = (
  key = "",
  url = "",
  joinSymbol: string[] = ["?", "&", "="]
): Record<string, string> | string => {
  const [s1, s2, s3] = joinSymbol;

  if (!url) url = location.href;
  // 若 url 中不包含 s1(如直接传入 "a=1&b=2"),则整体视为 query 字符串
  const queryStr = url.includes(s1) ? url.split(s1).slice(1).join(s1) : url;
  const items = queryStr.split(s2).filter(Boolean);

  if (!items.length) return key ? "" : {};

  const json = items.reduce<Record<string, string>>((acc, item) => {
    const [k, value = ""] = item.split(s3);
    if (k) acc[k] = value;
    return acc;
  }, {});

  return key ? (json[key] ?? "") : json;
};

json 转 query

typescript 复制代码
/**
 * JSON 对象转 query 字符串
 *
 * @param json         源对象
 * @param includeEmpty 是否包含空字符串字段,默认 false;为 true 时 "" 会以 "key=" 形式输出
 * @param excludeKeys  需要排除的字段名
 * @returns 拼接好的 query 字符串(不含开头的 "?",值会经过 encodeURIComponent 编码)
 *
 * @example
 * JsonToQuery({ a: 1, b: 2, c: "" })              // => "a=1&b=2"
 * JsonToQuery({ a: 1, b: 2, c: "" }, true)        // => "a=1&b=2&c="
 * JsonToQuery({ a: 1, b: 2, c: "" }, true, ["b"]) // => "a=1&c="
 */
export const JsonToQuery = (
  json: Record<string, any> = {},
  includeEmpty = false,
  excludeKeys: string[] = []
): string => {
  const filterFn = includeEmpty
    ? (value: any) => value != null && value !== ""
    : (value: any) => Boolean(value);

  return Object.entries(json)
    .filter(([key, value]) => !excludeKeys.includes(key) && filterFn(value))
    .map(([key, value]) => `${key}=${encodeURIComponent(value ?? "")}`)
    .join("&");
};

2. 字符串操作

根据下标返回对应文字

typescript 复制代码
/**
 * 根据下标返回对应文字(常用于行政级别 / 层级标签)
 *
 * @param level   级别,从 1 开始
 * @param textArr 级别对应的文字数组
 * @returns 对应文字;越界时返回 ""
 *
 * @example
 * LevelToLabel(1, ["省", "市", "区县", "街道"]) // => "省"
 */
export const LevelToLabel = (
  level: number,
  textArr: string[] = ["省", "市", "区县", "街道"]
): string => textArr[level - 1] ?? "";

3. 数字操作

数字转千分位

typescript 复制代码
/**
 * 数字转千分位
 *
 * @param num 数值
 * @returns 千分位字符串;无效值返回 0
 *
 * @example
 * NumberToLocal(260000) // => "260,000"
 */
export const NumberToLocal = (num?: number): string | number =>
  num ? num.toLocaleString() : 0;

补位函数

typescript 复制代码
/**
 * 补位函数:大于 9 的数值原样返回,否则前面补 0
 *
 * @param count 数值或数字字符串
 * @returns 补零后的字符串;大于 9 时原样返回
 *
 * @example
 * RepairZero(9)  // => "09"
 * RepairZero(10) // => 10
 */
export const RepairZero = (count: string | number): string | number =>
  Number(count) > 9 ? count : "0" + count;

4. 数组操作

数组转对象

typescript 复制代码
/**
 * 数组转对象
 *
 * @param arr      原数组
 * @param keyValCb 回调函数:(上一项, 当前项, 当前下标) => [key, value]
 * @returns 转换后的对象
 *
 * @example
 * ArrayToJson(["a=1", "b=2"], (_, item) => item.split("="))
 * // => { a: "1", b: "2" }
 *
 * ArrayToJson([1, 2, 3], (_, item, index) => [`tagId${index}`, item])
 * // => { tagId0: 1, tagId1: 2, tagId2: 3 }
 */
export const ArrayToJson = <T>(
  arr: T[],
  keyValCb: (prev: Record<string, any>, curr: T, index: number) => [string, any]
): Record<string, any> =>
  arr.reduce<Record<string, any>>((acc, curr, index) => {
    const [key, value] = keyValCb(acc, curr, index);
    acc[key] = value;
    return acc;
  }, {});

扁平数组按字段分组为二维数组

typescript 复制代码
/**
 * 将扁平数组按指定字段分组为二维数组
 *
 * @param data 原数组
 * @param key  分组字段名
 * @returns 二维数组,每组保持原数组内的相对顺序
 *
 * @example
 * FixFlatArr([{a:1,b:1},{a:1,b:2},{a:2,b:1}], "a")
 * // => [[{a:1,b:1},{a:1,b:2}], [{a:2,b:1}]]
 */
export const FixFlatArr = <T extends Record<string, any>>(
  data: T[] = [],
  key = "id"
): T[][] => {
  const fieldValues = [...new Set(data.map((e) => e[key]))];
  return fieldValues.map((v) => data.filter((e) => e[key] === v));
};

注:分组字段值请保证类型一致,使用 === 严格比较。

过滤出首字母相同且出现次数最多的项

typescript 复制代码
/**
 * 从字符串数组中过滤出首字母相同且出现次数最多的项
 *
 * @param arr   原数组
 * @param isRel 是否区分大小写,默认 true
 * @returns 过滤后的数组
 *
 * @example
 * FilterArrByStartStrMax(
 *   ["adwafda", "Addddw", "swdA", "adawf", "sDWW", "Addfawf", "Afffff"]
 * )
 * // => ["Addddw", "Addfawf", "Afffff"]
 *
 * FilterArrByStartStrMax(
 *   ["adwafda", "Addddw", "swdA", "adawf", "sDWW", "Addfawf", "Afffff"],
 *   false
 * )
 * // => ["adwafda", "Addddw", "adawf", "Addfawf", "Afffff"]
 */
export const FilterArrByStartStrMax = (
  arr: string[] = [],
  isRel = true
): string[] => {
  const countMap = arr.reduce<Record<string, number>>((acc, str) => {
    const firstChar = isRel ? str.charAt(0) : str.charAt(0).toLowerCase();
    acc[firstChar] = (acc[firstChar] ?? 0) + 1;
    return acc;
  }, {});

  const maxCount = Math.max(...Object.values(countMap));
  const maxFirstChar = Object.entries(countMap).find(
    ([, count]) => count === maxCount
  )![0];

  return arr.filter((item) => {
    const firstChar = isRel ? item.charAt(0) : item.charAt(0).toLowerCase();
    return firstChar === maxFirstChar;
  });
};

树形结构扁平化(保留叶子节点)

typescript 复制代码
/**
 * 树形结构扁平化,只保留叶子节点
 *
 * @param data       树形数组
 * @param childField 子节点字段名
 * @returns 扁平化后的叶子节点数组
 *
 * @example
 * FlatArr([{ id: 1, children: [{ id: 2 }, { id: 3 }] }, { id: 4 }])
 * // => [{ id: 2 }, { id: 3 }, { id: 4 }]
 */
export const FlatArr = <T extends Record<string, any>>(
  data: T[] = [],
  childField = "children"
): T[] => {
  const result: T[] = [];
  const deep = (arr: T[]) => {
    arr.forEach((item) => {
      const children = item[childField];
      if (Array.isArray(children) && children.length > 0) {
        deep(children);
      } else {
        result.push(item);
      }
    });
  };
  deep(data);
  return result;
};

注:如需保留含子节点的父节点,可在 else 分支改为 result.push(item) 后继续递归,按需调整。

多个等长数组合并为对象数组

typescript 复制代码
/**
 * 将多个等长数组按索引合并为对象数组
 *
 * @param obj 键为字段名、值为等长数组的对象
 * @returns 合并后的对象数组;对象为空时返回 []
 *
 * @example
 * AssignArr({ a: [1,2,3], b: [4,5,6], c: [7,8,9] })
 * // => [{a:1,b:4,c:7}, {a:2,b:5,c:8}, {a:3,b:6,c:9}]
 */
export const AssignArr = <T = any>(
  obj: Record<string, T[]> = {}
): Record<string, T>[] => {
  const keys = Object.keys(obj);
  const firstArr = obj[keys[0]];
  if (!keys.length || !firstArr) return [];

  return firstArr.map((_, index) =>
    keys.reduce((acc, key) => {
      acc[key] = obj[key][index];
      return acc;
    }, {} as Record<string, T>)
  );
};

对象数组按指定字段顺序排序

typescript 复制代码
/**
 * 对象数组按指定字段顺序排序
 *
 * @param dataSource 原数组
 * @param order      字段值的期望顺序
 * @param field      参与排序的字段名
 * @returns 排序后的新数组(不修改原数组);未出现在 order 中的项排在末尾
 *
 * @example
 * SortArr([{id:"aaa"},{id:"ccc"},{id:"bbb"}], ["aaa","bbb","ccc"], "id")
 * // => [{id:"aaa"},{id:"bbb"},{id:"ccc"}]
 */
export const SortArr = <T extends Record<string, any>>(
  dataSource: T[] = [],
  order: string[] = [],
  field = "id"
): T[] => {
  const orderMap = new Map(order.map((v, i) => [v, i]));
  return [...dataSource].sort(
    (a, b) =>
      (orderMap.get(a[field]) ?? Number.MAX_SAFE_INTEGER) -
      (orderMap.get(b[field]) ?? Number.MAX_SAFE_INTEGER)
  );
};

对象数组去重

typescript 复制代码
/**
 * 对象数组按指定字段去重(保留首次出现的项)
 *
 * @param dataSource 原数组
 * @param key        去重依据的字段名
 * @returns 去重后的新数组
 *
 * @example
 * ReRepeat([{id:1,name:"张三"},{id:2,name:"李四"},{id:1,name:"王五"}], "id")
 * // => [{id:1,name:"张三"},{id:2,name:"李四"}]
 */
export const ReRepeat = <T extends Record<string, any>>(
  dataSource: T[],
  key: string
): T[] => {
  const map = new Map();
  return dataSource.filter(
    (item) => !map.has(item[key]) && map.set(item[key], item)
  );
};

生成树结构

typescript 复制代码
/**
 * 生成树结构
 *
 * @param data     扁平数组
 * @param relation 父子关系字段 [子字段, 父字段],默认 ["dictCode", "parentCode"]
 * @param moreSon  是否允许多个父级共享同一子级;为 true 时父字段应为逗号拼接的多个父值
 * @returns 树形结构
 *
 * @example
 * MakeTree([
 *   { dictCode: "1", parentCode: "" },
 *   { dictCode: "11", parentCode: "1" },
 *   { dictCode: "12", parentCode: "1" },
 * ])
 * // => [{ dictCode: "1", children: [{ dictCode: "11" }, { dictCode: "12" }] }]
 */
export const MakeTree = <T extends Record<string, any>>(
  data: T[],
  relation: [string, string] = ["dictCode", "parentCode"],
  moreSon = false
): T[] => {
  const [sonField, parentField] = relation;

  // 清除 children,防止重复调用时数据累加
  data.forEach((item) => delete item.children);

  // 以子字段值为 key 建立索引
  const map = new Map<string, T>();
  data.forEach((item) => map.set(item[sonField], item));

  const result: T[] = [];

  const setVal = (parent: T | undefined, item: T) => {
    if (parent) {
      parent.children || (parent.children = []);
      parent.children.push(item);
    } else {
      result.push(item);
    }
  };

  data.forEach((item) => {
    const parentCodes = moreSon
      ? String(item[parentField]).split(",")
      : [item[parentField]];
    parentCodes.forEach((code) => setVal(map.get(code), item));
  });

  return result;
};

获取树节点指定级别数据集合

typescript 复制代码
/**
 * 获取树中指定层级的所有节点
 *
 * @param treeData   树形数据
 * @param level      目标层级(根节点为 0)
 * @param childField 子节点字段名
 * @returns 该层级下的节点数组
 *
 * @example
 * GetDatasByTreeLevel([{ id: 1, children: [{ id: 11 }, { id: 12 }] }], 1)
 * // => [{ id: 11 }, { id: 12 }]
 */
export const GetDatasByTreeLevel = <T extends Record<string, any>>(
  treeData: T[] = [],
  level = 0,
  childField = "children"
): T[] => {
  const result: T[] = [];

  const deepGet = (dataSource: T[], currentLevel: number) => {
    if (currentLevel === level) {
      result.push(...dataSource);
      return;
    }
    dataSource.forEach((item) =>
      deepGet(item[childField] || [], currentLevel + 1)
    );
  };

  deepGet(treeData, 0);
  return result;
};

根据起止数值生成数组

typescript 复制代码
/**
 * 生成从 start 到 end 的数值数组
 *
 * @param start      起始值
 * @param end        结束值
 * @param includeEnd 是否包含结束值,默认 true
 * @returns 数值数组(start > end 时降序生成)
 *
 * @example
 * MakeArrByNum(1, 5)        // => [1, 2, 3, 4, 5]
 * MakeArrByNum(1, 5, false) // => [1, 2, 3, 4]
 * MakeArrByNum(5, 1)        // => [5, 4, 3, 2, 1]
 */
export const MakeArrByNum = (
  start: number,
  end: number,
  includeEnd = true
): number[] => {
  const result: number[] = [];
  const step = start <= end ? 1 : -1;

  for (let i = start; i !== end; i += step) {
    result.push(i);
  }
  if (includeEnd) result.push(end);

  return result;
};

5. 时间处理

相对时间转换

typescript 复制代码
import dayjs from "dayjs";

export interface TransformDateOptions {
  justnow: number; // 多少秒内显示 "x 秒前"
  minute: number;  // 多少分钟内显示 "x 分钟前"
  hour: number;    // 多少小时内显示 "x 小时前"
  day: number;     // 多少天内显示 "x 天前"
  month: number;   // 多少个月内显示 "x 个月前"
  format: string;  // 超出上述范围时输出的日期格式
}

/**
 * 时间转相对描述:"x 秒前 / x 分钟前 / x 小时前 / x 天前 / x 个月前"
 *
 * @param date    目标时间
 * @param options 各档位阈值与兜底格式,默认
 *                { justnow: 60, minute: 60, hour: 24, day: 30, month: 12, format: "YYYY/MM/DD" }
 * @returns 相对时间字符串
 *
 * @example
 * TransformDate("2026-08-21 12:00:00", { day: 10 }) // 超过 10 天则输出日期
 */
export const TransformDate = (
  date: string,
  options?: Partial<TransformDateOptions>
): string => {
  const defaultOptions: TransformDateOptions = {
    justnow: 60,
    minute: 60,
    hour: 24,
    day: 30,
    month: 12,
    format: "YYYY/MM/DD",
  };
  const opts = { ...defaultOptions, ...options };

  const specifiedDate = dayjs(date);
  const diffInSecond = Math.abs(specifiedDate.diff(dayjs(), "second"));
  const diffInMinute = Math.abs(specifiedDate.diff(dayjs(), "minute"));
  const diffInHours = Math.abs(specifiedDate.diff(dayjs(), "hour"));
  const diffInDay = Math.abs(specifiedDate.diff(dayjs(), "day"));
  const diffInMonth = Math.abs(specifiedDate.diff(dayjs(), "month"));

  if (diffInSecond < opts.justnow) return `${diffInSecond} 秒前`;
  if (diffInMinute <= opts.minute) return `${diffInMinute} 分钟前`;
  if (diffInHours <= opts.hour) return `${diffInHours} 小时前`;
  if (diffInDay <= opts.day) return `${diffInDay} 天前`;
  if (diffInMonth <= opts.month) return `${diffInMonth} 个月前`;

  return specifiedDate.format(opts.format);
};

注:默认档位 justnow=60 秒、minute=60 分钟、hour=24 小时、day=30 天、month=12 个月,与代码实现保持一致(原文档描述与默认值不符,已修正)。


6. 网络操作

文件下载

typescript 复制代码
/**
 * 文件下载(Blob 方式,跨域场景兼容性更好)
 *
 * @param href  文件地址
 * @param title 保存的文件名
 */
export const DownloadFile = (href: string, title: string): void => {
  const xhr = new XMLHttpRequest();
  xhr.open("GET", href, true);
  xhr.responseType = "blob";

  xhr.onload = () => {
    const url = URL.createObjectURL(xhr.response);
    const link = document.createElement("a");
    link.href = url;
    link.setAttribute("download", title);
    link.style.display = "none";
    document.body.appendChild(link);
    link.click();
    URL.revokeObjectURL(url); // 释放内存
    link.remove();
  };

  xhr.onerror = () => console.error(`下载失败:${href}`);
  xhr.send();
};

跨域 GET 获取页面内容

typescript 复制代码
/**
 * 跨域 GET 请求获取页面内容(含 IE8/9 XDomainRequest 兜底,仅历史项目需要)
 *
 * @param url      请求地址
 * @param callback 请求成功回调
 * @param timeout  超时时间(毫秒),默认 5000
 */
export const MakeCorsRequest = (
  url: string,
  callback: (responseText: string) => void,
  timeout = 5000
): void => {
  let xhr: XMLHttpRequest | any;

  if ("withCredentials" in new XMLHttpRequest()) {
    // withCredentials 是 XMLHttpRequest2 独有的属性
    xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
  } else if (typeof (window as any).XDomainRequest !== "undefined") {
    xhr = new (window as any).XDomainRequest();
    xhr.open("GET", url);
  } else {
    console.error("当前环境不支持 CORS");
    return;
  }

  let timedout = false;
  const timer = setTimeout(() => {
    timedout = true;
    xhr.abort();
  }, timeout);

  xhr.onreadystatechange = () => {
    if (xhr.readyState !== 4 || timedout) return;
    clearTimeout(timer);
    if (xhr.status === 200) callback(xhr.responseText);
    else if (xhr.status === 404) console.error("路径请求错误");
  };

  xhr.onerror = () => {
    clearTimeout(timer);
    console.error("请求错误");
  };

  xhr.send();
};

现代浏览器建议直接使用 fetch(url, { mode: "cors" }),仅兼容 IE 时需要上面的实现。

递归创建目录(Node.js)

typescript 复制代码
import fs from "fs";
import path from "path";

/**
 * 递归创建目录(Node.js 环境)
 *
 * @param filePath 文件或目录路径,如 "/a/b/c/1.png"
 *
 * @example
 * MakeDir("/a/b/c/1.png") // 创建 ./a/b/c
 */
export const MakeDir = (filePath: string): void => {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
};

7. 枚举与常量

定义枚举

typescript 复制代码
/** 检测类型枚举 */
export enum DETECTION {
  LIUL = 10, // 流量
  JWND = 20, // 甲烷浓度
  GDYL = 30, // 管道压力
  YEWE = 40, // 液位
}

定义常量

typescript 复制代码
/** 检测类型元信息 */
export const DETECTION_TYPE = {
  [DETECTION.LIUL]: { LABEL: "流量" },
  [DETECTION.JWND]: { LABEL: "甲烷浓度" },
  [DETECTION.GDYL]: { LABEL: "管道压力" },
  [DETECTION.YEWE]: { LABEL: "液位" },
};

查询方法

typescript 复制代码
/**
 * 根据嵌套对象的 key 和 value 反查外层 key 及嵌套对象
 *
 * @param data  嵌套对象
 * @param key   嵌套对象内的字段名
 * @param value 嵌套对象内字段对应的值
 * @returns [外层key, 嵌套对象];未找到返回 []
 *
 * @example
 * GET_DATA(DETECTION_TYPE, "LABEL", "流量")
 * // => ["10", { LABEL: "流量" }]
 */
export const GET_DATA = <T extends Record<string, any>>(
  data: T = {} as T,
  key: string,
  value: string
): [string, T[keyof T]] | [] => {
  const target = Object.entries(data).find(([, obj]) => obj[key] === value);
  return target ? (target as [string, T[keyof T]]) : [];
};

/**
 * 收集所有嵌套对象中指定字段的值
 *
 * @param data     嵌套对象
 * @param key      需要的字段名
 * @param excludes 需要排除的值
 * @returns 值数组
 *
 * @example
 * GET_DATA_KEYS(DETECTION_TYPE, "LABEL", ["流量"])
 * // => ["甲烷浓度", "管道压力", "液位"]
 */
export const GET_DATA_KEYS = <T extends Record<string, any>>(
  data: T = {} as T,
  key: string,
  excludes: string[] = []
): string[] =>
  Object.values(data)
    .map((obj) => obj[key])
    .filter((value) => !excludes.includes(value));

8. Vite + Vue3

获取 assets 下的文件

typescript 复制代码
/**
 * 获取 assets 目录下资源(Vite 专属)
 *
 * @param src assets 下的相对路径
 * @returns 构建后的资源地址
 *
 * @example
 * GetImage("images/arrow.png")
 * // => "http://192.168.6.192:8080/src/assets/images/arrow.png"
 */
export const GetImage = (src: string): string =>
  new URL(`../assets/${src}`, import.meta.url).href;

9. 杂项

距指定时刻的倒计时

typescript 复制代码
/**
 * 持续输出当前时间距离今日指定时刻(h:m:s)的剩余时间
 *
 * @param h 目标时(0-23)
 * @param m 目标分(0-59)
 * @param s 目标秒(0-59)
 *
 * @example
 * GetTimeLong(18, 0, 0) // 每秒输出一次 "距离下班还有:x时x分x秒",归零后自动停止
 */
export const GetTimeLong = (h: number, m: number, s: number): void => {
  const target = new Date();
  target.setHours(h, m, s, 0);

  const timer = setInterval(() => {
    const diff = target.getTime() - Date.now();

    if (diff <= 0) {
      console.log("距离下班还有:0时0分0秒");
      clearInterval(timer);
      return;
    }

    const hours = Math.floor(diff / 3_600_000);
    const minutes = Math.floor((diff % 3_600_000) / 60_000);
    const seconds = Math.floor((diff % 60_000) / 1000);
    console.log(`距离下班还有:${hours}时${minutes}分${seconds}秒`);
  }, 1000);
};

设置页面标题

typescript 复制代码
/**
 * 设置页面标题
 *
 * @param title 标题内容,默认 "webxue"
 */
export const SetTitle = (title = "webxue"): void => {
  document.title = title;
};

全屏 / 取消全屏

typescript 复制代码
/**
 * 全屏控制
 */
export const Fullscreen: {
  open: (element?: HTMLElement) => void;
  close: () => void;
} = {
  open: (element: HTMLElement = document.body) => {
    element.requestFullscreen().catch(() => {});
  },
  close: () => {
    document.exitFullscreen().catch(() => {});
  },
};

/**
 * 监听全屏状态(Vue3 + ref 用法示例)
 * 开发环境:宽或高等于屏幕即视为全屏(便于开 F12 调试)
 * 生产环境:宽高需同时等于屏幕
 */
// 假设全局存在 const isFullScreen = ref(false)
// window.addEventListener("resize", () => {
//   if (import.meta.env.DEV) {
//     isFullScreen.value =
//       document.body.clientHeight === window.screen.height ||
//       document.body.clientWidth === window.screen.width;
//   } else {
//     isFullScreen.value =
//       document.body.clientHeight === window.screen.height &&
//       document.body.clientWidth === window.screen.width;
//   }
// });

函数式选择文件

typescript 复制代码
/**
 * 函数式选择文件(返回 Promise)
 *
 * @param accept 可选的文件类型过滤,如 "image/*"、".jpg,.png"
 * @returns 选中的第一个文件
 *
 * @example
 * const file = await GetFile("image/*");
 */
export const GetFile = (accept = ""): Promise<File> =>
  new Promise((resolve) => {
    const input = document.createElement("input");
    input.type = "file";
    input.accept = accept;
    input.onchange = () => {
      resolve(input.files![0]);
      input.remove();
    };
    input.click();
  });