You need to sign in or sign up before continuing.
tools.js 10.3 KB
/*
 * @Date: 2022-04-18 15:59:42
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2026-01-24 14:35:27
 * @FilePath: /mlaj/src/utils/tools.js
 * @Description: 文件描述
 */
import dayjs from 'dayjs';

// 格式化时间
const formatDate = (date) => {
  return dayjs(date).format('YYYY-MM-DD HH:mm');
};

/**
 * @description 判断浏览器属于平台
 * @returns
 */
const wxInfo = () => {
  let u = navigator.userAgent;
  let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //android终端或者uc浏览器
  let isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
  let isMobile = u.indexOf('Android') > -1 || u.indexOf('iPhone') > -1 || u.indexOf('iPad') > -1; // 移动端平台
  let isIpad = u.indexOf('iPad') > -1; // iPad平台
  let uAgent = navigator.userAgent.toLowerCase();
  let isWeiXin = (uAgent.match(/MicroMessenger/i) == 'micromessenger') ? true : false;
  let isWeiXinDesktop = isWeiXin && uAgent.indexOf('wechat') > -1 ? true : false;
  let isPC = (uAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone|micromessenger)/i)) ? false : true;
  let isIOS = /iphone|ipad|ipod/.test(uAgent);       // iOS设备
  let isWeChatBrowser = /micromessenger/.test(uAgent); // 微信浏览器
  let isIOSWeChat = isIOS && isWeChatBrowser;
  return {
    isAndroid,
    isiOS,
    isWeiXin,
    isMobile,
    isIpad,
    isPC,
    isWeiXinDesktop,
    isIOSWeChat
  };
};

/**
 * @description 判断多行省略文本
 * @param {*} id 目标dom标签
 * @returns
 */
const hasEllipsis = (id) => {
  let oDiv = document.getElementById(id);
  let flag = false;
  if (oDiv.scrollHeight > oDiv.clientHeight) {
    flag = true
  }
  return flag
}

/**
 * @description 解析URL参数
 * @param {*} url
 * @returns
 */
const parseQueryString = url => {
  var json = {};
  var arr = url.indexOf('?') >= 0 ? url.substr(url.indexOf('?') + 1).split('&') : [];
  arr.forEach(item => {
    var tmp = item.split('=');
    json[tmp[0]] = decodeURIComponent(tmp[1]);
  });
  return json;
}

/**
 * 字符串包含字符数组中字符的状态
 * @param {*} array 字符数组
 * @param {*} str 字符串
 * @returns 包含状态
 */
const strExist = (array, str) => {
  const exist = array.filter(arr => {
    if (str.indexOf(arr) >= 0) return str;
  })
  return exist.length > 0
}

// 获取参数key/value值对
const getUrlParams = (url) => {
  // 没有参数处理
  if (url.split('?').length === 1) return false;
  let arr = url.split('?');
  let res = arr[1].split('&');
  let items = {};
  for (let i = 0; i < res.length; i++) {
    let [key, value] = res[i].split('=');
    items[key] = value;
  }
  return items
}

// 格式化URL参数为字符串
const stringifyQuery = (params) => {
  const queryString = [];
  Object.keys(params || {}).forEach((k) => {
    queryString.push(k + '=' + params[k]);
  });

  return '?' + queryString.join('&');
};

// 格式化时长(秒转换为可读格式)
const formatDuration = (seconds) => {
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const remainingSeconds = seconds % 60;

  let result = '';
  if (hours > 0) {
    result += `${hours}小时`;
  }
  if (minutes > 0) {
    result += `${minutes}分钟`;
  }
  if (remainingSeconds > 0 || result === '') {
    result += `${remainingSeconds}秒`;
  }
  return result;
};

/**
 * @description 为 CDN 图片追加七牛压缩参数,降低首页等场景的图片体积。
 * @param {string} src 原始图片地址
 * @param {number} [width=200] 缩略图宽度(像素)
 * @param {number} [quality=70] 图片质量(1-100)
 * @returns {string} 处理后的图片地址
 */
const buildCdnImageUrl = (src, width = 200, quality = 70) => {
  const url = typeof src === 'string' ? src : '';
  if (!url) return '';

  // 已包含七牛处理参数时不重复追加,避免不确定行为
  if (url.includes('imageMogr2')) return url;

  try {
    const u = new URL(url, window.location.origin);
    // 兼容多个 CDN 域名:只要域名前缀以 cdn 开头,就允许追加七牛参数
    // 例如:cdn.ipadbiz.cn / cdn.xxx.com / cdn1.xxx.com
    if (!/^cdn/i.test(u.hostname)) return url;

    const [base, hash] = url.split('#');
    const param = `imageMogr2/thumbnail/${width}x/strip/quality/${quality}`;
    const next = base + (base.includes('?') ? '&' : '?') + param;
    return hash ? `${next}#${hash}` : next;
  } catch (e) {
    return url;
  }
};

/**
 * @description 归一化“打卡任务列表”字段,避免各页面散落 map 导致漏改。
 * @param {Array<any>} list 原始任务列表(通常为接口返回 task_list/timeout_task_list)
 * @returns {Array<{id: number|string, name: string, task_type: string, is_gray: boolean, is_finish: boolean, checkin_subtask_id: number|string|undefined}>}
 */
