sa2kit 1.6.91 → 1.6.92
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/dist/client-BlkUL2To.d.ts +26 -0
- package/dist/client-DpMIhrlS.d.mts +26 -0
- package/dist/index-C7yh6b5Q.d.mts +17 -0
- package/dist/index-CDapUIT5.d.mts +51 -0
- package/dist/index-Cv9jlnNz.d.ts +17 -0
- package/dist/index-D3UbkUai.d.ts +51 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1337 -58
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1306 -61
- package/dist/index.mjs.map +1 -1
- package/dist/mikuContest/index.d.mts +13 -0
- package/dist/mikuContest/index.d.ts +13 -0
- package/dist/mikuContest/index.js +1310 -0
- package/dist/mikuContest/index.js.map +1 -0
- package/dist/mikuContest/index.mjs +1253 -0
- package/dist/mikuContest/index.mjs.map +1 -0
- package/dist/mikuContest/logic/index.d.mts +32 -0
- package/dist/mikuContest/logic/index.d.ts +32 -0
- package/dist/mikuContest/logic/index.js +511 -0
- package/dist/mikuContest/logic/index.js.map +1 -0
- package/dist/mikuContest/logic/index.mjs +483 -0
- package/dist/mikuContest/logic/index.mjs.map +1 -0
- package/dist/mikuContest/routes/index.d.mts +80 -0
- package/dist/mikuContest/routes/index.d.ts +80 -0
- package/dist/mikuContest/routes/index.js +821 -0
- package/dist/mikuContest/routes/index.js.map +1 -0
- package/dist/mikuContest/routes/index.mjs +791 -0
- package/dist/mikuContest/routes/index.mjs.map +1 -0
- package/dist/mikuContest/server/index.d.mts +766 -0
- package/dist/mikuContest/server/index.d.ts +766 -0
- package/dist/mikuContest/server/index.js +705 -0
- package/dist/mikuContest/server/index.js.map +1 -0
- package/dist/mikuContest/server/index.mjs +672 -0
- package/dist/mikuContest/server/index.mjs.map +1 -0
- package/dist/mikuContest/service/index.d.mts +30 -0
- package/dist/mikuContest/service/index.d.ts +30 -0
- package/dist/mikuContest/service/index.js +139 -0
- package/dist/mikuContest/service/index.js.map +1 -0
- package/dist/mikuContest/service/index.mjs +135 -0
- package/dist/mikuContest/service/index.mjs.map +1 -0
- package/dist/mikuContest/types/index.d.mts +179 -0
- package/dist/mikuContest/types/index.d.ts +179 -0
- package/dist/mikuContest/types/index.js +4 -0
- package/dist/mikuContest/types/index.js.map +1 -0
- package/dist/mikuContest/types/index.mjs +3 -0
- package/dist/mikuContest/types/index.mjs.map +1 -0
- package/dist/mikuContest/ui/miniapp/index.d.mts +3 -0
- package/dist/mikuContest/ui/miniapp/index.d.ts +3 -0
- package/dist/mikuContest/ui/miniapp/index.js +566 -0
- package/dist/mikuContest/ui/miniapp/index.js.map +1 -0
- package/dist/mikuContest/ui/miniapp/index.mjs +540 -0
- package/dist/mikuContest/ui/miniapp/index.mjs.map +1 -0
- package/dist/mikuContest/ui/web/index.d.mts +4 -0
- package/dist/mikuContest/ui/web/index.d.ts +4 -0
- package/dist/mikuContest/ui/web/index.js +353 -0
- package/dist/mikuContest/ui/web/index.js.map +1 -0
- package/dist/mikuContest/ui/web/index.mjs +343 -0
- package/dist/mikuContest/ui/web/index.mjs.map +1 -0
- package/dist/service-D7DM1wW-.d.ts +38 -0
- package/dist/service-DPr2rlvH.d.mts +38 -0
- package/dist/types-BS7Xz09b.d.mts +14 -0
- package/dist/types-k4koMp4m.d.ts +14 -0
- package/package.json +41 -1
|
@@ -0,0 +1,1310 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React4 = require('react');
|
|
4
|
+
var XLSX = require('xlsx');
|
|
5
|
+
var pgCore = require('drizzle-orm/pg-core');
|
|
6
|
+
var drizzleOrm = require('drizzle-orm');
|
|
7
|
+
var server = require('next/server');
|
|
8
|
+
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
function _interopNamespace(e) {
|
|
12
|
+
if (e && e.__esModule) return e;
|
|
13
|
+
var n = Object.create(null);
|
|
14
|
+
if (e) {
|
|
15
|
+
Object.keys(e).forEach(function (k) {
|
|
16
|
+
if (k !== 'default') {
|
|
17
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
18
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () { return e[k]; }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
n.default = e;
|
|
26
|
+
return Object.freeze(n);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var React4__default = /*#__PURE__*/_interopDefault(React4);
|
|
30
|
+
var XLSX__namespace = /*#__PURE__*/_interopNamespace(XLSX);
|
|
31
|
+
|
|
32
|
+
var __defProp = Object.defineProperty;
|
|
33
|
+
var __export = (target, all) => {
|
|
34
|
+
for (var name in all)
|
|
35
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// src/mikuContest/logic/shared/defaults.ts
|
|
39
|
+
var defaultMikuVotingRules = {
|
|
40
|
+
maxVotesPerDay: 3,
|
|
41
|
+
forbidDuplicateVotePerWork: true,
|
|
42
|
+
maxVotesPerDevicePerDay: 20,
|
|
43
|
+
maxVotesPerIpPerDay: 100
|
|
44
|
+
};
|
|
45
|
+
var createDefaultMikuContestConfig = (overrides) => ({
|
|
46
|
+
id: overrides?.id || "miku-contest-default",
|
|
47
|
+
name: overrides?.name || "\u521D\u97F3\u672A\u6765\u793E\u56E2\u5F81\u7A3F\u5927\u8D5B",
|
|
48
|
+
theme: overrides?.theme || "\u521D\u97F3\u672A\u6765\u4E3B\u9898\u521B\u4F5C\u5F81\u7A3F",
|
|
49
|
+
organizer: overrides?.organizer || "\u521D\u97F3\u672A\u6765\u793E\u56E2",
|
|
50
|
+
awards: overrides?.awards || ["\u4E00\u7B49\u5956", "\u4E8C\u7B49\u5956", "\u4E09\u7B49\u5956", "\u4EBA\u6C14\u5956"],
|
|
51
|
+
rules: overrides?.rules || "\u8BF7\u786E\u4FDD\u4F5C\u54C1\u539F\u521B\u4E14\u7B26\u5408\u793E\u56E2\u89C4\u8303\u3002",
|
|
52
|
+
copyright: overrides?.copyright || "\u6295\u7A3F\u5373\u89C6\u4E3A\u6388\u6743\u8D5B\u4E8B\u5C55\u793A\u4E0E\u516C\u793A\u3002",
|
|
53
|
+
timeline: overrides?.timeline || {
|
|
54
|
+
submissionStartAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
55
|
+
submissionEndAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
56
|
+
votingStartAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
57
|
+
votingEndAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
58
|
+
publicResultAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
59
|
+
},
|
|
60
|
+
votingRules: {
|
|
61
|
+
...defaultMikuVotingRules,
|
|
62
|
+
...overrides?.votingRules || {}
|
|
63
|
+
},
|
|
64
|
+
toggles: {
|
|
65
|
+
submissionEnabled: overrides?.toggles?.submissionEnabled ?? true,
|
|
66
|
+
votingEnabled: overrides?.toggles?.votingEnabled ?? true,
|
|
67
|
+
resultEnabled: overrides?.toggles?.resultEnabled ?? false
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// src/mikuContest/logic/shared/validators.ts
|
|
72
|
+
var DESCRIPTION_LIMIT = 500;
|
|
73
|
+
var MINIAPP_DESCRIPTION_LIMIT = 200;
|
|
74
|
+
var TEXT_CONTENT_LIMIT = 2e3;
|
|
75
|
+
var MAX_TAGS = 3;
|
|
76
|
+
var hasValue = (value) => {
|
|
77
|
+
return typeof value === "string" ? value.trim().length > 0 : value !== null && value !== void 0;
|
|
78
|
+
};
|
|
79
|
+
var validateByType = (type, input) => {
|
|
80
|
+
const errors = [];
|
|
81
|
+
const { content } = input;
|
|
82
|
+
switch (type) {
|
|
83
|
+
case "visual": {
|
|
84
|
+
const imageCount = content.images?.length || 0;
|
|
85
|
+
if (imageCount < 1 || imageCount > 3) {
|
|
86
|
+
errors.push("\u89C6\u89C9\u7C7B\u4F5C\u54C1\u9700\u4E0A\u4F20 1-3 \u5F20\u56FE\u7247");
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "video": {
|
|
91
|
+
if (!hasValue(content.videoLink)) {
|
|
92
|
+
errors.push("\u89C6\u9891\u7C7B\u4F5C\u54C1\u9700\u63D0\u4F9B\u89C6\u9891\u94FE\u63A5");
|
|
93
|
+
}
|
|
94
|
+
if (!hasValue(content.coverImage)) {
|
|
95
|
+
errors.push("\u89C6\u9891\u7C7B\u4F5C\u54C1\u9700\u63D0\u4F9B\u5C01\u9762\u56FE");
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "text": {
|
|
100
|
+
const text2 = content.textContent || "";
|
|
101
|
+
if (!text2.trim()) {
|
|
102
|
+
errors.push("\u6587\u5B57\u7C7B\u4F5C\u54C1\u9700\u586B\u5199\u6B63\u6587");
|
|
103
|
+
}
|
|
104
|
+
if (text2.length > TEXT_CONTENT_LIMIT) {
|
|
105
|
+
errors.push(`\u6587\u5B57\u6B63\u6587\u4E0D\u80FD\u8D85\u8FC7 ${TEXT_CONTENT_LIMIT} \u5B57`);
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "audio": {
|
|
110
|
+
if (!hasValue(content.audioLink)) {
|
|
111
|
+
errors.push("\u97F3\u9891\u7C7B\u4F5C\u54C1\u9700\u63D0\u4F9B\u97F3\u9891\u94FE\u63A5");
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return errors;
|
|
117
|
+
};
|
|
118
|
+
var validateMikuSubmissionInput = (input, mode = "web") => {
|
|
119
|
+
const errors = [];
|
|
120
|
+
if (!input.contestId.trim()) errors.push("contestId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
121
|
+
if (!input.authorId.trim()) errors.push("authorId \u4E0D\u80FD\u4E3A\u7A7A");
|
|
122
|
+
if (!input.authorNickname.trim()) errors.push("\u4F5C\u8005\u6635\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
|
|
123
|
+
if (!input.title.trim()) errors.push("\u4F5C\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
|
|
124
|
+
const descriptionLimit = mode === "miniapp" ? MINIAPP_DESCRIPTION_LIMIT : DESCRIPTION_LIMIT;
|
|
125
|
+
if (input.description.length > descriptionLimit) {
|
|
126
|
+
errors.push(`\u4F5C\u54C1\u7B80\u4ECB\u4E0D\u80FD\u8D85\u8FC7 ${descriptionLimit} \u5B57`);
|
|
127
|
+
}
|
|
128
|
+
if ((input.tags?.length || 0) > MAX_TAGS) {
|
|
129
|
+
errors.push(`\u6807\u7B7E\u6700\u591A ${MAX_TAGS} \u4E2A`);
|
|
130
|
+
}
|
|
131
|
+
errors.push(...validateByType(input.type, input));
|
|
132
|
+
return errors;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// src/mikuContest/logic/shared/voting.ts
|
|
136
|
+
var toVoteDayKey = (date = /* @__PURE__ */ new Date()) => {
|
|
137
|
+
const y = date.getUTCFullYear();
|
|
138
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
139
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
140
|
+
return `${y}-${m}-${d}`;
|
|
141
|
+
};
|
|
142
|
+
var checkVoteEligibility = (context) => {
|
|
143
|
+
const { existingVotes, submissionId, voterId, dayKey, rules } = context;
|
|
144
|
+
const userTodayVotes = existingVotes.filter((vote) => vote.voterId === voterId && vote.dayKey === dayKey);
|
|
145
|
+
if (userTodayVotes.length >= rules.maxVotesPerDay) {
|
|
146
|
+
return { ok: false, reason: "\u5DF2\u8FBE\u5230\u4ECA\u65E5\u6295\u7968\u4E0A\u9650" };
|
|
147
|
+
}
|
|
148
|
+
if (rules.forbidDuplicateVotePerWork) {
|
|
149
|
+
const duplicated = userTodayVotes.some((vote) => vote.submissionId === submissionId);
|
|
150
|
+
if (duplicated) return { ok: false, reason: "\u4E0D\u53EF\u91CD\u590D\u6295\u540C\u4E00\u4F5C\u54C1" };
|
|
151
|
+
}
|
|
152
|
+
return { ok: true };
|
|
153
|
+
};
|
|
154
|
+
var sortByVotesDesc = (items) => {
|
|
155
|
+
return [...items].sort((a, b) => {
|
|
156
|
+
if (b.voteCount === a.voteCount) {
|
|
157
|
+
return (a.createdAt || "").localeCompare(b.createdAt || "");
|
|
158
|
+
}
|
|
159
|
+
return b.voteCount - a.voteCount;
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
var randomId = (prefix) => {
|
|
163
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
164
|
+
};
|
|
165
|
+
var serialNo = () => {
|
|
166
|
+
const now = /* @__PURE__ */ new Date();
|
|
167
|
+
const y = now.getFullYear();
|
|
168
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
169
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
170
|
+
const seq = Math.floor(Math.random() * 9e3 + 1e3);
|
|
171
|
+
return `MIKU-${y}${m}${d}-${seq}`;
|
|
172
|
+
};
|
|
173
|
+
var MikuContestService = class {
|
|
174
|
+
constructor(options = {}) {
|
|
175
|
+
this.submissions = /* @__PURE__ */ new Map();
|
|
176
|
+
this.votes = [];
|
|
177
|
+
this.announcements = /* @__PURE__ */ new Map();
|
|
178
|
+
this.voterRestrictions = /* @__PURE__ */ new Map();
|
|
179
|
+
this.contest = createDefaultMikuContestConfig(options.contestConfig);
|
|
180
|
+
}
|
|
181
|
+
getContestConfig() {
|
|
182
|
+
return this.contest;
|
|
183
|
+
}
|
|
184
|
+
updateContestConfig(patch) {
|
|
185
|
+
this.contest = {
|
|
186
|
+
...this.contest,
|
|
187
|
+
...patch,
|
|
188
|
+
votingRules: {
|
|
189
|
+
...this.contest.votingRules,
|
|
190
|
+
...patch.votingRules || {}
|
|
191
|
+
},
|
|
192
|
+
toggles: {
|
|
193
|
+
...this.contest.toggles,
|
|
194
|
+
...patch.toggles || {}
|
|
195
|
+
},
|
|
196
|
+
timeline: {
|
|
197
|
+
...this.contest.timeline,
|
|
198
|
+
...patch.timeline || {}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
return this.contest;
|
|
202
|
+
}
|
|
203
|
+
createSubmission(input, mode = "web") {
|
|
204
|
+
if (!this.contest.toggles.submissionEnabled) {
|
|
205
|
+
throw new Error("\u5F53\u524D\u672A\u5F00\u653E\u6295\u7A3F");
|
|
206
|
+
}
|
|
207
|
+
const errors = validateMikuSubmissionInput(input, mode);
|
|
208
|
+
if (errors.length > 0) {
|
|
209
|
+
throw new Error(errors.join("\uFF1B"));
|
|
210
|
+
}
|
|
211
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
212
|
+
const next = {
|
|
213
|
+
id: randomId("submission"),
|
|
214
|
+
serialNo: serialNo(),
|
|
215
|
+
contestId: input.contestId,
|
|
216
|
+
authorId: input.authorId,
|
|
217
|
+
authorNickname: input.authorNickname,
|
|
218
|
+
title: input.title,
|
|
219
|
+
type: input.type,
|
|
220
|
+
description: input.description,
|
|
221
|
+
tags: input.tags || [],
|
|
222
|
+
content: input.content,
|
|
223
|
+
voteCount: 0,
|
|
224
|
+
status: "pending",
|
|
225
|
+
createdAt: now,
|
|
226
|
+
updatedAt: now
|
|
227
|
+
};
|
|
228
|
+
this.submissions.set(next.id, next);
|
|
229
|
+
return next;
|
|
230
|
+
}
|
|
231
|
+
listSubmissions(filter) {
|
|
232
|
+
const authorKeyword = filter?.authorKeyword?.trim().toLowerCase();
|
|
233
|
+
const titleKeyword = filter?.titleKeyword?.trim().toLowerCase();
|
|
234
|
+
return [...this.submissions.values()].filter((item) => {
|
|
235
|
+
if (filter?.status && item.status !== filter.status) return false;
|
|
236
|
+
if (filter?.type && item.type !== filter.type) return false;
|
|
237
|
+
if (filter?.authorId && item.authorId !== filter.authorId) return false;
|
|
238
|
+
if (authorKeyword && !item.authorNickname.toLowerCase().includes(authorKeyword)) return false;
|
|
239
|
+
if (titleKeyword && !item.title.toLowerCase().includes(titleKeyword)) return false;
|
|
240
|
+
return true;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
getSubmission(submissionId) {
|
|
244
|
+
return this.submissions.get(submissionId) || null;
|
|
245
|
+
}
|
|
246
|
+
reviewSubmission(input) {
|
|
247
|
+
const current = this.submissions.get(input.submissionId);
|
|
248
|
+
if (!current) throw new Error("\u6295\u7A3F\u4E0D\u5B58\u5728");
|
|
249
|
+
if (input.action === "reject" && !input.rejectReason?.trim()) {
|
|
250
|
+
throw new Error("\u9A73\u56DE\u9700\u586B\u5199\u539F\u56E0");
|
|
251
|
+
}
|
|
252
|
+
const reviewed = {
|
|
253
|
+
...current,
|
|
254
|
+
status: input.action === "approve" ? "approved" : "rejected",
|
|
255
|
+
rejectReason: input.action === "reject" ? input.rejectReason?.trim() : void 0,
|
|
256
|
+
reviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
257
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
258
|
+
};
|
|
259
|
+
this.submissions.set(reviewed.id, reviewed);
|
|
260
|
+
return reviewed;
|
|
261
|
+
}
|
|
262
|
+
vote(input) {
|
|
263
|
+
if (!this.contest.toggles.votingEnabled) {
|
|
264
|
+
throw new Error("\u5F53\u524D\u672A\u5F00\u653E\u6295\u7968");
|
|
265
|
+
}
|
|
266
|
+
const restriction = this.voterRestrictions.get(input.voterId);
|
|
267
|
+
if (restriction?.banned) {
|
|
268
|
+
throw new Error("\u5F53\u524D\u8D26\u53F7\u5DF2\u88AB\u9650\u5236\u6295\u7968");
|
|
269
|
+
}
|
|
270
|
+
const target = this.submissions.get(input.submissionId);
|
|
271
|
+
if (!target) throw new Error("\u4F5C\u54C1\u4E0D\u5B58\u5728");
|
|
272
|
+
if (target.status !== "approved") throw new Error("\u4EC5\u53EF\u5BF9\u5DF2\u8FC7\u5BA1\u4F5C\u54C1\u6295\u7968");
|
|
273
|
+
const dayKey = toVoteDayKey();
|
|
274
|
+
const eligible = checkVoteEligibility({
|
|
275
|
+
existingVotes: this.votes,
|
|
276
|
+
submissionId: input.submissionId,
|
|
277
|
+
voterId: input.voterId,
|
|
278
|
+
dayKey,
|
|
279
|
+
rules: this.contest.votingRules
|
|
280
|
+
});
|
|
281
|
+
if (!eligible.ok) {
|
|
282
|
+
throw new Error(eligible.reason || "\u6295\u7968\u5931\u8D25");
|
|
283
|
+
}
|
|
284
|
+
const vote = {
|
|
285
|
+
id: randomId("vote"),
|
|
286
|
+
contestId: input.contestId,
|
|
287
|
+
submissionId: input.submissionId,
|
|
288
|
+
voterId: input.voterId,
|
|
289
|
+
votedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
290
|
+
dayKey,
|
|
291
|
+
deviceId: input.deviceId,
|
|
292
|
+
ip: input.ip
|
|
293
|
+
};
|
|
294
|
+
this.votes.push(vote);
|
|
295
|
+
const updated = {
|
|
296
|
+
...target,
|
|
297
|
+
voteCount: target.voteCount + 1,
|
|
298
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
299
|
+
};
|
|
300
|
+
this.submissions.set(updated.id, updated);
|
|
301
|
+
return updated;
|
|
302
|
+
}
|
|
303
|
+
getVoterRestriction(voterId) {
|
|
304
|
+
return this.voterRestrictions.get(voterId) || null;
|
|
305
|
+
}
|
|
306
|
+
setVoterRestriction(input) {
|
|
307
|
+
const next = {
|
|
308
|
+
voterId: input.voterId,
|
|
309
|
+
banned: input.banned,
|
|
310
|
+
reason: input.reason,
|
|
311
|
+
operatorId: input.operatorId,
|
|
312
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
313
|
+
};
|
|
314
|
+
this.voterRestrictions.set(input.voterId, next);
|
|
315
|
+
return next;
|
|
316
|
+
}
|
|
317
|
+
resetVotes(input) {
|
|
318
|
+
if (!input.submissionId && !input.voterId) {
|
|
319
|
+
throw new Error("submissionId \u4E0E voterId \u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A");
|
|
320
|
+
}
|
|
321
|
+
const before = this.votes.length;
|
|
322
|
+
const affected = /* @__PURE__ */ new Set();
|
|
323
|
+
const remained = this.votes.filter((vote) => {
|
|
324
|
+
const matchSubmission = input.submissionId ? vote.submissionId === input.submissionId : true;
|
|
325
|
+
const matchVoter = input.voterId ? vote.voterId === input.voterId : true;
|
|
326
|
+
const shouldRemove = matchSubmission && matchVoter;
|
|
327
|
+
if (shouldRemove) affected.add(vote.submissionId);
|
|
328
|
+
return !shouldRemove;
|
|
329
|
+
});
|
|
330
|
+
this.votes.length = 0;
|
|
331
|
+
this.votes.push(...remained);
|
|
332
|
+
this.recalculateVoteCounts();
|
|
333
|
+
return {
|
|
334
|
+
removedVotes: before - remained.length,
|
|
335
|
+
affectedSubmissions: [...affected]
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
listAnnouncements(contestId) {
|
|
339
|
+
const all = [...this.announcements.values()];
|
|
340
|
+
return contestId ? all.filter((item) => item.contestId === contestId) : all;
|
|
341
|
+
}
|
|
342
|
+
publishAnnouncement(input) {
|
|
343
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
344
|
+
const announcement = {
|
|
345
|
+
id: randomId("notice"),
|
|
346
|
+
contestId: input.contestId,
|
|
347
|
+
title: input.title,
|
|
348
|
+
content: input.content,
|
|
349
|
+
type: input.type,
|
|
350
|
+
createdBy: input.createdBy,
|
|
351
|
+
createdAt: now,
|
|
352
|
+
updatedAt: now
|
|
353
|
+
};
|
|
354
|
+
this.announcements.set(announcement.id, announcement);
|
|
355
|
+
return announcement;
|
|
356
|
+
}
|
|
357
|
+
getLeaderboard(limit = 10) {
|
|
358
|
+
const ranked = sortByVotesDesc(this.listSubmissions({ status: "approved" })).slice(0, limit);
|
|
359
|
+
return ranked.map((item, index) => ({
|
|
360
|
+
submissionId: item.id,
|
|
361
|
+
title: item.title,
|
|
362
|
+
authorNickname: item.authorNickname,
|
|
363
|
+
voteCount: item.voteCount,
|
|
364
|
+
rank: index + 1
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
getSnapshot() {
|
|
368
|
+
return {
|
|
369
|
+
contest: this.contest,
|
|
370
|
+
submissions: this.listSubmissions(),
|
|
371
|
+
announcements: this.listAnnouncements(),
|
|
372
|
+
leaderboard: this.getLeaderboard()
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
getSubmissionExportRows(filter) {
|
|
376
|
+
return this.listSubmissions(filter).map((item) => ({
|
|
377
|
+
\u6295\u7A3F\u7F16\u53F7: item.serialNo,
|
|
378
|
+
\u6295\u7A3FID: item.id,
|
|
379
|
+
\u8D5B\u4E8BID: item.contestId,
|
|
380
|
+
\u4F5C\u8005ID: item.authorId,
|
|
381
|
+
\u4F5C\u8005\u6635\u79F0: item.authorNickname,
|
|
382
|
+
\u4F5C\u54C1\u540D\u79F0: item.title,
|
|
383
|
+
\u4F5C\u54C1\u7C7B\u578B: item.type,
|
|
384
|
+
\u7B80\u4ECB: item.description,
|
|
385
|
+
\u6807\u7B7E: item.tags.join(","),
|
|
386
|
+
\u5BA1\u6838\u72B6\u6001: item.status,
|
|
387
|
+
\u9A73\u56DE\u539F\u56E0: item.rejectReason || "",
|
|
388
|
+
\u7968\u6570: item.voteCount,
|
|
389
|
+
\u63D0\u4EA4\u65F6\u95F4: item.createdAt,
|
|
390
|
+
\u66F4\u65B0\u65F6\u95F4: item.updatedAt
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
exportSubmissionExcel(filter) {
|
|
394
|
+
const rows = this.getSubmissionExportRows(filter);
|
|
395
|
+
const workbook = XLSX__namespace.utils.book_new();
|
|
396
|
+
const worksheet = XLSX__namespace.utils.json_to_sheet(rows);
|
|
397
|
+
XLSX__namespace.utils.book_append_sheet(workbook, worksheet, "submissions");
|
|
398
|
+
return XLSX__namespace.write(workbook, { bookType: "xlsx", type: "buffer" });
|
|
399
|
+
}
|
|
400
|
+
recalculateVoteCounts() {
|
|
401
|
+
const counts = /* @__PURE__ */ new Map();
|
|
402
|
+
for (const vote of this.votes) {
|
|
403
|
+
counts.set(vote.submissionId, (counts.get(vote.submissionId) || 0) + 1);
|
|
404
|
+
}
|
|
405
|
+
for (const [id, submission] of this.submissions.entries()) {
|
|
406
|
+
const nextCount = counts.get(id) || 0;
|
|
407
|
+
if (submission.voteCount === nextCount) continue;
|
|
408
|
+
this.submissions.set(id, {
|
|
409
|
+
...submission,
|
|
410
|
+
voteCount: nextCount,
|
|
411
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
exportPersistenceState() {
|
|
416
|
+
return {
|
|
417
|
+
contest: this.contest,
|
|
418
|
+
submissions: [...this.submissions.values()],
|
|
419
|
+
votes: [...this.votes],
|
|
420
|
+
announcements: [...this.announcements.values()],
|
|
421
|
+
voterRestrictions: [...this.voterRestrictions.values()],
|
|
422
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
importPersistenceState(state) {
|
|
426
|
+
this.contest = state.contest;
|
|
427
|
+
this.submissions.clear();
|
|
428
|
+
this.announcements.clear();
|
|
429
|
+
this.voterRestrictions.clear();
|
|
430
|
+
this.votes.length = 0;
|
|
431
|
+
for (const item of state.submissions) {
|
|
432
|
+
this.submissions.set(item.id, item);
|
|
433
|
+
}
|
|
434
|
+
for (const item of state.announcements) {
|
|
435
|
+
this.announcements.set(item.id, item);
|
|
436
|
+
}
|
|
437
|
+
for (const item of state.voterRestrictions) {
|
|
438
|
+
this.voterRestrictions.set(item.voterId, item);
|
|
439
|
+
}
|
|
440
|
+
this.votes.push(...state.votes);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
var createMikuContestService = (options) => {
|
|
444
|
+
return new MikuContestService(options);
|
|
445
|
+
};
|
|
446
|
+
var mikuContestConfigs = pgCore.pgTable("miku_contest_configs", {
|
|
447
|
+
contestId: pgCore.text("contest_id").primaryKey(),
|
|
448
|
+
config: pgCore.jsonb("config").$type().notNull(),
|
|
449
|
+
createdAt: pgCore.timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
450
|
+
updatedAt: pgCore.timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
|
|
451
|
+
});
|
|
452
|
+
var mikuContestSubmissions = pgCore.pgTable("miku_contest_submissions", {
|
|
453
|
+
id: pgCore.text("id").primaryKey(),
|
|
454
|
+
contestId: pgCore.text("contest_id").notNull(),
|
|
455
|
+
serialNo: pgCore.text("serial_no").notNull(),
|
|
456
|
+
authorId: pgCore.text("author_id").notNull(),
|
|
457
|
+
authorNickname: pgCore.text("author_nickname").notNull(),
|
|
458
|
+
title: pgCore.text("title").notNull(),
|
|
459
|
+
type: pgCore.text("type").notNull(),
|
|
460
|
+
description: pgCore.text("description").notNull(),
|
|
461
|
+
tags: pgCore.jsonb("tags").$type().notNull(),
|
|
462
|
+
content: pgCore.jsonb("content").$type().notNull(),
|
|
463
|
+
voteCount: pgCore.integer("vote_count").notNull().default(0),
|
|
464
|
+
status: pgCore.text("status").notNull(),
|
|
465
|
+
rejectReason: pgCore.text("reject_reason"),
|
|
466
|
+
createdAt: pgCore.timestamp("created_at", { withTimezone: true }).notNull(),
|
|
467
|
+
reviewedAt: pgCore.timestamp("reviewed_at", { withTimezone: true }),
|
|
468
|
+
updatedAt: pgCore.timestamp("updated_at", { withTimezone: true }).notNull()
|
|
469
|
+
});
|
|
470
|
+
var mikuContestVotes = pgCore.pgTable("miku_contest_votes", {
|
|
471
|
+
id: pgCore.text("id").primaryKey(),
|
|
472
|
+
contestId: pgCore.text("contest_id").notNull(),
|
|
473
|
+
submissionId: pgCore.text("submission_id").notNull(),
|
|
474
|
+
voterId: pgCore.text("voter_id").notNull(),
|
|
475
|
+
votedAt: pgCore.text("voted_at").notNull(),
|
|
476
|
+
dayKey: pgCore.text("day_key").notNull(),
|
|
477
|
+
deviceId: pgCore.text("device_id"),
|
|
478
|
+
ip: pgCore.text("ip")
|
|
479
|
+
});
|
|
480
|
+
var mikuContestNotices = pgCore.pgTable("miku_contest_notices", {
|
|
481
|
+
id: pgCore.text("id").primaryKey(),
|
|
482
|
+
contestId: pgCore.text("contest_id").notNull(),
|
|
483
|
+
title: pgCore.text("title").notNull(),
|
|
484
|
+
content: pgCore.text("content").notNull(),
|
|
485
|
+
type: pgCore.text("type").notNull(),
|
|
486
|
+
createdBy: pgCore.text("created_by").notNull(),
|
|
487
|
+
createdAt: pgCore.text("created_at").notNull(),
|
|
488
|
+
updatedAt: pgCore.text("updated_at").notNull()
|
|
489
|
+
});
|
|
490
|
+
var mikuContestVoterRestrictions = pgCore.pgTable("miku_contest_voter_restrictions", {
|
|
491
|
+
id: pgCore.text("id").primaryKey(),
|
|
492
|
+
contestId: pgCore.text("contest_id").notNull(),
|
|
493
|
+
data: pgCore.jsonb("data").$type().notNull()
|
|
494
|
+
});
|
|
495
|
+
var MikuContestStateDbService = class {
|
|
496
|
+
constructor(db) {
|
|
497
|
+
this.db = db;
|
|
498
|
+
}
|
|
499
|
+
async loadState(contestId) {
|
|
500
|
+
const configRows = await this.db.select({ config: mikuContestConfigs.config }).from(mikuContestConfigs).where(drizzleOrm.eq(mikuContestConfigs.contestId, contestId)).limit(1);
|
|
501
|
+
const config = configRows[0]?.config;
|
|
502
|
+
if (!config) return null;
|
|
503
|
+
const [submissions, votes, announcements, restrictions] = await Promise.all([
|
|
504
|
+
this.db.select().from(mikuContestSubmissions).where(drizzleOrm.eq(mikuContestSubmissions.contestId, contestId)),
|
|
505
|
+
this.db.select().from(mikuContestVotes).where(drizzleOrm.eq(mikuContestVotes.contestId, contestId)),
|
|
506
|
+
this.db.select().from(mikuContestNotices).where(drizzleOrm.eq(mikuContestNotices.contestId, contestId)),
|
|
507
|
+
this.db.select({ data: mikuContestVoterRestrictions.data }).from(mikuContestVoterRestrictions).where(drizzleOrm.eq(mikuContestVoterRestrictions.contestId, contestId))
|
|
508
|
+
]);
|
|
509
|
+
return {
|
|
510
|
+
contest: config,
|
|
511
|
+
submissions,
|
|
512
|
+
votes,
|
|
513
|
+
announcements,
|
|
514
|
+
voterRestrictions: restrictions.map((item) => item.data),
|
|
515
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
async saveState(state) {
|
|
519
|
+
const contestId = state.contest.id;
|
|
520
|
+
const exists = await this.db.select({ contestId: mikuContestConfigs.contestId }).from(mikuContestConfigs).where(drizzleOrm.eq(mikuContestConfigs.contestId, contestId)).limit(1);
|
|
521
|
+
if (exists[0]) {
|
|
522
|
+
await this.db.update(mikuContestConfigs).set({
|
|
523
|
+
config: state.contest,
|
|
524
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
525
|
+
}).where(drizzleOrm.eq(mikuContestConfigs.contestId, contestId));
|
|
526
|
+
} else {
|
|
527
|
+
await this.db.insert(mikuContestConfigs).values({
|
|
528
|
+
contestId,
|
|
529
|
+
config: state.contest
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
await this.db.delete(mikuContestSubmissions).where(drizzleOrm.eq(mikuContestSubmissions.contestId, contestId));
|
|
533
|
+
await this.db.delete(mikuContestVotes).where(drizzleOrm.eq(mikuContestVotes.contestId, contestId));
|
|
534
|
+
await this.db.delete(mikuContestNotices).where(drizzleOrm.eq(mikuContestNotices.contestId, contestId));
|
|
535
|
+
await this.db.delete(mikuContestVoterRestrictions).where(drizzleOrm.eq(mikuContestVoterRestrictions.contestId, contestId));
|
|
536
|
+
if (state.submissions.length > 0) {
|
|
537
|
+
await this.db.insert(mikuContestSubmissions).values(
|
|
538
|
+
state.submissions.map((item) => ({
|
|
539
|
+
...item,
|
|
540
|
+
contestId,
|
|
541
|
+
createdAt: new Date(item.createdAt),
|
|
542
|
+
reviewedAt: item.reviewedAt ? new Date(item.reviewedAt) : null,
|
|
543
|
+
updatedAt: new Date(item.updatedAt)
|
|
544
|
+
}))
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
if (state.votes.length > 0) {
|
|
548
|
+
await this.db.insert(mikuContestVotes).values(state.votes);
|
|
549
|
+
}
|
|
550
|
+
if (state.announcements.length > 0) {
|
|
551
|
+
await this.db.insert(mikuContestNotices).values(state.announcements);
|
|
552
|
+
}
|
|
553
|
+
if (state.voterRestrictions.length > 0) {
|
|
554
|
+
await this.db.insert(mikuContestVoterRestrictions).values(
|
|
555
|
+
state.voterRestrictions.map((item) => ({
|
|
556
|
+
id: `${contestId}:${item.voterId}`,
|
|
557
|
+
contestId,
|
|
558
|
+
data: item
|
|
559
|
+
}))
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// src/mikuContest/server/persistence/drizzle-adapter.ts
|
|
566
|
+
var createMikuContestDrizzlePersistenceAdapter = (db) => {
|
|
567
|
+
const service = new MikuContestStateDbService(db);
|
|
568
|
+
return {
|
|
569
|
+
loadState: (contestId) => service.loadState(contestId),
|
|
570
|
+
saveState: (state) => service.saveState(state)
|
|
571
|
+
};
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// src/mikuContest/server/persistence/service.ts
|
|
575
|
+
var MikuContestPersistentService = class {
|
|
576
|
+
constructor(options) {
|
|
577
|
+
this.options = options;
|
|
578
|
+
this.hydrated = false;
|
|
579
|
+
this.hydrationPromise = null;
|
|
580
|
+
this.engine = createMikuContestService(options);
|
|
581
|
+
}
|
|
582
|
+
async ensureHydrated() {
|
|
583
|
+
if (this.hydrated) return;
|
|
584
|
+
if (this.hydrationPromise) return this.hydrationPromise;
|
|
585
|
+
this.hydrationPromise = (async () => {
|
|
586
|
+
const contestId = this.engine.getContestConfig().id;
|
|
587
|
+
const loaded = await this.options.persistenceAdapter.loadState(contestId);
|
|
588
|
+
if (loaded) {
|
|
589
|
+
this.engine.importPersistenceState(loaded);
|
|
590
|
+
} else {
|
|
591
|
+
await this.options.persistenceAdapter.saveState(this.engine.exportPersistenceState());
|
|
592
|
+
}
|
|
593
|
+
this.hydrated = true;
|
|
594
|
+
})();
|
|
595
|
+
await this.hydrationPromise;
|
|
596
|
+
}
|
|
597
|
+
async persist() {
|
|
598
|
+
await this.options.persistenceAdapter.saveState(this.engine.exportPersistenceState());
|
|
599
|
+
}
|
|
600
|
+
async getContestConfig() {
|
|
601
|
+
await this.ensureHydrated();
|
|
602
|
+
return this.engine.getContestConfig();
|
|
603
|
+
}
|
|
604
|
+
async updateContestConfig(patch) {
|
|
605
|
+
await this.ensureHydrated();
|
|
606
|
+
const data = this.engine.updateContestConfig(patch);
|
|
607
|
+
await this.persist();
|
|
608
|
+
return data;
|
|
609
|
+
}
|
|
610
|
+
async createSubmission(input, mode = "web") {
|
|
611
|
+
await this.ensureHydrated();
|
|
612
|
+
const data = this.engine.createSubmission(input, mode);
|
|
613
|
+
await this.persist();
|
|
614
|
+
return data;
|
|
615
|
+
}
|
|
616
|
+
async listSubmissions(filter) {
|
|
617
|
+
await this.ensureHydrated();
|
|
618
|
+
return this.engine.listSubmissions(filter);
|
|
619
|
+
}
|
|
620
|
+
async getSubmission(submissionId) {
|
|
621
|
+
await this.ensureHydrated();
|
|
622
|
+
return this.engine.getSubmission(submissionId);
|
|
623
|
+
}
|
|
624
|
+
async reviewSubmission(input) {
|
|
625
|
+
await this.ensureHydrated();
|
|
626
|
+
const data = this.engine.reviewSubmission(input);
|
|
627
|
+
await this.persist();
|
|
628
|
+
return data;
|
|
629
|
+
}
|
|
630
|
+
async vote(input) {
|
|
631
|
+
await this.ensureHydrated();
|
|
632
|
+
const data = this.engine.vote(input);
|
|
633
|
+
await this.persist();
|
|
634
|
+
return data;
|
|
635
|
+
}
|
|
636
|
+
async getVoterRestriction(voterId) {
|
|
637
|
+
await this.ensureHydrated();
|
|
638
|
+
return this.engine.getVoterRestriction(voterId);
|
|
639
|
+
}
|
|
640
|
+
async setVoterRestriction(input) {
|
|
641
|
+
await this.ensureHydrated();
|
|
642
|
+
const data = this.engine.setVoterRestriction(input);
|
|
643
|
+
await this.persist();
|
|
644
|
+
return data;
|
|
645
|
+
}
|
|
646
|
+
async resetVotes(input) {
|
|
647
|
+
await this.ensureHydrated();
|
|
648
|
+
const data = this.engine.resetVotes(input);
|
|
649
|
+
await this.persist();
|
|
650
|
+
return data;
|
|
651
|
+
}
|
|
652
|
+
async listAnnouncements(contestId) {
|
|
653
|
+
await this.ensureHydrated();
|
|
654
|
+
return this.engine.listAnnouncements(contestId);
|
|
655
|
+
}
|
|
656
|
+
async publishAnnouncement(input) {
|
|
657
|
+
await this.ensureHydrated();
|
|
658
|
+
const data = this.engine.publishAnnouncement(input);
|
|
659
|
+
await this.persist();
|
|
660
|
+
return data;
|
|
661
|
+
}
|
|
662
|
+
async getLeaderboard(limit = 10) {
|
|
663
|
+
await this.ensureHydrated();
|
|
664
|
+
return this.engine.getLeaderboard(limit);
|
|
665
|
+
}
|
|
666
|
+
async getSnapshot() {
|
|
667
|
+
await this.ensureHydrated();
|
|
668
|
+
return this.engine.getSnapshot();
|
|
669
|
+
}
|
|
670
|
+
async getSubmissionExportRows(filter) {
|
|
671
|
+
await this.ensureHydrated();
|
|
672
|
+
return this.engine.getSubmissionExportRows(filter);
|
|
673
|
+
}
|
|
674
|
+
async exportSubmissionExcel(filter) {
|
|
675
|
+
await this.ensureHydrated();
|
|
676
|
+
return this.engine.exportSubmissionExcel(filter);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
var createMikuContestPersistentService = (options) => {
|
|
680
|
+
return new MikuContestPersistentService(options);
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// src/mikuContest/server/db.ts
|
|
684
|
+
var MikuContestDbService = class {
|
|
685
|
+
constructor() {
|
|
686
|
+
this._db = null;
|
|
687
|
+
}
|
|
688
|
+
setDb(db) {
|
|
689
|
+
this._db = db;
|
|
690
|
+
}
|
|
691
|
+
isConfigured() {
|
|
692
|
+
return Boolean(this._db);
|
|
693
|
+
}
|
|
694
|
+
get db() {
|
|
695
|
+
if (!this._db) {
|
|
696
|
+
throw new Error("MikuContestDbService: Database instance not set. Call setDb(db) first.");
|
|
697
|
+
}
|
|
698
|
+
return this._db;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
var mikuContestDbService = new MikuContestDbService();
|
|
702
|
+
|
|
703
|
+
// src/mikuContest/logic/hooks/useMikuContest.ts
|
|
704
|
+
var useMikuContest = (options) => {
|
|
705
|
+
const [service] = React4.useState(() => createMikuContestService(options));
|
|
706
|
+
const [version, setVersion] = React4.useState(0);
|
|
707
|
+
const refresh = () => setVersion((value) => value + 1);
|
|
708
|
+
const snapshot = React4.useMemo(() => {
|
|
709
|
+
return service.getSnapshot();
|
|
710
|
+
}, [service, version]);
|
|
711
|
+
return {
|
|
712
|
+
service,
|
|
713
|
+
snapshot,
|
|
714
|
+
refresh
|
|
715
|
+
};
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
// src/mikuContest/service/api/client.ts
|
|
719
|
+
var toQueryString = (filter) => {
|
|
720
|
+
if (!filter) return "";
|
|
721
|
+
const params = new URLSearchParams();
|
|
722
|
+
if (filter.status) params.set("status", filter.status);
|
|
723
|
+
if (filter.type) params.set("type", filter.type);
|
|
724
|
+
if (filter.authorId) params.set("authorId", filter.authorId);
|
|
725
|
+
if (filter.authorKeyword) params.set("authorKeyword", filter.authorKeyword);
|
|
726
|
+
if (filter.titleKeyword) params.set("titleKeyword", filter.titleKeyword);
|
|
727
|
+
const query = params.toString();
|
|
728
|
+
return query ? `?${query}` : "";
|
|
729
|
+
};
|
|
730
|
+
var unwrap = (result) => {
|
|
731
|
+
if (!result.success || result.data === void 0) {
|
|
732
|
+
throw new Error(result.error || "\u8BF7\u6C42\u5931\u8D25");
|
|
733
|
+
}
|
|
734
|
+
return result.data;
|
|
735
|
+
};
|
|
736
|
+
var createMikuContestApiClient = (basePath, requester) => {
|
|
737
|
+
return {
|
|
738
|
+
async getSnapshot() {
|
|
739
|
+
const result = await requester(`${basePath}/contest`, { method: "GET" });
|
|
740
|
+
return unwrap(result);
|
|
741
|
+
},
|
|
742
|
+
async updateContestConfig(patch) {
|
|
743
|
+
const result = await requester(`${basePath}/contest`, {
|
|
744
|
+
method: "PATCH",
|
|
745
|
+
body: patch
|
|
746
|
+
});
|
|
747
|
+
return unwrap(result);
|
|
748
|
+
},
|
|
749
|
+
async createSubmission(input, mode = "web") {
|
|
750
|
+
const result = await requester(`${basePath}/submissions`, {
|
|
751
|
+
method: "POST",
|
|
752
|
+
body: { payload: input, mode }
|
|
753
|
+
});
|
|
754
|
+
return unwrap(result);
|
|
755
|
+
},
|
|
756
|
+
async listSubmissions(filter) {
|
|
757
|
+
const result = await requester(
|
|
758
|
+
`${basePath}/submissions${toQueryString(filter)}`,
|
|
759
|
+
{ method: "GET" }
|
|
760
|
+
);
|
|
761
|
+
return unwrap(result);
|
|
762
|
+
},
|
|
763
|
+
async reviewSubmission(input) {
|
|
764
|
+
const result = await requester(`${basePath}/submissions/review`, {
|
|
765
|
+
method: "POST",
|
|
766
|
+
body: input
|
|
767
|
+
});
|
|
768
|
+
return unwrap(result);
|
|
769
|
+
},
|
|
770
|
+
async vote(input) {
|
|
771
|
+
const result = await requester(`${basePath}/votes`, {
|
|
772
|
+
method: "POST",
|
|
773
|
+
body: input
|
|
774
|
+
});
|
|
775
|
+
return unwrap(result);
|
|
776
|
+
},
|
|
777
|
+
async setVoterRestriction(input) {
|
|
778
|
+
const result = await requester(`${basePath}/admin/voter-restrictions`, {
|
|
779
|
+
method: "POST",
|
|
780
|
+
body: input
|
|
781
|
+
});
|
|
782
|
+
return unwrap(result);
|
|
783
|
+
},
|
|
784
|
+
async resetVotes(input) {
|
|
785
|
+
const result = await requester(
|
|
786
|
+
`${basePath}/admin/votes/reset`,
|
|
787
|
+
{
|
|
788
|
+
method: "POST",
|
|
789
|
+
body: input
|
|
790
|
+
}
|
|
791
|
+
);
|
|
792
|
+
return unwrap(result);
|
|
793
|
+
},
|
|
794
|
+
async exportSubmissions(filter) {
|
|
795
|
+
const response = await fetch(`${basePath}/admin/submissions/export${toQueryString(filter)}`);
|
|
796
|
+
if (!response.ok) {
|
|
797
|
+
throw new Error(`\u5BFC\u51FA\u5931\u8D25: ${response.status}`);
|
|
798
|
+
}
|
|
799
|
+
return response.arrayBuffer();
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
// src/mikuContest/service/web/index.ts
|
|
805
|
+
var web_exports = {};
|
|
806
|
+
__export(web_exports, {
|
|
807
|
+
createMikuContestWebClient: () => createMikuContestWebClient
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// src/mikuContest/service/web/client.ts
|
|
811
|
+
var defaultRequester = (options) => {
|
|
812
|
+
const baseUrl = options.baseUrl || "";
|
|
813
|
+
const commonHeaders = options.headers || {};
|
|
814
|
+
return async (url, requestOptions) => {
|
|
815
|
+
const response = await fetch(`${baseUrl}${url}`, {
|
|
816
|
+
method: requestOptions?.method || "GET",
|
|
817
|
+
headers: {
|
|
818
|
+
"Content-Type": "application/json",
|
|
819
|
+
...commonHeaders
|
|
820
|
+
},
|
|
821
|
+
body: requestOptions?.body ? JSON.stringify(requestOptions.body) : void 0
|
|
822
|
+
});
|
|
823
|
+
const json = await response.json();
|
|
824
|
+
return json;
|
|
825
|
+
};
|
|
826
|
+
};
|
|
827
|
+
var createMikuContestWebClient = (options = {}) => {
|
|
828
|
+
const basePath = options.basePath || "/api/miku-contest";
|
|
829
|
+
return createMikuContestApiClient(basePath, defaultRequester(options));
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
// src/mikuContest/service/miniapp/index.ts
|
|
833
|
+
var miniapp_exports = {};
|
|
834
|
+
__export(miniapp_exports, {
|
|
835
|
+
createMikuContestMiniappClient: () => createMikuContestMiniappClient
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// src/mikuContest/service/miniapp/client.ts
|
|
839
|
+
var createMikuContestMiniappClient = (options) => {
|
|
840
|
+
const basePath = options.basePath || "/api/miku-contest";
|
|
841
|
+
return createMikuContestApiClient(basePath, options.requester);
|
|
842
|
+
};
|
|
843
|
+
var isDrizzleDb = (value) => {
|
|
844
|
+
if (!value || typeof value !== "object") return false;
|
|
845
|
+
const candidate = value;
|
|
846
|
+
return typeof candidate.select === "function" && typeof candidate.insert === "function" && typeof candidate.update === "function" && typeof candidate.delete === "function";
|
|
847
|
+
};
|
|
848
|
+
var resolveService = (config) => {
|
|
849
|
+
if (config?.service) return config.service;
|
|
850
|
+
const adapter = config?.persistenceAdapter || (isDrizzleDb(config?.db) ? createMikuContestDrizzlePersistenceAdapter(config.db) : null);
|
|
851
|
+
if (adapter) {
|
|
852
|
+
return createMikuContestPersistentService({
|
|
853
|
+
persistenceAdapter: adapter
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
return createMikuContestService();
|
|
857
|
+
};
|
|
858
|
+
var createGetContestSnapshotHandler = (config) => {
|
|
859
|
+
const service = resolveService(config);
|
|
860
|
+
return async (_request) => {
|
|
861
|
+
const data = await service.getSnapshot();
|
|
862
|
+
return server.NextResponse.json({ success: true, data });
|
|
863
|
+
};
|
|
864
|
+
};
|
|
865
|
+
var createUpdateContestConfigHandler = (config) => {
|
|
866
|
+
const service = resolveService(config);
|
|
867
|
+
return async (request) => {
|
|
868
|
+
try {
|
|
869
|
+
const payload = await request.json();
|
|
870
|
+
const data = await service.updateContestConfig(payload);
|
|
871
|
+
return server.NextResponse.json({ success: true, data });
|
|
872
|
+
} catch (error) {
|
|
873
|
+
return server.NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
};
|
|
877
|
+
var createCreateSubmissionHandler = (config) => {
|
|
878
|
+
const service = resolveService(config);
|
|
879
|
+
return async (request) => {
|
|
880
|
+
try {
|
|
881
|
+
const body = await request.json();
|
|
882
|
+
const mode = body.mode || "web";
|
|
883
|
+
const payload = body.payload;
|
|
884
|
+
if (!payload) {
|
|
885
|
+
return server.NextResponse.json({ success: false, error: "payload \u4E0D\u80FD\u4E3A\u7A7A" }, { status: 400 });
|
|
886
|
+
}
|
|
887
|
+
const data = await service.createSubmission(payload, mode);
|
|
888
|
+
return server.NextResponse.json({ success: true, data });
|
|
889
|
+
} catch (error) {
|
|
890
|
+
return server.NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
};
|
|
894
|
+
var createVoteHandler = (config) => {
|
|
895
|
+
const service = resolveService(config);
|
|
896
|
+
return async (request) => {
|
|
897
|
+
try {
|
|
898
|
+
const payload = await request.json();
|
|
899
|
+
const data = await service.vote(payload);
|
|
900
|
+
return server.NextResponse.json({ success: true, data });
|
|
901
|
+
} catch (error) {
|
|
902
|
+
return server.NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
};
|
|
906
|
+
var createReviewSubmissionHandler = (config) => {
|
|
907
|
+
const service = resolveService(config);
|
|
908
|
+
return async (request) => {
|
|
909
|
+
try {
|
|
910
|
+
const payload = await request.json();
|
|
911
|
+
const data = await service.reviewSubmission(payload);
|
|
912
|
+
return server.NextResponse.json({ success: true, data });
|
|
913
|
+
} catch (error) {
|
|
914
|
+
return server.NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
};
|
|
918
|
+
var buildSubmissionFilterFromQuery = (request) => {
|
|
919
|
+
const search = request.nextUrl.searchParams;
|
|
920
|
+
const status = search.get("status");
|
|
921
|
+
const type = search.get("type");
|
|
922
|
+
return {
|
|
923
|
+
status: status ? status : void 0,
|
|
924
|
+
type: type ? type : void 0,
|
|
925
|
+
authorId: search.get("authorId") || void 0,
|
|
926
|
+
authorKeyword: search.get("authorKeyword") || void 0,
|
|
927
|
+
titleKeyword: search.get("titleKeyword") || void 0
|
|
928
|
+
};
|
|
929
|
+
};
|
|
930
|
+
var createListSubmissionsHandler = (config) => {
|
|
931
|
+
const service = resolveService(config);
|
|
932
|
+
return async (request) => {
|
|
933
|
+
const filter = buildSubmissionFilterFromQuery(request);
|
|
934
|
+
const data = await service.listSubmissions(filter);
|
|
935
|
+
return server.NextResponse.json({ success: true, data });
|
|
936
|
+
};
|
|
937
|
+
};
|
|
938
|
+
var createSetVoterRestrictionHandler = (config) => {
|
|
939
|
+
const service = resolveService(config);
|
|
940
|
+
return async (request) => {
|
|
941
|
+
try {
|
|
942
|
+
const payload = await request.json();
|
|
943
|
+
const data = await service.setVoterRestriction(payload);
|
|
944
|
+
return server.NextResponse.json({ success: true, data });
|
|
945
|
+
} catch (error) {
|
|
946
|
+
return server.NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
};
|
|
950
|
+
var createResetVotesHandler = (config) => {
|
|
951
|
+
const service = resolveService(config);
|
|
952
|
+
return async (request) => {
|
|
953
|
+
try {
|
|
954
|
+
const payload = await request.json();
|
|
955
|
+
const data = await service.resetVotes(payload);
|
|
956
|
+
return server.NextResponse.json({ success: true, data });
|
|
957
|
+
} catch (error) {
|
|
958
|
+
return server.NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
};
|
|
962
|
+
var createExportSubmissionsHandler = (config) => {
|
|
963
|
+
const service = resolveService(config);
|
|
964
|
+
return async (request) => {
|
|
965
|
+
const filter = buildSubmissionFilterFromQuery(request);
|
|
966
|
+
const data = await service.exportSubmissionExcel(filter);
|
|
967
|
+
const body = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
968
|
+
return new server.NextResponse(body, {
|
|
969
|
+
status: 200,
|
|
970
|
+
headers: {
|
|
971
|
+
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
972
|
+
"Content-Disposition": 'attachment; filename="miku-submissions.xlsx"'
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
};
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
// src/mikuContest/ui/web/index.ts
|
|
979
|
+
var web_exports2 = {};
|
|
980
|
+
__export(web_exports2, {
|
|
981
|
+
MikuContestAdminPage: () => MikuContestAdminPage_default,
|
|
982
|
+
MikuContestArtistPage: () => MikuContestArtistPage_default,
|
|
983
|
+
MikuContestAudiencePage: () => MikuContestAudiencePage_default,
|
|
984
|
+
MikuContestDashboard: () => MikuContestDashboard_default,
|
|
985
|
+
MikuContestPage: () => MikuContestPage_default
|
|
986
|
+
});
|
|
987
|
+
var MikuContestDashboard = ({ snapshot }) => {
|
|
988
|
+
return /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("h2", null, snapshot.contest.name), /* @__PURE__ */ React4__default.default.createElement("p", null, snapshot.contest.theme), /* @__PURE__ */ React4__default.default.createElement("p", null, "\u6295\u7A3F\u6570\uFF1A", snapshot.submissions.length), /* @__PURE__ */ React4__default.default.createElement("p", null, "\u516C\u544A\u6570\uFF1A", snapshot.announcements.length), /* @__PURE__ */ React4__default.default.createElement("ul", null, snapshot.leaderboard.map((item) => /* @__PURE__ */ React4__default.default.createElement("li", { key: item.submissionId }, "#", item.rank, " ", item.title, " - ", item.voteCount, "\u7968"))));
|
|
989
|
+
};
|
|
990
|
+
var MikuContestDashboard_default = MikuContestDashboard;
|
|
991
|
+
var MikuContestAudiencePage = ({
|
|
992
|
+
client,
|
|
993
|
+
voterId,
|
|
994
|
+
title = "\u89C2\u4F17\u6295\u7968\u533A"
|
|
995
|
+
}) => {
|
|
996
|
+
const api = React4.useMemo(() => client || createMikuContestWebClient(), [client]);
|
|
997
|
+
const [snapshot, setSnapshot] = React4.useState(null);
|
|
998
|
+
const [loading, setLoading] = React4.useState(false);
|
|
999
|
+
const [error, setError] = React4.useState(null);
|
|
1000
|
+
const approvedWorks = React4.useMemo(() => {
|
|
1001
|
+
if (!snapshot) return [];
|
|
1002
|
+
return snapshot.submissions.filter((item) => item.status === "approved");
|
|
1003
|
+
}, [snapshot]);
|
|
1004
|
+
const loadSnapshot = async () => {
|
|
1005
|
+
setLoading(true);
|
|
1006
|
+
setError(null);
|
|
1007
|
+
try {
|
|
1008
|
+
const data = await api.getSnapshot();
|
|
1009
|
+
setSnapshot(data);
|
|
1010
|
+
} catch (e) {
|
|
1011
|
+
setError(e.message);
|
|
1012
|
+
} finally {
|
|
1013
|
+
setLoading(false);
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
React4.useEffect(() => {
|
|
1017
|
+
void loadSnapshot();
|
|
1018
|
+
}, []);
|
|
1019
|
+
const handleVote = async (submission) => {
|
|
1020
|
+
if (!snapshot) return;
|
|
1021
|
+
try {
|
|
1022
|
+
await api.vote({
|
|
1023
|
+
contestId: snapshot.contest.id,
|
|
1024
|
+
submissionId: submission.id,
|
|
1025
|
+
voterId
|
|
1026
|
+
});
|
|
1027
|
+
await loadSnapshot();
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
setError(e.message);
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
return /* @__PURE__ */ React4__default.default.createElement("section", null, /* @__PURE__ */ React4__default.default.createElement("h2", null, title), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void loadSnapshot(), disabled: loading }, loading ? "\u5237\u65B0\u4E2D..." : "\u5237\u65B0\u6570\u636E"), error ? /* @__PURE__ */ React4__default.default.createElement("p", { style: { color: "crimson" } }, "\u9519\u8BEF\uFF1A", error) : null, !snapshot ? null : /* @__PURE__ */ React4__default.default.createElement(React4__default.default.Fragment, null, /* @__PURE__ */ React4__default.default.createElement("p", null, "\u8D5B\u4E8B\uFF1A", snapshot.contest.name, "\uFF5C\u4E3B\u9898\uFF1A", snapshot.contest.theme), /* @__PURE__ */ React4__default.default.createElement("p", null, "\u5DF2\u8FC7\u5BA1\u4F5C\u54C1\uFF1A", approvedWorks.length, "\uFF5C\u6BCF\u65E5\u4E0A\u9650\uFF1A", snapshot.contest.votingRules.maxVotesPerDay), /* @__PURE__ */ React4__default.default.createElement("ul", null, approvedWorks.map((work) => /* @__PURE__ */ React4__default.default.createElement("li", { key: work.id }, /* @__PURE__ */ React4__default.default.createElement("strong", null, work.title), "\uFF08", work.authorNickname, "\uFF09- \u5F53\u524D ", work.voteCount, " \u7968", " ", /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void handleVote(work) }, "\u6295\u7968"))))));
|
|
1033
|
+
};
|
|
1034
|
+
var MikuContestAudiencePage_default = MikuContestAudiencePage;
|
|
1035
|
+
var workTypes = ["visual", "video", "text", "audio"];
|
|
1036
|
+
var MikuContestArtistPage = ({
|
|
1037
|
+
client,
|
|
1038
|
+
authorId,
|
|
1039
|
+
authorNickname,
|
|
1040
|
+
title = "\u753B\u5E08\u6295\u7A3F\u533A"
|
|
1041
|
+
}) => {
|
|
1042
|
+
const api = React4.useMemo(() => client || createMikuContestWebClient(), [client]);
|
|
1043
|
+
const [snapshot, setSnapshot] = React4.useState(null);
|
|
1044
|
+
const [mySubmissions, setMySubmissions] = React4.useState([]);
|
|
1045
|
+
const [submitting, setSubmitting] = React4.useState(false);
|
|
1046
|
+
const [loading, setLoading] = React4.useState(false);
|
|
1047
|
+
const [error, setError] = React4.useState(null);
|
|
1048
|
+
const [titleInput, setTitleInput] = React4.useState("");
|
|
1049
|
+
const [descInput, setDescInput] = React4.useState("");
|
|
1050
|
+
const [coverImage, setCoverImage] = React4.useState("");
|
|
1051
|
+
const [workType, setWorkType] = React4.useState("visual");
|
|
1052
|
+
const loadData = async () => {
|
|
1053
|
+
setLoading(true);
|
|
1054
|
+
setError(null);
|
|
1055
|
+
try {
|
|
1056
|
+
const [contest, mine] = await Promise.all([
|
|
1057
|
+
api.getSnapshot(),
|
|
1058
|
+
api.listSubmissions({ authorId })
|
|
1059
|
+
]);
|
|
1060
|
+
setSnapshot(contest);
|
|
1061
|
+
setMySubmissions(mine);
|
|
1062
|
+
} catch (e) {
|
|
1063
|
+
setError(e.message);
|
|
1064
|
+
} finally {
|
|
1065
|
+
setLoading(false);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
React4.useEffect(() => {
|
|
1069
|
+
void loadData();
|
|
1070
|
+
}, []);
|
|
1071
|
+
const submitWork = async () => {
|
|
1072
|
+
if (!snapshot) return;
|
|
1073
|
+
const payload = {
|
|
1074
|
+
contestId: snapshot.contest.id,
|
|
1075
|
+
authorId,
|
|
1076
|
+
authorNickname,
|
|
1077
|
+
title: titleInput,
|
|
1078
|
+
description: descInput,
|
|
1079
|
+
type: workType,
|
|
1080
|
+
tags: ["web"],
|
|
1081
|
+
content: {
|
|
1082
|
+
coverImage,
|
|
1083
|
+
images: coverImage ? [coverImage] : void 0
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
setSubmitting(true);
|
|
1087
|
+
setError(null);
|
|
1088
|
+
try {
|
|
1089
|
+
await api.createSubmission(payload, "web");
|
|
1090
|
+
setTitleInput("");
|
|
1091
|
+
setDescInput("");
|
|
1092
|
+
setCoverImage("");
|
|
1093
|
+
await loadData();
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
setError(e.message);
|
|
1096
|
+
} finally {
|
|
1097
|
+
setSubmitting(false);
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
return /* @__PURE__ */ React4__default.default.createElement("section", null, /* @__PURE__ */ React4__default.default.createElement("h2", null, title), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void loadData(), disabled: loading }, loading ? "\u5237\u65B0\u4E2D..." : "\u5237\u65B0\u6570\u636E"), error ? /* @__PURE__ */ React4__default.default.createElement("p", { style: { color: "crimson" } }, "\u9519\u8BEF\uFF1A", error) : null, /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("h3", null, "\u65B0\u5EFA\u6295\u7A3F"), /* @__PURE__ */ React4__default.default.createElement("input", { value: titleInput, onChange: (e) => setTitleInput(e.target.value), placeholder: "\u4F5C\u54C1\u6807\u9898" }), /* @__PURE__ */ React4__default.default.createElement("br", null), /* @__PURE__ */ React4__default.default.createElement("textarea", { value: descInput, onChange: (e) => setDescInput(e.target.value), placeholder: "\u4F5C\u54C1\u7B80\u4ECB" }), /* @__PURE__ */ React4__default.default.createElement("br", null), /* @__PURE__ */ React4__default.default.createElement("input", { value: coverImage, onChange: (e) => setCoverImage(e.target.value), placeholder: "\u5C01\u9762 URL" }), /* @__PURE__ */ React4__default.default.createElement("br", null), /* @__PURE__ */ React4__default.default.createElement("select", { value: workType, onChange: (e) => setWorkType(e.target.value) }, workTypes.map((item) => /* @__PURE__ */ React4__default.default.createElement("option", { value: item, key: item }, item))), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void submitWork(), disabled: submitting || !snapshot }, submitting ? "\u63D0\u4EA4\u4E2D..." : "\u63D0\u4EA4\u7A3F\u4EF6")), /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("h3", null, "\u6211\u7684\u6295\u7A3F\uFF08", mySubmissions.length, "\uFF09"), /* @__PURE__ */ React4__default.default.createElement("ul", null, mySubmissions.map((item) => /* @__PURE__ */ React4__default.default.createElement("li", { key: item.id }, item.title, "\uFF5C\u72B6\u6001\uFF1A", item.status, "\uFF5C\u7968\u6570\uFF1A", item.voteCount, item.rejectReason ? `\uFF5C\u9A73\u56DE\uFF1A${item.rejectReason}` : "")))));
|
|
1101
|
+
};
|
|
1102
|
+
var MikuContestArtistPage_default = MikuContestArtistPage;
|
|
1103
|
+
var MikuContestAdminPage = ({
|
|
1104
|
+
client,
|
|
1105
|
+
adminId,
|
|
1106
|
+
title = "\u7BA1\u7406\u5458\u9762\u677F"
|
|
1107
|
+
}) => {
|
|
1108
|
+
const api = React4.useMemo(() => client || createMikuContestWebClient(), [client]);
|
|
1109
|
+
const [snapshot, setSnapshot] = React4.useState(null);
|
|
1110
|
+
const [submissions, setSubmissions] = React4.useState([]);
|
|
1111
|
+
const [loading, setLoading] = React4.useState(false);
|
|
1112
|
+
const [error, setError] = React4.useState(null);
|
|
1113
|
+
const [voterId, setVoterId] = React4.useState("");
|
|
1114
|
+
const loadData = async () => {
|
|
1115
|
+
setLoading(true);
|
|
1116
|
+
setError(null);
|
|
1117
|
+
try {
|
|
1118
|
+
const [contest, list] = await Promise.all([api.getSnapshot(), api.listSubmissions()]);
|
|
1119
|
+
setSnapshot(contest);
|
|
1120
|
+
setSubmissions(list);
|
|
1121
|
+
} catch (e) {
|
|
1122
|
+
setError(e.message);
|
|
1123
|
+
} finally {
|
|
1124
|
+
setLoading(false);
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
React4.useEffect(() => {
|
|
1128
|
+
void loadData();
|
|
1129
|
+
}, []);
|
|
1130
|
+
const review = async (item, action) => {
|
|
1131
|
+
try {
|
|
1132
|
+
await api.reviewSubmission({
|
|
1133
|
+
submissionId: item.id,
|
|
1134
|
+
reviewerId: adminId,
|
|
1135
|
+
action,
|
|
1136
|
+
rejectReason: action === "reject" ? "\u7BA1\u7406\u5458\u9A73\u56DE" : void 0
|
|
1137
|
+
});
|
|
1138
|
+
await loadData();
|
|
1139
|
+
} catch (e) {
|
|
1140
|
+
setError(e.message);
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
const toggleVoting = async (enabled) => {
|
|
1144
|
+
if (!snapshot) return;
|
|
1145
|
+
try {
|
|
1146
|
+
await api.updateContestConfig({
|
|
1147
|
+
toggles: {
|
|
1148
|
+
...snapshot.contest.toggles,
|
|
1149
|
+
votingEnabled: enabled
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
await loadData();
|
|
1153
|
+
} catch (e) {
|
|
1154
|
+
setError(e.message);
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
const setRestriction = async (banned) => {
|
|
1158
|
+
if (!snapshot || !voterId.trim()) return;
|
|
1159
|
+
try {
|
|
1160
|
+
await api.setVoterRestriction({
|
|
1161
|
+
voterId: voterId.trim(),
|
|
1162
|
+
banned,
|
|
1163
|
+
reason: banned ? "\u7BA1\u7406\u5458\u624B\u52A8\u5C01\u7981" : "\u7BA1\u7406\u5458\u89E3\u9664\u5C01\u7981",
|
|
1164
|
+
operatorId: adminId
|
|
1165
|
+
});
|
|
1166
|
+
setVoterId("");
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
setError(e.message);
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
const resetVotesByVoter = async () => {
|
|
1172
|
+
if (!voterId.trim()) return;
|
|
1173
|
+
try {
|
|
1174
|
+
await api.resetVotes({ voterId: voterId.trim() });
|
|
1175
|
+
setVoterId("");
|
|
1176
|
+
await loadData();
|
|
1177
|
+
} catch (e) {
|
|
1178
|
+
setError(e.message);
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
const exportExcel = async () => {
|
|
1182
|
+
try {
|
|
1183
|
+
const data = await api.exportSubmissions();
|
|
1184
|
+
const blob = new Blob([data], {
|
|
1185
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
1186
|
+
});
|
|
1187
|
+
const url = URL.createObjectURL(blob);
|
|
1188
|
+
const a = document.createElement("a");
|
|
1189
|
+
a.href = url;
|
|
1190
|
+
a.download = "miku-submissions.xlsx";
|
|
1191
|
+
a.click();
|
|
1192
|
+
URL.revokeObjectURL(url);
|
|
1193
|
+
} catch (e) {
|
|
1194
|
+
setError(e.message);
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
return /* @__PURE__ */ React4__default.default.createElement("section", null, /* @__PURE__ */ React4__default.default.createElement("h2", null, title), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void loadData(), disabled: loading }, loading ? "\u5237\u65B0\u4E2D..." : "\u5237\u65B0\u6570\u636E"), error ? /* @__PURE__ */ React4__default.default.createElement("p", { style: { color: "crimson" } }, "\u9519\u8BEF\uFF1A", error) : null, /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("h3", null, "\u8D5B\u4E8B\u5F00\u5173"), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void toggleVoting(true), disabled: !snapshot }, "\u5F00\u542F\u6295\u7968"), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void toggleVoting(false), disabled: !snapshot }, "\u5173\u95ED\u6295\u7968")), /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("h3", null, "\u6295\u7A3F\u5BA1\u6838\uFF08", submissions.length, "\uFF09"), /* @__PURE__ */ React4__default.default.createElement("ul", null, submissions.map((item) => /* @__PURE__ */ React4__default.default.createElement("li", { key: item.id }, /* @__PURE__ */ React4__default.default.createElement("strong", null, item.title), "\uFF5C\u4F5C\u8005\uFF1A", item.authorNickname, "\uFF5C\u72B6\u6001\uFF1A", item.status, "\uFF5C\u7968\u6570\uFF1A", item.voteCount, item.status === "pending" ? /* @__PURE__ */ React4__default.default.createElement(React4__default.default.Fragment, null, " ", /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void review(item, "approve") }, "\u901A\u8FC7"), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void review(item, "reject") }, "\u9A73\u56DE")) : null)))), /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("h3", null, "\u6295\u7968\u98CE\u63A7"), /* @__PURE__ */ React4__default.default.createElement("input", { value: voterId, onChange: (e) => setVoterId(e.target.value), placeholder: "voterId" }), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void setRestriction(true), disabled: !snapshot }, "\u5C01\u7981\u6295\u7968"), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void setRestriction(false), disabled: !snapshot }, "\u89E3\u9664\u5C01\u7981"), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void resetVotesByVoter() }, "\u6E05\u96F6\u8BE5\u7528\u6237\u7968\u6570")), /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("h3", null, "\u5BFC\u51FA"), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => void exportExcel() }, "\u5BFC\u51FA\u6295\u7A3F Excel")));
|
|
1198
|
+
};
|
|
1199
|
+
var MikuContestAdminPage_default = MikuContestAdminPage;
|
|
1200
|
+
|
|
1201
|
+
// src/mikuContest/ui/web/pages/MikuContestPage.tsx
|
|
1202
|
+
var MikuContestPage = ({
|
|
1203
|
+
defaultView = "audience",
|
|
1204
|
+
viewerVoterId = "viewer-demo",
|
|
1205
|
+
artistId = "artist-demo",
|
|
1206
|
+
artistNickname = "Demo \u753B\u5E08",
|
|
1207
|
+
adminId = "admin-demo"
|
|
1208
|
+
}) => {
|
|
1209
|
+
const [view, setView] = React4.useState(defaultView);
|
|
1210
|
+
return /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("h1", null, "Miku Contest"), /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => setView("audience") }, "\u89C2\u4F17\u7AEF"), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => setView("artist") }, "\u753B\u5E08\u7AEF"), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => setView("admin") }, "\u7BA1\u7406\u5458\u7AEF")), view === "audience" ? /* @__PURE__ */ React4__default.default.createElement(MikuContestAudiencePage_default, { voterId: viewerVoterId }) : null, view === "artist" ? /* @__PURE__ */ React4__default.default.createElement(MikuContestArtistPage_default, { authorId: artistId, authorNickname: artistNickname }) : null, view === "admin" ? /* @__PURE__ */ React4__default.default.createElement(MikuContestAdminPage_default, { adminId }) : null);
|
|
1211
|
+
};
|
|
1212
|
+
var MikuContestPage_default = MikuContestPage;
|
|
1213
|
+
|
|
1214
|
+
// src/mikuContest/ui/miniapp/index.ts
|
|
1215
|
+
var miniapp_exports2 = {};
|
|
1216
|
+
__export(miniapp_exports2, {
|
|
1217
|
+
MikuContestMiniappHome: () => MikuContestMiniappHome_default,
|
|
1218
|
+
MikuContestMiniappPage: () => MikuContestMiniappPage_default
|
|
1219
|
+
});
|
|
1220
|
+
var MikuContestMiniappHome = ({ snapshot }) => {
|
|
1221
|
+
return /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("h3", null, snapshot.contest.name), /* @__PURE__ */ React4__default.default.createElement("p", null, "\u6295\u7A3F\uFF1A", snapshot.submissions.length, " | \u516C\u544A\uFF1A", snapshot.announcements.length), /* @__PURE__ */ React4__default.default.createElement("ol", null, snapshot.leaderboard.slice(0, 3).map((item) => /* @__PURE__ */ React4__default.default.createElement("li", { key: item.submissionId }, item.title, "\uFF08", item.voteCount, "\u7968\uFF09"))));
|
|
1222
|
+
};
|
|
1223
|
+
var MikuContestMiniappHome_default = MikuContestMiniappHome;
|
|
1224
|
+
var MikuContestMiniappPage = () => {
|
|
1225
|
+
const { service, snapshot, refresh } = useMikuContest();
|
|
1226
|
+
const [tab, setTab] = React4.useState("vote");
|
|
1227
|
+
const [voterId, setVoterId] = React4.useState("miniapp-voter");
|
|
1228
|
+
const [authorId, setAuthorId] = React4.useState("miniapp-author");
|
|
1229
|
+
const [authorNickname, setAuthorNickname] = React4.useState("\u5C0F\u7A0B\u5E8F\u753B\u5E08");
|
|
1230
|
+
const [title, setTitle] = React4.useState("");
|
|
1231
|
+
const [desc, setDesc] = React4.useState("");
|
|
1232
|
+
const [type, setType] = React4.useState("visual");
|
|
1233
|
+
const [error, setError] = React4.useState(null);
|
|
1234
|
+
const approvedWorks = React4.useMemo(() => {
|
|
1235
|
+
return snapshot.submissions.filter((item) => item.status === "approved");
|
|
1236
|
+
}, [snapshot.submissions]);
|
|
1237
|
+
const vote = (submissionId) => {
|
|
1238
|
+
try {
|
|
1239
|
+
service.vote({
|
|
1240
|
+
contestId: snapshot.contest.id,
|
|
1241
|
+
submissionId,
|
|
1242
|
+
voterId
|
|
1243
|
+
});
|
|
1244
|
+
refresh();
|
|
1245
|
+
setError(null);
|
|
1246
|
+
} catch (e) {
|
|
1247
|
+
setError(e.message);
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
const submit = () => {
|
|
1251
|
+
try {
|
|
1252
|
+
service.createSubmission(
|
|
1253
|
+
{
|
|
1254
|
+
contestId: snapshot.contest.id,
|
|
1255
|
+
authorId,
|
|
1256
|
+
authorNickname,
|
|
1257
|
+
title,
|
|
1258
|
+
description: desc,
|
|
1259
|
+
type,
|
|
1260
|
+
content: {}
|
|
1261
|
+
},
|
|
1262
|
+
"miniapp"
|
|
1263
|
+
);
|
|
1264
|
+
setTitle("");
|
|
1265
|
+
setDesc("");
|
|
1266
|
+
refresh();
|
|
1267
|
+
setError(null);
|
|
1268
|
+
} catch (e) {
|
|
1269
|
+
setError(e.message);
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
return /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("h3", null, snapshot.contest.name), /* @__PURE__ */ React4__default.default.createElement("p", null, "\u5C0F\u7A0B\u5E8F\u7AEF\u793A\u4F8B\u9875\u9762"), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => setTab("vote") }, "\u89C2\u4F17\u6295\u7968"), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => setTab("submit") }, "\u753B\u5E08\u6295\u7A3F"), error ? /* @__PURE__ */ React4__default.default.createElement("p", { style: { color: "crimson" } }, "\u9519\u8BEF\uFF1A", error) : null, tab === "vote" ? /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("input", { value: voterId, onChange: (e) => setVoterId(e.target.value), placeholder: "voterId" }), /* @__PURE__ */ React4__default.default.createElement("ul", null, approvedWorks.map((item) => /* @__PURE__ */ React4__default.default.createElement("li", { key: item.id }, item.title, "\uFF08", item.voteCount, "\u7968\uFF09", /* @__PURE__ */ React4__default.default.createElement("button", { onClick: () => vote(item.id) }, "\u6295\u7968"))))) : null, tab === "submit" ? /* @__PURE__ */ React4__default.default.createElement("div", null, /* @__PURE__ */ React4__default.default.createElement("input", { value: authorId, onChange: (e) => setAuthorId(e.target.value), placeholder: "authorId" }), /* @__PURE__ */ React4__default.default.createElement("input", { value: authorNickname, onChange: (e) => setAuthorNickname(e.target.value), placeholder: "\u4F5C\u8005\u6635\u79F0" }), /* @__PURE__ */ React4__default.default.createElement("input", { value: title, onChange: (e) => setTitle(e.target.value), placeholder: "\u4F5C\u54C1\u6807\u9898" }), /* @__PURE__ */ React4__default.default.createElement("input", { value: desc, onChange: (e) => setDesc(e.target.value), placeholder: "\u4F5C\u54C1\u7B80\u4ECB" }), /* @__PURE__ */ React4__default.default.createElement("select", { value: type, onChange: (e) => setType(e.target.value) }, /* @__PURE__ */ React4__default.default.createElement("option", { value: "visual" }, "visual"), /* @__PURE__ */ React4__default.default.createElement("option", { value: "video" }, "video"), /* @__PURE__ */ React4__default.default.createElement("option", { value: "text" }, "text"), /* @__PURE__ */ React4__default.default.createElement("option", { value: "audio" }, "audio")), /* @__PURE__ */ React4__default.default.createElement("button", { onClick: submit }, "\u63D0\u4EA4")) : null);
|
|
1273
|
+
};
|
|
1274
|
+
var MikuContestMiniappPage_default = MikuContestMiniappPage;
|
|
1275
|
+
|
|
1276
|
+
exports.MikuContestPersistentService = MikuContestPersistentService;
|
|
1277
|
+
exports.MikuContestService = MikuContestService;
|
|
1278
|
+
exports.MikuContestStateDbService = MikuContestStateDbService;
|
|
1279
|
+
exports.checkVoteEligibility = checkVoteEligibility;
|
|
1280
|
+
exports.createCreateSubmissionHandler = createCreateSubmissionHandler;
|
|
1281
|
+
exports.createDefaultMikuContestConfig = createDefaultMikuContestConfig;
|
|
1282
|
+
exports.createExportSubmissionsHandler = createExportSubmissionsHandler;
|
|
1283
|
+
exports.createGetContestSnapshotHandler = createGetContestSnapshotHandler;
|
|
1284
|
+
exports.createListSubmissionsHandler = createListSubmissionsHandler;
|
|
1285
|
+
exports.createMikuContestApiClient = createMikuContestApiClient;
|
|
1286
|
+
exports.createMikuContestDrizzlePersistenceAdapter = createMikuContestDrizzlePersistenceAdapter;
|
|
1287
|
+
exports.createMikuContestPersistentService = createMikuContestPersistentService;
|
|
1288
|
+
exports.createMikuContestService = createMikuContestService;
|
|
1289
|
+
exports.createResetVotesHandler = createResetVotesHandler;
|
|
1290
|
+
exports.createReviewSubmissionHandler = createReviewSubmissionHandler;
|
|
1291
|
+
exports.createSetVoterRestrictionHandler = createSetVoterRestrictionHandler;
|
|
1292
|
+
exports.createUpdateContestConfigHandler = createUpdateContestConfigHandler;
|
|
1293
|
+
exports.createVoteHandler = createVoteHandler;
|
|
1294
|
+
exports.defaultMikuVotingRules = defaultMikuVotingRules;
|
|
1295
|
+
exports.mikuContestConfigs = mikuContestConfigs;
|
|
1296
|
+
exports.mikuContestDbService = mikuContestDbService;
|
|
1297
|
+
exports.mikuContestNotices = mikuContestNotices;
|
|
1298
|
+
exports.mikuContestSubmissions = mikuContestSubmissions;
|
|
1299
|
+
exports.mikuContestVoterRestrictions = mikuContestVoterRestrictions;
|
|
1300
|
+
exports.mikuContestVotes = mikuContestVotes;
|
|
1301
|
+
exports.miniappService = miniapp_exports;
|
|
1302
|
+
exports.miniappUI = miniapp_exports2;
|
|
1303
|
+
exports.sortByVotesDesc = sortByVotesDesc;
|
|
1304
|
+
exports.toVoteDayKey = toVoteDayKey;
|
|
1305
|
+
exports.useMikuContest = useMikuContest;
|
|
1306
|
+
exports.validateMikuSubmissionInput = validateMikuSubmissionInput;
|
|
1307
|
+
exports.webService = web_exports;
|
|
1308
|
+
exports.webUI = web_exports2;
|
|
1309
|
+
//# sourceMappingURL=index.js.map
|
|
1310
|
+
//# sourceMappingURL=index.js.map
|