剧本杀活动通知生成器

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

目前为 2024-03-11 提交的版本。查看 最新版本

// ==UserScript==
// @name         剧本杀活动通知生成器
// @namespace    https://github.com/heiyexing
// @version      2024-03-12
// @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;
  }

  function chineseToArabic(chineseNum) {
    let num = chineseNum
      .replace(/零/g, '0')
      .replace(/一/g, '1')
      .replace(/二/g, '2')
      .replace(/三/g, '3')
      .replace(/四/g, '4')
      .replace(/五/g, '5')
      .replace(/六/g, '6')
      .replace(/七/g, '7')
      .replace(/八/g, '8')
      .replace(/九/g, '9');
    num = num
      .replace(/十/g, '10')
      .replace(/百/g, '100')
      .replace(/千/g, '1000')
      .replace(/万/g, '10000');
    return num;
  }

  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) {
    requestAnimationFrame(() => {
      document
        .querySelector('#murder-activity-btn')
        ?.addEventListener('click', () => {
          if (list.every((item) => item.isFull)) {
            window.layui?.layer?.msg('所有活动已满人,无需生成 Markdown');
            return;
          }
          const text = `
# 📢 剧本杀活动通知

---
${list
  .filter((item) => !item.isFull)
  .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.inputValue ?? ''}·${
      item.duration
    }】[报名](${item.url})
`;
  })
  .join('')}

---
📎  本周其他剧本活动信息
${list
  .filter((item) => item.isFull)
  .map((item) => {
    return `
${item.month}月${item.date}日《${item.name}》【满】
`;
  })
  .join('')}
---
🙋‍ [玩家报名须知](https://yuque.antfin.com/yuhmb7/pksdw8/igri3gwp127v3v32?singleDoc#),防跳车押金以报名页面为准!

🔜 加入钉群:14575023754,获取更多活动信息!

`;

          exeCommandCopyText(text);
          window.layui?.layer?.msg('已复制到剪贴板');
        });
    });
    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: 12px; height: 400px; overflow: auto;">
                  ${list
                    .map((item) => {
                      let manCount = 0;
                      let womanCount = 0;
                      let unknownCount = 0;

                      item.comments.forEach((comment) => {
                        const content = chineseToArabic(
                          getInnerText(comment.body) ?? '',
                        );
                        comment.checked = true;
                        if (/(\d+)\s?男/.test(content)) {
                          manCount += +/(\d+)\s?男/.exec(content)[1];
                        } else if (/男[\s+]*(\d+)/.test(content)) {
                          manCount += +/男[\s+]*(\d+)/.exec(content)[1];
                        } else if (/^\+?男$/.test(content)) {
                          manCount += 1;
                        } else if (/(\d+)\s?女/.test(content)) {
                          womanCount += +/(\d+)\s?女/.exec(content)[1];
                        } else if (/女[\s+]*(\d+)/.test(content)) {
                          womanCount += +/女[\s+]*(\d+)/.exec(content)[1];
                        } else if (/^\+?女$/.test(content)) {
                          womanCount += 1;
                        } else if (/\+(\d+)/.test(content)) {
                          unknownCount += +/\+(\d+)/.exec(content)[1];
                        } else if (content === '+') {
                          unknownCount += 1;
                        } else if (/\d+/.test(content)) {
                          unknownCount += +/\d+/.exec(content)[0];
                        } else {
                          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 personCount = manCount + womanCount + unknownCount;
                      const status = (() => {
                        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 '';
                      })();

                      item.isFull = status.indexOf('已满人') > -1;
                      item.inputValue = (() => {
                        if (
                          item.personCount &&
                          personCount < item.personCount
                        ) {
                          return `=${item.personCount - personCount}`;
                        }
                        if (
                          item.manCount &&
                          item.womanCount &&
                          !item.reversable
                        ) {
                          let result = '=';
                          if (manCount < item.manCount) {
                            result += `${item.manCount - manCount}男`;
                          }
                          if (womanCount < item.womanCount) {
                            result += `${item.womanCount - womanCount}女`;
                          }
                          if (result.length > 1) {
                            return result;
                          }
                        }
                        return '';
                      })();

                      const operation = document.createElement('div');
                      operation.style.width = '120px';
                      const operationId = `murder-operation-${item.uuid}`;
                      operation.id = operationId;

                      operation.style =
                        'display: flex; align-items: center;text-wrap: nowrap;';

                      const updateOperation = () => {
                        const checkboxId = `murder-checkbox-${item.uuid}`;
                        const inputId = `murder-input-${item.uuid}`;
                        let innerHTML = '';
                        if (!item.isFull) {
                          innerHTML += `<input value="${item.inputValue}" type="text" id="${inputId}" class="layui-input" style="margin-right: 6px; width: 80px;" />`;
                        }
                        innerHTML += `<input type="checkbox" id="${checkboxId}" ${
                          item.isFull ? 'checked' : ''
                        } /> 满人`;
                        const target =
                          document.querySelector(`#${operationId}`) ??
                          operation;
                        target.innerHTML = innerHTML;
                        requestAnimationFrame(() => {
                          document
                            .querySelector(`#${checkboxId}`)
                            ?.addEventListener(
                              'change',
                              (e) => {
                                item.isFull = !!e.target.checked;
                                updateOperation();
                              },
                              {
                                once: true,
                              },
                            );

                          document
                            .querySelector(`#${inputId}`)
                            ?.addEventListener('change', (e) => {
                              item.inputValue = e.target.value;
                              console.log('chagne', item.inputValue);
                            });
                        });
                      };

                      updateOperation();

                      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 + unknownCount}人</span>
                            ${operation.outerHTML}
                          </div>
                        </div>
                      </div>
                    `;
                    })
                    .join('')}
                </div>
                <div style="padding: 4px 12px; position: absolute; width: 100%; bottom: 0; left: 0; text-align: right;">
                    <button type="button" class="layui-btn" id="murder-activity-btn">生成 Markdown</button>
                </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或关注我们的公众号极客氢云获取最新地址