const normalizeCheckinTaskItems = (list) => {
  const raw_list = Array.isArray(list) ? list : [];
  return raw_list.map((item) => {
    const safe_item = item || {};
    const id = safe_item.id;
    const name = safe_item.title || safe_item.name || '';
    const task_type = safe_item.task_type || safe_item.taskType || '';
    const is_gray = !!safe_item.is_gray;
    const is_finish = !!safe_item.is_finish;
    const checkin_subtask_id = safe_item.checkin_subtask_id || safe_item.subtask_id;
    return {
      id,
      name,
      task_type,
      is_gray,
      is_finish,
      checkin_subtask_id
    };
  });
};


/**
 * @description 归一化作业提交类型配置(兼容多种后端格式),并提取各类型的上传大小限制(MB)。
 * @param {any} attachment_type 后端返回的 attachment_type 字段,可能是多种结构。
 * @returns {{options: Array<{key: string, value: string}>, upload_size_limit_mb_map: (null|{image?: number, video?: number, audio?: number})}}
 */
const normalizeAttachmentTypeConfig = (attachment_type) => {
  const type_map = {
    text: '文本',
    image: '图片',
    audio: '音频',
    video: '视频'
  };

  // 支持的类型 key(用于过滤未知字段,避免误把 max_size / 其他字段当成类型)
  const known_type_keys = ['text', 'image', 'audio', 'video'];
  const upload_size_limit_mb_map = {};
  let options = [];

  if (Array.isArray(attachment_type)) {
    const first = attachment_type[0];
    if (first && typeof first === 'object' && !Array.isArray(first)) {
      // 情况1:新结构(数组对象风格),例如:
      // [{ type: 'image', max_size: 500 }, { type: 'video', max_size: 1000 }]
      const has_type_style = attachment_type.some(
        (item) => item && typeof item === 'object' && !Array.isArray(item) && ('type' in item || 'max_size' in item)
      );

      if (has_type_style) {
        // 生成“可选类型”列表
        options = attachment_type
          .map((item) => {
            const key = item?.type || item?.key || item?.name || '';
            return {
              key,
              value: type_map[key] || key
            };
          })
          .filter((item) => !!item.key);

        // 提取上传大小限制(只处理 image/video/audio)
        attachment_type.forEach((item) => {
          const key = item?.type || item?.key || item?.name;
          const size = Number(item?.max_size);
          if ((key === 'image' || key === 'video' || key === 'audio') && Number.isFinite(size) && size > 0) {
            upload_size_limit_mb_map[key] = size;
          }
        });
      } else {
        // 情况2:映射结构(数组里装对象映射),例如:
        // [{ image: 500, video: 500 }]
        // 这里先合并为一个对象,再统一处理
        const merged = {};
        attachment_type.forEach((item) => {
          if (item && typeof item === 'object' && !Array.isArray(item)) {
            Object.assign(merged, item);
          }
        });

        // 映射结构里 key 就是类型,value 就是大小(MB)
        const keys = Object.keys(merged).filter((k) => known_type_keys.includes(k));
        options = keys.map((key) => ({
          key,
          value: type_map[key] || key
        }));

        ['image', 'video', 'audio'].forEach((key) => {
          const size = Number(merged[key]);
          if (Number.isFinite(size) && size > 0) {
            upload_size_limit_mb_map[key] = size;
          }
        });
      }
    } else {
      // 情况3:旧结构(字符串数组),例如:['image', 'video']
      options = attachment_type.map((key) => ({
        key,
        value: type_map[key] || key
      }));
    }
  } else if (attachment_type && typeof attachment_type === 'object') {
    // 情况4:对象结构(可能是类型中文映射,也可能是类型->大小)
    const keys = Object.keys(attachment_type);
    const has_size_mapping = keys.some((k) => (k === 'image' || k === 'video' || k === 'audio') && Number.isFinite(Number(attachment_type[k])));

    if (has_size_mapping) {
      // 类型 -> 大小(MB),例如:{ image: 500, video: 1000 }
      options = keys
        .filter((k) => known_type_keys.includes(k))
        .map((key) => ({
          key,
          value: type_map[key] || key
        }));

      ['image', 'video', 'audio'].forEach((key) => {
        const size = Number(attachment_type[key]);
        if (Number.isFinite(size) && size > 0) {
          upload_size_limit_mb_map[key] = size;
        }
      });
    } else {
      // 类型 -> 中文显示,或其他自定义结构,例如:{ image: '图片', video: '视频' }
      options = Object.entries(attachment_type).map(([key, value]) => ({
        key,
        value
      }));
    }
  } else {
    options = [];
  }

  if (options.length === 0) {
    // 兜底:后端未配置时,默认给出四种类型
    options = [
      { key: 'text', value: '文本' },
      { key: 'image', value: '图片' },
      { key: 'audio', value: '音频' },
      { key: 'video', value: '视频' }
    ];
  }

  return {
    options,
    // 没解析到任何大小限制时,返回 null,避免覆盖 useCheckin 内部默认值
    upload_size_limit_mb_map: Object.keys(upload_size_limit_mb_map).length ? upload_size_limit_mb_map : null
  };
};

export {
  formatDate,
  wxInfo,
  hasEllipsis,
  parseQueryString,
  strExist,
  getUrlParams,
  stringifyQuery,
  formatDuration,
  buildCdnImageUrl,
  normalizeCheckinTaskItems,
  normalizeAttachmentTypeConfig,
};