minitest2.0 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.editorconfig +8 -0
  2. package/.env.development +3 -0
  3. package/.env.production +3 -0
  4. package/.env.test +3 -0
  5. package/.gitattributes +1 -0
  6. package/.oxlintrc.json +10 -0
  7. package/.prettierrc.json +6 -0
  8. package/.vscode/extensions.json +9 -0
  9. package/.vscode/settings.json +13 -0
  10. package/README.md +179 -0
  11. package/auto-imports.d.ts +629 -0
  12. package/components.d.ts +21 -0
  13. package/design-qa.md +36 -0
  14. package/docs/MX_API.md +244 -0
  15. package/docs/REQUEST.md +217 -0
  16. package/docs/jsapi.js +515 -0
  17. package/docs/package-json-guide.md +132 -0
  18. package/env.d.ts +15 -0
  19. package/eslint.config.ts +26 -0
  20. package/index.html +16 -0
  21. package/package.json +83 -0
  22. package/plugins/bump-version.ts +61 -0
  23. package/postcss.config.ts +15 -0
  24. package/public/favicon.ico +0 -0
  25. package/public/images/12a73787-86a9-4891-a65f-66104746f6a8.png +0 -0
  26. package/public/images/5798d7aa-ba8b-4605-8079-58b35495ac55.png +0 -0
  27. package/public/images/73fef1e4-0fd0-4a1a-9b8b-a70a5b6acbbc.png +0 -0
  28. package/public/images/bc685b4c-0cca-4a79-924c-a8ee10e6f8eb.png +0 -0
  29. package/public/images/c3dbbd9d-be56-490e-b9f4-6ee17ebefffc.png +0 -0
  30. package/public/images/ea745a10-42aa-4f44-8d7f-3ab02cc0adcd.png +0 -0
  31. package/public/images/f5876785-b927-4347-ba19-999114240649.png +0 -0
  32. package/public/images/img.png +0 -0
  33. package/public/images/opening-reserve-estimate.png +0 -0
  34. package/public/images/position-estimate-report.png +0 -0
  35. package/src/App.vue +131 -0
  36. package/src/api/announcement.ts +405 -0
  37. package/src/api/health.ts +13 -0
  38. package/src/api/pbc-position.ts +178 -0
  39. package/src/api/rmb-position.ts +233 -0
  40. package/src/api/tam.ts +173 -0
  41. package/src/api/user.ts +92 -0
  42. package/src/auto-imports.d.ts +633 -0
  43. package/src/components/AppTitleBar.vue +376 -0
  44. package/src/components.d.ts +33 -0
  45. package/src/config/config.properties +3 -0
  46. package/src/config/env.ts +6 -0
  47. package/src/config/plugin.properties.pro +6 -0
  48. package/src/config/plugin.properties.test +6 -0
  49. package/src/core/mxApi/index.ts +451 -0
  50. package/src/core/request/index.ts +135 -0
  51. package/src/main.ts +40 -0
  52. package/src/router/index.ts +144 -0
  53. package/src/stores/app.ts +103 -0
  54. package/src/stores/counter.ts +12 -0
  55. package/src/stores/user.ts +137 -0
  56. package/src/styles/nprogress.css +22 -0
  57. package/src/styles/vant-overrides.css +42 -0
  58. package/src/types/api.d.ts +14 -0
  59. package/src/types/nprogress.d.ts +8 -0
  60. package/src/utils/auth-token.ts +241 -0
  61. package/src/utils/code-highlight.ts +165 -0
  62. package/src/utils/copy.ts +36 -0
  63. package/src/utils/query.ts +27 -0
  64. package/src/utils/request.ts +238 -0
  65. package/src/utils/rmb-forecast.ts +84 -0
  66. package/src/utils/toast.ts +61 -0
  67. package/src/views/article-detail/index.vue +289 -0
  68. package/src/views/article-edit/index.vue +600 -0
  69. package/src/views/articles/index.vue +293 -0
  70. package/src/views/clearing-detail/index.vue +26 -0
  71. package/src/views/dashboard/index.vue +18 -0
  72. package/src/views/foreign-position/index.vue +26 -0
  73. package/src/views/home/index.vue +213 -0
  74. package/src/views/login/index.vue +275 -0
  75. package/src/views/mine/index.vue +147 -0
  76. package/src/views/net-debit/index.vue +26 -0
  77. package/src/views/opening-reserve-estimate/index.vue +28 -0
  78. package/src/views/pbc-position/index.vue +357 -0
  79. package/src/views/position-estimate/index.vue +67 -0
  80. package/src/views/position-estimate-report/index.vue +28 -0
  81. package/src/views/rmb-position/index.vue +1013 -0
  82. package/src/views/rmb-position-create/index.vue +221 -0
  83. package/src/views/rmb-position-detail/index.vue +355 -0
  84. package/src/views/settings/index.vue +67 -0
  85. package/src/views/warning/index.vue +26 -0
  86. package/tsconfig.app.json +18 -0
  87. package/tsconfig.json +11 -0
  88. package/tsconfig.node.json +28 -0
  89. package/vite.config.ts +100 -0
