koishi-plugin-rocom 1.0.17 → 1.0.19

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.
Files changed (57) hide show
  1. package/lib/announcement-service.d.ts +21 -0
  2. package/lib/announcement-service.js +63 -0
  3. package/lib/async-control.d.ts +3 -0
  4. package/lib/async-control.js +48 -0
  5. package/lib/client.d.ts +49 -9
  6. package/lib/client.js +250 -141
  7. package/lib/commands/egg.js +101 -9
  8. package/lib/commands/merchant.js +144 -146
  9. package/lib/commands/query.js +148 -117
  10. package/lib/commands/ranking.d.ts +2 -0
  11. package/lib/commands/ranking.js +31 -0
  12. package/lib/commands/share-code.d.ts +2 -0
  13. package/lib/commands/share-code.js +66 -0
  14. package/lib/commands/tools.js +47 -59
  15. package/lib/egg-service.d.ts +125 -20
  16. package/lib/egg-service.js +364 -96
  17. package/lib/img//345/274/202/350/211/262.png +0 -0
  18. package/lib/img//347/202/253/345/275/251.png +0 -0
  19. package/lib/index.d.ts +5 -0
  20. package/lib/index.js +193 -8740
  21. package/lib/merchant-clock.d.ts +22 -0
  22. package/lib/merchant-clock.js +95 -0
  23. package/lib/merchant-service.d.ts +24 -0
  24. package/lib/merchant-service.js +148 -0
  25. package/lib/permissions.d.ts +16 -0
  26. package/lib/permissions.js +66 -0
  27. package/lib/pet-data-service.js +7 -9
  28. package/lib/pet-size.d.ts +13 -0
  29. package/lib/pet-size.js +54 -0
  30. package/lib/player-service.d.ts +37 -0
  31. package/lib/player-service.js +201 -0
  32. package/lib/ranking-service.d.ts +46 -0
  33. package/lib/ranking-service.js +137 -0
  34. package/lib/render-templates/announcement/detail.html +8 -0
  35. package/lib/render-templates/announcement/list.html +5 -0
  36. package/lib/render-templates/announcement/style.css +1 -0
  37. package/lib/render-templates/pet-ranking/index.html +99 -0
  38. package/lib/render-templates/pet-ranking/style.css +338 -0
  39. package/lib/render-templates/player-search/index.html +7 -0
  40. package/lib/render-templates/searcheggs/index.html +1 -1
  41. package/lib/render-templates/searcheggs/size.html +70 -68
  42. package/lib/render-templates/share-code-team/index.html +105 -0
  43. package/lib/render-templates/share-code-team/style.css +345 -0
  44. package/lib/render.d.ts +15 -2
  45. package/lib/render.js +120 -37
  46. package/lib/send-image.d.ts +1 -1
  47. package/lib/send-image.js +12 -1
  48. package/lib/share-code-service.d.ts +32 -0
  49. package/lib/share-code-service.js +184 -0
  50. package/lib/subscription-runner.d.ts +6 -0
  51. package/lib/subscription-runner.js +21 -0
  52. package/lib/subscription-send.d.ts +4 -2
  53. package/lib/subscription-send.js +45 -20
  54. package/lib/types.d.ts +5 -0
  55. package/lib/user.d.ts +5 -0
  56. package/package.json +4 -1
  57. package/readme.md +145 -124
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resourceUrl = resourceUrl;
4
+ exports.playerRows = playerRows;
5
+ exports.formatPlayerTime = formatPlayerTime;
6
+ exports.cleanPlayerFieldValue = cleanPlayerFieldValue;
7
+ exports.parseIngamePlayerPayload = parseIngamePlayerPayload;
8
+ exports.playerField = playerField;
9
+ exports.buildPlayerView = buildPlayerView;
10
+ exports.mergePlayerPayloads = mergePlayerPayloads;
11
+ exports.playerPayloadUid = playerPayloadUid;
12
+ exports.buildPlayerText = buildPlayerText;
13
+ const client_1 = require("./client");
14
+ // 相对资源地址转绝对地址(上游 v4.1.0);非字符串/非法协议返回空串,避免渲染裂图。
15
+ function resourceUrl(value, apiBaseUrl) {
16
+ if (typeof value !== 'string' || !value.trim())
17
+ return '';
18
+ // `relative/` 是后端资源标记,不属于资源路径。
19
+ const text = value.trim().replace(/^relative\//, '');
20
+ if (!text)
21
+ return '';
22
+ if (/^data:/i.test(text))
23
+ return text;
24
+ try {
25
+ const url = new URL(text, apiBaseUrl.replace(/\/$/, '') + '/');
26
+ return ['https:', 'http:'].includes(url.protocol) ? url.href : '';
27
+ }
28
+ catch {
29
+ return '';
30
+ }
31
+ }
32
+ // 将 v4.1.0 玩家嵌套结构与旧 rows 统一成 IngamePlayerRow[] 目标展示。
33
+ // 值为 0/false 需要保留,不能用 `value || ''` 清空。
34
+ function playerRows(payload, uid) {
35
+ const legacy = Array.isArray(payload?.rows) ? payload.rows : [];
36
+ const p = payload?.player_info ?? {};
37
+ const c = payload?.player_card_brief_info ?? {};
38
+ const stats = c.card_pet_info ?? {};
39
+ const home = p.home_info ?? {};
40
+ const visit = p.visit_info ?? {};
41
+ const appearance = c.card_appearance_info ?? {};
42
+ const business = c.business_card_info ?? {};
43
+ const fields = [
44
+ ['uin', 'UID', p.uin ?? p.uid ?? legacy.find(row => row.field === 'uin')?.value ?? uid],
45
+ ['name', '昵称', p.name],
46
+ ['level', '等级', p.level],
47
+ ['online', '在线状态', p.online ?? p.is_online ?? payload?.online],
48
+ ['sex', '性别', p.sex ?? p.gender],
49
+ ['gender', '性别', p.gender ?? p.sex],
50
+ ['signature', '个性签名', p.signature ?? c.card_signature],
51
+ ['home_name', '家园名称', home.home_name ?? home.name],
52
+ ['home_level', '家园等级', home.home_level],
53
+ ['room_level', '房间等级', home.room_level],
54
+ ['home_experience', '家园经验', home.home_experience],
55
+ ['home_comfort_level', '舒适度', home.home_comfort_level ?? home.comfort_level],
56
+ ['visitor_num', '访客数量', visit.visitor_num],
57
+ ['world_level', '世界等级', p.world_level],
58
+ ['card_handbook_collect_num', '图鉴收集', p.card_handbook_collect_num ?? c.card_handbook_collect_num],
59
+ ['last_logout_time', '最后离线', p.last_logout_time],
60
+ ['collected_shining_pet_count', '异色收集', stats.collected_shining_pet_count],
61
+ ['collected_glass_pet_count', '炫彩收集', stats.collected_glass_pet_count],
62
+ ['card_skin_selected', '名片皮肤', appearance.card_skin_selected ?? p.card_skin_selected ?? business.card_skin_selected],
63
+ ['card_icon_selected', '名片头像', c.card_icon_selected ?? p.card_icon_selected ?? appearance.card_icon_selected ?? business.card_icon_selected],
64
+ ['card_bussiness_card_url', '名片图片', business.cur_card_url ?? c.cur_card_url ?? p.card_bussiness_card_url],
65
+ ];
66
+ const mapped = fields
67
+ .filter(([, , v]) => v !== undefined && v !== null)
68
+ .map(([field, label, value]) => ({ field, label, value }));
69
+ return [...new Map([...legacy, ...mapped].map(row => [row.field, row])).values()];
70
+ }
71
+ /** 玩家时间统一按北京时间展示,不依赖 Bot 所在服务器的时区。 */
72
+ function formatPlayerTime(value) {
73
+ if (typeof value !== 'string' && typeof value !== 'number')
74
+ return '未知';
75
+ const text = String(value).trim();
76
+ if (!text)
77
+ return '未知';
78
+ let timestamp;
79
+ if (/^[+-]?\d+(?:\.\d+)?$/.test(text)) {
80
+ const numeric = Number(text);
81
+ if (!Number.isFinite(numeric) || numeric <= 0)
82
+ return '未知';
83
+ timestamp = numeric < 100_000_000_000 ? numeric * 1000 : numeric;
84
+ }
85
+ else {
86
+ // 已格式化的无时区文本视作北京时间;带 Z/偏移的 ISO 日期按原偏移解析。
87
+ const match = /^(\d{4})[-/](\d{2})[-/](\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?))?(Z|[+-]\d{2}:?\d{2})?(?:(北京时间))?$/i.exec(text);
88
+ if (!match)
89
+ return '未知';
90
+ timestamp = Date.parse(`${match[1]}-${match[2]}-${match[3]}T${match[4] || '00:00:00'}${match[5] || '+08:00'}`);
91
+ }
92
+ const beijing = new Date(timestamp + 8 * 3600_000);
93
+ if (!Number.isFinite(beijing.getTime()))
94
+ return '未知';
95
+ return `${beijing.toISOString().slice(0, 19).replace('T', ' ')}(北京时间)`;
96
+ }
97
+ function cleanPlayerFieldValue(field, value) {
98
+ if (field === 'last_logout_time')
99
+ return formatPlayerTime(value);
100
+ const text = String(value ?? '').trim().replace(/^'+|'+$/g, '');
101
+ if (!text || ['<0B>', '<0b>', '<0B >', '<0b >'].includes(text))
102
+ return '未设置';
103
+ if (['is_online', 'online', 'chat_top_unlock', 'is_friend', 'is_black', 'is_black_role', 'is_chat_node_unlock'].includes(field)) {
104
+ return ['1', 'true', 'True', '是'].includes(text) ? '是' : '否';
105
+ }
106
+ if (['sex', 'gender'].includes(field)) {
107
+ return { '0': '未知', '1': '男', '2': '女' }[text] || text;
108
+ }
109
+ if (field === 'friend_type') {
110
+ return { '0': '默认', '1': '特殊' }[text] || text;
111
+ }
112
+ if (field === 'battle_state') {
113
+ return { '0': '空闲', '1': '对战中' }[text] || text;
114
+ }
115
+ return text;
116
+ }
117
+ function parseIngamePlayerPayload(payload, uid) {
118
+ const rows = playerRows(payload, uid);
119
+ const rowMap = {};
120
+ const labelMap = {};
121
+ for (const row of rows) {
122
+ const field = String(row.field || '');
123
+ if (!field)
124
+ continue;
125
+ rowMap[field] = String(row.value ?? '');
126
+ labelMap[field] = String(row.label || field);
127
+ }
128
+ const playerUid = cleanPlayerFieldValue('uin', rowMap.uin || uid);
129
+ const signature = cleanPlayerFieldValue('signature', rowMap.signature || '');
130
+ return {
131
+ title: String(payload?.title || '玩家搜索'),
132
+ nickname: cleanPlayerFieldValue('name', rowMap.name || '-'),
133
+ uid: playerUid,
134
+ level: cleanPlayerFieldValue('level', rowMap.level || '-'),
135
+ signature: signature === '未设置' ? '' : signature,
136
+ rowMap,
137
+ labelMap,
138
+ };
139
+ }
140
+ function playerField(parsed, field, defaultValue = '未设置') {
141
+ if (!parsed)
142
+ return defaultValue;
143
+ const raw = parsed.rowMap[field];
144
+ if (field === 'last_logout_time')
145
+ return formatPlayerTime(raw);
146
+ if (raw == null || raw === '')
147
+ return defaultValue;
148
+ const value = cleanPlayerFieldValue(field, raw);
149
+ return value && value !== '-' && value !== '未设置' ? value : defaultValue;
150
+ }
151
+ // 合并搜索结果与名片结果;名片失败时保留基础信息(上游 v4.1.0)。
152
+ function buildPlayerView(searchPayload, cardPayload, uid, apiBaseUrl) {
153
+ const merged = mergePlayerPayloads(searchPayload, cardPayload);
154
+ const parsed = parseIngamePlayerPayload(merged, uid);
155
+ const cardRaw = playerField(parsed, 'card_bussiness_card_url', '');
156
+ return {
157
+ parsed,
158
+ cardImageUrl: cardRaw ? resourceUrl(cardRaw, apiBaseUrl) : '',
159
+ };
160
+ }
161
+ function mergePlayerPayloads(searchPayload, cardPayload) {
162
+ const result = { ...(searchPayload || {}) };
163
+ const searchUid = playerPayloadUid(searchPayload);
164
+ const cardUid = playerPayloadUid(cardPayload);
165
+ if (searchUid && cardUid && searchUid !== cardUid)
166
+ return result;
167
+ if (!(0, client_1.isObject)(cardPayload))
168
+ return result;
169
+ const searchInfo = (0, client_1.isObject)(result.player_info) ? result.player_info : {};
170
+ const cardInfo = (0, client_1.isObject)(cardPayload.player_info) ? cardPayload.player_info : {};
171
+ if (Object.keys(cardInfo).length)
172
+ result.player_info = { ...searchInfo, ...cardInfo };
173
+ for (const key of ['home_info', 'visit_info', 'battle_brief_info', 'pos_info']) {
174
+ if ((0, client_1.isObject)(searchInfo[key]) && (0, client_1.isObject)(cardInfo[key])) {
175
+ result.player_info[key] = { ...searchInfo[key], ...cardInfo[key] };
176
+ }
177
+ }
178
+ if ((0, client_1.isObject)(cardPayload.player_card_brief_info)) {
179
+ const base = (0, client_1.isObject)(result.player_card_brief_info) ? result.player_card_brief_info : {};
180
+ result.player_card_brief_info = { ...base, ...cardPayload.player_card_brief_info };
181
+ for (const key of ['card_pet_info', 'business_card_info', 'card_appearance_info']) {
182
+ if ((0, client_1.isObject)(base[key]) && (0, client_1.isObject)(cardPayload.player_card_brief_info[key])) {
183
+ result.player_card_brief_info[key] = { ...base[key], ...cardPayload.player_card_brief_info[key] };
184
+ }
185
+ }
186
+ }
187
+ if (Array.isArray(result.rows) && result.rows.length && !(0, client_1.isObject)(cardPayload.player_card_brief_info)) {
188
+ return result;
189
+ }
190
+ return result;
191
+ }
192
+ function playerPayloadUid(payload) {
193
+ return String(payload?.player_info?.uin ?? payload?.player_info?.uid ?? payload?.uin ?? payload?.uid
194
+ ?? payload?.rows?.find((row) => row.field === 'uin')?.value ?? '');
195
+ }
196
+ function buildPlayerText(payload, uid) {
197
+ const p = parseIngamePlayerPayload(payload, uid);
198
+ return ['洛克玩家 ' + p.nickname + ' · UID ' + p.uid,
199
+ ...Object.entries(p.rowMap).filter(([key]) => !['uin', 'name'].includes(key))
200
+ .map(([key, value]) => (p.labelMap[key] || key) + ':' + cleanPlayerFieldValue(key, value))].join('\n');
201
+ }
@@ -0,0 +1,46 @@
1
+ export type RankingKind = 'shining' | 'glass';
2
+ interface RankingMeta {
3
+ title: string;
4
+ subtitle: string;
5
+ label: string;
6
+ otherLabel: string;
7
+ countField: string;
8
+ otherCountField: string;
9
+ theme: string;
10
+ }
11
+ export declare const RANKING_META: Record<RankingKind, RankingMeta>;
12
+ export interface RankingArgs {
13
+ uid: string;
14
+ limit: number;
15
+ }
16
+ export declare function parseRankingArgs(text: string, primaryUid?: string): RankingArgs;
17
+ export declare function formatRankingTime(value: unknown): string;
18
+ export declare function buildRankingView(payload: any, rankType: RankingKind, baseUrl: string, requestedUid?: string, now?: Date): {
19
+ total: number;
20
+ items: {
21
+ rank: number;
22
+ podiumClass: string;
23
+ playerName: string;
24
+ avatar: string;
25
+ signature: string;
26
+ primaryCount: number;
27
+ secondaryCount: number;
28
+ sampleCount: number;
29
+ lastSeen: string;
30
+ }[];
31
+ current: any;
32
+ requestedUid: string;
33
+ shownCount: number;
34
+ updatedAt: string;
35
+ commandHint: string;
36
+ copyright: string;
37
+ title: string;
38
+ subtitle: string;
39
+ label: string;
40
+ otherLabel: string;
41
+ countField: string;
42
+ otherCountField: string;
43
+ theme: string;
44
+ };
45
+ export declare function buildRankingText(data: any): string;
46
+ export {};
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RANKING_META = void 0;
4
+ exports.parseRankingArgs = parseRankingArgs;
5
+ exports.formatRankingTime = formatRankingTime;
6
+ exports.buildRankingView = buildRankingView;
7
+ exports.buildRankingText = buildRankingText;
8
+ const client_1 = require("./client");
9
+ exports.RANKING_META = {
10
+ shining: {
11
+ title: '异色精灵排行榜',
12
+ subtitle: '按异色精灵收集数排名',
13
+ label: '异色',
14
+ otherLabel: '炫彩',
15
+ countField: 'collected_shining_pet_count',
16
+ otherCountField: 'collected_glass_pet_count',
17
+ theme: 'shining',
18
+ },
19
+ glass: {
20
+ title: '炫彩精灵排行榜',
21
+ subtitle: '按炫彩精灵收集数排名',
22
+ label: '炫彩',
23
+ otherLabel: '异色',
24
+ countField: 'collected_glass_pet_count',
25
+ otherCountField: 'collected_shining_pet_count',
26
+ theme: 'glass',
27
+ },
28
+ };
29
+ // 单参数 1–50 视为数量;其他纯数字视为 UID;两参数分别为 UID/数量。
30
+ function parseRankingArgs(text, primaryUid = '') {
31
+ const parts = String(text || '').trim().split(/\s+/).filter(Boolean);
32
+ if (parts.length > 2)
33
+ throw new Error('用法:排行榜 [UID] [数量]');
34
+ let uid = '';
35
+ let limit = 10;
36
+ if (parts.length === 1) {
37
+ const value = parts[0];
38
+ if (/^\d+$/.test(value) && Number(value) >= 1 && Number(value) <= 50) {
39
+ limit = Number(value);
40
+ }
41
+ else {
42
+ uid = value;
43
+ }
44
+ }
45
+ else if (parts.length === 2) {
46
+ uid = parts[0];
47
+ if (!/^\d+$/.test(parts[1]))
48
+ throw new Error('数量必须是 1–50 的整数');
49
+ limit = Number(parts[1]);
50
+ }
51
+ if (uid && !/^\d+$/.test(uid))
52
+ throw new Error('UID 只能包含数字');
53
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
54
+ throw new Error('数量必须是 1–50 的整数');
55
+ }
56
+ return { uid: uid || primaryUid, limit };
57
+ }
58
+ function toInt(value, fallback = 0) {
59
+ const n = Number(value);
60
+ return Number.isFinite(n) ? Math.trunc(n) : fallback;
61
+ }
62
+ // 无时区的时间字符串按 +08:00 解释(与上游一致)。
63
+ function formatRankingTime(value) {
64
+ const text = String(value ?? '').trim();
65
+ if (!text)
66
+ return '未知';
67
+ const normalized = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(text) && !/(Z|[+-]\d{2}:?\d{2})$/.test(text)
68
+ ? `${text.replace(' ', 'T')}+08:00`
69
+ : text.replace('Z', '+00:00');
70
+ const date = new Date(normalized);
71
+ if (Number.isNaN(date.getTime()))
72
+ return text;
73
+ const shifted = new Date(date.getTime() + 8 * 3600_000);
74
+ const pad = (v) => String(v).padStart(2, '0');
75
+ return `${pad(shifted.getUTCMonth() + 1)}-${pad(shifted.getUTCDate())} ${pad(shifted.getUTCHours())}:${pad(shifted.getUTCMinutes())}`;
76
+ }
77
+ function avatarUrl(baseUrl, cardIcon) {
78
+ const iconId = String(cardIcon ?? '').trim();
79
+ if (!iconId)
80
+ return '';
81
+ return `${String(baseUrl || '').replace(/\/$/, '')}/api/v1/resources/wiki/assets/profile/avatar/${iconId}.png`;
82
+ }
83
+ function normalizeItem(item, meta, baseUrl) {
84
+ const rank = toInt(item?.rank);
85
+ return {
86
+ rank,
87
+ podiumClass: rank >= 1 && rank <= 3 ? `podium-${rank}` : '',
88
+ playerName: String(item?.player_name || '未记录名称'),
89
+ avatar: avatarUrl(baseUrl, item?.card_icon_selected),
90
+ signature: String(item?.card_signature || '暂无签名'),
91
+ primaryCount: toInt(item?.[meta.countField]),
92
+ secondaryCount: toInt(item?.[meta.otherCountField]),
93
+ sampleCount: toInt(item?.sample_count),
94
+ lastSeen: formatRankingTime(item?.last_seen_at),
95
+ };
96
+ }
97
+ function buildRankingView(payload, rankType, baseUrl, requestedUid = '', now = new Date()) {
98
+ const meta = exports.RANKING_META[rankType] || exports.RANKING_META.shining;
99
+ const data = (0, client_1.isObject)(payload) ? payload : {};
100
+ const items = (Array.isArray(data.items) ? data.items : [])
101
+ .filter(item => (0, client_1.isObject)(item))
102
+ .map(item => normalizeItem(item, meta, baseUrl));
103
+ let current = null;
104
+ if ((0, client_1.isObject)(data.current)) {
105
+ current = normalizeItem(data.current, meta, baseUrl);
106
+ current.uid = String(data.current.uid || requestedUid || '');
107
+ current.titleText = '该玩家名次';
108
+ }
109
+ const pad = (v) => String(v).padStart(2, '0');
110
+ const updatedAt = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
111
+ return {
112
+ ...meta,
113
+ total: toInt(data.total),
114
+ items,
115
+ current,
116
+ requestedUid: String(requestedUid || ''),
117
+ shownCount: items.length,
118
+ updatedAt,
119
+ commandHint: `/${meta.label}排行榜 [UID] [数量] · 数量支持 1-50`,
120
+ copyright: 'Koishi & WeGame 洛克王国插件',
121
+ };
122
+ }
123
+ function buildRankingText(data) {
124
+ const lines = [`${data.title || '精灵排行榜'}(共 ${data.total || 0} 名)`];
125
+ for (const item of data.items || []) {
126
+ lines.push(`#${item.rank} ${item.playerName}:${data.label} ${item.primaryCount},${data.otherLabel} ${item.secondaryCount}`);
127
+ }
128
+ if (data.current) {
129
+ lines.push(`该玩家名次:#${data.current.rank} ${data.current.playerName},${data.label} ${data.current.primaryCount}`);
130
+ }
131
+ else if (data.requestedUid) {
132
+ lines.push(`UID ${data.requestedUid} 暂无排行榜记录。`);
133
+ }
134
+ if (!data.items?.length && !data.current)
135
+ lines.push('当前排行榜暂无记录。');
136
+ return lines.join('\n');
137
+ }
@@ -0,0 +1,8 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN"><head><meta charset="utf-8"><link rel="stylesheet" href="{{_res_path}}render-templates/announcement/style.css"></head>
3
+ <body><main class="announcement-detail-page">
4
+ <header><span>洛克王国 · 公告 {{id}}</span><h1>{{title}}</h1><p>{{publishedAt}}</p></header>
5
+ {{each blocks entry}}<section>{{if entry.text}}<p class="content">{{entry.text}}</p>{{/if}}{{if entry.image}}<img src="{{entry.image}}" alt="公告图片" onerror="this.style.display='none'">{{/if}}</section>{{/each}}
6
+ {{if !blocks.length}}<p>暂无正文</p>{{/if}}
7
+ <footer>{{commandHint}}<br>{{copyright}}</footer>
8
+ </main></body></html>
@@ -0,0 +1,5 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN"><head><meta charset="utf-8"><link rel="stylesheet" href="{{_res_path}}render-templates/announcement/style.css"></head>
3
+ <body><main class="announcement-list-page"><header><span>洛克王国 · 第 {{page}} 页</span><h1>{{title}}</h1></header>
4
+ {{each items item}}<article><h2>{{item.title}}</h2><p>{{item.summary}}</p><small>#{{item.id}} · {{item.publishedAt}}</small></article>{{/each}}
5
+ {{if !items.length}}<p>当前页暂无公告</p>{{/if}}<footer>{{commandHint}}<br>{{copyright}}</footer></main></body></html>
@@ -0,0 +1 @@
1
+ @font-face{font-family:Rocom;src:url('../../ttf/HYWenHei-85W-1.ttf')}*{box-sizing:border-box}body{margin:0;background:#eee7d9;color:#473828;font-family:Rocom,'Microsoft YaHei',sans-serif}.announcement-detail-page,.announcement-list-page{width:1080px;padding:48px;background:#faf5e9}header{border-bottom:3px solid #bc914a;padding-bottom:22px}h1{font-size:38px;line-height:1.5}h2{font-size:27px}p{font-size:23px;line-height:1.8;overflow-wrap:anywhere}header span,small{color:#947346;font-size:18px}.content{white-space:pre-wrap}section img{display:block;width:auto;max-width:100%;height:auto;margin:20px auto}article{padding:20px 0;border-bottom:1px solid #dac9ad}footer{font-size:18px;line-height:1.8;margin-top:36px;color:#947346}
@@ -0,0 +1,99 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=1080, initial-scale=1">
6
+ <title>{{title}}</title>
7
+ <link rel="stylesheet" href="{{_res_path}}render/pet-ranking/style.css">
8
+ </head>
9
+ <body>
10
+ <section class="pet-ranking-page theme-{{theme}}">
11
+ <header class="ranking-header">
12
+ <div class="title-icon">
13
+ {{if theme == "shining"}}
14
+ <img src="{{_res_path}}img/异色.png" alt="">
15
+ {{else}}
16
+ <img src="{{_res_path}}img/炫彩.png" alt="">
17
+ {{/if}}
18
+ </div>
19
+ <div class="title-copy">
20
+ <h1>{{title}}</h1>
21
+ <p>{{subtitle}} · 当前收录 {{total}} 名玩家</p>
22
+ </div>
23
+ <div class="header-stat">
24
+ <span>TOP</span>
25
+ <strong>{{shownCount}}</strong>
26
+ </div>
27
+ </header>
28
+
29
+ {{if current}}
30
+ <section class="current-rank">
31
+ <div class="current-label">{{current.titleText || '该玩家名次'}}</div>
32
+ <div class="current-rank-value">#{{current.rank}}</div>
33
+ <div class="avatar-shell current-avatar">
34
+ <img class="avatar-fallback" src="{{_res_path}}img/头像黑色圆底.png" alt="">
35
+ {{if current.avatar}}
36
+ <img class="avatar-image" src="{{current.avatar}}" alt="" onerror="this.style.display='none'">
37
+ {{/if}}
38
+ </div>
39
+ <div class="current-player">
40
+ <strong>{{current.playerName}}</strong>
41
+ <span>UID {{current.uid}}</span>
42
+ </div>
43
+ <div class="current-count">
44
+ <strong>{{current.primaryCount}}</strong>
45
+ <span>{{label}}精灵</span>
46
+ </div>
47
+ <div class="current-count secondary">
48
+ <strong>{{current.secondaryCount}}</strong>
49
+ <span>{{otherLabel}}精灵</span>
50
+ </div>
51
+ </section>
52
+ {{else if requestedUid}}
53
+ <section class="current-rank current-empty">
54
+ <div class="current-label">该玩家名次</div>
55
+ <div class="empty-copy">UID {{requestedUid}} 暂无排行榜记录</div>
56
+ </section>
57
+ {{/if}}
58
+
59
+ <main class="ranking-list">
60
+ <div class="ranking-columns">
61
+ <span>名次 / 玩家</span>
62
+ <span>收集统计</span>
63
+ </div>
64
+ {{each items item}}
65
+ <article class="ranking-row {{item.podiumClass}}">
66
+ <div class="rank-number">{{item.rank}}</div>
67
+ <div class="avatar-shell">
68
+ <img class="avatar-fallback" src="{{_res_path}}img/头像黑色圆底.png" alt="">
69
+ {{if item.avatar}}
70
+ <img class="avatar-image" src="{{item.avatar}}" alt="" onerror="this.style.display='none'">
71
+ {{/if}}
72
+ </div>
73
+ <div class="player-copy">
74
+ <div class="player-name">{{item.playerName}}</div>
75
+ <div class="player-signature">{{item.signature}}</div>
76
+ <div class="player-meta">更新 {{item.lastSeen}} · 样本 {{item.sampleCount}}</div>
77
+ </div>
78
+ <div class="count-block primary-count">
79
+ <strong>{{item.primaryCount}}</strong>
80
+ <span>{{label}}</span>
81
+ </div>
82
+ <div class="count-block secondary-count">
83
+ <strong>{{item.secondaryCount}}</strong>
84
+ <span>{{otherLabel}}</span>
85
+ </div>
86
+ </article>
87
+ {{/each}}
88
+ {{if !items.length && !current}}
89
+ <div class="ranking-empty">当前排行榜暂无记录。</div>
90
+ {{/if}}
91
+ </main>
92
+
93
+ <footer class="ranking-footer">
94
+ <span>{{commandHint}}</span>
95
+ <span>{{updatedAt}} · {{copyright}}</span>
96
+ </footer>
97
+ </section>
98
+ </body>
99
+ </html>