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 次数据**,并且只有在用户访问卡片时才会请求数据。
## 简介





`luogu-stats-card`是一个动态生成洛谷用户练习数据卡片的工具,可以展示自己的做题情况。可以用于个人主页、博客、github 等可以插入图片的地方。
## TODO
- [x] ~~修复获取数据错误和用户设置数据不可见的 bug~~
- [x] ~~增加黑暗模式~~
- [x] ~~增加咕值卡片~~
- [ ] 增加用户 tag
- [ ] 缓存 top 500 用户咕值,使高估值用户可自动获取估值信息
## 效果预览


*(上面的咕值仅为展示效果,本人咕值并没有这么高)*
## 如何使用
### 练习情况
练习情况可以自动获取用户的数据,但是前提是没有开启“完全隐私保护”,具体使用方法如下:
1. Markdown:复制以下内容到任意 markdown 编辑器中,并将`?id=`后面的数字更改为自己的 id 即可(id 是洛谷个人主页地址的一串数字)。
```markdown

```
2. HTML:复制以下内容到 HTML 代码中,并将`?id=`后面的数字更改为自己的 id 即可(id 是洛谷个人主页地址的一串数字)。
```html
```
### 咕值信息
咕值信息无法自动获取数据,如果需要必须要提供 cookie ,但是 这种方法十分不安全,并且不方便,所以获取咕值卡片需要手动输入咕值信息,具体使用方法如下。
复制以下内容到任意 markdown 编辑器中,并将 `?id=`后面的数字更改为自己的 id,将`scores=`后面更换为自己的咕值信息,一共 5 个数字,用逗号分隔。
1. Markdown:复制以下内容到任意 markdown 编辑器中,并将 `?id=`后面的数字更改为自己的 id,将`scores=`后面更换为自己的咕值信息,一共 5 个数字,用逗号分隔。
```markdown

```
2. HTML:复制以下内容到 HTML 代码中,并将 `?id=`后面的数字更改为自己的 id,将`scores=`后面更换为自己的咕值信息,一共 5 个数字,用逗号分隔。
```html
```
### 自定义选项
使用卡片时,支持设定自定义效果选项,可以组合使用。
1. **隐藏标题**,只需在链接最后带上`&hide_title=true`即可,例如:
```markdown

```
效果:

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

```
效果:

3. **自定义宽度**,默认 500,限制宽度在 500 到 1920 之间,只需在链接最后带上`&card_width=需要的宽度`即可,例如:
```markdown

```
效果:

## 如何参与贡献
#### 提供 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
[](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 `
`;
}
}
/**
* 渲染错误卡片
* @param {string} e 描述错误的文本
* @param {Object} option 其余选项
*/
const renderError = (e, option) => {
const css = `.t {font: 600 18px 'Microsoft Yahei UI'; fill: #e74c3c;}`
const text = `${e}`
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 `
`
}
/**
* 渲染柱状图
* @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 += `
${datas[i].label}
${datas[i].data + unit}
`
}
const bodyHeight = datas.length * 30 + 10;
const dw = progressWidth / 4;
let coordinate = "";
for(let i = 0; i <= 4; ++i) {
coordinate += `
${maxNum*i/4}
`;
}
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 `
${name}
${ccfLevel < 3 ? "" : renderCCFBadge(ccfLevel, nameLength + 5)}
${title}
${rightTop}
`;
}
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
================================================
洛谷个人练习数据卡片
================================================
FILE: tcb/src/App.vue
================================================
================================================
FILE: tcb/src/Homepage.vue
================================================
洛谷个人练习数据卡片
为了不滥用洛谷服务器流量,本项目将缓存 12 小时用户数据,即同一个用户卡片
24 小时内最多只会向洛谷服务器请求 2
次数据,并且只有在用户访问卡片时才会请求数据。
本项目完全开源,如果对您有帮助的话希望能
给该项目点一个star
================================================
FILE: tcb/src/components/Luogu.vue
================================================
练习情况
咕值信息
{{codes[codeMode]}}
================================================
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"]
}