koishi-plugin-aipo-asbbot 0.1.107

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 (56) hide show
  1. package/client/announcement.vue +227 -0
  2. package/client/audit.vue +99 -0
  3. package/client/index.scss +299 -0
  4. package/client/index.ts +23 -0
  5. package/client/markdown.ts +97 -0
  6. package/dist/index.js +4 -0
  7. package/dist/style.css +1 -0
  8. package/lib/announcement/console.d.ts +54 -0
  9. package/lib/announcement/deliver.d.ts +16 -0
  10. package/lib/announcement/index.d.ts +4 -0
  11. package/lib/announcement/store.d.ts +72 -0
  12. package/lib/announcement/types.d.ts +44 -0
  13. package/lib/backup.d.ts +103 -0
  14. package/lib/chatSend.d.ts +49 -0
  15. package/lib/consoleEntry.d.ts +9 -0
  16. package/lib/getMusic.d.ts +42 -0
  17. package/lib/giftCode.d.ts +74 -0
  18. package/lib/index.d.ts +102 -0
  19. package/lib/index.js +7903 -0
  20. package/lib/keyboards.d.ts +85 -0
  21. package/lib/migration.d.ts +40 -0
  22. package/lib/opLog.d.ts +37 -0
  23. package/lib/points/admin.d.ts +8 -0
  24. package/lib/points/convert.d.ts +16 -0
  25. package/lib/points/exp.d.ts +25 -0
  26. package/lib/points/index.d.ts +20 -0
  27. package/lib/points/privilege.d.ts +14 -0
  28. package/lib/points/profile.d.ts +59 -0
  29. package/lib/points/queryCost.d.ts +48 -0
  30. package/lib/points/signin.d.ts +19 -0
  31. package/lib/points/transaction.d.ts +35 -0
  32. package/lib/points/types.d.ts +76 -0
  33. package/lib/requestChache.d.ts +55 -0
  34. package/lib/riskcontrol/admin.d.ts +13 -0
  35. package/lib/riskcontrol/auditQueue.d.ts +43 -0
  36. package/lib/riskcontrol/console.d.ts +33 -0
  37. package/lib/riskcontrol/cooldown.d.ts +15 -0
  38. package/lib/riskcontrol/identityCheck.d.ts +26 -0
  39. package/lib/riskcontrol/index.d.ts +82 -0
  40. package/lib/riskcontrol/inputCheck.d.ts +31 -0
  41. package/lib/riskcontrol/level.d.ts +69 -0
  42. package/lib/riskcontrol/outputAudit.d.ts +140 -0
  43. package/lib/riskcontrol/types.d.ts +108 -0
  44. package/lib/riskcontrol/violation.d.ts +34 -0
  45. package/lib/riskcontrol/violationLib.d.ts +30 -0
  46. package/lib/roles.d.ts +50 -0
  47. package/lib/search/formatters.d.ts +81 -0
  48. package/lib/search/rolimons.d.ts +37 -0
  49. package/lib/search/sessionStore.d.ts +48 -0
  50. package/lib/search/types.d.ts +127 -0
  51. package/lib/seenGuild.d.ts +35 -0
  52. package/lib/storageQueue.d.ts +9 -0
  53. package/lib/userLocal.d.ts +76 -0
  54. package/lib/verifiedAudit.d.ts +37 -0
  55. package/package.json +43 -0
  56. package/readme.md +5 -0
