Full Code of wao3/luogu-stats-card for AI

master b90de7ab4125 cached
30 files
32.5 KB
12.1k tokens
8 symbols
1 requests
Download .txt
Repository: wao3/luogu-stats-card
Branch: master
Commit: b90de7ab4125
Files: 30
Total size: 32.5 KB

Directory structure:
gitextract_b_dpcgck/

├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── api/
│   ├── guzhi.js
│   └── index.js
├── index.js
├── package.json
├── src/
│   ├── common.js
│   ├── guzhi-card.js
│   └── stats-card.js
├── tcb/
│   ├── .gitignore
│   ├── babel.config.js
│   ├── cloudbaserc.json
│   ├── functions/
│   │   ├── index/
│   │   │   ├── index.js
│   │   │   └── package.json
│   │   └── luogu/
│   │       ├── cache.js
│   │       ├── fetchStats.js
│   │       ├── index.js
│   │       ├── package.json
│   │       ├── route/
│   │       │   ├── guzhi.js
│   │       │   └── practice.js
│   │       └── test.js
│   ├── package.json
│   ├── public/
│   │   └── index.html
│   └── src/
│       ├── App.vue
│       ├── Homepage.vue
│       ├── components/
│       │   └── Luogu.vue
│       └── main.js
└── vercel.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
package-lock.json
test.svg
node_modules
.vercel

================================================
FILE: .npmignore
================================================
package-lock.json
test.svg
node_modules
.vercel
api
tcb
vercel.json

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 Anurag Hazra

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: README.md
================================================
> 注意:为了不滥用洛谷服务器流量,本项目将缓存 12 小时用户数据,即同一个用户卡片 **24 小时内最多只会向洛谷服务器请求 2 次数据**,并且只有在用户访问卡片时才会请求数据。
## 简介

