koishi-plugin-jrys-plus 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/ranks.js ADDED
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerRanks = registerRanks;
4
+ const koishi_1 = require("koishi");
5
+ const fs_1 = require("fs");
6
+ const path_1 = require("path");
7
+ /* ── 模板路径候选 ── */
8
+ const TEMPLATE_CANDIDATES = [
9
+ (0, path_1.resolve)(__dirname, 'templates', 'rank-card.html'),
10
+ (0, path_1.resolve)(process.cwd(), 'src', 'templates', 'rank-card.html'),
11
+ (0, path_1.resolve)(process.cwd(), 'lib', 'templates', 'rank-card.html'),
12
+ (0, path_1.resolve)(process.cwd(), 'node_modules', 'koishi-plugin-jrys-plus', 'lib', 'templates', 'rank-card.html'),
13
+ ];
14
+ async function resolveTemplatePath() {
15
+ for (const candidate of TEMPLATE_CANDIDATES) {
16
+ try {
17
+ await fs_1.promises.access(candidate);
18
+ return candidate;
19
+ }
20
+ catch { /* next */ }
21
+ }
22
+ throw new Error('未找到 rank-card.html 模板文件');
23
+ }
24
+ /* ── 等级查询 ── */
25
+ const DEFAULT_LEVEL = { level: 0, levelExp: 0, levelName: '无等级', levelColor: '#666666' };
26
+ function getLevelInfo(exp, levels) {
27
+ if (!levels?.length)
28
+ return DEFAULT_LEVEL;
29
+ const sorted = [...levels].sort((a, b) => b.levelExp - a.levelExp);
30
+ return sorted.find(l => exp >= l.levelExp) || sorted[sorted.length - 1];
31
+ }
32
+ /* ── 文本渲染 ── */
33
+ function renderExpText(users, levelConfig, config) {
34
+ const divider = '┏' + '—'.repeat(config.borderwidth) + '┓';
35
+ const midDivider = '┣' + '—'.repeat(config.borderwidth) + '┫';
36
+ const endDivider = '┗' + '—'.repeat(config.borderwidth) + '┛';
37
+ const header = [divider, `┃ 🏆 经验排行榜 TOP.${config.limit} `, midDivider].join('\n');
38
+ const rankings = users.map((user, index) => {
39
+ const medal = index < 3 ? ['👑', '⭐', '✧'][index] : '•';
40
+ const expStr = user.exp.toLocaleString();
41
+ let lines = [`┃ ${medal} ${index + 1}. ${user.displayName}`];
42
+ if (config.next_ExpDisplay) {
43
+ const sorted = [...levelConfig].sort((a, b) => a.levelExp - b.levelExp);
44
+ const cur = getLevelInfo(user.exp, levelConfig);
45
+ const idx = sorted.findIndex(l => l.levelExp === cur.levelExp);
46
+ const next = sorted[idx + 1];
47
+ lines.push(next ? `┃ ⚡${expStr} exp (下一级:${next.levelExp} exp)` : `┃ ⚡${expStr} (Max)`);
48
+ }
49
+ else {
50
+ lines.push(`┃ ⚡${expStr} exp`);
51
+ }
52
+ if (config.pre_next_LevelDisplay && levelConfig.length) {
53
+ const sorted = [...levelConfig].sort((a, b) => a.levelExp - b.levelExp);
54
+ const cur = getLevelInfo(user.exp, levelConfig);
55
+ const idx = sorted.findIndex(l => l.levelExp === cur.levelExp);
56
+ const prev = sorted[idx - 1]?.levelName;
57
+ const next = sorted[idx + 1]?.levelName;
58
+ let line = '┃ ✨';
59
+ if (prev)
60
+ line += `${prev} → `;
61
+ line += `「${cur.levelName}」`;
62
+ if (next)
63
+ line += ` → ${next}`;
64
+ lines.push(line);
65
+ }
66
+ else if (levelConfig.length) {
67
+ lines.push(`┃ ✨${getLevelInfo(user.exp, levelConfig).levelName}`);
68
+ }
69
+ return lines.join('\n');
70
+ }).join('\n\n');
71
+ return [header, rankings, endDivider].join('\n');
72
+ }
73
+ function renderSignText(users, levelConfig, config) {
74
+ const divider = '┏' + '—'.repeat(config.borderwidth) + '┓';
75
+ const midDivider = '┣' + '—'.repeat(config.borderwidth) + '┫';
76
+ const endDivider = '┗' + '—'.repeat(config.borderwidth) + '┛';
77
+ const header = [divider, `┃ 🏆 签到排行榜 TOP.${config.limit} `, midDivider].join('\n');
78
+ const rankings = users.map((user, index) => {
79
+ const medal = index < 3 ? ['👑', '⭐', '✧'][index] : '•';
80
+ let lines = [`┃ ${medal} ${index + 1}. ${user.displayName}`, `┃ 📅${user.signCount.toLocaleString()} 天`];
81
+ if (config.pre_next_LevelDisplay && levelConfig.length) {
82
+ const sorted = [...levelConfig].sort((a, b) => a.levelExp - b.levelExp);
83
+ const cur = getLevelInfo(user.exp, levelConfig);
84
+ const idx = sorted.findIndex(l => l.levelExp === cur.levelExp);
85
+ const prev = sorted[idx - 1]?.levelName;
86
+ const next = sorted[idx + 1]?.levelName;
87
+ let line = '┃ ✨';
88
+ if (prev)
89
+ line += `${prev} → `;
90
+ line += `「${cur.levelName}」`;
91
+ if (next)
92
+ line += ` → ${next}`;
93
+ lines.push(line);
94
+ }
95
+ return lines.join('\n');
96
+ }).join('\n\n');
97
+ return [header, rankings, endDivider].join('\n');
98
+ }
99
+ /* ── 图片渲染 ── */
100
+ async function renderRankImage(ctx, type, users, totalUsers, limit, getLevelConfig) {
101
+ try {
102
+ const path = await resolveTemplatePath();
103
+ let template = await fs_1.promises.readFile(path, 'utf-8');
104
+ const levelConfig = getLevelConfig();
105
+ const data = {
106
+ type,
107
+ limit,
108
+ channelName: '当前频道',
109
+ totalUsers,
110
+ updateTime: new Date().toLocaleString('zh-CN'),
111
+ users: users.map(user => {
112
+ const sorted = [...levelConfig].sort((a, b) => a.levelExp - b.levelExp);
113
+ const cur = getLevelInfo(user.exp, levelConfig);
114
+ const idx = sorted.findIndex(l => l.levelExp === cur.levelExp);
115
+ const prev = sorted[idx - 1]?.levelName;
116
+ const nextObj = sorted[idx + 1];
117
+ const nextName = nextObj?.levelName;
118
+ let levelProgression = '';
119
+ if (prev)
120
+ levelProgression += `${prev} → `;
121
+ levelProgression += `「${cur.levelName}」`;
122
+ if (nextName)
123
+ levelProgression += ` → ${nextName}`;
124
+ return {
125
+ displayName: user.displayName,
126
+ originalId: user.name,
127
+ value: type === 'exp' ? user.exp : user.signCount,
128
+ levelName: cur.levelName,
129
+ levelColor: cur.levelColor,
130
+ currentLevelExp: cur.levelExp,
131
+ nextLevelExp: nextObj?.levelExp ?? null,
132
+ levelProgression,
133
+ };
134
+ }),
135
+ };
136
+ template = template.replace('{{DATA}}', JSON.stringify(data));
137
+ const page = await ctx.puppeteer.page();
138
+ try {
139
+ await page.setContent(template);
140
+ const element = await page.$('.card');
141
+ if (!element)
142
+ throw new Error('找不到 .card 元素');
143
+ const imgBuf = await element.screenshot({ encoding: 'binary' });
144
+ return koishi_1.h.image(imgBuf, 'image/png');
145
+ }
146
+ finally {
147
+ await page.close();
148
+ }
149
+ }
150
+ catch (e) {
151
+ ctx.logger('jrys-plus').error('rank-img:', e);
152
+ return null;
153
+ }
154
+ }
155
+ /* ── 注册排行命令 ── */
156
+ function registerRanks(ctx, config, getLevelConfig) {
157
+ const logger = ctx.logger('jrys-plus');
158
+ function canUseImage() {
159
+ return config.imageMode && !!ctx.puppeteer;
160
+ }
161
+ async function getRankedUsers(session, sortField) {
162
+ const all = await ctx.database.get('jrys', {}, { sort: { [sortField]: 'desc' } });
163
+ if (!all.length)
164
+ return null;
165
+ const users = all.slice(0, config.limit).map(u => ({
166
+ ...u,
167
+ displayName: String(u.name),
168
+ }));
169
+ return users;
170
+ }
171
+ ctx.command(config.expCommand || 'jrysranks')
172
+ .action(async ({ session }) => {
173
+ const users = await getRankedUsers(session, 'exp');
174
+ if (users === null)
175
+ return '暂无数据';
176
+ if (!users.length)
177
+ return '当前频道暂无数据';
178
+ if (canUseImage()) {
179
+ const total = (await ctx.database.get('jrys', {})).length;
180
+ const img = await renderRankImage(ctx, 'exp', users, total, config.limit, getLevelConfig);
181
+ if (img)
182
+ return img;
183
+ }
184
+ return renderExpText(users, getLevelConfig(), config);
185
+ });
186
+ ctx.command(config.signCommand || 'jrysranksign')
187
+ .action(async ({ session }) => {
188
+ const users = await getRankedUsers(session, 'signCount');
189
+ if (users === null)
190
+ return '暂无数据';
191
+ if (!users.length)
192
+ return '当前频道暂无数据';
193
+ if (canUseImage()) {
194
+ const total = (await ctx.database.get('jrys', {})).length;
195
+ const img = await renderRankImage(ctx, 'sign', users, total, config.limit, getLevelConfig);
196
+ if (img)
197
+ return img;
198
+ }
199
+ return renderSignText(users, getLevelConfig(), config);
200
+ });
201
+ }
package/lib/roll.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export declare class Jrys {
2
+ constructor();
3
+ seededRandom(seed: number): number;
4
+ /** 同一天同一用户运势值固定(0~maxRange) */
5
+ getFortune(uid: number, maxRange?: number): Promise<number>;
6
+ /** 抽 4 个不重复的黄历事件 */
7
+ getRandomObjects(jsonObject: Array<any>, uid: number): Promise<Array<any>>;
8
+ }
package/lib/roll.js ADDED
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ // 每日运势值、随机黄历事件
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.Jrys = void 0;
5
+ class Jrys {
6
+ constructor() { }
7
+ seededRandom(seed) {
8
+ const x = Math.sin(seed) * 10000;
9
+ return x - Math.floor(x);
10
+ }
11
+ /** 同一天同一用户运势值固定(0~maxRange) */
12
+ async getFortune(uid, maxRange = 100) {
13
+ const etime = new Date().setHours(0, 0, 0, 0);
14
+ const todaySeed = (Number(uid) * etime) % 1000000001;
15
+ return Math.floor(this.seededRandom(todaySeed) * maxRange);
16
+ }
17
+ /** 抽 4 个不重复的黄历事件 */
18
+ async getRandomObjects(jsonObject, uid) {
19
+ if (!Array.isArray(jsonObject) || jsonObject.length < 4) {
20
+ throw new Error('事件列表至少需要 4 个');
21
+ }
22
+ const seed = await this.getFortune(uid);
23
+ const randomIndexes = new Set();
24
+ let counter = 0;
25
+ while (randomIndexes.size < 4) {
26
+ randomIndexes.add(Math.floor(this.seededRandom(seed + counter) * jsonObject.length));
27
+ counter++;
28
+ }
29
+ return Array.from(randomIndexes).map(i => jsonObject[i]);
30
+ }
31
+ }
32
+ exports.Jrys = Jrys;
@@ -0,0 +1,63 @@
1
+ import { Context } from 'koishi';
2
+ declare module 'koishi' {
3
+ interface Tables {
4
+ jrys: _UserFortune;
5
+ username: _UsernameRecord;
6
+ }
7
+ }
8
+ export interface _UserFortune {
9
+ id: number;
10
+ name: string;
11
+ time: Date;
12
+ exp: number;
13
+ signCount: number;
14
+ }
15
+ export interface _UsernameRecord {
16
+ id: number;
17
+ userId: string;
18
+ username: string;
19
+ platform: string;
20
+ channelId: string;
21
+ uid: string;
22
+ }
23
+ export interface LevelInfo {
24
+ level: number;
25
+ levelExp: number;
26
+ levelName: string;
27
+ levelColor: string;
28
+ }
29
+ export declare const defaultLevelInfo: LevelInfo[];
30
+ export interface FortuneInfo {
31
+ luck: number;
32
+ desc: string;
33
+ }
34
+ export declare const defaultFortuneInfo: FortuneInfo[];
35
+ export declare function initDatabase(ctx: Context): void;
36
+ export interface SigninConfig {
37
+ signExp: [number, number];
38
+ levelSet: LevelInfo[];
39
+ fortuneSet: FortuneInfo[];
40
+ }
41
+ export declare class Signin {
42
+ private ctx;
43
+ private cfg;
44
+ constructor(ctx: Context, cfg: SigninConfig);
45
+ /** 执行签到。返回 0=成功, 1=已签到 */
46
+ callSignin(uid: number, userid: string, luck: number): Promise<{
47
+ status: number;
48
+ allExp: number;
49
+ signTime: Date;
50
+ count: number;
51
+ } | {
52
+ status: number;
53
+ allExp?: undefined;
54
+ signTime?: undefined;
55
+ count?: undefined;
56
+ }>;
57
+ getLevelInfo(exp: number): {
58
+ levelInfo: LevelInfo;
59
+ nextExp: string | number;
60
+ };
61
+ getFortuneInfo(luck: number): string;
62
+ getGreeting(hour: number): string;
63
+ }
package/lib/signin.js ADDED
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Signin = exports.defaultFortuneInfo = exports.defaultLevelInfo = void 0;
4
+ exports.initDatabase = initDatabase;
5
+ const timeGreetings = [
6
+ { range: [0, 5], message: '晚安' },
7
+ { range: [5, 9], message: '早上好' },
8
+ { range: [9, 11], message: '上午好' },
9
+ { range: [11, 14], message: '中午好' },
10
+ { range: [14, 18], message: '下午好' },
11
+ { range: [18, 20], message: '傍晚好' },
12
+ { range: [20, 24], message: '晚上好' },
13
+ ];
14
+ exports.defaultLevelInfo = [
15
+ { level: 0, levelExp: 0, levelName: '不知名杂鱼', levelColor: '#838383' },
16
+ { level: 1, levelExp: 500, levelName: '荒野漫步者', levelColor: '#838383' },
17
+ { level: 2, levelExp: 1000, levelName: '拓荒者', levelColor: '#838383' },
18
+ { level: 3, levelExp: 1500, levelName: '冒险家', levelColor: '#838383' },
19
+ { level: 4, levelExp: 2000, levelName: '传说的冒险家', levelColor: '#000000' },
20
+ { level: 5, levelExp: 3000, levelName: '隐秘收藏家', levelColor: '#000000' },
21
+ { level: 6, levelExp: 4000, levelName: '言灵探索者', levelColor: '#42bc05' },
22
+ { level: 7, levelExp: 5000, levelName: '水系魔法师', levelColor: '#42bc05' },
23
+ { level: 8, levelExp: 6000, levelName: '水系魔导师', levelColor: '#42bc05' },
24
+ { level: 9, levelExp: 8000, levelName: '藏书的魔女', levelColor: '#2003da' },
25
+ { level: 10, levelExp: 10000, levelName: '人形图书馆', levelColor: '#2003da' },
26
+ { level: 11, levelExp: 15000, levelName: '文明归档员', levelColor: '#2003da' },
27
+ { level: 12, levelExp: 20000, levelName: '高塔思索者', levelColor: '#03a4da' },
28
+ { level: 13, levelExp: 25000, levelName: '未知探索者', levelColor: '#03a4da' },
29
+ { level: 14, levelExp: 30000, levelName: '背负真相之人', levelColor: '#9d03da' },
30
+ { level: 15, levelExp: 35000, levelName: '守密人', levelColor: '#9d03da' },
31
+ { level: 16, levelExp: 40000, levelName: '被缚的倒吊者', levelColor: '#9d03da' },
32
+ { level: 17, levelExp: 45000, levelName: '崩毁世界之人', levelColor: '#f10171' },
33
+ { level: 18, levelExp: 50000, levelName: '命运眷顾者', levelColor: '#f10171' },
34
+ { level: 19, levelExp: 100000, levelName: '文明领航员', levelColor: '#c9b86d' },
35
+ { level: 20, levelExp: 1000000, levelName: '天选之人', levelColor: '#ffd000' },
36
+ ];
37
+ exports.defaultFortuneInfo = [
38
+ { luck: 0, desc: '走平坦的路但会摔倒的程度' },
39
+ { luck: 5, desc: '吃泡面会没有调味包的程度' },
40
+ { luck: 15, desc: '上厕所会忘记带纸的程度' },
41
+ { luck: 20, desc: '上学/上班路上会堵车的程度' },
42
+ { luck: 25, desc: '点外卖很晚才会送到的程度' },
43
+ { luck: 30, desc: '点外卖会多给予赠品的程度' },
44
+ { luck: 35, desc: '出门能捡到几枚硬币的程度' },
45
+ { luck: 40, desc: '踩到香蕉皮不会滑倒的程度' },
46
+ { luck: 50, desc: '玩滑梯能流畅滑到底的程度' },
47
+ { luck: 60, desc: '晚上走森林不会迷路的程度' },
48
+ { luck: 70, desc: '打游戏能够轻松过关的程度' },
49
+ { luck: 80, desc: '抽卡能够大成功的程度' },
50
+ { luck: 95, desc: '天选之人' },
51
+ ];
52
+ /* ── 数据库初始化 ── */
53
+ function initDatabase(ctx) {
54
+ ctx.model.extend('jrys', {
55
+ id: 'integer',
56
+ name: 'string',
57
+ time: 'timestamp',
58
+ exp: 'unsigned',
59
+ signCount: 'unsigned',
60
+ });
61
+ }
62
+ /* ── 签到逻辑(无 monetary 依赖) ── */
63
+ const roll_1 = require("./roll");
64
+ class Signin {
65
+ constructor(ctx, cfg) {
66
+ this.ctx = ctx;
67
+ this.cfg = cfg;
68
+ }
69
+ /** 执行签到。返回 0=成功, 1=已签到 */
70
+ async callSignin(uid, userid, luck) {
71
+ const date = new Date();
72
+ const roll = new roll_1.Jrys();
73
+ // 经验值仅用于后端等级/排行计算,不在签到卡上展示
74
+ const exp = Math.round((Math.random() * 0.5 + luck / 200) *
75
+ (this.cfg.signExp[1] - this.cfg.signExp[0])) + this.cfg.signExp[0];
76
+ const userData = await this.ctx.database.get('jrys', { id: uid });
77
+ if (userData.length === 0) {
78
+ const accExp = exp;
79
+ await this.ctx.database.create('jrys', {
80
+ id: uid,
81
+ name: userid,
82
+ time: date,
83
+ exp: accExp,
84
+ signCount: 1,
85
+ });
86
+ return { status: 0, allExp: accExp, signTime: date, count: 1 };
87
+ }
88
+ if (userData[0].time.getDate() === date.getDate()) {
89
+ return { status: 1 };
90
+ }
91
+ const accExp = userData[0].exp + exp;
92
+ const accCount = userData[0].signCount + 1;
93
+ await this.ctx.database.set('jrys', { id: uid }, {
94
+ name: userid,
95
+ time: date,
96
+ exp: accExp,
97
+ signCount: accCount,
98
+ });
99
+ return { status: 0, allExp: accExp, signTime: date, count: accCount };
100
+ }
101
+ getLevelInfo(exp) {
102
+ let index = 0;
103
+ for (let i = 0; i < this.cfg.levelSet.length; i++) {
104
+ if (exp >= this.cfg.levelSet[i].levelExp)
105
+ index++;
106
+ else
107
+ break;
108
+ }
109
+ let nExp;
110
+ if (index >= this.cfg.levelSet.length)
111
+ nExp = '???';
112
+ else
113
+ nExp = this.cfg.levelSet[index].levelExp;
114
+ index--;
115
+ return { levelInfo: this.cfg.levelSet[index], nextExp: nExp };
116
+ }
117
+ getFortuneInfo(luck) {
118
+ let index = 0;
119
+ for (let i = 0; i < this.cfg.fortuneSet.length; i++) {
120
+ if (luck >= this.cfg.fortuneSet[i].luck)
121
+ index++;
122
+ else
123
+ break;
124
+ }
125
+ index--;
126
+ return this.cfg.fortuneSet[index].desc;
127
+ }
128
+ getGreeting(hour) {
129
+ const g = timeGreetings.find(t => hour >= t.range[0] && hour < t.range[1]);
130
+ return g ? g.message : '你好';
131
+ }
132
+ }
133
+ exports.Signin = Signin;
@@ -0,0 +1,170 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>运势签到</title>
7
+ <style>
8
+ @font-face {
9
+ font-family: 'emoji';
10
+ src: url("notoemoji.subset.woff2") format("woff2");
11
+ }
12
+ @font-face {
13
+ font-family: 'osans4';
14
+ src: url("osans4.subset.woff2") format("woff2");
15
+ }
16
+ * { margin: 0; padding: 0; box-sizing: border-box; }
17
+ body {
18
+ font-family: osans4, emoji, Arial, sans-serif;
19
+ }
20
+ .container { max-width: 600px; }
21
+
22
+ /* ── 头部:头像 + 日期问候 ── */
23
+ .header {
24
+ display: flex;
25
+ flex-direction: row;
26
+ justify-content: space-between;
27
+ margin-left: 24px;
28
+ margin-right: 24px;
29
+ margin-top: -50px;
30
+ }
31
+ .avatar {
32
+ max-width: 80px;
33
+ max-height: 80px;
34
+ border-radius: 10px;
35
+ border: 3px solid #ffffff;
36
+ }
37
+ .dateInfo {
38
+ max-height: 83px;
39
+ width: 80%;
40
+ border-radius: 10px;
41
+ display: flex;
42
+ flex-direction: row;
43
+ justify-content: space-between;
44
+ align-items: center;
45
+ font-size: 60px;
46
+ font-weight: bold;
47
+ }
48
+ .dateInfo span {
49
+ margin-left: 16px;
50
+ margin-right: 16px;
51
+ }
52
+
53
+ /* ── 一言 ── */
54
+ .hitokoto {
55
+ color: #838383;
56
+ font-size: 16px;
57
+ text-align: center;
58
+ margin-top: 6px;
59
+ }
60
+
61
+ /* ── 内容区 ── */
62
+ .content {
63
+ padding: 5px 30px 10px 30px;
64
+ }
65
+ .signin {
66
+ color: #838383;
67
+ margin: 0 0 10px 0;
68
+ font-size: 20px;
69
+ font-weight: 500;
70
+ }
71
+ .levelInfo {
72
+ display: flex;
73
+ justify-content: center;
74
+ font-size: 30px;
75
+ font-weight: bold;
76
+ margin-bottom: 10px;
77
+ }
78
+
79
+ /* ── 好运值 ── */
80
+ .fortune {
81
+ display: flex;
82
+ flex-direction: row;
83
+ justify-content: space-between;
84
+ align-items: center;
85
+ margin: 10px 5px 0 0;
86
+ }
87
+
88
+ /* ── 宜/忌 ── */
89
+ .toDo {
90
+ display: flex;
91
+ flex-direction: row;
92
+ margin-top: 18px;
93
+ align-items: center;
94
+ }
95
+ .toDo p {
96
+ margin: 0 0 0 20px;
97
+ font-size: 20px;
98
+ font-weight: 500;
99
+ color: #434343;
100
+ }
101
+ .toDoBg {
102
+ border-radius: 50%;
103
+ width: 48px;
104
+ height: 48px;
105
+ display: flex;
106
+ justify-content: center;
107
+ align-items: center;
108
+ }
109
+ .toDoBg span {
110
+ font-size: 32px;
111
+ font-weight: bold;
112
+ color: white;
113
+ }
114
+ .credit {
115
+ margin-top: 5px;
116
+ text-align: center;
117
+ color: #999;
118
+ font-size: 12px;
119
+ padding: 10px;
120
+ }
121
+ </style>
122
+ </head>
123
+ <body id="body">
124
+ <div class="container">
125
+
126
+ <img style="width: 100%;" src="{{BG_URL}}" alt="Top Image">
127
+
128
+ <div class="header">
129
+ <img class="avatar" src="{{AVATAR_URL}}" alt="Avatar">
130
+ <div class="dateInfo">
131
+ <span class="greeting">{{GREETING}}</span>
132
+ <span style="color: #666666;">{{MONTH}}/{{DAY}}</span>
133
+ </div>
134
+ </div>
135
+
136
+ <div class="hitokoto">
137
+ <p>{{HITOKOTO}}</p>
138
+ </div>
139
+
140
+ <div class="content">
141
+ <div class="signin"><strong>{{NAME}}</strong> 签到成功!</div>
142
+
143
+ <div class="levelInfo">
144
+ <span style="color: {{LEVEL_COLOR}};">{{LEVEL_NAME}}</span>
145
+ </div>
146
+
147
+ <div class="fortune">
148
+ <span style="font-size: 36px; font-weight: bold;">
149
+ <span style="font-size: 28px;">⭐</span>{{LUCK}}
150
+ </span>
151
+ <span style="font-size: 28px; color: #838383;">🌠{{LUCK_INFO}}</span>
152
+ </div>
153
+
154
+ <hr>
155
+
156
+ <div class="toDo">
157
+ <div class="toDoBg" style="background-color: #D4473D;"><span>宜</span></div>
158
+ <p style="text-shadow: 0px 0px 1px #ffbbbb;">{{GOODDO}}</p>
159
+ </div>
160
+
161
+ <div class="toDo">
162
+ <div class="toDoBg" style="background-color: #000000;"><span>忌</span></div>
163
+ <p style="text-shadow: 0px 0px 1px #bcdbff;">{{BADDO}}</p>
164
+ </div>
165
+ </div>
166
+
167
+ <div class="credit">随机生成 请勿迷信 | JRYS+</div>
168
+ </div>
169
+ </body>
170
+ </html>