@@ -0,0 +1,227 @@
1
+ <template>
2
+ <k-layout>
3
+ <div class="roblox-announcement-page">
4
+ <h2>{{ editing ? (form.id ? '编辑公告' : '创建公告') : '公告列表' }}(共 {{ list.length }} 条)</h2>
5
+
6
+ <div v-if="!editing" class="ann-toolbar">
7
+ <button class="btn-primary" @click="startCreate">+ 新建公告</button>
8
+ </div>
9
+
10
+ <!-- 编辑表单 -->
11
+ <div v-if="editing" class="ann-editor">
12
+ <div class="ann-field">
13
+ <label>标题</label>
14
+ <input v-model="form.title" placeholder="公告标题" />
15
+ </div>
16
+ <div class="ann-field">
17
+ <label>内容(原生 Markdown)</label>
18
+ <textarea v-model="form.content" rows="10" placeholder="支持 # 标题、**粗体**、*斜体*、- 列表、[链接](url)、`代码` 等原生 Markdown 语法"></textarea>
19
+ </div>
20
+ <div class="ann-field">
21
+ <label>定向等级(0 级 = 全体用户,默认)</label>
22
+ <div class="ann-levels">
23
+ <label v-for="lv in 8" :key="lv - 1" class="ann-level-item">
24
+ <input type="checkbox" :checked="form.levels.includes(lv - 1)" @change="toggleLevel(lv - 1)" />
25
+ <span v-if="lv - 1 === 0">0 级(全体用户)</span>
26
+ <span v-else>{{ lv - 1 }} 级</span>
27
+ </label>
28
+ </div>
29
+ <div class="ann-audience">
30
+ 投放范围预览:
31
+ <template v-if="audience">符合条件用户 <b>{{ audience.count }}</b> / 已知用户 {{ audience.total }}</template>
32
+ <template v-else>计算中…</template>
33
+ </div>
34
+ </div>
35
+ <div class="ann-field ann-inline">
36
+ <label>触发次数(0=不触发;1=首次使用触发一次,默认;N=最多触发 N 次)</label>
37
+ <input type="number" min="0" step="1" v-model.number="form.maxTriggers" />
38
+ </div>
39
+ <div class="ann-field ann-inline">
40
+ <label>过期时间(留空 = 永不过期)</label>
41
+ <input type="datetime-local" v-model="expiresLocal" />
42
+ </div>
43
+ <div class="ann-field">
44
+ <label>预览</label>
45
+ <div class="ann-preview" v-html="previewHtml"></div>
46
+ </div>
47
+ <div class="ann-actions">
48
+ <button class="btn-primary" :disabled="busy" @click="save">保存</button>
49
+ <button :disabled="busy" @click="cancelEdit">取消</button>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- 公告列表 -->
54
+ <table v-else class="ann-table">
55
+ <thead>
56
+ <tr>
57
+ <th>ID</th>
58
+ <th>标题</th>
59
+ <th>状态</th>
60
+ <th>定向等级</th>
61
+ <th>触发次数</th>
62
+ <th>已触达(人 / 次)</th>
63
+ <th>创建时间</th>
64
+ <th>发布时间</th>
65
+ <th>过期时间</th>
66
+ <th>操作</th>
67
+ </tr>
68
+ </thead>
69
+ <tbody>
70
+ <tr v-if="!list.length"><td colspan="10" class="ann-empty">暂无公告,点击「新建公告」创建</td></tr>
71
+ <tr v-for="item in list" :key="item.id">
72
+ <td class="ann-id" :title="item.id">{{ item.id.slice(0, 12) }}…</td>
73
+ <td>{{ item.title }}</td>
74
+ <td :class="'status-' + item.status">{{ statusName(item.status) }}</td>
75
+ <td>{{ levelsLabel(item.levels) }}</td>
76
+ <td>{{ item.maxTriggers }}</td>
77
+ <td>{{ stats[item.id]?.users ?? 0 }} / {{ stats[item.id]?.triggers ?? 0 }}</td>
78
+ <td>{{ fmtTime(item.createdAt) }}</td>
79
+ <td>{{ item.publishedAt ? fmtTime(item.publishedAt) : '-' }}</td>
80
+ <td>{{ item.expiresAt ? fmtTime(item.expiresAt) : '永不过期' }}</td>
81
+ <td class="ann-ops">
82
+ <button :disabled="busy" @click="startEdit(item)">编辑</button>
83
+ <button v-if="item.status !== 'published'" class="btn-primary" :disabled="busy" @click="act('publish', item.id)">发布</button>
84
+ <button v-if="item.status === 'published'" :disabled="busy" @click="act('expire', item.id)">设为过期</button>
85
+ <button class="btn-danger" :disabled="busy" @click="remove(item.id)">删除</button>
86
+ </td>
87
+ </tr>
88
+ </tbody>
89
+ </table>
90
+ </div>
91
+ </k-layout>
92
+ </template>
93
+
94
+ <script lang="ts" setup>
95
+ import { computed, ref, watch } from 'vue'
96
+ import { store, send, message } from '@koishijs/client'
97
+ import { renderMarkdown } from './markdown'
98
+
99
+ const busy = ref(false)
100
+ const editing = ref(false)
101
+ const audience = ref<{ count: number; total: number } | null>(null)
102
+
103
+ const list = computed(() => store['roblox-announcement']?.list ?? [])
104
+ const stats = computed(() => store['roblox-announcement']?.stats ?? {})
105
+
106
+ const emptyForm = () => ({ id: '', title: '', content: '', levels: [0] as number[], maxTriggers: 1, expiresAt: 0 })
107
+ const form = ref(emptyForm())
108
+ const expiresLocal = ref('')
109
+
110
+ const previewHtml = computed(() => renderMarkdown(form.value.content))
111
+
112
+ watch(() => form.value.levels.slice(), refreshAudience, { immediate: false })
113
+
114
+ async function refreshAudience() {
115
+ audience.value = null
116
+ try {
117
+ audience.value = await send('roblox-announcement/audience', form.value.levels as any)
118
+ } catch {
119
+ audience.value = { count: 0, total: 0 }
120
+ }
121
+ }
122
+
123
+ function toggleLevel(level: number) {
124
+ const levels = form.value.levels
125
+ const idx = levels.indexOf(level)
126
+ if (idx >= 0) levels.splice(idx, 1)
127
+ else levels.push(level)
128
+ if (!levels.length) levels.push(0)
129
+ levels.sort((a, b) => a - b)
130
+ }
131
+
132
+ function startCreate() {
133
+ form.value = emptyForm()
134
+ expiresLocal.value = ''
135
+ editing.value = true
136
+ refreshAudience()
137
+ }
138
+
139
+ function startEdit(item: any) {
140
+ form.value = {
141
+ id: item.id,
142
+ title: item.title,
143
+ content: item.content,
144
+ levels: [...item.levels],
145
+ maxTriggers: item.maxTriggers,
146
+ expiresAt: item.expiresAt,
147
+ }
148
+ expiresLocal.value = item.expiresAt ? toLocalInput(item.expiresAt) : ''
149
+ editing.value = true
150
+ refreshAudience()
151
+ }
152
+
153
+ function cancelEdit() {
154
+ editing.value = false
155
+ }
156
+
157
+ async function save() {
158
+ busy.value = true
159
+ try {
160
+ const payload = {
161
+ ...form.value,
162
+ id: form.value.id || undefined,
163
+ expiresAt: expiresLocal.value ? new Date(expiresLocal.value).getTime() : 0,
164
+ }
165
+ const r = await send('roblox-announcement/save', payload as any)
166
+ if (r?.ok) {
167
+ message.success(r.message)
168
+ editing.value = false
169
+ } else {
170
+ message.error(r?.message || '保存失败')
171
+ }
172
+ } catch (e: any) {
173
+ message.error(e?.message || '保存失败')
174
+ } finally {
175
+ busy.value = false
176
+ }
177
+ }
178
+
179
+ async function act(action: 'publish' | 'expire', id: string) {
180
+ busy.value = true
181
+ try {
182
+ const r = await send(`roblox-announcement/${action}` as any, id)
183
+ if (r?.ok) message.success(r.message)
184
+ else message.error(r?.message || '操作失败')
185
+ } catch (e: any) {
186
+ message.error(e?.message || '操作失败')
187
+ } finally {
188
+ busy.value = false
189
+ }
190
+ }
191
+
192
+ async function remove(id: string) {
193
+ if (!window.confirm('确认删除该公告?相关接收记录将一并删除。')) return
194
+ busy.value = true
195
+ try {
196
+ const r = await send('roblox-announcement/delete', id)
197
+ if (r?.ok) message.success(r.message)
198
+ else message.error(r?.message || '删除失败')
199
+ } catch (e: any) {
200
+ message.error(e?.message || '删除失败')
201
+ } finally {
202
+ busy.value = false
203
+ }
204
+ }
205
+
206
+ function statusName(status?: string) {
207
+ return { draft: '草稿', published: '已发布', expired: '已过期' }[status ?? ''] || status
208
+ }
209
+
210
+ function levelsLabel(levels: number[]) {
211
+ if (levels.includes(0)) return '全体用户'
212
+ return levels.map((v) => `${v} 级`).join('、')
213
+ }
214
+
215
+ function fmtTime(ts?: number) {
216
+ if (!ts) return '-'
217
+ const d = new Date(ts)
218
+ const p = (n: number) => String(n).padStart(2, '0')
219
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
220
+ }
221
+
222
+ function toLocalInput(ts: number) {
223
+ const d = new Date(ts)
224
+ const p = (n: number) => String(n).padStart(2, '0')
225
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`
226
+ }
227
+ </script>
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <k-layout>
3
+ <div class="roblox-audit-page">
4
+ <h2>待人工审核({{ pending.length }} 条)</h2>
5
+ <div v-if="!pending.length" class="audit-empty">暂无待审记录</div>
6
+ <div v-else class="audit-cards">
7
+ <div v-for="item in pending" :key="item.id" class="audit-card">
8
+ <img v-if="item.pic" :src="item.pic" class="audit-pic" referrerpolicy="no-referrer" />
9
+ <div v-else class="audit-pic-none">(无图片,文本命中)</div>
10
+ <div class="audit-body">
11
+ <div class="audit-line"><b>记录 ID:</b>{{ item.id }}</div>
12
+ <div class="audit-line"><b>调用者 ID:</b>{{ item.reporterId }}<template v-if="item.reporterName && item.reporterName !== item.reporterId">({{ item.reporterName }})</template></div>
13
+ <div class="audit-line"><b>目标:</b>{{ targetTypeName(item.targetType) }} {{ item.targetName || item.targetId || '-' }}<template v-if="item.targetId">(ID: {{ item.targetId }})</template></div>
14
+ <div class="audit-line"><b>违规类型:</b>{{ item.violationType }}</div>
15
+ <div class="audit-line"><b>命中原因:</b>{{ item.reason || '-' }}</div>
16
+ <div class="audit-line"><b>提交时间:</b>{{ fmtTime(item.createdAt) }}</div>
17
+ <div class="audit-summary">{{ item.summary }}</div>
18
+ </div>
19
+ <div class="audit-actions">
20
+ <button class="btn-violation" :disabled="busy === item.id" @click="resolve(item.id, 'violation')">违规</button>
21
+ <button class="btn-ok" :disabled="busy === item.id" @click="resolve(item.id, 'ok')">不违规</button>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ <h2>审核日志(最近 {{ log.length }} 条)</h2>
27
+ <table class="audit-log-table">
28
+ <thead>
29
+ <tr>
30
+ <th>记录 ID</th>
31
+ <th>调用者 ID</th>
32
+ <th>目标</th>
33
+ <th>命中原因</th>
34
+ <th>提交时间</th>
35
+ <th>审核前状态</th>
36
+ <th>审核后状态</th>
37
+ <th>裁定结果</th>
38
+ <th>处罚</th>
39
+ <th>审核时间</th>
40
+ <th>审核人</th>
41
+ </tr>
42
+ </thead>
43
+ <tbody>
44
+ <tr v-for="item in log" :key="item.id">
45
+ <td>{{ item.id }}</td>
46
+ <td>{{ item.reporterId }}</td>
47
+ <td>{{ targetTypeName(item.targetType) }} {{ item.targetName || item.targetId || '-' }}</td>
48
+ <td>{{ item.reason || '-' }}</td>
49
+ <td>{{ fmtTime(item.createdAt) }}</td>
50
+ <td>待审核</td>
51
+ <td :class="'status-' + item.status">{{ statusName(item.status) }}</td>
52
+ <td>{{ item.verdict === 'violation' ? '违规' : item.verdict === 'ok' ? '不违规' : '-' }}</td>
53
+ <td>{{ item.appliedLevel ? `${item.appliedLevel} / ${item.appliedDays ?? '-'} 天` : '-' }}</td>
54
+ <td>{{ item.resolvedAt ? fmtTime(item.resolvedAt) : '-' }}</td>
55
+ <td>{{ item.resolvedBy || '-' }}</td>
56
+ </tr>
57
+ </tbody>
58
+ </table>
59
+ </div>
60
+ </k-layout>
61
+ </template>
62
+
63
+ <script lang="ts" setup>
64
+ import { computed, ref } from 'vue'
65
+ import { store, send, message } from '@koishijs/client'
66
+
67
+ const busy = ref('')
68
+
69
+ const pending = computed(() => store['roblox-audit']?.pending ?? [])
70
+ const log = computed(() => store['roblox-audit']?.log ?? [])
71
+
72
+ function fmtTime(ts?: number) {
73
+ if (!ts) return '-'
74
+ const d = new Date(ts)
75
+ const p = (n: number) => String(n).padStart(2, '0')
76
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
77
+ }
78
+
79
+ function targetTypeName(type?: string) {
80
+ return { user: '用户', game: '游戏', group: '群组' }[type ?? ''] || ''
81
+ }
82
+
83
+ function statusName(status?: string) {
84
+ return { pending: '待审核', approved: '已裁定违规', rejected: '已裁定误判' }[status ?? ''] || status
85
+ }
86
+
87
+ async function resolve(id: string, verdict: 'violation' | 'ok') {
88
+ busy.value = id
89
+ try {
90
+ const r = await send('roblox-audit/resolve', id, verdict as any)
91
+ if (r?.ok) message.success(r.message)
92
+ else message.error(r?.message || '操作失败')
93
+ } catch (e: any) {
94
+ message.error(e?.message || '操作失败')
95
+ } finally {
96
+ busy.value = ''
97
+ }
98
+ }
99
+ </script>
@@ -0,0 +1,299 @@
1
+ .roblox-audit-page {
2
+ padding: 1rem 1.5rem 3rem;
3
+
4
+ h2 {
5
+ margin: 0.5rem 0 1rem;
6
+ font-size: 1.15rem;
7
+ }
8
+
9
+ .audit-empty {
10
+ color: var(--k-color-disabled);
11
+ padding: 1rem 0 2rem;
12
+ }
13
+
14
+ .audit-cards {
15
+ display: flex;
16
+ flex-wrap: wrap;
17
+ gap: 1rem;
18
+ }
19
+
20
+ .audit-card {
21
+ width: 300px;
22
+ border: 1px solid var(--k-color-divider);
23
+ border-radius: 8px;
24
+ background: var(--k-card-bg);
25
+ overflow: hidden;
26
+ display: flex;
27
+ flex-direction: column;
28
+
29
+ .audit-pic {
30
+ width: 100%;
31
+ height: 180px;
32
+ object-fit: contain;
33
+ background: #00000010;
34
+ }
35
+
36
+ .audit-pic-none {
37
+ width: 100%;
38
+ height: 180px;
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ color: var(--k-color-disabled);
43
+ background: #00000010;
44
+ }
45
+
46
+ .audit-body {
47
+ padding: 0.6rem 0.8rem;
48
+ font-size: 13px;
49
+ line-height: 1.7;
50
+ flex: 1;
51
+
52
+ .audit-line b {
53
+ margin-right: 0.3em;
54
+ }
55
+
56
+ .audit-summary {
57
+ margin-top: 0.3rem;
58
+ padding: 0.35rem 0.5rem;
59
+ background: #00000010;
60
+ border-radius: 4px;
61
+ max-height: 6em;
62
+ overflow: auto;
63
+ white-space: pre-wrap;
64
+ word-break: break-all;
65
+ }
66
+ }
67
+
68
+ .audit-actions {
69
+ display: flex;
70
+ gap: 0.6rem;
71
+ padding: 0.6rem 0.8rem 0.8rem;
72
+
73
+ button {
74
+ flex: 1;
75
+ padding: 0.4rem 0;
76
+ border: none;
77
+ border-radius: 4px;
78
+ color: #fff;
79
+ cursor: pointer;
80
+ font-size: 13px;
81
+
82
+ &:disabled {
83
+ opacity: 0.5;
84
+ cursor: not-allowed;
85
+ }
86
+ }
87
+
88
+ .btn-violation {
89
+ background: #d9534f;
90
+ }
91
+
92
+ .btn-ok {
93
+ background: #5cb85c;
94
+ }
95
+ }
96
+ }
97
+
98
+ .audit-log-table {
99
+ width: 100%;
100
+ border-collapse: collapse;
101
+ font-size: 13px;
102
+
103
+ th, td {
104
+ border: 1px solid var(--k-color-divider);
105
+ padding: 0.35rem 0.6rem;
106
+ text-align: left;
107
+ word-break: break-all;
108
+ }
109
+
110
+ th {
111
+ background: #00000010;
112
+ }
113
+
114
+ .status-pending { color: #e6a23c; }
115
+ .status-approved { color: #d9534f; }
116
+ .status-rejected { color: #5cb85c; }
117
+ }
118
+ }
119
+
120
+ .roblox-announcement-page {
121
+ padding: 1rem 1.5rem 3rem;
122
+
123
+ h2 {
124
+ margin: 0.5rem 0 1rem;
125
+ font-size: 1.15rem;
126
+ }
127
+
128
+ button {
129
+ padding: 0.35rem 0.9rem;
130
+ border: 1px solid var(--k-color-divider);
131
+ border-radius: 4px;
132
+ background: var(--k-card-bg);
133
+ cursor: pointer;
134
+ font-size: 13px;
135
+
136
+ &:disabled {
137
+ opacity: 0.5;
138
+ cursor: not-allowed;
139
+ }
140
+ }
141
+
142
+ .btn-primary {
143
+ background: #409eff;
144
+ border-color: #409eff;
145
+ color: #fff;
146
+ }
147
+
148
+ .btn-danger {
149
+ background: #d9534f;
150
+ border-color: #d9534f;
151
+ color: #fff;
152
+ }
153
+
154
+ .ann-toolbar {
155
+ margin-bottom: 1rem;
156
+ }
157
+
158
+ .ann-editor {
159
+ max-width: 860px;
160
+
161
+ .ann-field {
162
+ margin-bottom: 1rem;
163
+
164
+ > label {
165
+ display: block;
166
+ margin-bottom: 0.35rem;
167
+ font-weight: 600;
168
+ font-size: 13px;
169
+ }
170
+
171
+ input, textarea {
172
+ width: 100%;
173
+ box-sizing: border-box;
174
+ padding: 0.45rem 0.6rem;
175
+ border: 1px solid var(--k-color-divider);
176
+ border-radius: 4px;
177
+ background: var(--k-card-bg);
178
+ color: inherit;
179
+ font-size: 13px;
180
+ font-family: inherit;
181
+ }
182
+
183
+ textarea {
184
+ font-family: monospace;
185
+ resize: vertical;
186
+ }
187
+ }
188
+
189
+ .ann-inline input {
190
+ width: 240px;
191
+ }
192
+
193
+ .ann-levels {
194
+ display: flex;
195
+ flex-wrap: wrap;
196
+ gap: 0.4rem 1.2rem;
197
+
198
+ .ann-level-item {
199
+ display: flex;
200
+ align-items: center;
201
+ gap: 0.3rem;
202
+ font-size: 13px;
203
+ cursor: pointer;
204
+
205
+ input {
206
+ width: auto;
207
+ }
208
+ }
209
+ }
210
+
211
+ .ann-audience {
212
+ margin-top: 0.5rem;
213
+ font-size: 13px;
214
+ color: var(--k-color-disabled);
215
+
216
+ b {
217
+ color: #409eff;
218
+ }
219
+ }
220
+
221
+ .ann-preview {
222
+ border: 1px solid var(--k-color-divider);
223
+ border-radius: 4px;
224
+ padding: 0.8rem 1rem;
225
+ min-height: 4em;
226
+ font-size: 14px;
227
+ line-height: 1.7;
228
+
229
+ pre {
230
+ background: #00000010;
231
+ padding: 0.5rem 0.7rem;
232
+ border-radius: 4px;
233
+ overflow: auto;
234
+ }
235
+
236
+ code {
237
+ background: #00000010;
238
+ padding: 0.1em 0.35em;
239
+ border-radius: 3px;
240
+ }
241
+
242
+ blockquote {
243
+ margin: 0.4em 0;
244
+ padding: 0.2em 0.8em;
245
+ border-left: 3px solid var(--k-color-divider);
246
+ color: var(--k-color-disabled);
247
+ }
248
+
249
+ img {
250
+ max-width: 100%;
251
+ }
252
+ }
253
+
254
+ .ann-actions {
255
+ display: flex;
256
+ gap: 0.6rem;
257
+ }
258
+ }
259
+
260
+ .ann-table {
261
+ width: 100%;
262
+ border-collapse: collapse;
263
+ font-size: 13px;
264
+
265
+ th, td {
266
+ border: 1px solid var(--k-color-divider);
267
+ padding: 0.35rem 0.6rem;
268
+ text-align: left;
269
+ word-break: break-all;
270
+ }
271
+
272
+ th {
273
+ background: #00000010;
274
+ }
275
+
276
+ .ann-empty {
277
+ text-align: center;
278
+ color: var(--k-color-disabled);
279
+ padding: 1.2rem 0;
280
+ }
281
+
282
+ .ann-id {
283
+ font-family: monospace;
284
+ white-space: nowrap;
285
+ }
286
+
287
+ .ann-ops {
288
+ white-space: nowrap;
289
+
290
+ button {
291
+ margin-right: 0.4rem;
292
+ }
293
+ }
294
+
295
+ .status-draft { color: #e6a23c; }
296
+ .status-published { color: #5cb85c; }
297
+ .status-expired { color: var(--k-color-disabled); }
298
+ }
299
+ }
@@ -0,0 +1,23 @@
1
+ import { Context } from '@koishijs/client'
2
+ import Page from './audit.vue'
3
+ import AnnouncementPage from './announcement.vue'
4
+ import './index.scss'
5
+
6
+ export default (ctx: Context) => {
7
+ ctx.page({
8
+ name: 'Roblox 人工审核',
9
+ path: '/roblox-audit',
10
+ // 需要控制台登录用户具备 3 级及以上权限(配合 @koishijs/plugin-auth)
11
+ authority: 3,
12
+ fields: ['roblox-audit'],
13
+ component: Page,
14
+ })
15
+ ctx.page({
16
+ name: 'Roblox 公告管理',
17
+ path: '/roblox-announcement',
18
+ // 公告管理仅限管理员(4 级及以上权限,配合 @koishijs/plugin-auth)
19
+ authority: 4,
20
+ fields: ['roblox-announcement'],
21
+ component: AnnouncementPage,
22
+ })
23
+ }