剧本杀活动通知生成器

用于获取本周剧本杀活动信息并生成 Markdown 代码

目前为 2024-02-18 提交的版本。查看 最新版本

// ==UserScript==
// @name         剧本杀活动通知生成器
// @namespace    https://github.com/heiyexing
// @version      2024-02-18
// @description  用于获取本周剧本杀活动信息并生成 Markdown 代码
// @author       炎熊
// @match        https://yuque.antfin-inc.com/yuhmb7/pksdw8/**
// @match        https://yuque.antfin.com/yuhmb7/pksdw8/**
// @icon         https://www.google.com/s2/favicons?sz=64&domain=antfin-inc.com
// @require      https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.9/dayjs.min.js
// @require      https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.9/plugin/isSameOrAfter.js
// @require      https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.9/plugin/isSameOrBefore.js
// @require      https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.9/locale/zh-cn.min.js
// @require      https://cdn.bootcdn.net/ajax/libs/layui/2.8.17/layui.min.js
// @run-at       document-end
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  dayjs.locale(dayjs_locale_zh_cn);
  dayjs.extend(dayjs_plugin_isSameOrAfter);
  dayjs.extend(dayjs_plugin_isSameOrBefore);

  const BTN_ID = 'murder-mystery-btn';
  const USER_LIST_CLASS_NAME = 'murder-user-list';
  const USER_ITEM_CLASS_NAME = 'murder-user-item';

  let timeRange = [dayjs().startOf('week'), dayjs().endOf('week')];

  function initStyle() {
    const style = document.createElement('style');
    style.innerHTML = `
            #${BTN_ID} {
                position: fixed;
                bottom: 25px;
                right: 80px;
                width: 40px;
                height: 40px;
                background-color: #fff;
                border-radius: 50%;
                box-shadow: 0 0 10px rgba(0, 0, 0, .2);
                cursor: pointer;
                display: inline-flex;
                justify-content: center;
                align-items: center;
                z-index: 2;
            }
            #${BTN_ID} img {
                width: 20px;
            }
            .${USER_LIST_CLASS_NAME} {
              display: flex;
              flex-wrap: wrap;
            }
            .${USER_ITEM_CLASS_NAME} {
              margin-right: 12px;
              margin-bottom: 12px;
              display: flex;
              justify-content: space-between;
              align-items: center;
              flex-wrap: wrap;
              line-height: 14px;
              border-radius: 6px;
              padding: 6px;
              border: 1px solid #E7E9E8;
            }
            .${USER_ITEM_CLASS_NAME}.unchecked {
              border-color: #ff0000;
            }
            .${USER_ITEM_CLASS_NAME} span {
              white-space: nowrap;
            }
            .${USER_ITEM_CLASS_NAME} img {
              width: 30px;
              height: 30px;
              border-radius: 30px;
              margin-right: 6px;
            }
            .layui-card-body {
              width: 100%;
            }
            .layui-card-footer {
              display: flex;
              justify-content: space-between;
              align-items: center;
            }
            `;
    const link = document.createElement('link');
    link.setAttribute('rel', 'stylesheet');
    link.setAttribute('type', 'text/css');
    link.href =
      'https://cdn.bootcdn.net/ajax/libs/layui/2.8.17/css/layui.min.css';
    document.head.appendChild(style);
    document.head.appendChild(link);
    return style;
  }

  function initBtn() {
    const btn = document.createElement('div');
    btn.id = BTN_ID;
    const logo = document.createElement('img');
    logo.src =
      'https://mdn.alipayobjects.com/huamei_baaa7a/afts/img/A*f8MvQYdbHPoAAAAAAAAAAAAADqSCAQ/original';
    btn.appendChild(logo);
    document.body.appendChild(btn);
    return btn;
  }

  function getTitleInfo(title) {
    const month = title.match(/\d+(?=\s*月)/)?.[0];
    const date = title.match(/\d+(?=\s*日)/)?.[0];
    const name = title.match(/(?<=《).*?(?=》)/)?.[0];
    if (!month || !date || !name) {
      return null;
    }
    return {
      month: +month,
      date: +date,
      name,
    };
  }

  function getRegExpStr(strList, regexp) {
    for (const str of strList) {
      const result = str.match(regexp);
      if (result) {
        return result[0].trim();
      }
    }
    return '';
  }

  function exeCommandCopyText(text) {
    try {
      const t = document.createElement('textarea');
      t.nodeValue = text;
      t.value = text;
      document.body.appendChild(t);
      t.select();
      document.execCommand('copy');
      document.body.removeChild(t);
      return true;
    } catch (e) {
      console.log(e);
      return false;
    }
  }

  function getInnerText(content) {
    const div = document.createElement('div');
    div.style = 'height: 0px; overflow: hidden;';
    div.innerHTML = content;
    document.body.appendChild(div);
    return div.innerText;
  }

  async function getActivesInfo(start, end) {
    if (!window.appData || !Array.isArray(window.appData?.book.toc)) {
      return;
    }
    const tocList = window.appData?.book.toc;
    const pathList = location.pathname.split('/');
    if (pathList.length <= 0) {
      return;
    }
    const docUrl = pathList[pathList.length - 1];
    const currentToc = tocList.find((item) => item.url === docUrl);
    if (!currentToc) {
      return;
    }
    const parentToc = tocList.find(
      (item) => item.uuid === currentToc.parent_uuid,
    );
    if (!parentToc) {
      return;
    }
    const targetTocList = tocList.filter(
      (item) => item.parent_uuid === parentToc.uuid,
    );

    const targetTimeRangeList = targetTocList
      .map((item) => {
        const titleInfo = getTitleInfo(item.title);
        if (!titleInfo) {
          return item;
        }
        return {
          ...item,
          ...titleInfo,
          dayjs: dayjs()
            .set('month', titleInfo.month - 1)
            .set('date', titleInfo.date),
        };
      })
      .filter((item) => {
        return (
          item.dayjs.isSameOrAfter(start, 'date') &&
          item.dayjs.isSameOrBefore(end, 'date')
        );
      })
      .sort((a, b) => a.dayjs - b.dayjs);

    return await Promise.all(
      targetTimeRangeList.map((item) => {
        return fetch(
          `${location.origin}/api/docs/${item.url}?book_id=${window.appData?.book.id}&include_contributors=true&include_like=true&include_hits=true&merge_dynamic_data=false`,
        )
          .then((res) => res.json())
          .then((res) => {
            const rowList = getInnerText(res.data.content).split('\n');

            const tag = getRegExpStr(rowList, /(?<=类型\s*[::]\s*).+/)
              ?.split(/[/||]/)
              .join('/');

            const level = getRegExpStr(
              rowList,
              /(?<=(难度|适合)\s*[::\s*]).+/,
            );

            const dm = getRegExpStr(rowList, /(?<=(dm|DM)\s*[::]\s*).+/);

            let place = getRegExpStr(rowList, /(?<=(地点|场地)\s*[::]\s*).+/);

            if (/[Aa]\s?空间/.test(place)) {
              place = 'A空间';
            }
            if (/元空间/.test(place)) {
              place = '元空间';
            }

            const persons = getRegExpStr(rowList, /(?<=(人数)\s*[::]\s*).+/)
              .split(/[,,\(\)()「」]/)
              .map((item) => item.replace(/(回复报名|注明男女|及人数)/, ''))
              .filter((item) => item.trim())
              .join('·');

            const manCount = +persons.match(/(\d+)\s?男/)?.[1] || undefined;
            const womanCount = +persons.match(/(\d+)\s?女/)?.[1] || undefined;
            const personCount = (() => {
              if (manCount && womanCount) {
                return manCount + womanCount;
              }
              if (/(\d+)[~~到-](\d+)/.test(persons.replace(/\s/g, ''))) {
                return +/(\d+)[~~到-](\d+)/.exec(
                  persons.replaceAll(' ', ''),
                )[1];
              }
              if (/(\d+)人/.test(persons.replaceAll(/\s/g, ''))) {
                return +/(\d+)人/.exec(persons.replaceAll(' ', ''))[1];
              }
              return undefined;
            })();

            const reversable = !/不[^反]*反串/.test(persons);

            const week =
              getRegExpStr(rowList, /周[一二三四五六日]/) ||
              `周${
                ['日', '一', '二', '三', '四', '五', '六'][item.dayjs.day()]
              }`;

            const time = getRegExpStr(rowList, /\d{1,2}[::]\d{2}/);

            const [hour = '', minute = ''] = time.split(/[::]/);

            const duration = getRegExpStr(
              rowList,
              /(?<=(预计时.|时长)\s*[::]\s*).+/,
            ).replace(/(h|小时)/, 'H');

            const url = `https://yuque.antfin.com/yuhmb7/pksdw8/${item.url}?singleDoc#`;

            return {
              ...item,
              tag,
              level,
              dm,
              week,
              hour,
              minute,
              place,
              persons,
              duration,
              url,
              manCount,
              womanCount,
              personCount,
              reversable,
            };
          });
      }),
    );
  }

  async function copyMarkdownInfo(list) {
    const text = `
  # 📢 剧本杀活动通知
  
  ---
  ${list
    .map((item) => {
      return `
  🎬 《${item.name}》${item.tag}${item.level ? `/${item.level}` : ''}
  
  🕙  ${item.month}.${item.date} ${item.week} ${item.hour}:${item.minute} 📍${
        item.place
      }
  
  💎  DM ${item.dm}【${item.persons}·${item.duration}】[报名](${item.url})
  
  ---
  `;
    })
    .join('')}
  🙋‍ [玩家报名须知](https://yuque.antfin.com/yuhmb7/pksdw8/igri3gwp127v3v32?singleDoc#),防跳车押金以报名页面为准!
  
  🔜 加入钉群:14575023754,获取更多活动信息!
  
  `;

    exeCommandCopyText(text);
    window.layui?.layer?.msg('已复制到剪贴板');
  }

  async function getCommentsList(list) {
    return Promise.all(
      list.map((item) => {
        return fetch(
          `https://yuque.antfin-inc.com/api/comments/floor?commentable_type=Doc&commentable_id=${item.id}&include_section=true&include_to_user=true&include_reactions=true`,
          {
            headers: {
              accept: 'application/json',
              'accept-language': 'zh-CN,zh;q=0.9',
              'content-type': 'application/json',
              'sec-ch-ua':
                '"Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"',
              'sec-ch-ua-mobile': '?0',
              'sec-ch-ua-platform': '"macOS"',
              'sec-fetch-dest': 'empty',
              'sec-fetch-mode': 'cors',
              'sec-fetch-site': 'same-origin',
              'x-csrf-token': '7g3LVrMMDcljwFdl3GBLLIRy',
              'x-requested-with': 'XMLHttpRequest',
            },
            referrerPolicy: 'strict-origin-when-cross-origin',
            body: null,
            method: 'GET',
            mode: 'cors',
            credentials: 'include',
          },
        )
          .then((res) => res.json())
          .then((res) => {
            return {
              ...item,
              comments: res.data.comments,
            };
          });
      }),
    );
  }

  function openActivityModal(list) {
    layui.layer.open(
      {
        type: 1, // page 层类型
        area: ['800px', '500px'],
        title: '活动报名情况',
        shade: 0.6, // 遮罩透明度
        shadeClose: true, // 点击遮罩区域,关闭弹层
        maxmin: true, // 允许全屏最小化
        anim: 0, // 0-6 的动画形式,-1 不开启
        content: `
          <div style="padding: 24px">
            ${list.map((item) => {
              let manCount = 0;
              let womanCount = 0;
              let unknownCount = 0;

              item.comments.forEach((comment) => {
                const content = getInnerText(comment.body);
                comment.checked = true;
                const MAN_REG = /(\d+)\s?男/;
                const WOMAN_REG = /(\d+)\s?女/;
                const UNKNOWN_REG = /\+(\d+)/;
                if (MAN_REG.test(content)) {
                  manCount += +MAN_REG.exec(content)[1];
                  return;
                }
                if (WOMAN_REG.test(content)) {
                  womanCount += +WOMAN_REG.exec(content)[1];
                  return;
                }
                if (UNKNOWN_REG.test(content)) {
                  unknownCount += +UNKNOWN_REG.exec(content)[1];
                  return;
                }
                comment.checked = false;
              });

              const listHTML = item.comments
                .map((comment) => {
                  const content = getInnerText(comment.body);
                  return `<a class="${USER_ITEM_CLASS_NAME} ${
                    !comment.checked ? 'unchecked' : ''
                  }" href="https://yuque.antfin-inc.com/${
                    comment.user.login
                  }" target="_blank">
                  <img src="${comment.user.avatar_url}"/>
                  <div>
                    <div>${comment.user.name}</div>
                    <div style="font-size: 12px; color: gray; margin-top: 4px;">${content}</div>
                  </div>
                </a>`;
                })
                .join('');

              const status = (() => {
                const personCount = manCount + womanCount + unknownCount;
                if (item.manCount && item.womanCount && !item.reversable) {
                  if (
                    manCount >= item.manCount &&
                    womanCount >= item.womanCount
                  ) {
                    return `<span class="layui-badge layui-bg-green">已满人</span>`;
                  }
                  if (personCount >= item.manCount + item.womanCount) {
                    return `<span class="layui-badge layui-bg-orange">满人,但男女未满</span>`;
                  }
                  return `<span class="layui-badge layui-bg-red">未满人</span>`;
                }
                if (item.personCount) {
                  if (personCount >= item.personCount) {
                    return `<span class="layui-badge layui-bg-green">已满人</span>`;
                  }
                  return `<span class="layui-badge layui-bg-red">未满人</span>`;
                }
              })();

              return `
                <div class="layui-card">
                  <div class="layui-card-header" style="display: flex; justify-content: space-between;">
                    <a href="${item.url}" target="_blank">🔗 ${item.title}</a>
  
                  </div>
                  <div class="layui-card-body">
                    <div class="${USER_LIST_CLASS_NAME}">
                      ${listHTML}
                    </div>
                    <div class="layui-card-footer">
                      <span>要求:${item.persons}</span>
                      <span>当前:${manCount}男${womanCount}女${
                unknownCount ? `${unknownCount}未知` : ''
              },共${manCount + womanCount}人</span>
                      ${status}
                    </div>
                  </div>
                </div>
              `;
            })}
          </div>
        `,
      },
      2000,
    );
  }

  function openDatePickerModal([start, end]) {
    const modalIndex = layui.layer.open(
      {
        type: 1, // page 层类型
        title: '请选择日期范围',
        shade: 0.6, // 遮罩透明度
        area: ['655px', '400px'],
        shadeClose: true, // 点击遮罩区域,关闭弹层
        maxmin: true, // 允许全屏最小化
        anim: 0, // 0-6 的动画形式,-1 不开启
        content: `
            <div style="padding: 12px">
                <div id="date"></div>
            </div>
        `,
      },
      2000,
    );
    layui.laydate.render({
      elem: '#date',
      range: true,
      type: 'date',
      rangeLinked: true,
      weekStart: 1,
      show: true,
      theme: '#0271BD',
      position: 'static',
      value: `${start.format('YYYY-MM-DD')} - ${end.format('YYYY-MM-DD')}`,
      mark: {
        [dayjs().format('YYYY-MM-DD')]: '今天',
      },
      shortcuts: [
        {
          text: '本周',
          value: [
            new Date(+dayjs().startOf('week')),
            new Date(+dayjs().endOf('week')),
          ],
        },
        {
          text: '上周',
          value: [
            new Date(+dayjs().startOf('week').subtract(1, 'week')),
            new Date(+dayjs().endOf('week').subtract(1, 'week')),
          ],
        },
        {
          text: '下周',
          value: [
            new Date(+dayjs().startOf('week').add(1, 'week')),
            new Date(+dayjs().endOf('week').add(1, 'week')),
          ],
        },
        {
          text: '本月',
          value: [
            new Date(+dayjs().startOf('month')),
            new Date(+dayjs().endOf('month')),
          ],
        },
        // 更多选项 …
      ],
      done: function (value, startDate, endDate) {
        const [startStr, endStr] = value.split(' - ');
        timeRange = [
          dayjs(startStr, 'YYYY-MM-DD'),
          dayjs(endStr, 'YYYY-MM-DD'),
        ];
        layui.dropdown.reload(BTN_ID, {
          data: getDropdownItems(),
        });
        layui.layer.close(modalIndex);
      },
    });
  }

  initStyle();
  initBtn();

  function getDropdownItems() {
    return [
      {
        title: `日期范围:${timeRange[0].format('M-D')} - ${timeRange[1].format(
          'M-D',
        )}`,
        disabled: true,
      },
      {
        title: `更改日期范围`,
        id: 'edit date range',
      },
      {
        title: '复制活动信息 Markdown',
        id: 'copy week markdown',
      },
      {
        title: '查看活动报名情况',
        id: 'check sign up',
      },
    ];
  }

  layui.dropdown.render({
    elem: `#${BTN_ID}`,
    data: getDropdownItems(),
    click: async function ({ id }) {
      let list = await getActivesInfo(...timeRange);
      if (id === 'edit date range') {
        openDatePickerModal(timeRange);
      }
      if (id === 'copy week markdown') {
        copyMarkdownInfo(list);
      }
      if (id === 'check sign up') {
        list = await getCommentsList(list);
        openActivityModal(list);
      }
    },
  });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址