![stars](https://badgen.net/github/stars/wao3/luogu-stats-card?cache=600)
![forks](https://badgen.net/github/forks/wao3/luogu-stats-card?cache=600)
![visitor](https://visitor-badge.laobi.icu/badge?page_id=luogu-stats-card)
![last commit](https://badgen.net/github/last-commit/wao3/luogu-stats-card?cache=600)
![top language](https://img.shields.io/github/languages/top/wao3/luogu-stats-card)

`luogu-stats-card`是一个动态生成洛谷用户练习数据卡片的工具,可以展示自己的做题情况。可以用于个人主页、博客、github 等可以插入图片的地方。

## TODO

- [x] ~~修复获取数据错误和用户设置数据不可见的 bug~~
- [x] ~~增加黑暗模式~~
- [x] ~~增加咕值卡片~~
- [ ] 增加用户 tag
- [ ] 缓存 top 500 用户咕值,使高估值用户可自动获取估值信息

## 效果预览

![wangao 的练习情况](https://luogu.wao3.cn/api/practice?id=313209)

![wangao 的咕值信息](https://luogu.wao3.cn/api/guzhi?id=313209&scores=100,65,45,15,0)

*(上面的咕值仅为展示效果,本人咕值并没有这么高)*

## 如何使用

### 练习情况

练习情况可以自动获取用户的数据,但是前提是没有开启“完全隐私保护”,具体使用方法如下:

1. Markdown:复制以下内容到任意 markdown 编辑器中,并将`?id=`后面的数字更改为自己的 id 即可(id 是洛谷个人主页地址的一串数字)。

   ```markdown
   ![我的练习情况](https://luogu.wao3.cn/api/practice?id=313209)
   ```

2. HTML:复制以下内容到 HTML 代码中,并将`?id=`后面的数字更改为自己的 id 即可(id 是洛谷个人主页地址的一串数字)。

    ```html
    <img alt="我的练习情况" src="https://luogu.wao3.cn/api/practice?id=313209">
    ```

### 咕值信息

咕值信息无法自动获取数据,如果需要必须要提供 cookie ,但是 这种方法十分不安全,并且不方便,所以获取咕值卡片需要手动输入咕值信息,具体使用方法如下。

复制以下内容到任意 markdown 编辑器中,并将 `?id=`后面的数字更改为自己的 id,将`scores=`后面更换为自己的咕值信息,一共 5 个数字,用逗号分隔。

1. Markdown:复制以下内容到任意 markdown 编辑器中,并将 `?id=`后面的数字更改为自己的 id,将`scores=`后面更换为自己的咕值信息,一共 5 个数字,用逗号分隔。

   ```markdown
   ![我的咕值信息](http://luogu.wao3.cn/api/guzhi?id=313209&scores=100,65,45,15,0)
   ```
   
2. HTML:复制以下内容到 HTML 代码中,并将 `?id=`后面的数字更改为自己的 id,将`scores=`后面更换为自己的咕值信息,一共 5 个数字,用逗号分隔。
   ```html
   <img alt="我的练习情况" src="https://luogu.wao3.cn/api/practice?id=313209">
   ```
   


### 自定义选项

使用卡片时,支持设定自定义效果选项,可以组合使用。

1. **隐藏标题**,只需在链接最后带上`&hide_title=true`即可,例如:

   ```markdown
   ![wangao 的练习情况](https://luogu.wao3.cn/api/practice?id=313209&hide_title=true)
   ```

   效果:

   ![wangao 的练习情况](https://luogu.wao3.cn/api/practice?id=313209&hide_title=1)

2. **黑暗模式**,只需在链接最后带上`&dark_mode=true`即可,例如:

   ```markdown
   ![wangao 的练习情况](https://luogu.wao3.cn/api/practice?id=313209&dark_mode=true)
   ```

   效果:

   ![wangao 的练习情况](https://luogu.wao3.cn/api/practice?id=313209&dark_mode=1)
3. **自定义宽度**,默认 500,限制宽度在 500 到 1920 之间,只需在链接最后带上`&card_width=需要的宽度`即可,例如:

   ```markdown
   ![wangao 的练习情况](https://luogu.wao3.cn/api/practice?id=313209&card_width=750)
   ```

   效果:

   ![wangao 的练习情况](https://luogu.wao3.cn/api/practice?id=313209&card_width=750)
   

## 如何参与贡献

#### 提供 bug 反馈或建议

使用 [issue](https://github.com/wao3/luogu-stats-card/issues) 反馈 bug 时,尽可能详细描述 bug 及其复现步骤

#### 贡献代码的步骤

1. fork 项目到自己的 repo
2. 把 fork 过去的项目也就是你的项目 clone 到你的本地
3. 修改代码
4. commit 后 push 到自己的库
5. 在 Github 首页可以看到一个 pull request 按钮,点击它,填写一些说明信息,然后提交即可。
6. 等待作者合并

## 其他

如果对你有所帮助的话,希望能在右上角点一个 star (★ ω ★)

## LICENSE

[![MIT License](https://badgen.net/github/license/wao3/luogu-stats-card)](https://github.com/wao3/luogu-stats-card/blob/master/LICENSE)


================================================
FILE: api/guzhi.js
================================================
const { renderGuzhiCard } = require("../src/guzhi-card");
const { fetchStats } = require("../src/stats-card");
const { renderError } = require("../src/common.js")

module.exports = async (req, res) => {
  const { id, scores, hide_title, dark_mode, card_width = 500 } = req.query;

  res.setHeader("Content-Type", "image/svg+xml");
  // res.setHeader("Cache-Control", "public, max-age=43200"); // 43200s(12h) cache

  return res.send(
    renderError(`访问 https://luogu.wao3.cn 更换域名,造成不便敬请谅解`, { darkMode: dark_mode })
  );

  const regNum = /^[1-9]\d*$/;
  const clamp = (min, max, n) => Math.max(min, Math.min(max, n));

  if (!regNum.test(card_width)) {
    return res.send(
      renderError(`卡片宽度"${card_width}"不合法`, { darkMode: dark_mode })
    );
  }
  if(id != undefined && !regNum.test(id)) {
    return res.send(renderError(`"${id}"不是一个合法uid`, {darkMode: dark_mode}));
  }

  let stats = null;

  if(id != undefined) {
    stats = await fetchStats(id, true);
  }

  return res.send(
    renderGuzhiCard(stats, scores, {
      hideTitle: stats === null ? true : hide_title,
      darkMode: dark_mode,
      cardWidth: clamp(500, 1920, card_width),
    })
  );
};


================================================
FILE: api/index.js
================================================
const { fetchStats, renderSVG } = require("../src/stats-card");
const { renderError } = require("../src/common.js")

module.exports = async (req, res) => {
  const { 
    id, 
    hide_title, 
    dark_mode,
    card_width = 500,
  } = req.query;

  res.setHeader("Content-Type", "image/svg+xml");
  // res.setHeader("Cache-Control", "public, max-age=43200"); // 43200s(12h) cache
  
  return res.send(
    renderError(`访问 https://luogu.wao3.cn 更换域名,造成不便敬请谅解`, { darkMode: dark_mode })
  );

  const validId = /^[1-9]\d*$/;
  const clamp = (min, max, n) => Math.max(min, Math.min(max, n));

  if(!validId.test(id)) {
    return res.send(renderError(`"${id}"不是一个合法uid`, {darkMode: dark_mode}));
  }
  if(!validId.test(card_width)) {
    return res.send(renderError(`卡片宽度"${card_width}"不合法`, {darkMode: dark_mode}));
  }

  const stats = await fetchStats(id, true);
  return res.send(renderSVG(stats, {
    hideTitle: hide_title,
    darkMode: dark_mode,
    cardWidth: clamp(500, 1920, card_width),
  }));
};


================================================
FILE: index.js
================================================
module.exports = {
    ...require("./src/guzhi-card"),
    ...require("./src/stats-card"),
    ...require("./src/common.js"),
}

================================================
FILE: package.json
================================================
{
  "name": "luogu-stats-card",
  "version": "1.0.1",
  "description": "洛谷数据渲染组件",
  "main": "index.js",
  "scripts": {},
  "keywords": [],
  "author": "wao",
  "license": "MIT",
  "dependencies": {
    "anafanafo": "^1.0.0",
    "axios": "^0.21.1"
  }
}


================================================
FILE: src/common.js
================================================
const anafanafo = require('anafanafo');

const NAMECOLOR = {
  "Gray": "#bbbbbb",
  "Blue": "#0e90d2",
  "Green": "#5eb95e",
  "Orange": "#e67e22",
  "Red": "#e74c3c",
  "Purple": "#9d3dcf",
  "Cheater": "#ad8b00"
}

class Card {
  constructor({
    width = 450,
    height = 250,
    title = "",
    body = "",
    titleHeight = 25,
    hideTitle = false,
    css = "",
    darkMode = "",
    paddingX = 25,
    paddingY = 35,
    hideBorder = false,
  }) {
    this.width = width;
    this.height = height;
    this.titleHeight = titleHeight;
    this.title = title;
    this.body = body;
    this.hideTitle = hideTitle;
    this.css = css;
    this.darkMode = darkMode;
    this.paddingX = paddingX;
    this.paddingY = paddingY;
    this.hideBorder = hideBorder;
  }

  render() {
    const cardSize = {
      width: this.width + 2*this.paddingX,
      height: this.height + 2*this.paddingY,
    };
    if(!this.hideTitle) cardSize.height += this.titleHeight;

    const bgColor = this.darkMode?"#444444":"#fffefe";
    let borderColor = "";
    if(!this.hideBorder) borderColor = this.darkMode?"#444444":"#E4E2E2";

    return `
      <svg xmlns="http://www.w3.org/2000/svg" width="${cardSize.width}" height="${cardSize.height}" viewBox="0 0 ${cardSize.width} ${cardSize.height}" fill="none">
        <style>
          .text { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${this.darkMode?"#fffefe":"#333333"} }
          .title {fill: ${this.darkMode?"#fffefe":"#333333"}}
          .line { stroke:${this.darkMode?"#666666":"#dddddd"}; stroke-width:1 }
          ${this.css}
        </style>
        <rect x="0.5" y="0.5" rx="4.5" height="99%" stroke="${borderColor}" width="99%" fill="${bgColor}" stroke-opacity="1" />
        
        ${this.hideTitle ? `` : `
        <g transform="translate(${this.paddingX}, ${this.paddingY})">
          ${this.title}
        </g>`}

        <g transform="translate(${this.paddingX}, ${this.hideTitle ? this.paddingY : this.paddingY + this.titleHeight})">
          ${this.body}
        </g>
      </svg>`;
  }
}

/**
 * 渲染错误卡片
 * @param {string} e 描述错误的文本
 * @param {Object} option 其余选项
 */
const renderError = (e, option) => {
  const css = `.t {font: 600 18px 'Microsoft Yahei UI'; fill: #e74c3c;}`
  const text = `<text class="t" dominant-baseline="text-before-edge">${e}</text>`
  return new Card({
    width: 500,
    height: 23,
    hideTitle: true,
    css,
    body: text,
    paddingY: 20,
    paddingX: 20,
    ...option,
  }).render();
};

/**
 * 渲染 ccf badge
 * @param {number} level CCF等级
 * @param {number} x badge的x坐标
 * @returns {string} ccf badge的svg字符串
 */
const renderCCFBadge = (level, x) => {
  const ccfColor = (ccf) => {
    if(ccf >= 3 && ccf <= 5) return "#5eb95e";
    if(ccf >= 6 && ccf <= 7) return "#3498db";
    if(ccf >= 8) return "#f1c40f";
    return null;
  }
  return `
  <svg xmlns="http://www.w3.org/2000/svg" x="${x}" y="-14" width="18" height="18" viewBox="0 0 18 18" fill="${ccfColor(level)}" style="margin-bottom: -3px;">
    <path d="M16 8C16 6.84375 15.25 5.84375 14.1875 5.4375C14.6562 4.4375 14.4688 3.1875 13.6562 2.34375C12.8125 1.53125 11.5625 1.34375 10.5625 1.8125C10.1562 0.75 9.15625 0 8 0C6.8125 0 5.8125 0.75 5.40625 1.8125C4.40625 1.34375 3.15625 1.53125 2.34375 2.34375C1.5 3.1875 1.3125 4.4375 1.78125 5.4375C0.71875 5.84375 0 6.84375 0 8C0 9.1875 0.71875 10.1875 1.78125 10.5938C1.3125 11.5938 1.5 12.8438 2.34375 13.6562C3.15625 14.5 4.40625 14.6875 5.40625 14.2188C5.8125 15.2812 6.8125 16 8 16C9.15625 16 10.1562 15.2812 10.5625 14.2188C11.5938 14.6875 12.8125 14.5 13.6562 13.6562C14.4688 12.8438 14.6562 11.5938 14.1875 10.5938C15.25 10.1875 16 9.1875 16 8ZM11.4688 6.625L7.375 10.6875C7.21875 10.8438 7 10.8125 6.875 10.6875L4.5 8.3125C4.375 8.1875 4.375 7.96875 4.5 7.8125L5.3125 7C5.46875 6.875 5.6875 6.875 5.8125 7.03125L7.125 8.34375L10.1562 5.34375C10.3125 5.1875 10.5312 5.1875 10.6562 5.34375L11.4688 6.15625C11.5938 6.28125 11.5938 6.5 11.4688 6.625Z">
    </path>
  </svg>`
}

/**
 * 渲染柱状图
 * @param {Object[]} datas 柱状图的数据数组
 * @param {string} datas.label 一条数据的标签
 * @param {string} datas.color 一条数据的颜色
 * @param {number} datas.data 一条数据的数值
 * @param {number} labelWidth 标签宽度
 * @param {number} progressWidth 柱状图的长度
 * @param {string} [unit] 数据单位
 */
const renderChart = (datas, labelWidth, progressWidth, unit) => { //(label, color, height, num, unit) => {
  let chart = "";
  let maxNum = datas.reduce((a, b) => Math.max(a, b.data), 0);
  maxNum = (parseInt((maxNum-1) / 100) + 1) * 100;

  for(let i = 0; i < datas.length; ++i) {
    const width = (datas[i].data+1) / (maxNum+1) * progressWidth;
    chart += `
    <g transform="translate(0, ${i*30})">
      <text x="0" y="15" class="text">${datas[i].label}</text>
      <text x="${width + labelWidth + 10}" y="15" class="text">${datas[i].data + unit}</text>
      <rect height="11" fill="${datas[i].color}" rx="5" ry="5" x="${labelWidth}" y="5" width="${width}"></rect>
    </g>
    `
  }

  const bodyHeight = datas.length * 30 + 10;
  const dw = progressWidth / 4;
  let coordinate = "";
  for(let i = 0; i <= 4; ++i) {
    coordinate += `
    <line x1="${labelWidth + dw*i}" y1="0" x2="${labelWidth + dw*i}" y2="${bodyHeight - 10}"  class="line"/>
    <text x="${labelWidth + dw*i - (i==0?3:5) }" y="${bodyHeight}"  class="text">${maxNum*i/4}</text>
    `;
  }
  return coordinate + chart;
}

/**
 * 
 * @param {string} name 用户名
 * @param {string} color 用户颜色
 * @param {number} ccfLevel 用户ccf等级
 * @param {string} title 标题的后缀
 * @param {string} rightTop 右上角的标签(展示总数)
 */
const renderNameTitle = (name, color, ccfLevel, title, cardWidth, rightTop) => {
  const nameLength = anafanafo(name)/10*1.8;
  const nameColor = NAMECOLOR[color];

  return `
  <g transform="translate(0, 0)" font-family="Verdana, Microsoft Yahei" text-rendering="geometricPrecision" font-size="18">
    <text x="0" y="0" fill="${nameColor}" font-weight="bold" textLength="${nameLength}">
      ${name}
    </text>
    ${ccfLevel < 3 ? "" : renderCCFBadge(ccfLevel, nameLength + 5)}
    <text x="${nameLength + (ccfLevel < 3 ? 10 : 28)}" y="0" class="title" font-weight="normal">
      ${title}
    </text>
    <text x="${cardWidth - 160}" y="0" class="title" font-weight="normal" font-size="13px">
      ${rightTop}
    </text>
  </g>`;
}

module.exports = { 
  NAMECOLOR,
  Card,
  renderError,
  renderCCFBadge,
  renderChart,
  renderNameTitle,
};


================================================
FILE: src/guzhi-card.js
================================================
const {
  Card,
  renderError,
  renderChart,
  renderNameTitle,
} = require("./common.js");

const renderGuzhiCard = (userInfo, scores, options) => {
  const regNum = /^\d*$/;
  if(!scores || typeof scores !== 'string') {
    return renderError('咕值信息不能为空', {width: 400});
  }
  let sp = ',';
  if(scores.indexOf(',') >= 0) {
    sp = ',';
  }
  const scoreArray = scores.split(sp).filter(x => regNum.test(x)).map(x => parseInt(x)).filter(x => x >= 0 && x <= 100);
  if(scoreArray.length != 5) {
    return renderError(`咕值信息"${scores}"不合法`, {width: 400});
  }
  const scoreSum = scoreArray.reduce((a, b) => a+b);

  const {
    name,
    color,
    ccfLevel,
  } = userInfo || {};

  const { 
    hideTitle, 
    darkMode,
    cardWidth = 500, 
  } = options || {};

  const paddingX = 25;
  const labelWidth = 90;  //柱状图头部文字长度
  const progressWidth = cardWidth - 2*paddingX - labelWidth - 60; //500 - 25*2(padding) - 90(头部文字长度) - 60(预留尾部文字长度),暂时固定,后序提供自定义选项;

  const getScoreColor = (score) => {
    if (score >= 80) return "#52c41a";
    if (score >= 60) return "#fadb14";
    if (score >= 30) return "#f39c11";
    return "#e74c3c";
  }
  const datas = [
    {label: "基础信用", data: scoreArray[0], color: getScoreColor(scoreArray[0])},
    {label: "练习情况", data: scoreArray[1], color: getScoreColor(scoreArray[1])},
    {label: "社区贡献", data: scoreArray[2], color: getScoreColor(scoreArray[2])},
    {label: "比赛情况", data: scoreArray[3], color: getScoreColor(scoreArray[3])},
    {label: "获得成就", data: scoreArray[4], color: getScoreColor(scoreArray[4])},
  ]

  const title = userInfo != null ? renderNameTitle(name, color, ccfLevel, "的咕值信息", cardWidth, `总咕值: ${scoreSum}分`) : "";

  const body = renderChart(datas, labelWidth, progressWidth, "分");

  return new Card({
    width: cardWidth - 2*paddingX,
    height: datas.length*30 + 10,
    hideTitle,
    darkMode,
    title,
    body,
  }).render();
}

module.exports = { renderGuzhiCard }

================================================
FILE: src/stats-card.js
================================================
const axios = require("axios");
const {
  Card,
  renderError,
  renderChart,
  renderNameTitle,
} = require("./common.js");

/**
 * 
 * @param {number} id 用户id
 * @param {boolean} useProxy 使用代理
 * @returns {Object} 获取的用户数据 {name, color, ccfLevel, passed, hideInfo}
 */
async function fetchStats(id, useProxy) {
  //debug 测试请求
  let reqUrl = `https://www.luogu.com.cn/user/${id}?_contentOnly`;
  if (useProxy) {
    reqUrl = `https://a-1c37c2-1300876583.ap-shanghai.service.tcloudbase.com/luogu?id=${id}`;
  }
  const res = await axios.get(reqUrl);

  const stats = {
    name: "NULL",
    color: "Gray",
    ccfLevel: 0,
    passed: [0,0,0,0,0,0,0,0],
    hideInfo: false
  }
  if(res.data.code !== 200) {
    return stats;
  }

  const user = res.data.currentData.user;
  const passed = res.data.currentData.passedProblems;

  stats.name = user.name;
  stats.color = user.color;
  stats.ccfLevel = user.ccfLevel;

  if(!passed) {
    stats.hideInfo = true;
    return stats;
  }

  for(let i of passed) {
    stats.passed[i.difficulty]++;
  }

  return stats;
} 

const renderSVG = (stats, options) => {
  const {
    name,
    color,
    ccfLevel,
    passed,
    hideInfo
  } = stats;

  const { 
    hideTitle, 
    darkMode,
    cardWidth = 500, 
  } = options || {};

  if(hideInfo) {
    return renderError("用户开启了“完全隐私保护”,获取数据失败");
  }
  
  const paddingX = 25;
  const labelWidth = 90;  //柱状图头部文字长度
  const progressWidth = cardWidth - 2*paddingX - labelWidth - 60; //500 - 25*2(padding) - 90(头部文字长度) - 60(预留尾部文字长度),暂时固定,后序提供自定义选项;

  const datas = [
    {label: "未评定", color:"#bfbfbf", data: passed[0]},
    {label: "入门", color:"#fe4c61", data: passed[1]},
    {label: "普及-", color:"#f39c11", data: passed[2]},
    {label: "普及/提高-", color:"#ffc116", data: passed[3]},
    {label: "普及+/提高", color:"#52c41a", data: passed[4]},
    {label: "提高+/省选-", color:"#3498db", data: passed[5]},
    {label: "省选/NOI-", color:"#9d3dcf", data: passed[6]},
    {label: "NOI/NOI+/CTSC", color:"#0e1d69", data: passed[7]}
  ]
  const passedSum = passed.reduce((a, b) => a + b);
  const body = renderChart(datas, labelWidth, progressWidth, "题");

  const title = renderNameTitle(name, color, ccfLevel, "的练习情况", cardWidth, `已通过: ${passedSum}题`);

  return new Card({
    width: cardWidth - 2*paddingX,
    height: datas.length*30 + 10,
    hideTitle,
    darkMode,
    title,
    body,
  }).render();
}

module.exports = { fetchStats, renderSVG }


================================================
FILE: tcb/.gitignore
================================================
.DS_Store
node_modules
/dist


# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: tcb/babel.config.js
================================================
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ]
}


================================================
FILE: tcb/cloudbaserc.json
================================================
{
  "version": "2.0",
  "envId": "cloudbase-baas-5g6v8dai30476fe3",
  "$schema": "https://framework-1258016615.tcloudbaseapp.com/schema/latest.json",
  "functionRoot": "./functions",
  "functions": [
    {
      "name": "index",
      "timeout": 10,
      "envVariables": {},
      "runtime": "Nodejs12.16",
      "memorySize": 128,
      "handler": "index.main"
    },
    {
      "name": "luogu",
      "timeout": 10,
      "envVariables": {},
      "runtime": "Nodejs12.16",
      "memorySize": 128,
      "handler": "index.main"
    }
  ],
  "framework": {
    "name": "luogu-stats-card",
    "plugins": {
      "function": {
        "use": "@cloudbase/framework-plugin-function",
        "inputs": {}
      },
      "client": {
        "use": "@cloudbase/framework-plugin-database",
        "inputs": {
          "collections": [
            {
              "collectionName": "cache"
            }
          ]
        }
      },
      "homepage": {
        "use": "@cloudbase/framework-plugin-website",
        "inputs": {
          "installCommand": "npm install",
          "buildCommand": "npm run build",
          "outputPath": "dist",
          "ignore": [
            ".git",
            ".github",
            "node_modules",
            "cloudbaserc.js"
          ]
        }
      }
    }
  },
  "region": "ap-shanghai"
}


================================================
FILE: tcb/functions/index/index.js
================================================
const axios = require('axios');

module.exports.main = async function (event, context) {
  const baseUrl = "https://www.wao3.cn";
  if (event.path === "/") event.path = "/luogu/index.html"
  const res = await axios.get(baseUrl + event.path);
  return {
    statusCode: res.status,
    headers: res.headers,
    body: res.data,
  }
};

================================================
FILE: tcb/functions/index/package.json
================================================
{
    "name": "luogu-index",
    "version": "1.0.0",
    "description": "a proxy to https://wao3.cn/luogu",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "wao",
    "license": "MIT",
    "dependencies": {
        "axios": "^0.21.1"
    }
}


================================================
FILE: tcb/functions/luogu/cache.js
================================================
const cloudbase = require('@cloudbase/node-sdk')

const app = cloudbase.init({
  env: cloudbase.SYMBOL_CURRENT_ENV,
})
const db = app.database();

// 12 hour
const EXPIRE_TIME_MILLISECOND = 1000 * 60 * 60 * 12;
const CACHE_NAME = 'cache';

const cache = {
  get: async (key) => {
    const res = await db.collection(CACHE_NAME)
      .doc(key)
      .get();
    if (res.data === null || res.data.length === 0) {
      return null;
    }
    const data = res.data[0];
    if (!data || !data.updateTime) {
      return null;
    }
    const updateTime = new Date(data.updateTime);
    const now = new Date();
    if (now.valueOf() - updateTime.valueOf() > EXPIRE_TIME_MILLISECOND) {
      return null;
    }
    return data.value;
  },
  put: async (key, value) => {
    if (key == null) {
      return null;
    }
    const res = await db
      .collection(CACHE_NAME)
      .doc(key)
      .set({
        value,
        updateTime: new Date(),
      });
    if (!res.updated || !res.upsertedId) {
      return value;
    }
    return null;
  }
}

module.exports = cache;

================================================
FILE: tcb/functions/luogu/fetchStats.js
================================================
const { fetchStats } = require('luogu-stats-card');
const cache = require('./cache');

module.exports = async id => {
  const cacheKey = 'uid:' + id;
  let stats = await cache.get(cacheKey);
  if (!stats) {
    stats = await fetchStats(id);
    if (stats) {
      await cache.put(cacheKey, stats);
    }
  }
  return stats;
}

================================================
FILE: tcb/functions/luogu/index.js
================================================
const axios = require('axios');
const { renderError } = require('luogu-stats-card');
const practice = require('./route/practice');
const guzhi = require('./route/guzhi');

module.exports.main = async function (event, context) {
  checkParam(event.queryStringParameters);
  let result = null;
  if (event.path.startsWith("/practice")) {
    result = await practice(event);
  } else if (event.path.startsWith("/guzhi")) {
    result = await guzhi(event);
  } else {
    result = renderError(`路径错误:${event.path}`, { darkMode: dark_mode });
  }
  return {
    statusCode: 200,
    headers: {
      "content-type": "image/svg+xml; charset=utf-8",
      "Cache-Control": "public, max-age=43200",
    },
    body: result
  };
};

function checkParam(queryParam) {
  const { id, dark_mode, card_width = 500 } = queryParam;

  const regNum = /^[1-9]\d*$/;
  const clamp = (min, max, n) => Math.max(min, Math.min(max, n));

  if (!regNum.test(card_width)) {
    return renderError(`卡片宽度"${card_width}"不合法`, { darkMode: dark_mode });
  }

  if(id != undefined && !regNum.test(id)) {
    return renderError(`卡片宽度"${card_width}"不合法`, { darkMode: dark_mode });
  }

  queryParam.card_width = clamp(500, 1920, card_width);
}

================================================
FILE: tcb/functions/luogu/package.json
================================================
{
  "name": "luogu-tcb",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "wao",
  "license": "MIT",
  "dependencies": {
    "@cloudbase/node-sdk": "^2.7.1",
    "luogu-stats-card": "^1.0.1"
  }
}


================================================
FILE: tcb/functions/luogu/route/guzhi.js
================================================
const { renderGuzhiCard } = require('luogu-stats-card');
const fetchStats = require('../fetchStats');

module.exports = async function (event) {
  const {
    id,
    scores,
    hide_title,
    dark_mode,
    card_width = 500,
  } = event.queryStringParameters;

  const stats = await fetchStats(id);

  return renderGuzhiCard(stats, scores, {
    hideTitle: stats === null ? true : hide_title,
    darkMode: dark_mode,
    cardWidth: card_width,
  });
};

================================================
FILE: tcb/functions/luogu/route/practice.js
================================================
const { renderSVG } = require('luogu-stats-card');
const fetchStats = require('../fetchStats');

module.exports = async function (event) {
  const { 
    id, 
    hide_title, 
    dark_mode,
    card_width = 500,
  } = event.queryStringParameters;

  const stats = await fetchStats(id);

  return renderSVG(stats, {
    hideTitle: hide_title,
    darkMode: dark_mode,
    cardWidth: card_width,
  });
};

================================================
FILE: tcb/functions/luogu/test.js
================================================
// const { main } = require('./index');

// main({
//   queryStringParameters: {
//     id: 313209,
//   },
//   path: '/practice',
// }).then(res => console.log(res));

///////

// const fetchStats = require('./fetchStats');

// fetchStats(313209).then(res => console.log(res));


================================================
FILE: tcb/package.json
================================================
{
  "name": "guide",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
    "build": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build",
    "lint": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service lint"
  },
  "dependencies": {
    "ant-design-vue": "^1.7.7",
    "copy-to-clipboard": "^3.3.1",
    "core-js": "^3.6.5",
    "debounce": "^1.2.1",
    "vue": "^2.6.11"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "vue-template-compiler": "^2.6.11",
    "babel": "^6.23.0"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}


================================================
FILE: tcb/public/index.html
================================================
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>洛谷个人练习数据卡片</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: tcb/src/App.vue
================================================
<template>
  <div id="app">
    <Homepage/>
  </div>
</template>

<script>
import Homepage from './Homepage.vue'

export default {
  name: 'App',
  components: {
    Homepage
  }
}
</script>

<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button{
	-webkit-appearance: none !important;
	margin: 0;
}
#app {
  max-width: 1000px;
  margin: 50px auto;
  padding: 20px
}
</style>


================================================
FILE: tcb/src/Homepage.vue
================================================
<template>
  <div>
    <h1>洛谷个人练习数据卡片</h1>
    <a-alert type="info" show-icon style="margin-bottom: 1em">
      <p slot="description" style="margin-bottom: 0">
        为了不滥用洛谷服务器流量,本项目将缓存 12 小时用户数据,即同一个用户卡片
        24 小时内最多只会向洛谷服务器请求 2
        次数据,并且只有在用户访问卡片时才会请求数据。
        <br />
        <br />
        本项目完全开源,如果对您有帮助的话希望能
        <a href="https://github.com/wao3/luogu-stats-card" target="_blank">
          给该项目点一个star
        </a>
      </p>
    </a-alert>
    <Luogu />
    <a
      href="https://github.com/wao3/luogu-stats-card"
      class="github-corner"
      aria-label="View source on GitHub"
      target="_blank"
    >
      <svg
        width="80"
        height="80"
        viewBox="0 0 250 250"
        style="
          fill: #1890FF;
          color: #fff;
          position: absolute;
          top: 0;
          border: 0;
          right: 0;
        "
        aria-hidden="true"
      >
        <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
        <path
          d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
          fill="currentColor"
          style="transform-origin: 130px 106px"
          class="octo-arm"
        ></path>
        <path
          d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
          fill="currentColor"
          class="octo-body"
        ></path>
      </svg>
    </a>
  </div>
</template>

<script>
import Luogu from "./components/Luogu.vue";

export default {
  name: "Homepage",
  components: {
    Luogu,
  },
  data() {
    return {};
  },
};
</script>

<style>
.github-corner:hover .octo-arm {
  animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
  0%,
  100% {
    transform: rotate(0);
  }
  20%,
  60% {
    transform: rotate(-25deg);
  }
  40%,
  80% {
    transform: rotate(10deg);
  }
}
@media (max-width: 500px) {
  .github-corner:hover .octo-arm {
    animation: none;
  }
  .github-corner .octo-arm {
    animation: octocat-wave 560ms ease-in-out;
  }
}
</style>

================================================
FILE: tcb/src/components/Luogu.vue
================================================
<template>
  <a-form-model :model="form" labelAlign="left" :label-col="labelCol" :wrapper-col="wrapperCol">
    <a-form-model-item label="卡片类型">
      <a-radio-group
        v-model="form.type"
        default-value="练习情况"
        button-style="solid"
      >
        <a-radio-button value="practice"> 练习情况 </a-radio-button>
        <a-radio-button value="guzhi"> 咕值信息 </a-radio-button>
      </a-radio-group>
    </a-form-model-item>

    <a-form-model-item label="用户 UID">
      <a-input type="number" v-model.number="form.uid" />
    </a-form-model-item>

    <a-form-model-item
      v-show="form.type == 'guzhi'"
      :label="itm"
      v-for="(itm, idx) in guzhiItems"
      :key="'guzhi' + itm"
    >
      <a-row>
        <a-col :span="20">
          <a-slider v-model="form.guzhi[idx]" :min="0" :max="100" />
        </a-col>
        <a-col :span="4">
          <a-input-number
            v-model="form.guzhi[idx]"
            :min="0"
            :max="100"
            style="marginleft: 16px"
          />
        </a-col>
      </a-row>
    </a-form-model-item>

    <a-form-model-item label="暗黑模式">
      <a-switch v-model="form.darkMode" />
    </a-form-model-item>

    <a-form-model-item label="隐藏标题">
      <a-switch v-model="form.hideTitle" />
    </a-form-model-item>

    <a-form-model-item label="卡片宽度">
      <a-row>
        <a-col :span="20">
          <a-slider v-model="form.cardWidth" :min="500" :max="1920" />
        </a-col>
        <a-col :span="4">
          <a-input-number
            v-model="form.cardWidth"
            :min="500"
            :max="1920"
            style="marginleft: 16px"
          />
        </a-col>
      </a-row>
    </a-form-model-item>

    <a-form-model-item label="效果预览">
      <img alt="" :src="imgUrl" />
    </a-form-model-item>

    <a-form-model-item label="复制代码">
      <a-tabs v-model="codeMode" @change="copyCode">
        <a-tab-pane
          v-for="codeMode in codeModes"
          :key="codeMode"
          :tab="codeMode"
        >
          <pre><code>{{codes[codeMode]}}</code></pre>
        </a-tab-pane>
      </a-tabs>
    </a-form-model-item>
  </a-form-model>
</template>

<script>
import { debounce } from "debounce";
import copy from 'copy-to-clipboard';

export default {
  data() {
    return {
      labelCol: { span: 3 },
      wrapperCol: { span: 21},
      guzhiItems: ["基础信用", "练习情况", "社区贡献", "比赛情况", "获得成就"],
      form: {
        type: "practice",
        uid: 313209,
        guzhi: [0, 0, 0, 0, 0],
        darkMode: false,
        hideTitle: false,
        cardWidth: 500,
      },
      codeModes: ["Markdown", "HTML", "URL"],
      codeMode: "Markdown",
      codes: {
        "Markdown": '',
        "HTML": '',
        "URL": '',
      },
      imgUrl: "https://luogu.wao3.cn/api/practice?id=313209",
    };
  },
  methods: {
    copyCode() {
      const code = this.codes[this.codeMode];
      copy(code);
      this.$message.success('已复制到剪切板');
    }
  },
  watch: {
    form: {
      handler() {
        const url = this.realtimeImgUrl;
        debounce(() => {
          this.imgUrl = url;
        }, 200)();
        this.codes["Markdown"] = `![我的练习情况](${url})`;
        this.codes["HTML"] = `<img src="${url}" alt="我的练习情况"/>`;
        this.codes["URL"] = url;
      },
      deep: true,
      immediate: true,
    }
  },
  computed: {
    realtimeImgUrl() {
      const form = this.form;
      let url = `https://luogu.wao3.cn/api/${form.type}?id=${form.uid}`;
      if (form.type == 'guzhi') {
        url += `&scores=${form.guzhi.join(',')}`;
      }
      if (form.darkMode) {
        url += '&dark_mode=true';
      }
      if (form.hideTitle) {
        url += '&hide_title=true';
      }
      if (form.cardWidth !== 500) {
        url += `&card_width=${form.cardWidth}`;
      }
      return url;
    }
  }
};
</script>

<style>
pre code {
  display: block;
  padding: 16px;
  overflow: auto;
  line-height: 1.3;
  color: #476582;
  background-color: rgba(27, 31, 35, 0.05);
  border-radius: 4px;
}

img {
  max-width: 100%;
}

.ant-input-number {
  width: 100% !important;
}
</style>

================================================
FILE: tcb/src/main.js
================================================
import Vue from 'vue'
import App from './App.vue'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';

Vue.config.productionTip = false

Vue.use(Antd);

new Vue({
  render: h => h(App),
}).$mount('#app')


================================================
FILE: vercel.json
================================================
{
  "builds": [
    { "src": "api/*.js", "use": "@vercel/node" }
  ],
  "routes": [
    { "src": "/practice", "dest": "/api/index.js" },
    { "src": "/guzhi", "dest": "/api/guzhi.js" },
    { "src": "/", "status": 301, "headers": { "Location": "https://luogu.wao3.cn" } }
  ],
  "regions": ["hkg1"]
}
Download .txt
gitextract_b_dpcgck/

├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── api/
│   ├── guzhi.js
│   └── index.js
├── index.js
├── package.json
├── src/
│   ├── common.js
│   ├── guzhi-card.js
│   └── stats-card.js
├── tcb/
│   ├── .gitignore
│   ├── babel.config.js
│   ├── cloudbaserc.json
│   ├── functions/
│   │   ├── index/
│   │   │   ├── index.js
│   │   │   └── package.json
│   │   └── luogu/
│   │       ├── cache.js
│   │       ├── fetchStats.js
│   │       ├── index.js
│   │       ├── package.json
│   │       ├── route/
│   │       │   ├── guzhi.js
│   │       │   └── practice.js
│   │       └── test.js
│   ├── package.json
│   ├── public/
│   │   └── index.html
│   └── src/
│       ├── App.vue
│       ├── Homepage.vue
│       ├── components/
│       │   └── Luogu.vue
│       └── main.js
└── vercel.json
Download .txt
SYMBOL INDEX (8 symbols across 4 files)

FILE: src/common.js
  constant NAMECOLOR (line 3) | const NAMECOLOR = {
  class Card (line 13) | class Card {
    method constructor (line 14) | constructor({
    method render (line 40) | render() {

FILE: src/stats-card.js
  function fetchStats (line 15) | async function fetchStats(id, useProxy) {

FILE: tcb/functions/luogu/cache.js
  constant EXPIRE_TIME_MILLISECOND (line 9) | const EXPIRE_TIME_MILLISECOND = 1000 * 60 * 60 * 12;
  constant CACHE_NAME (line 10) | const CACHE_NAME = 'cache';

FILE: tcb/functions/luogu/index.js
  function checkParam (line 26) | function checkParam(queryParam) {
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (40K chars).
[
  {
    "path": ".gitignore",
    "chars": 47,
    "preview": "package-lock.json\ntest.svg\nnode_modules\n.vercel"
  },
  {
    "path": ".npmignore",
    "chars": 67,
    "preview": "package-lock.json\ntest.svg\nnode_modules\n.vercel\napi\ntcb\nvercel.json"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2020 Anurag Hazra\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 3112,
    "preview": "> 注意:为了不滥用洛谷服务器流量,本项目将缓存 12 小时用户数据,即同一个用户卡片 **24 小时内最多只会向洛谷服务器请求 2 次数据**,并且只有在用户访问卡片时才会请求数据。\n## 简介\n\n![stars](https://bad"
  },
  {
    "path": "api/guzhi.js",
    "chars": 1170,
    "preview": "const { renderGuzhiCard } = require(\"../src/guzhi-card\");\nconst { fetchStats } = require(\"../src/stats-card\");\nconst { r"
  },
  {
    "path": "api/index.js",
    "chars": 1008,
    "preview": "const { fetchStats, renderSVG } = require(\"../src/stats-card\");\nconst { renderError } = require(\"../src/common.js\")\n\nmod"
  },
  {
    "path": "index.js",
    "chars": 127,
    "preview": "module.exports = {\n    ...require(\"./src/guzhi-card\"),\n    ...require(\"./src/stats-card\"),\n    ...require(\"./src/common."
  },
  {
    "path": "package.json",
    "chars": 255,
    "preview": "{\n  \"name\": \"luogu-stats-card\",\n  \"version\": \"1.0.1\",\n  \"description\": \"洛谷数据渲染组件\",\n  \"main\": \"index.js\",\n  \"scripts\": {}"
  },
  {
    "path": "src/common.js",
    "chars": 6409,
    "preview": "const anafanafo = require('anafanafo');\n\nconst NAMECOLOR = {\n  \"Gray\": \"#bbbbbb\",\n  \"Blue\": \"#0e90d2\",\n  \"Green\": \"#5eb9"
  },
  {
    "path": "src/guzhi-card.js",
    "chars": 1942,
    "preview": "const {\n  Card,\n  renderError,\n  renderChart,\n  renderNameTitle,\n} = require(\"./common.js\");\n\nconst renderGuzhiCard = (u"
  },
  {
    "path": "src/stats-card.js",
    "chars": 2436,
    "preview": "const axios = require(\"axios\");\nconst {\n  Card,\n  renderError,\n  renderChart,\n  renderNameTitle,\n} = require(\"./common.j"
  },
  {
    "path": "tcb/.gitignore",
    "chars": 231,
    "preview": ".DS_Store\nnode_modules\n/dist\n\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyar"
  },
  {
    "path": "tcb/babel.config.js",
    "chars": 73,
    "preview": "module.exports = {\n  presets: [\n    '@vue/cli-plugin-babel/preset'\n  ]\n}\n"
  },
  {
    "path": "tcb/cloudbaserc.json",
    "chars": 1337,
    "preview": "{\n  \"version\": \"2.0\",\n  \"envId\": \"cloudbase-baas-5g6v8dai30476fe3\",\n  \"$schema\": \"https://framework-1258016615.tcloudbas"
  },
  {
    "path": "tcb/functions/index/index.js",
    "chars": 333,
    "preview": "const axios = require('axios');\n\nmodule.exports.main = async function (event, context) {\n  const baseUrl = \"https://www."
  },
  {
    "path": "tcb/functions/index/package.json",
    "chars": 318,
    "preview": "{\n    \"name\": \"luogu-index\",\n    \"version\": \"1.0.0\",\n    \"description\": \"a proxy to https://wao3.cn/luogu\",\n    \"main\": "
  },
  {
    "path": "tcb/functions/luogu/cache.js",
    "chars": 1070,
    "preview": "const cloudbase = require('@cloudbase/node-sdk')\n\nconst app = cloudbase.init({\n  env: cloudbase.SYMBOL_CURRENT_ENV,\n})\nc"
  },
  {
    "path": "tcb/functions/luogu/fetchStats.js",
    "chars": 325,
    "preview": "const { fetchStats } = require('luogu-stats-card');\nconst cache = require('./cache');\n\nmodule.exports = async id => {\n  "
  },
  {
    "path": "tcb/functions/luogu/index.js",
    "chars": 1209,
    "preview": "const axios = require('axios');\nconst { renderError } = require('luogu-stats-card');\nconst practice = require('./route/p"
  },
  {
    "path": "tcb/functions/luogu/package.json",
    "chars": 303,
    "preview": "{\n  \"name\": \"luogu-tcb\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"ec"
  },
  {
    "path": "tcb/functions/luogu/route/guzhi.js",
    "chars": 456,
    "preview": "const { renderGuzhiCard } = require('luogu-stats-card');\nconst fetchStats = require('../fetchStats');\n\nmodule.exports = "
  },
  {
    "path": "tcb/functions/luogu/route/practice.js",
    "chars": 403,
    "preview": "const { renderSVG } = require('luogu-stats-card');\nconst fetchStats = require('../fetchStats');\n\nmodule.exports = async "
  },
  {
    "path": "tcb/functions/luogu/test.js",
    "chars": 280,
    "preview": "// const { main } = require('./index');\n\n// main({\n//   queryStringParameters: {\n//     id: 313209,\n//   },\n//   path: '"
  },
  {
    "path": "tcb/package.json",
    "chars": 1105,
    "preview": "{\n  \"name\": \"guide\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"SET NODE_OPTIONS=--openssl-le"
  },
  {
    "path": "tcb/public/index.html",
    "chars": 583,
    "preview": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=ed"
  },
  {
    "path": "tcb/src/App.vue",
    "chars": 396,
    "preview": "<template>\n  <div id=\"app\">\n    <Homepage/>\n  </div>\n</template>\n\n<script>\nimport Homepage from './Homepage.vue'\n\nexport"
  },
  {
    "path": "tcb/src/Homepage.vue",
    "chars": 2610,
    "preview": "<template>\n  <div>\n    <h1>洛谷个人练习数据卡片</h1>\n    <a-alert type=\"info\" show-icon style=\"margin-bottom: 1em\">\n      <p slot="
  },
  {
    "path": "tcb/src/components/Luogu.vue",
    "chars": 4099,
    "preview": "<template>\n  <a-form-model :model=\"form\" labelAlign=\"left\" :label-col=\"labelCol\" :wrapper-col=\"wrapperCol\">\n    <a-form-"
  },
  {
    "path": "tcb/src/main.js",
    "chars": 226,
    "preview": "import Vue from 'vue'\nimport App from './App.vue'\nimport Antd from 'ant-design-vue';\nimport 'ant-design-vue/dist/antd.cs"
  },
  {
    "path": "vercel.json",
    "chars": 302,
    "preview": "{\n  \"builds\": [\n    { \"src\": \"api/*.js\", \"use\": \"@vercel/node\" }\n  ],\n  \"routes\": [\n    { \"src\": \"/practice\", \"dest\": \"/"
  }
]

About this extraction

This page contains the full source code of the wao3/luogu-stats-card GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 30 files (32.5 KB), approximately 12.1k tokens, and a symbol index with 8 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!