@@ -0,0 +1,600 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ createArticle,
4
+ extractTextFromHtml,
5
+ getArticleDetail,
6
+ updateArticle,
7
+ } from '@/api/announcement'
8
+ import { showAppToast } from '@/utils/toast'
9
+
10
+ defineOptions({ name: 'ArticleEditPage' })
11
+
12
+ const route = useRoute()
13
+ const router = useRouter()
14
+
15
+ const editorRef = ref<HTMLElement>()
16
+ const savedRange = ref<Range | null>(null)
17
+ const title = ref('')
18
+ const contentHtml = ref('')
19
+ const detailLoading = ref(false)
20
+ const saving = ref(false)
21
+ const showLinkPopup = ref(false)
22
+ const showCodePopup = ref(false)
23
+ const linkForm = reactive({
24
+ text: '',
25
+ url: '',
26
+ })
27
+ const codeForm = reactive({
28
+ language: 'javascript',
29
+ code: '',
30
+ })
31
+
32
+ const articleId = computed(() => String(route.params.id || ''))
33
+ const isEditing = computed(() => Boolean(articleId.value))
34
+ const pageTitle = computed(() => (isEditing.value ? '编辑公告' : '发布公告'))
35
+
36
+ onMounted(async () => {
37
+ if (!isEditing.value) {
38
+ setEditorHtml('')
39
+ return
40
+ }
41
+
42
+ detailLoading.value = true
43
+
44
+ try {
45
+ const article = await getArticleDetail(articleId.value)
46
+ title.value = article.title
47
+ setEditorHtml(article.content)
48
+ } catch (error) {
49
+ console.error('[公告] 编辑详情加载失败', error)
50
+ showAppToast('公告加载失败')
51
+ router.replace('/articles')
52
+ } finally {
53
+ detailLoading.value = false
54
+ }
55
+ })
56
+
57
+ function setEditorHtml(value: string) {
58
+ contentHtml.value = value
59
+ nextTick(() => {
60
+ if (editorRef.value) {
61
+ editorRef.value.innerHTML = value
62
+ }
63
+ })
64
+ }
65
+
66
+ function syncEditor() {
67
+ contentHtml.value = editorRef.value?.innerHTML || ''
68
+ }
69
+
70
+ function rememberSelection() {
71
+ const selection = window.getSelection()
72
+
73
+ if (!selection || !selection.rangeCount || !editorRef.value) {
74
+ return
75
+ }
76
+
77
+ const anchorNode = selection.anchorNode
78
+
79
+ if (anchorNode && editorRef.value.contains(anchorNode)) {
80
+ savedRange.value = selection.getRangeAt(0).cloneRange()
81
+ }
82
+ }
83
+
84
+ function restoreSelection() {
85
+ if (!savedRange.value) {
86
+ return
87
+ }
88
+
89
+ const selection = window.getSelection()
90
+
91
+ if (!selection) {
92
+ return
93
+ }
94
+
95
+ selection.removeAllRanges()
96
+ selection.addRange(savedRange.value)
97
+ }
98
+
99
+ function focusEditor() {
100
+ editorRef.value?.focus()
101
+ restoreSelection()
102
+ }
103
+
104
+ function runCommand(command: string, value?: string) {
105
+ focusEditor()
106
+ document.execCommand(command, false, value)
107
+ syncEditor()
108
+ rememberSelection()
109
+ }
110
+
111
+ function setBlock(tag: 'P' | 'H2' | 'H3' | 'BLOCKQUOTE') {
112
+ runCommand('formatBlock', tag)
113
+ }
114
+
115
+ function clearFormat() {
116
+ runCommand('removeFormat')
117
+ setBlock('P')
118
+ }
119
+
120
+ function openLinkPopup() {
121
+ rememberSelection()
122
+ linkForm.text = savedRange.value?.toString().trim() || ''
123
+ linkForm.url = ''
124
+ showLinkPopup.value = true
125
+ }
126
+
127
+ function insertLink() {
128
+ const url = normalizeUrl(linkForm.url)
129
+
130
+ if (!url) {
131
+ showAppToast('请输入链接地址')
132
+ return
133
+ }
134
+
135
+ const text = linkForm.text.trim() || url
136
+ focusEditor()
137
+ document.execCommand(
138
+ 'insertHTML',
139
+ false,
140
+ `<a href="${escapeAttribute(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(text)}</a>`,
141
+ )
142
+ syncEditor()
143
+ rememberSelection()
144
+ showLinkPopup.value = false
145
+ }
146
+
147
+ function openCodePopup() {
148
+ rememberSelection()
149
+ codeForm.language = 'javascript'
150
+ codeForm.code = ''
151
+ showCodePopup.value = true
152
+ }
153
+
154
+ function insertCodeBlock() {
155
+ const code = codeForm.code.trimEnd()
156
+
157
+ if (!code.trim()) {
158
+ showAppToast('请输入代码内容')
159
+ return
160
+ }
161
+
162
+ const language = normalizeCodeLanguage(codeForm.language)
163
+ const languageClass = language ? `language-${language}` : 'language-plaintext'
164
+ const languageAttr = language || 'plaintext'
165
+
166
+ focusEditor()
167
+ document.execCommand(
168
+ 'insertHTML',
169
+ false,
170
+ `<pre><code class="${languageClass}" data-language="${escapeAttribute(languageAttr)}">${escapeHtml(code)}</code></pre><p><br></p>`,
171
+ )
172
+ syncEditor()
173
+ rememberSelection()
174
+ showCodePopup.value = false
175
+ }
176
+
177
+ function handleCancel() {
178
+ if (isEditing.value) {
179
+ router.replace(`/articles/${articleId.value}`)
180
+ return
181
+ }
182
+
183
+ router.replace('/articles')
184
+ }
185
+
186
+ async function handleSave() {
187
+ syncEditor()
188
+
189
+ const text = extractTextFromHtml(contentHtml.value)
190
+
191
+ if (!title.value.trim()) {
192
+ showAppToast('请输入公告标题')
193
+ return
194
+ }
195
+
196
+ if (!text) {
197
+ showAppToast('请输入公告正文')
198
+ return
199
+ }
200
+
201
+ saving.value = true
202
+
203
+ const payload = {
204
+ title: title.value,
205
+ content: contentHtml.value,
206
+ }
207
+
208
+ try {
209
+ const article = isEditing.value
210
+ ? await updateArticle(articleId.value, payload)
211
+ : await createArticle(payload)
212
+
213
+ showAppToast(isEditing.value ? '已保存' : '已发布')
214
+ router.replace(article.id ? `/articles/${article.id}` : '/articles')
215
+ } catch (error) {
216
+ console.error('[公告] 保存失败', error)
217
+ } finally {
218
+ saving.value = false
219
+ }
220
+ }
221
+
222
+ function normalizeUrl(value: string) {
223
+ const url = value.trim()
224
+
225
+ if (!url) {
226
+ return ''
227
+ }
228
+
229
+ if (/^(https?:\/\/|mailto:|tel:|#)/i.test(url)) {
230
+ return url
231
+ }
232
+
233
+ return `https://${url}`
234
+ }
235
+
236
+ function escapeHtml(value: string) {
237
+ const div = document.createElement('div')
238
+ div.textContent = value
239
+
240
+ return div.innerHTML
241
+ }
242
+
243
+ function escapeAttribute(value: string) {
244
+ return escapeHtml(value).replace(/"/g, '&quot;')
245
+ }
246
+
247
+ function normalizeCodeLanguage(value: string) {
248
+ return value
249
+ .trim()
250
+ .toLowerCase()
251
+ .replace(/[^a-z0-9_-]/g, '')
252
+ .slice(0, 24)
253
+ }
254
+ </script>
255
+
256
+ <template>
257
+ <main class="article-edit-page">
258
+ <section class="form-panel">
259
+ <van-field
260
+ v-model="title"
261
+ label="标题"
262
+ placeholder="请输入公告标题"
263
+ clearable
264
+ maxlength="40"
265
+ show-word-limit
266
+ />
267
+ </section>
268
+
269
+ <section v-if="detailLoading" class="state-section">
270
+ <van-loading vertical>加载中...</van-loading>
271
+ </section>
272
+
273
+ <section v-else class="editor-panel">
274
+ <div class="editor-title">
275
+ <h2>正文</h2>
276
+ <span>富文本编辑</span>
277
+ </div>
278
+
279
+ <div class="editor-toolbar" aria-label="富文本工具栏">
280
+ <button type="button" title="正文" @click="setBlock('P')">P</button>
281
+ <button type="button" title="二级标题" @click="setBlock('H2')">H2</button>
282
+ <button type="button" title="三级标题" @click="setBlock('H3')">H3</button>
283
+ <button type="button" title="加粗" @click="runCommand('bold')"><strong>B</strong></button>
284
+ <button type="button" title="斜体" @click="runCommand('italic')"><em>I</em></button>
285
+ <button type="button" title="下划线" @click="runCommand('underline')"><u>U</u></button>
286
+ <button type="button" title="引用" @click="setBlock('BLOCKQUOTE')">
287
+ <span aria-hidden="true">“</span>
288
+ </button>
289
+ <button type="button" title="无序列表" @click="runCommand('insertUnorderedList')">
290
+ <van-icon name="orders-o" />
291
+ </button>
292
+ <button type="button" title="有序列表" @click="runCommand('insertOrderedList')">1.</button>
293
+ <button type="button" title="插入链接" @click="openLinkPopup">
294
+ <van-icon name="link-o" />
295
+ </button>
296
+ <button type="button" title="插入代码块" @click="openCodePopup">
297
+ <van-icon name="font-o" />
298
+ </button>
299
+ <button type="button" title="清除格式" @click="clearFormat">
300
+ <van-icon name="close" />
301
+ </button>
302
+ </div>
303
+
304
+ <div
305
+ ref="editorRef"
306
+ class="rich-editor"
307
+ contenteditable="true"
308
+ data-placeholder="请输入公告正文"
309
+ @input="syncEditor"
310
+ @mouseup="rememberSelection"
311
+ @keyup="rememberSelection"
312
+ @blur="rememberSelection"
313
+ ></div>
314
+ </section>
315
+
316
+ <footer class="edit-actions">
317
+ <van-button block plain type="default" @click="handleCancel">取消</van-button>
318
+ <van-button
319
+ block
320
+ type="danger"
321
+ :loading="saving"
322
+ :disabled="detailLoading"
323
+ @click="handleSave"
324
+ >
325
+ {{ isEditing ? '保存公告' : '发布公告' }}
326
+ </van-button>
327
+ </footer>
328
+
329
+ <van-popup v-model:show="showLinkPopup" round position="bottom">
330
+ <div class="link-popup">
331
+ <header>
332
+ <h2>插入链接</h2>
333
+ <button type="button" aria-label="关闭" @click="showLinkPopup = false">
334
+ <van-icon name="cross" />
335
+ </button>
336
+ </header>
337
+ <van-cell-group inset>
338
+ <van-field v-model="linkForm.text" label="文字" placeholder="链接显示文字" />
339
+ <van-field v-model="linkForm.url" label="链接" placeholder="https://example.com" />
340
+ </van-cell-group>
341
+ <div class="link-actions">
342
+ <van-button block plain @click="showLinkPopup = false">取消</van-button>
343
+ <van-button block type="danger" @click="insertLink">插入</van-button>
344
+ </div>
345
+ </div>
346
+ </van-popup>
347
+
348
+ <van-popup v-model:show="showCodePopup" round position="bottom">
349
+ <div class="code-popup">
350
+ <header>
351
+ <h2>插入代码块</h2>
352
+ <button type="button" aria-label="关闭" @click="showCodePopup = false">
353
+ <van-icon name="cross" />
354
+ </button>
355
+ </header>
356
+ <van-cell-group inset>
357
+ <van-field v-model="codeForm.language" label="语言" placeholder="javascript" />
358
+ <van-field
359
+ v-model="codeForm.code"
360
+ class="code-field"
361
+ label="代码"
362
+ type="textarea"
363
+ rows="8"
364
+ autosize
365
+ placeholder="请输入代码"
366
+ />
367
+ </van-cell-group>
368
+ <div class="code-actions">
369
+ <van-button block plain @click="showCodePopup = false">取消</van-button>
370
+ <van-button block type="danger" @click="insertCodeBlock">插入</van-button>
371
+ </div>
372
+ </div>
373
+ </van-popup>
374
+ </main>
375
+ </template>
376
+
377
+ <style scoped>
378
+ .article-edit-page {
379
+ width: 100%;
380
+ max-width: 750px;
381
+ min-height: var(--app-page-min-height, 100vh);
382
+ margin: 0 auto;
383
+ padding: 0 11px 24px;
384
+ color: #171b20;
385
+ background: #f7f8fa;
386
+ box-sizing: border-box;
387
+ }
388
+
389
+ .form-panel,
390
+ .editor-panel {
391
+ overflow: hidden;
392
+ border: 1px solid #edf0f4;
393
+ border-radius: 8px;
394
+ background: #ffffff;
395
+ box-shadow: 0 5px 16px rgba(23, 27, 32, 0.04);
396
+ }
397
+
398
+ .form-panel {
399
+ margin-top: 12px;
400
+ }
401
+
402
+ .form-panel :deep(.van-cell) {
403
+ --van-cell-vertical-padding: 14px;
404
+ --van-field-label-width: 44px;
405
+ }
406
+
407
+ .editor-panel {
408
+ margin-top: 12px;
409
+ padding: 14px;
410
+ }
411
+
412
+ .state-section {
413
+ display: flex;
414
+ justify-content: center;
415
+ padding: 110px 0;
416
+ }
417
+
418
+ .editor-title {
419
+ display: flex;
420
+ align-items: center;
421
+ justify-content: space-between;
422
+ gap: 10px;
423
+ margin-bottom: 12px;
424
+ }
425
+
426
+ .editor-title h2 {
427
+ margin: 0;
428
+ color: #171b20;
429
+ font-size: 16px;
430
+ font-weight: 750;
431
+ line-height: 1.2;
432
+ }
433
+
434
+ .editor-title span {
435
+ color: #8a929f;
436
+ font-size: 12px;
437
+ line-height: 1.2;
438
+ }
439
+
440
+ .editor-toolbar {
441
+ display: flex;
442
+ flex-wrap: wrap;
443
+ gap: 7px;
444
+ padding-bottom: 12px;
445
+ border-bottom: 1px solid #f0f1f3;
446
+ }
447
+
448
+ .editor-toolbar button {
449
+ display: inline-flex;
450
+ align-items: center;
451
+ justify-content: center;
452
+ min-width: 32px;
453
+ height: 32px;
454
+ padding: 0 8px;
455
+ border: 1px solid #e8ebf0;
456
+ border-radius: 6px;
457
+ color: #252a31;
458
+ font-size: 13px;
459
+ font-weight: 700;
460
+ line-height: 1;
461
+ background: #fbfcfd;
462
+ cursor: pointer;
463
+ }
464
+
465
+ .editor-toolbar button:active {
466
+ color: #f22f3d;
467
+ border-color: #ffd1d6;
468
+ background: #fff4f5;
469
+ }
470
+
471
+ .rich-editor {
472
+ height: clamp(300px, 52vh, 430px);
473
+ max-height: clamp(300px, 52vh, 430px);
474
+ overflow-y: auto;
475
+ overscroll-behavior: contain;
476
+ padding: 14px 2px 18px 0;
477
+ color: #252a31;
478
+ font-size: 16px;
479
+ line-height: 1.75;
480
+ outline: none;
481
+ word-break: break-word;
482
+ box-sizing: border-box;
483
+ }
484
+
485
+ .rich-editor:empty::before {
486
+ content: attr(data-placeholder);
487
+ color: #b6bdc8;
488
+ }
489
+
490
+ .rich-editor :deep(h2),
491
+ .rich-editor :deep(h3),
492
+ .rich-editor :deep(p),
493
+ .rich-editor :deep(blockquote),
494
+ .rich-editor :deep(ul),
495
+ .rich-editor :deep(ol) {
496
+ margin: 0 0 12px;
497
+ }
498
+
499
+ .rich-editor :deep(h2) {
500
+ font-size: 20px;
501
+ line-height: 1.4;
502
+ }
503
+
504
+ .rich-editor :deep(h3) {
505
+ font-size: 18px;
506
+ line-height: 1.4;
507
+ }
508
+
509
+ .rich-editor :deep(blockquote) {
510
+ padding: 10px 12px;
511
+ border-left: 4px solid #d8dde6;
512
+ border-radius: 6px;
513
+ color: #596273;
514
+ background: #f6f7f9;
515
+ }
516
+
517
+ .rich-editor :deep(pre) {
518
+ overflow-x: auto;
519
+ margin: 0 0 12px;
520
+ padding: 12px;
521
+ border: 1px solid #e4e8ef;
522
+ border-radius: 7px;
523
+ background: #101820;
524
+ }
525
+
526
+ .rich-editor :deep(code) {
527
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
528
+ font-size: 13px;
529
+ }
530
+
531
+ .rich-editor :deep(pre code) {
532
+ display: block;
533
+ color: #eef3f7;
534
+ line-height: 1.62;
535
+ white-space: pre;
536
+ }
537
+
538
+ .edit-actions {
539
+ display: grid;
540
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
541
+ gap: 10px;
542
+ margin-top: 16px;
543
+ }
544
+
545
+ .edit-actions :deep(.van-button) {
546
+ height: 42px;
547
+ font-weight: 600;
548
+ }
549
+
550
+ .link-popup,
551
+ .code-popup {
552
+ padding: 18px 0 max(18px, env(safe-area-inset-bottom));
553
+ background: #ffffff;
554
+ }
555
+
556
+ .link-popup header,
557
+ .code-popup header {
558
+ display: flex;
559
+ align-items: center;
560
+ justify-content: space-between;
561
+ padding: 0 16px 14px;
562
+ }
563
+
564
+ .link-popup h2,
565
+ .code-popup h2 {
566
+ margin: 0;
567
+ color: #171b20;
568
+ font-size: 18px;
569
+ font-weight: 750;
570
+ line-height: 1.2;
571
+ }
572
+
573
+ .link-popup header button,
574
+ .code-popup header button {
575
+ display: flex;
576
+ align-items: center;
577
+ justify-content: center;
578
+ width: 32px;
579
+ height: 32px;
580
+ border: 0;
581
+ border-radius: 50%;
582
+ color: #6b7280;
583
+ font-size: 18px;
584
+ background: #f5f6f8;
585
+ }
586
+
587
+ .link-actions,
588
+ .code-actions {
589
+ display: grid;
590
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
591
+ gap: 10px;
592
+ padding: 16px 16px 0;
593
+ }
594
+
595
+ .code-field :deep(textarea) {
596
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
597
+ font-size: 13px;
598
+ line-height: 1.55;
599
+ }
600
+ </style>