poi-plugin-koushu-rate 2.0.17 → 2.0.18

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.
@@ -0,0 +1,175 @@
1
+ {
2
+ "source": "用户装备拆解指南 2026-08-25(合并稀有/次级稀有)",
3
+ "rare": [
4
+ "10.5cm連装砲",
5
+ "10cm連装高角砲(砲架)",
6
+ "12.7cm単装高角砲(後期型)",
7
+ "12.7cm連装高角砲(後期型)",
8
+ "12.7cm連装砲C型改二",
9
+ "12.7cm連装砲D型改二",
10
+ "120mm/50 連装砲",
11
+ "12cm30連装噴進砲",
12
+ "12cm単装高角砲",
13
+ "12cm単装高角砲E型",
14
+ "13.8cm連装砲",
15
+ "130mm B-13連装砲",
16
+ "13号対空電探",
17
+ "13号対空電探改",
18
+ "13号対空電探改(後期型)",
19
+ "14cm連装砲改",
20
+ "15.2cm連装砲改",
21
+ "15.2cm三連装砲",
22
+ "15cm連装副砲",
23
+ "16inch Mk.I連装砲",
24
+ "16inch Mk.I三連装砲",
25
+ "16inch Mk.V連装砲",
26
+ "16inch三連装砲 Mk.6",
27
+ "16inch三連装砲 Mk.6 mod.2",
28
+ "16inch三連装砲 Mk.7",
29
+ "20.3cm(3号)連装砲",
30
+ "203mm/53 連装砲",
31
+ "20連装7inch UP Rocket Launchers",
32
+ "22号対水上電探改四",
33
+ "25mm三連装機銃",
34
+ "25mm三連装機銃 集中配備",
35
+ "2cm 四連装FlaK 38",
36
+ "3.7cm FlaK M42",
37
+ "30.5cm三連装砲改",
38
+ "32号対水上電探",
39
+ "33号対水上電探",
40
+ "35.6cm連装砲(ダズル迷彩)",
41
+ "35.6cm連装砲改",
42
+ "38.1cm Mk.I連装砲",
43
+ "381mm/50 三連装砲",
44
+ "381mm/50 三連装砲改",
45
+ "38cm連装砲",
46
+ "38cm連装砲改",
47
+ "38cm四連装砲",
48
+ "38cm四連装砲改",
49
+ "42号対空電探",
50
+ "46cm三連装砲",
51
+ "46cm三連装砲改",
52
+ "533mm 三連装魚雷",
53
+ "533mm五連装魚雷(初期型)",
54
+ "53cm連装魚雷",
55
+ "5inch 単装高角砲群",
56
+ "5inch単装砲 Mk.30",
57
+ "5inch連装砲 Mk.28 mod.2",
58
+ "61cm三連装(酸素)魚雷",
59
+ "61cm五連装(酸素)魚雷",
60
+ "6inch Mk.XXIII三連装砲",
61
+ "6inch三連装速射砲 Mk.16 mod.2",
62
+ "8cm高角砲",
63
+ "8inch三連装砲 Mk.9",
64
+ "90mm単装高角砲",
65
+ "94式高射装置",
66
+ "彩雲",
67
+ "大発動艇(八九式中戦車&陸戦隊)",
68
+ "二式12cm迫撃砲改",
69
+ "二式爆雷",
70
+ "二式大艇",
71
+ "二式複戦 屠龍",
72
+ "二式複戦 屠龍 丙型",
73
+ "二式艦上偵察機",
74
+ "発煙装置(煙幕)",
75
+ "彗星(六〇一空)",
76
+ "艦艇修理施設",
77
+ "艦載型 四式20cm対地噴進砲",
78
+ "九八式水上偵察機(夜偵)",
79
+ "九九式艦爆(江草隊)",
80
+ "九九式艦爆(熟練)",
81
+ "九九式艦爆二二型",
82
+ "九六式艦戦改",
83
+ "九六式陸攻",
84
+ "九七式艦攻(九三一空)",
85
+ "九七式艦攻(熟練)",
86
+ "九七式艦攻(友永隊)",
87
+ "九七式艦攻改 試製三号戊型(空六号電探改装備機)",
88
+ "九一式徹甲弾",
89
+ "烈風 一一型",
90
+ "零式艦戦21型(熟練)",
91
+ "零式艦戦32型(熟練)",
92
+ "零式艦戦62型(爆戦)",
93
+ "零式艦戦63型(爆戦)",
94
+ "零式水上偵察機11型乙",
95
+ "零式小型水上機",
96
+ "零戦52型丙(六〇一空)",
97
+ "流星(六〇一空)",
98
+ "流星改",
99
+ "流星改(熟練)",
100
+ "毘式40mm連装機銃",
101
+ "秋刀魚の缶詰",
102
+ "瑞雲12型",
103
+ "瑞雲12型(六三四空)",
104
+ "三式爆雷投射機 集中配備",
105
+ "三式弾",
106
+ "試製35.6cm三連装砲",
107
+ "試製41cm三連装砲",
108
+ "試製46cm連装砲",
109
+ "試製51cm連装砲",
110
+ "試製烈風 後期型",
111
+ "試製晴嵐",
112
+ "試製FaT仕様九五式酸素魚雷改",
113
+ "熟練甲板要員",
114
+ "熟練見張員",
115
+ "四式水中聴音機",
116
+ "天山(六〇一空)",
117
+ "天山一二型甲",
118
+ "通信装置&要員",
119
+ "新型高温高圧缶",
120
+ "洋上補給",
121
+ "一式陸攻",
122
+ "一式陸攻 二二型甲",
123
+ "一式戦 隼II型",
124
+ "増設バルジ(大型艦)",
125
+ "増設バルジ(中型艦)",
126
+ "戦闘糧食",
127
+ "戦闘糧食(特別なおにぎり)",
128
+ "照明弾",
129
+ "紫電二一型 紫電改",
130
+ "紫電改二",
131
+ "紫電一一型",
132
+ "Ar196改",
133
+ "Bf109T改",
134
+ "Bofors 40mm四連装機関砲",
135
+ "Bofors15.2cm連装砲 Model1930",
136
+ "F4F-3",
137
+ "F4F-4",
138
+ "F4U-1D",
139
+ "F6F-3",
140
+ "FM-2",
141
+ "Fulmar",
142
+ "FuMO25 レーダー",
143
+ "Fw190T改",
144
+ "GFCS Mk.37",
145
+ "Ju87C改",
146
+ "OS2U",
147
+ "OTO 152mm三連装速射砲",
148
+ "QF 2ポンド8連装ポンポン砲",
149
+ "QF 4.7inch砲 Mk.XII改",
150
+ "Re.2001 G改",
151
+ "Re.2001 OR改",
152
+ "Ro.44水上戦闘機",
153
+ "S9 Osprey",
154
+ "SB2C-3",
155
+ "SBD",
156
+ "SG レーダー(初期型)",
157
+ "SKC34 20.3cm連装砲",
158
+ "Skua",
159
+ "SKレーダー",
160
+ "SM.79",
161
+ "SOC Seagull",
162
+ "Spitfire Mk.I",
163
+ "Spitfire Mk.V",
164
+ "Swordfish",
165
+ "Swordfish Mk.II(熟練)",
166
+ "Swordfish Mk.II改(水偵型)",
167
+ "TBD",
168
+ "TBF",
169
+ "Type124 ASDIC",
170
+ "Type144/147 ASDIC",
171
+ "オ号観測機改",
172
+ "プリエーゼ式水中防御隔壁"
173
+ ],
174
+ "secondary": []
175
+ }
package/index.js CHANGED
@@ -1,10 +1,38 @@
1
1
  const React = require("react");
2
2
  const fs = require("fs");
3
- const path = require("path");
4
- const { WindowEnv } = require("views/components/etc/window-env");
5
- const { SlotitemIcon, MaterialIcon } = require("views/components/etc/icon");
6
-
7
- const LEVEL_ONE_ROWS = [
3
+ const path = require("path");
4
+ const { WindowEnv } = require("views/components/etc/window-env");
5
+ const { SlotitemIcon, MaterialIcon } = require("views/components/etc/icon");
6
+
7
+ function readPluginVersion() {
8
+ try {
9
+ const info = require("./package.json")
10
+ return String((info && info.version) || "0.0.0")
11
+ } catch (_) {
12
+ return "0.0.0"
13
+ }
14
+ }
15
+
16
+ const PLUGIN_VERSION = readPluginVersion()
17
+ const HELP_PAGE_REVISION = "1"
18
+ const HELP_VERSION = PLUGIN_VERSION + "." + HELP_PAGE_REVISION
19
+ const HELP_UPDATE_NOTES = {
20
+ "2.0.17": [
21
+ "新增使用说明页与更新提示。",
22
+ "新增稀有素材消耗提示。",
23
+ "素材计算新增排序与开发资材期望统计。",
24
+ "“我变强了!”新增完成度、库存合并与进化目标标记。",
25
+ ],
26
+ "2.0.18": [
27
+ "新增稀有素材消耗提示;",
28
+ "优化了装备进化在我变强了页中的显示逻辑,现在会和进化后的改修计划分开统计,以方便检查阶段性目标的完成情况;",
29
+ "优化了我变强了页中的目标/已有装备的显示,现在不会自动拆分已完成的装备单独置顶显示了;",
30
+ "在改修列表页新增了改修计划的显示(装备名称的下方)。",
31
+ ],
32
+ }
33
+ const CURRENT_HELP_UPDATE_NOTES = HELP_UPDATE_NOTES[PLUGIN_VERSION] || ["本次更新暂无详细说明。"]
34
+
35
+ const LEVEL_ONE_ROWS = [
8
36
  { range: "★0→1", p: 1.0 },
9
37
  { range: "★1→2", p: 1.0 },
10
38
  { range: "★2→3", p: 1.0 },
@@ -37,10 +65,33 @@ const PLAN_LEVELS_MAX = PLAN_LEVELS_6.concat([
37
65
  ]);
38
66
 
39
67
  const WEEKDAY_KEYS = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"];
40
- const WEEKDAY_LABELS = { monday: "周一", tuesday: "周二", wednesday: "周三", thursday: "周四", friday: "周五", saturday: "周六", sunday: "周日" };
41
- const ALL_DAYS_KEY = "all";
42
-
43
- function japanWeekdayKey() {
68
+ const WEEKDAY_LABELS = { monday: "周一", tuesday: "周二", wednesday: "周三", thursday: "周四", friday: "周五", saturday: "周六", sunday: "周日" };
69
+ const ALL_DAYS_KEY = "all";
70
+ const RARE_MATERIAL_KEYS = new Set([
71
+ "新型砲熕兵装資材",
72
+ "新型兵装資材",
73
+ "戦闘詳報",
74
+ "勲章",
75
+ "ネ式エンジン",
76
+ "熟練搭乗員",
77
+ "海外艦最新技術",
78
+ "新型航空兵装資材",
79
+ "工廠資源",
80
+ "新型噴進装備開発資材",
81
+ "潜水艦補給物資",
82
+ ]);
83
+
84
+ let RARE_EQUIP_NAMES = null
85
+ let SECONDARY_EQUIP_NAMES = null
86
+ function getRareEquipSets() {
87
+ if (RARE_EQUIP_NAMES) return { rare: RARE_EQUIP_NAMES, secondary: SECONDARY_EQUIP_NAMES }
88
+ const data = loadJson("equipment_rarity.json") || {}
89
+ RARE_EQUIP_NAMES = new Set(data.rare || [])
90
+ SECONDARY_EQUIP_NAMES = new Set(data.secondary || [])
91
+ return { rare: RARE_EQUIP_NAMES, secondary: SECONDARY_EQUIP_NAMES }
92
+ }
93
+
94
+ function japanWeekdayKey() {
44
95
  const japanNow = new Date(Date.now() + 9 * 60 * 60 * 1000)
45
96
  return ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][japanNow.getUTCDay()]
46
97
  }
@@ -158,17 +209,36 @@ const CSS = `
158
209
  padding: 12px 14px 20px;
159
210
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
160
211
  }
161
- .kr2-title-row {
162
- display: flex;
163
- flex-direction: column;
164
- align-items: stretch;
165
- justify-content: flex-start;
166
- gap: 12px;
167
- }
168
- .kr2-title {
169
- font-size: 18px;
170
- font-weight: 800;
171
- }
212
+ .kr2-title-row {
213
+ display: flex;
214
+ flex-direction: column;
215
+ align-items: stretch;
216
+ justify-content: flex-start;
217
+ gap: 12px;
218
+ }
219
+ .kr2-title-line {
220
+ display: flex;
221
+ align-items: center;
222
+ gap: 10px;
223
+ flex-wrap: wrap;
224
+ }
225
+ .kr2-title {
226
+ font-size: 18px;
227
+ font-weight: 800;
228
+ }
229
+ .kr2-help-btn {
230
+ padding: 4px 10px;
231
+ border: 1px solid #5f6b7c;
232
+ border-radius: 4px;
233
+ background: #2f343c;
234
+ color: #8abbff;
235
+ font-size: 12px;
236
+ cursor: pointer;
237
+ }
238
+ .kr2-help-btn:hover {
239
+ background: #383e47;
240
+ color: #a8c8ff;
241
+ }
172
242
  .kr2-title-note {
173
243
  margin-top: 2px;
174
244
  font-size: 18px;
@@ -570,17 +640,41 @@ const CSS = `
570
640
  color: #d6e4f5;
571
641
  }
572
642
 
573
- .kr2-strong {
574
- margin-top: 12px;
575
- flex: 1;
643
+ .kr2-strong {
644
+ margin-top: 12px;
645
+ flex: 1;
576
646
  min-height: 60vh;
577
647
  overflow-x: hidden;
578
648
  overflow-y: auto;
579
649
  }
580
- .kr2-strong-table {
581
- border-collapse: separate;
582
- border-spacing: 0;
583
- }
650
+ .kr2-table.kr2-strong-table {
651
+ border-collapse: separate;
652
+ border-spacing: 0;
653
+ table-layout: fixed;
654
+ width: 100%;
655
+ }
656
+ .kr2-strong-table th:nth-child(1),
657
+ .kr2-strong-table td:nth-child(1) {
658
+ width: 100px;
659
+ }
660
+ .kr2-strong-table th:nth-child(2),
661
+ .kr2-strong-table td:nth-child(2) {
662
+ width: 32%;
663
+ }
664
+ .kr2-strong-table th:nth-child(4),
665
+ .kr2-strong-table td:nth-child(4) {
666
+ width: 48px;
667
+ padding-left: 2px !important;
668
+ padding-right: 2px !important;
669
+ white-space: nowrap;
670
+ }
671
+ .kr2-strong-table th:nth-child(5),
672
+ .kr2-strong-table td:nth-child(5) {
673
+ width: 64px;
674
+ padding-left: 2px !important;
675
+ padding-right: 2px !important;
676
+ white-space: nowrap;
677
+ }
584
678
  .kr2-strong-cat-th {
585
679
  position: relative;
586
680
  cursor: pointer;
@@ -631,9 +725,10 @@ const CSS = `
631
725
  .kr2-strong-table tbody tr:last-child td {
632
726
  border-bottom: 0;
633
727
  }
634
- .kr2-strong-equip {
635
- text-align: left !important;
636
- white-space: normal;
728
+ .kr2-strong-equip {
729
+ text-align: center !important;
730
+ white-space: normal;
731
+ font-weight: 700;
637
732
  }
638
733
  .kr2-strong-level {
639
734
  display: inline-flex;
@@ -649,10 +744,20 @@ const CSS = `
649
744
  color: #ffffff;
650
745
  font-weight: 700;
651
746
  }
652
- .kr2-strong-stock {
653
- white-space: normal;
654
- }
655
- .kr2-strong-stock-name {
747
+ .kr2-strong-stock {
748
+ white-space: normal;
749
+ text-align: center !important;
750
+ }
751
+ .kr2-strong-stock-th {
752
+ text-align: center !important;
753
+ }
754
+ .kr2-strong-stock-line {
755
+ display: block;
756
+ }
757
+ .kr2-strong-stock-line + .kr2-strong-stock-line {
758
+ margin-top: 2px;
759
+ }
760
+ .kr2-strong-stock-name {
656
761
  color: #ffffff;
657
762
  font-weight: 700;
658
763
  }
@@ -687,10 +792,28 @@ const CSS = `
687
792
  .kr2-strong-owned.kr2-strong-stock-short {
688
793
  color: #ff6b6b;
689
794
  }
690
- .kr2-strong-owned.kr2-strong-stock-clear {
691
- color: #4fc3f7;
692
- }
693
- .kr2-strong-clear-row td {
795
+ .kr2-strong-owned.kr2-strong-stock-clear {
796
+ color: #4fc3f7;
797
+ }
798
+ .kr2-strong-owned-line {
799
+ display: block;
800
+ }
801
+ .kr2-strong-owned-line + .kr2-strong-owned-line {
802
+ margin-top: 2px;
803
+ }
804
+ .kr2-strong-owned-line-clear {
805
+ color: #4fc3f7;
806
+ }
807
+ .kr2-strong-owned-line-short {
808
+ color: #ff6b6b;
809
+ }
810
+ .kr2-strong-owned-line-missing {
811
+ color: #abb3bf;
812
+ }
813
+ .kr2-strong-completion-done {
814
+ color: #4fc3f7;
815
+ }
816
+ .kr2-strong-clear-row td {
694
817
  color: #4fc3f7;
695
818
  }
696
819
  .kr2-strong-clear-row .kr2-strong-target,
@@ -866,12 +989,18 @@ const CSS = `
866
989
  color: #abb3bf;
867
990
  cursor: default;
868
991
  }
869
- .kr2-row-not-improveable .kr2-name,
870
- .kr2-row-not-improveable .kr2-secretary,
871
- .kr2-row-not-improveable .kr2-meta {
872
- color: #abb3bf;
873
- }
874
- .kr2-reco-empty {
992
+ .kr2-row-not-improveable .kr2-name,
993
+ .kr2-row-not-improveable .kr2-secretary,
994
+ .kr2-row-not-improveable .kr2-meta {
995
+ color: #abb3bf;
996
+ }
997
+ .kr2-row-clear {
998
+ background: rgba(79, 195, 247, 0.14);
999
+ }
1000
+ .kr2-row-clear:hover {
1001
+ background: rgba(79, 195, 247, 0.2);
1002
+ }
1003
+ .kr2-reco-empty {
875
1004
  min-height: 34px;
876
1005
  }
877
1006
  .kr2-not-improveable-group {
@@ -961,14 +1090,35 @@ const CSS = `
961
1090
  font-size: 9px;
962
1091
  line-height: 1.2;
963
1092
  }
964
- .kr2-name {
965
- flex: 1 1 0;
966
- min-width: 0;
967
- font-weight: 600;
968
- white-space: nowrap;
969
- overflow: hidden;
970
- text-overflow: ellipsis;
971
- }
1093
+ .kr2-name-cell {
1094
+ flex: 1 1 0;
1095
+ min-width: 0;
1096
+ display: flex;
1097
+ flex-direction: column;
1098
+ justify-content: center;
1099
+ gap: 1px;
1100
+ }
1101
+ .kr2-name {
1102
+ font-weight: 600;
1103
+ white-space: nowrap;
1104
+ overflow: hidden;
1105
+ text-overflow: ellipsis;
1106
+ }
1107
+ .kr2-name-completion {
1108
+ font-size: 11px;
1109
+ line-height: 1.2;
1110
+ white-space: nowrap;
1111
+ font-weight: 400;
1112
+ }
1113
+ .kr2-name-completion-incomplete {
1114
+ color: #ff6b6b;
1115
+ }
1116
+ .kr2-name-completion-done-active {
1117
+ color: #4fc3f7;
1118
+ }
1119
+ .kr2-name-completion-clear {
1120
+ color: #4fc3f7;
1121
+ }
972
1122
  .kr2-secretary {
973
1123
  flex: 1 1 0;
974
1124
  min-width: 0;
@@ -1117,7 +1267,17 @@ const CSS = `
1117
1267
  align-items: flex-end;
1118
1268
  gap: 6px;
1119
1269
  }
1120
- .kr2-material-list {
1270
+ .kr2-rare-warning {
1271
+ color: #ff6b6b;
1272
+ font-size: 11px;
1273
+ white-space: nowrap;
1274
+ }
1275
+ .kr2-rare-warning-secondary {
1276
+ color: #ffb3b3;
1277
+ font-size: 11px;
1278
+ white-space: nowrap;
1279
+ }
1280
+ .kr2-material-list {
1121
1281
  display: flex;
1122
1282
  flex-direction: column;
1123
1283
  align-items: flex-end;
@@ -1235,11 +1395,64 @@ const CSS = `
1235
1395
  font-size: 12px;
1236
1396
  margin-top: 2px;
1237
1397
  }
1238
- .kr2-modal-actions {
1239
- display: flex;
1240
- gap: 8px;
1241
- }
1242
- .kr2-dev-scroll {
1398
+ .kr2-modal-actions {
1399
+ display: flex;
1400
+ gap: 8px;
1401
+ }
1402
+ .kr2-modal-close {
1403
+ width: 30px;
1404
+ height: 30px;
1405
+ padding: 0;
1406
+ display: flex;
1407
+ align-items: center;
1408
+ justify-content: center;
1409
+ border: 1px solid #5f6b7c;
1410
+ border-radius: 4px;
1411
+ background: #2f343c;
1412
+ color: #f6f7f9;
1413
+ font-size: 18px;
1414
+ line-height: 1;
1415
+ cursor: pointer;
1416
+ }
1417
+ .kr2-modal-close:hover {
1418
+ background: #383e47;
1419
+ color: #fff;
1420
+ }
1421
+ .kr2-help-modal {
1422
+ width: 720px;
1423
+ }
1424
+ .kr2-help-body {
1425
+ flex: 1;
1426
+ min-height: 0;
1427
+ overflow-y: auto;
1428
+ padding: 14px 16px 18px;
1429
+ }
1430
+ .kr2-help-lead {
1431
+ margin-bottom: 10px;
1432
+ color: #8abbff;
1433
+ font-size: 14px;
1434
+ font-weight: 800;
1435
+ }
1436
+ .kr2-help-section + .kr2-help-section {
1437
+ margin-top: 14px;
1438
+ }
1439
+ .kr2-help-section-title {
1440
+ margin-bottom: 6px;
1441
+ color: #8abbff;
1442
+ font-size: 14px;
1443
+ font-weight: 800;
1444
+ }
1445
+ .kr2-help-list {
1446
+ margin: 0;
1447
+ padding-left: 20px;
1448
+ color: #d6d9de;
1449
+ font-size: 13px;
1450
+ line-height: 1.7;
1451
+ }
1452
+ .kr2-help-list li + li {
1453
+ margin-top: 2px;
1454
+ }
1455
+ .kr2-dev-scroll {
1243
1456
  flex: 1;
1244
1457
  min-height: 0;
1245
1458
  overflow-y: auto;
@@ -2365,7 +2578,20 @@ function buildRecommendation(normal, certain, p) {
2365
2578
  return { expected, text: "相同", tone: "same" }
2366
2579
  }
2367
2580
 
2368
- function MaterialDetail({ materials, onMaterialClick, inventoryByEquip, onInventoryClick, useItemCounts }) {
2581
+ function classifyRareMaterial(materials, kcDevData) {
2582
+ const sets = getRareEquipSets()
2583
+ let hasSecondary = false
2584
+ for (const item of materials || []) {
2585
+ if (!item) continue
2586
+ if (item.item_material_key != null && RARE_MATERIAL_KEYS.has(String(item.item_material_key))) return "rare"
2587
+ const name = String(item.item_name || "")
2588
+ if (sets.rare.has(name)) return "rare"
2589
+ if (sets.secondary.has(name)) hasSecondary = true
2590
+ }
2591
+ return hasSecondary ? "secondary" : null
2592
+ }
2593
+
2594
+ function MaterialDetail({ materials, onMaterialClick, inventoryByEquip, onInventoryClick, useItemCounts }) {
2369
2595
  const items = (materials || []).filter((item) => item && (item.item_name || item.item_material_key || item.item_equipment_id != null))
2370
2596
  if (items.length === 0) {
2371
2597
  return React.createElement("div", { className: "kr2-material-empty" }, "无消耗装备")
@@ -2877,7 +3103,143 @@ function PlanningPage({ rows, inventoryByEquip, useItemCounts, selection, onTogg
2877
3103
  )
2878
3104
  }
2879
3105
 
2880
- class StrongPage extends React.Component {
3106
+ function formatStrongLevel(level) {
3107
+ const n = Number(level)
3108
+ if (n >= 10) return "max"
3109
+ return "+" + String(n)
3110
+ }
3111
+
3112
+ function buildStrongEntries(rows, inventoryByEquip, selection) {
3113
+ const inventoryLoaded = inventoryByEquip && Object.keys(inventoryByEquip).length > 0
3114
+ const entries = []
3115
+ const demandMap = {}
3116
+ for (const row of rows || []) {
3117
+ const sel = (selection || {})[row.id] || { selected: true, target: "max", qty: 1 }
3118
+ const qty = Math.max(1, Math.min(99, Math.round(Number(sel.qty) || 1)))
3119
+ const evoUpgradeId = sel.evoUpgradeId || null
3120
+ const evoUpgrade = evoUpgradeId ? (row.upgrades || []).find((u) => String(u.upgrade_id) === String(evoUpgradeId)) : null
3121
+ const displayRow = evoUpgrade
3122
+ ? Object.assign({}, row, { id: row.id + ":evo:" + evoUpgrade.upgrade_id, name: evoUpgrade.targetName, category: evoUpgrade.targetCategory || row.category })
3123
+ : row
3124
+ const targetEquipId = evoUpgrade ? String(evoUpgrade.upgrade_id) : row.id
3125
+ const targetLevelValue = evoUpgrade ? 0 : (row.improveable === false ? 0 : (sel.target === "max" ? 10 : 6))
3126
+ const bucket = demandMap[targetEquipId] || (demandMap[targetEquipId] = { equipId: targetEquipId, displayRow, sourceRowIds: [], sourceRowIdsByLevel: {}, demands: {}, evolvedLevels: {} })
3127
+ if (bucket.sourceRowIds.indexOf(row.id) < 0) bucket.sourceRowIds.push(row.id)
3128
+ const levelKey = String(targetLevelValue)
3129
+ const levelSources = bucket.sourceRowIdsByLevel[levelKey] || (bucket.sourceRowIdsByLevel[levelKey] = [])
3130
+ if (levelSources.indexOf(row.id) < 0) levelSources.push(row.id)
3131
+ if (evoUpgrade) bucket.evolvedLevels[String(targetLevelValue)] = true
3132
+ bucket.demands[String(targetLevelValue)] = (bucket.demands[String(targetLevelValue)] || 0) + qty
3133
+ }
3134
+ for (const bucket of Object.values(demandMap)) {
3135
+ const inv = (inventoryByEquip || {})[bucket.equipId]
3136
+ const inventoryLevels = []
3137
+ if (inv && inv.levels) {
3138
+ for (const level of Object.keys(inv.levels)) {
3139
+ const count = Math.max(0, Math.round(Number(inv.levels[level]) || 0))
3140
+ if (count > 0) inventoryLevels.push({ level: Number(level), count })
3141
+ }
3142
+ inventoryLevels.sort((a, b) => b.level - a.level)
3143
+ }
3144
+ const remaining = {}
3145
+ for (const lv of inventoryLevels) remaining[String(lv.level)] = lv.count
3146
+ const targetLevels = Object.keys(bucket.demands).map(Number).sort((a, b) => b - a)
3147
+ const allocation = {}
3148
+ const shortfall = {}
3149
+ for (const tl of targetLevels) {
3150
+ let need = bucket.demands[String(tl)] || 0
3151
+ for (const lv of inventoryLevels) {
3152
+ if (need <= 0) break
3153
+ const avail = remaining[String(lv.level)] || 0
3154
+ if (avail <= 0) continue
3155
+ const take = Math.min(need, avail)
3156
+ const key = String(tl) + "|" + String(lv.level)
3157
+ const alloc = allocation[key] || (allocation[key] = { targetLevelValue: tl, inventoryLevel: lv.level, qty: 0, stock: 0 })
3158
+ alloc.qty += take
3159
+ alloc.stock += take
3160
+ remaining[String(lv.level)] = avail - take
3161
+ need -= take
3162
+ }
3163
+ if (need > 0) shortfall[String(tl)] = need
3164
+ }
3165
+ for (const key of Object.keys(allocation)) {
3166
+ const alloc = allocation[key]
3167
+ entries.push({ key: bucket.equipId + ":" + key, row: bucket.displayRow, sourceRowIds: bucket.sourceRowIdsByLevel[String(alloc.targetLevelValue)] || bucket.sourceRowIds, evolved: !!bucket.evolvedLevels[String(alloc.targetLevelValue)], targetLevel: formatStrongLevel(alloc.targetLevelValue), targetLevelValue: alloc.targetLevelValue, qty: alloc.qty, inventoryLevel: alloc.inventoryLevel, stock: alloc.stock })
3168
+ }
3169
+ for (const tl of Object.keys(shortfall)) {
3170
+ const value = Number(tl)
3171
+ entries.push({ key: bucket.equipId + ":short:" + tl, row: bucket.displayRow, sourceRowIds: bucket.sourceRowIdsByLevel[String(value)] || bucket.sourceRowIds, evolved: !!bucket.evolvedLevels[String(value)], targetLevel: formatStrongLevel(value), targetLevelValue: value, qty: shortfall[tl], inventoryLevel: null, stock: inventoryLoaded ? 0 : null, notFound: true })
3172
+ }
3173
+ }
3174
+ for (const item of entries) {
3175
+ item.clear = item.qty > 0 && item.stock != null && item.inventoryLevel >= item.targetLevelValue && item.stock >= item.qty
3176
+ item.completed = item.clear ? item.qty : 0
3177
+ item.levelShort = item.inventoryLevel != null && item.inventoryLevel < item.targetLevelValue
3178
+ item.countShort = item.stock != null && item.qty > 0 && item.stock < item.qty
3179
+ item.countEnough = item.stock != null && item.stock >= item.qty
3180
+ }
3181
+ const levelRank = (item) => item.inventoryLevel == null ? -1 : item.inventoryLevel
3182
+ entries.sort((a, b) => {
3183
+ const ac = a.clear ? 1 : 0
3184
+ const bc = b.clear ? 1 : 0
3185
+ if (ac !== bc) return bc - ac
3186
+ if (a.clear) return b.qty - a.qty || levelRank(b) - levelRank(a)
3187
+ const sa = a.stock == null ? -1 : a.stock
3188
+ const sb = b.stock == null ? -1 : b.stock
3189
+ return sb - sa || b.qty - a.qty || levelRank(b) - levelRank(a)
3190
+ })
3191
+ return { entries, inventoryLoaded }
3192
+ }
3193
+
3194
+ function buildStrongGroups(entries) {
3195
+ const groupsByKey = {}
3196
+ for (const item of entries || []) {
3197
+ const key = item.row.id + "|" + String(item.targetLevelValue)
3198
+ const group = groupsByKey[key] || (groupsByKey[key] = {
3199
+ key,
3200
+ row: item.row,
3201
+ targetLevel: item.targetLevel,
3202
+ targetLevelValue: item.targetLevelValue,
3203
+ items: [],
3204
+ sourceRowIds: [],
3205
+ qty: 0,
3206
+ completed: 0,
3207
+ stock: 0,
3208
+ stockLoaded: false,
3209
+ maxInventoryLevel: -1,
3210
+ evolved: false,
3211
+ })
3212
+ group.items.push(item)
3213
+ group.qty += item.qty
3214
+ group.completed += item.completed || 0
3215
+ if (item.stock != null) {
3216
+ group.stock += item.stock
3217
+ group.stockLoaded = true
3218
+ }
3219
+ if (item.inventoryLevel != null) group.maxInventoryLevel = Math.max(group.maxInventoryLevel, item.inventoryLevel)
3220
+ if (item.evolved) group.evolved = true
3221
+ for (const rowId of item.sourceRowIds || []) {
3222
+ if (group.sourceRowIds.indexOf(rowId) < 0) group.sourceRowIds.push(rowId)
3223
+ }
3224
+ }
3225
+ const groups = Object.keys(groupsByKey).map((key) => groupsByKey[key])
3226
+ for (const group of groups) {
3227
+ group.items.sort((a, b) => (b.inventoryLevel == null ? -1 : b.inventoryLevel) - (a.inventoryLevel == null ? -1 : a.inventoryLevel))
3228
+ group.clear = group.qty > 0 && group.completed >= group.qty
3229
+ }
3230
+ groups.sort((a, b) => {
3231
+ const ac = a.clear ? 1 : 0
3232
+ const bc = b.clear ? 1 : 0
3233
+ if (ac !== bc) return bc - ac
3234
+ if (a.clear) return b.qty - a.qty || b.maxInventoryLevel - a.maxInventoryLevel
3235
+ const sa = a.stockLoaded ? a.stock : -1
3236
+ const sb = b.stockLoaded ? b.stock : -1
3237
+ return sb - sa || b.qty - a.qty || b.maxInventoryLevel - a.maxInventoryLevel
3238
+ })
3239
+ return groups
3240
+ }
3241
+
3242
+ class StrongPage extends React.Component {
2881
3243
  catRef = React.createRef()
2882
3244
  state = {
2883
3245
  categoryOpen: false,
@@ -2916,96 +3278,28 @@ class StrongPage extends React.Component {
2916
3278
  })
2917
3279
  }
2918
3280
 
2919
- render() {
2920
- const { rows, inventoryByEquip, selection } = this.props
2921
- const inventoryLoaded = inventoryByEquip && Object.keys(inventoryByEquip).length > 0
2922
- const entries = []
2923
- const levelText = (level) => {
2924
- const n = Number(level)
2925
- if (n >= 10) return "max"
2926
- return "+" + String(n)
2927
- }
2928
- const demandMap = {}
2929
- for (const row of rows || []) {
2930
- const sel = (selection || {})[row.id] || { selected: true, target: "max", qty: 1 }
2931
- const qty = Math.max(1, Math.min(99, Math.round(Number(sel.qty) || 1)))
2932
- const evoUpgradeId = sel.evoUpgradeId || null
2933
- const evoUpgrade = evoUpgradeId ? (row.upgrades || []).find((u) => String(u.upgrade_id) === String(evoUpgradeId)) : null
2934
- const displayRow = evoUpgrade
2935
- ? Object.assign({}, row, { id: row.id + ":evo:" + evoUpgrade.upgrade_id, name: evoUpgrade.targetName, category: evoUpgrade.targetCategory || row.category })
2936
- : row
2937
- const targetEquipId = evoUpgrade ? String(evoUpgrade.upgrade_id) : row.id
2938
- const targetLevelValue = evoUpgrade ? 0 : (row.improveable === false ? 0 : (sel.target === "max" ? 10 : 6))
2939
- const bucket = demandMap[targetEquipId] || (demandMap[targetEquipId] = { equipId: targetEquipId, displayRow, demands: {} })
2940
- bucket.demands[String(targetLevelValue)] = (bucket.demands[String(targetLevelValue)] || 0) + qty
2941
- }
2942
- for (const bucket of Object.values(demandMap)) {
2943
- const inv = (inventoryByEquip || {})[bucket.equipId]
2944
- const inventoryLevels = []
2945
- if (inv && inv.levels) {
2946
- for (const level of Object.keys(inv.levels)) {
2947
- const count = Math.max(0, Math.round(Number(inv.levels[level]) || 0))
2948
- if (count > 0) inventoryLevels.push({ level: Number(level), count })
2949
- }
2950
- inventoryLevels.sort((a, b) => b.level - a.level)
2951
- }
2952
- const remaining = {}
2953
- for (const lv of inventoryLevels) remaining[String(lv.level)] = lv.count
2954
- const targetLevels = Object.keys(bucket.demands).map(Number).sort((a, b) => b - a)
2955
- const allocation = {}
2956
- const shortfall = {}
2957
- for (const tl of targetLevels) {
2958
- let need = bucket.demands[String(tl)] || 0
2959
- for (const lv of inventoryLevels) {
2960
- if (need <= 0) break
2961
- const avail = remaining[String(lv.level)] || 0
2962
- if (avail <= 0) continue
2963
- const take = Math.min(need, avail)
2964
- const key = String(tl) + "|" + String(lv.level)
2965
- const alloc = allocation[key] || (allocation[key] = { targetLevelValue: tl, inventoryLevel: lv.level, qty: 0, stock: 0 })
2966
- alloc.qty += take
2967
- alloc.stock += take
2968
- remaining[String(lv.level)] = avail - take
2969
- need -= take
2970
- }
2971
- if (need > 0) shortfall[String(tl)] = need
2972
- }
2973
- for (const key of Object.keys(allocation)) {
2974
- const alloc = allocation[key]
2975
- entries.push({ key: bucket.equipId + ":" + key, row: bucket.displayRow, targetLevel: levelText(alloc.targetLevelValue), targetLevelValue: alloc.targetLevelValue, qty: alloc.qty, inventoryLevel: alloc.inventoryLevel, stock: alloc.stock })
2976
- }
2977
- for (const tl of Object.keys(shortfall)) {
2978
- const value = Number(tl)
2979
- entries.push({ key: bucket.equipId + ":short:" + tl, row: bucket.displayRow, targetLevel: levelText(value), targetLevelValue: value, qty: shortfall[tl], inventoryLevel: null, stock: inventoryLoaded ? 0 : null, notFound: true })
2980
- }
2981
- }
2982
- for (const item of entries) {
2983
- item.clear = item.qty > 0 && item.stock != null && item.inventoryLevel === item.targetLevelValue && item.stock >= item.qty
2984
- item.levelShort = item.inventoryLevel != null && item.inventoryLevel < item.targetLevelValue
2985
- item.countShort = item.stock != null && item.qty > 0 && item.stock < item.qty
2986
- item.countEnough = item.stock != null && item.stock >= item.qty
2987
- }
2988
- const levelRank = (item) => item.inventoryLevel == null ? -1 : item.inventoryLevel
2989
- entries.sort((a, b) => {
2990
- const ac = a.clear ? 1 : 0
2991
- const bc = b.clear ? 1 : 0
2992
- if (ac !== bc) return bc - ac
2993
- if (a.clear) return b.qty - a.qty || levelRank(b) - levelRank(a)
2994
- const sa = a.stock == null ? -1 : a.stock
2995
- const sb = b.stock == null ? -1 : b.stock
2996
- return sb - sa || b.qty - a.qty || levelRank(b) - levelRank(a)
2997
- })
2998
- const categories = Array.from(new Set(entries.map((item) => item.row.category).filter(Boolean))).sort((a, b) => a.localeCompare(b, "zh-Hans-CN"))
2999
- const selectedCategories = this.state.selectedCategories
3000
- const visibleEntries = selectedCategories == null ? entries : entries.filter((item) => selectedCategories.indexOf(item.row.category) >= 0)
3001
- const stockText = (item) => {
3002
- const nameNode = React.createElement("span", { className: "kr2-strong-stock-name" }, item.row.name)
3003
- if (item.notFound || item.stock == null) return nameNode
3004
- const levelPart = levelText(item.inventoryLevel)
3005
- return React.createElement("span", null, nameNode, " ", levelPart)
3006
- }
3007
- const stockClass = (item) => item.notFound || item.stock == null ? "kr2-strong-stock-notfound" : item.clear ? "kr2-strong-stock-clear" : item.levelShort ? "kr2-strong-stock-short" : "kr2-strong-stock-ok"
3008
- const ownedClass = (item) => item.stock == null ? "kr2-strong-stock-missing" : item.clear || item.countEnough ? "kr2-strong-stock-clear" : item.countShort ? "kr2-strong-stock-short" : "kr2-strong-stock-ok"
3281
+ render() {
3282
+ const { rows, inventoryByEquip, selection } = this.props
3283
+ const { entries } = buildStrongEntries(rows, inventoryByEquip, selection)
3284
+ const groups = buildStrongGroups(entries)
3285
+ const categories = Array.from(new Set(groups.map((group) => group.row.category).filter(Boolean))).sort((a, b) => a.localeCompare(b, "zh-Hans-CN"))
3286
+ const selectedCategories = this.state.selectedCategories
3287
+ const visibleGroups = selectedCategories == null ? groups : groups.filter((group) => selectedCategories.indexOf(group.row.category) >= 0)
3288
+ const stockText = (item) => {
3289
+ const nameNode = React.createElement("span", { className: "kr2-strong-stock-name" }, item.row.name)
3290
+ if (item.notFound || item.stock == null) return nameNode
3291
+ const levelPart = formatStrongLevel(item.inventoryLevel)
3292
+ return React.createElement("span", null, nameNode, " ", levelPart)
3293
+ }
3294
+ const stockClass = (item) => item.notFound || item.stock == null ? "kr2-strong-stock-notfound" : item.clear ? "kr2-strong-stock-clear" : item.levelShort ? "kr2-strong-stock-short" : "kr2-strong-stock-ok"
3295
+ const groupOwnedClass = (group) => !group.stockLoaded ? "kr2-strong-stock-missing" : group.clear ? "kr2-strong-stock-clear" : "kr2-strong-stock-short"
3296
+ const completionLineClass = (item, group) => "kr2-strong-owned-line " + (item.stock == null ? "kr2-strong-owned-line-missing" : group.qty > 0 && (item.completed || 0) >= group.qty ? "kr2-strong-owned-line-clear" : "kr2-strong-owned-line-short")
3297
+ const completionLineText = (item, group) => {
3298
+ if (item.stock == null) return "--"
3299
+ const completed = item.completed || 0
3300
+ if (group.qty > 0 && completed >= group.qty) return String(completed)
3301
+ return completed > 0 ? React.createElement("span", { className: "kr2-strong-completion-done" }, String(completed)) : String(completed)
3302
+ }
3009
3303
  const catStyle = this.state.catPos ? { position: "fixed", top: this.state.catPos.top, left: this.state.catPos.left, minWidth: Math.max(180, this.state.catPos.width) } : null
3010
3304
  const categoryHead = React.createElement(
3011
3305
  "th",
@@ -3036,24 +3330,30 @@ class StrongPage extends React.Component {
3036
3330
  React.createElement(
3037
3331
  "thead",
3038
3332
  null,
3039
- React.createElement("tr", null, categoryHead, React.createElement("th", null, "目标装备"), React.createElement("th", null, "目标数"), React.createElement("th", null, "库存"), React.createElement("th", null, "完成数"))
3333
+ React.createElement("tr", null, categoryHead, React.createElement("th", null, "目标装备"), React.createElement("th", { className: "kr2-strong-stock-th" }, "库存装备"), React.createElement("th", null, "完成数"), React.createElement("th", null, "目标数"))
3040
3334
  ),
3041
3335
  React.createElement(
3042
3336
  "tbody",
3043
3337
  null,
3044
- visibleEntries.length === 0
3045
- ? React.createElement("tr", null, React.createElement("td", { colSpan: 5 }, "无匹配分类"))
3046
- : visibleEntries.map((item) =>
3047
- React.createElement(
3048
- "tr",
3049
- { key: item.key, className: item.clear ? "kr2-strong-clear-row" : null },
3050
- React.createElement("td", { className: "kr2-strong-cat" }, item.row.category || "-"),
3051
- React.createElement("td", { className: "kr2-strong-equip" }, item.row.name, " ", React.createElement("span", { className: "kr2-strong-level" }, item.targetLevel)),
3052
- React.createElement("td", { className: "kr2-strong-target" }, String(item.qty)),
3053
- React.createElement("td", { className: "kr2-strong-stock " + stockClass(item) }, stockText(item)),
3054
- React.createElement("td", { className: "kr2-strong-owned " + ownedClass(item) }, item.stock == null ? "--" : item.clear ? "clear!" : String(item.stock))
3055
- )
3056
- )
3338
+ visibleGroups.length === 0
3339
+ ? React.createElement("tr", null, React.createElement("td", { colSpan: 5 }, "无匹配分类"))
3340
+ : visibleGroups.map((group) =>
3341
+ React.createElement(
3342
+ "tr",
3343
+ { key: group.key, className: group.clear ? "kr2-strong-clear-row" : null },
3344
+ React.createElement("td", { className: "kr2-strong-cat" }, group.row.category || "-"),
3345
+ React.createElement(
3346
+ "td",
3347
+ { className: "kr2-strong-equip" },
3348
+ group.row.name + (group.evolved ? "(进化)" : ""),
3349
+ !group.evolved && group.row.improveable !== false ? " " : null,
3350
+ !group.evolved && group.row.improveable !== false ? React.createElement("span", { className: "kr2-strong-level" }, group.targetLevel) : null
3351
+ ),
3352
+ React.createElement("td", { className: "kr2-strong-stock" }, group.items.map((item) => React.createElement("div", { key: item.key, className: "kr2-strong-stock-line " + stockClass(item), "data-level": item.inventoryLevel == null ? "" : String(item.inventoryLevel) }, stockText(item)))),
3353
+ React.createElement("td", { className: "kr2-strong-owned " + groupOwnedClass(group), "data-completed": String(group.completed || 0), "data-stock": group.stockLoaded ? String(group.stock) : "", "data-level": String(group.maxInventoryLevel) }, group.items.map((item) => React.createElement("div", { key: item.key, className: completionLineClass(item, group), "data-level": item.inventoryLevel == null ? "" : String(item.inventoryLevel) }, completionLineText(item, group)))),
3354
+ React.createElement("td", { className: "kr2-strong-target", "data-target": String(group.qty) }, String(group.qty))
3355
+ )
3356
+ )
3057
3357
  )
3058
3358
  )
3059
3359
  )
@@ -3087,7 +3387,8 @@ class LevelOneTable extends React.Component {
3087
3387
  const normal = phase.consume_improvement_min
3088
3388
  const materials = phase.materials || []
3089
3389
  const hasMaterials = materials.some((item) => item && (item.item_name || item.item_material_key || item.item_equipment_id != null))
3090
- const rows = []
3390
+ const rareWarning = null
3391
+ const rows = []
3091
3392
  LEVEL_ONE_ROWS.forEach((rate) => {
3092
3393
  const key = rate.range
3093
3394
  rows.push(
@@ -3106,7 +3407,8 @@ class LevelOneTable extends React.Component {
3106
3407
  "div",
3107
3408
  { className: "kr2-material-cell" },
3108
3409
  React.createElement(RateCell, { range: "素材详情", open: !!this.state.open[key], onToggle: () => this.toggle(key) }),
3109
- !!this.state.open[key]
3410
+ rareWarning,
3411
+ !!this.state.open[key]
3110
3412
  ? React.createElement(MaterialDetail, { materials, onMaterialClick: this.props.onMaterialClick, inventoryByEquip: this.props.inventoryByEquip, useItemCounts: this.props.useItemCounts, onInventoryClick: this.props.onInventoryClick })
3111
3413
  : null
3112
3414
  )
@@ -3156,7 +3458,9 @@ class LevelTwoTable extends React.Component {
3156
3458
  const rec = buildRecommendation(normal, certain, p)
3157
3459
  const materials = step ? step.materials || [] : []
3158
3460
  const hasMaterials = materials.some((item) => item && (item.item_name || item.item_material_key || item.item_equipment_id != null))
3159
- const rangeNode = maxBlue
3461
+ const rareTier = rec.tone === "save" ? classifyRareMaterial(materials, kcDevData) : null
3462
+ const rareWarning = rareTier === "rare" ? React.createElement("div", { className: "kr2-rare-warning" }, "稀有素材消耗注意!") : rareTier === "secondary" ? React.createElement("div", { className: "kr2-rare-warning-secondary" }, "次级稀有素材消耗注意!") : null
3463
+ const rangeNode = maxBlue
3160
3464
  ? React.createElement("span", null, "★9→", React.createElement("span", { className: "kr2-level-max-blue" }, "max"))
3161
3465
  : range
3162
3466
  rows.push(
@@ -3192,7 +3496,8 @@ class LevelTwoTable extends React.Component {
3192
3496
  "div",
3193
3497
  { className: "kr2-material-cell" },
3194
3498
  React.createElement(RateCell, { range: rec.text, open: !!this.state.open[key], onToggle: () => this.toggle(key), tone: rec.tone }),
3195
- !!this.state.open[key]
3499
+ rareWarning,
3500
+ !!this.state.open[key]
3196
3501
  ? React.createElement(MaterialDetail, { materials, onMaterialClick: this.props.onMaterialClick, inventoryByEquip: this.props.inventoryByEquip, useItemCounts: this.props.useItemCounts, onInventoryClick: this.props.onInventoryClick })
3197
3502
  : null
3198
3503
  )
@@ -3270,13 +3575,14 @@ class LevelSection extends React.Component {
3270
3575
  }
3271
3576
  }
3272
3577
 
3273
- function EquipmentRow({ row, expanded, onToggle, onMaterialClick, inventoryByEquip, useItemCounts, onInventoryClick, isFavorite, onFavoriteClick, secretaryText, kcDevData }) {
3274
- return React.createElement(
3275
- "div",
3276
- null,
3277
- React.createElement(
3278
- "div",
3279
- { className: "kr2-row" + (row.improveable === false ? " kr2-row-not-improveable" : ""), onClick: row.improveable === false ? null : onToggle },
3578
+ function EquipmentRow({ row, expanded, onToggle, onMaterialClick, inventoryByEquip, useItemCounts, onInventoryClick, isFavorite, onFavoriteClick, secretaryText, kcDevData, completion }) {
3579
+ const clear = !!(completion && completion.target > 0 && completion.completed >= completion.target)
3580
+ return React.createElement(
3581
+ "div",
3582
+ null,
3583
+ React.createElement(
3584
+ "div",
3585
+ { className: "kr2-row" + (row.improveable === false ? " kr2-row-not-improveable" : "") + (clear ? " kr2-row-clear" : ""), onClick: row.improveable === false ? null : onToggle },
3280
3586
  React.createElement(
3281
3587
  "button",
3282
3588
  { className: "kr2-fav-btn" + (isFavorite ? " kr2-fav-active" : ""), onClick: (e) => { e.stopPropagation(); onFavoriteClick && onFavoriteClick(row.id) }, title: isFavorite ? "取消收藏" : "收藏" },
@@ -3288,7 +3594,24 @@ function EquipmentRow({ row, expanded, onToggle, onMaterialClick, inventoryByEqu
3288
3594
  React.createElement(SlotitemIcon, { slotitemId: row.icon, className: "kr2-icon" }),
3289
3595
  React.createElement("span", { className: "kr2-arrow" }, row.improveable === false ? "" : expanded ? "▼" : "▶")
3290
3596
  ),
3291
- React.createElement("span", { className: "kr2-name" }, row.name),
3597
+ React.createElement(
3598
+ "span",
3599
+ { className: "kr2-name-cell" },
3600
+ React.createElement("span", { className: "kr2-name" }, row.name),
3601
+ completion
3602
+ ? clear
3603
+ ? React.createElement("span", { className: "kr2-name-completion kr2-name-completion-clear" }, "(", String(completion.completed), "/", String(completion.target), ")clear!")
3604
+ : React.createElement(
3605
+ "span",
3606
+ { className: "kr2-name-completion kr2-name-completion-incomplete" },
3607
+ "完成数(",
3608
+ React.createElement("span", { className: "kr2-name-completion-done" + (completion.completed > 0 ? " kr2-name-completion-done-active" : "") }, String(completion.completed)),
3609
+ "/",
3610
+ String(completion.target),
3611
+ ")"
3612
+ )
3613
+ : null
3614
+ ),
3292
3615
  React.createElement("span", { className: "kr2-secretary" }, row.improveable === false ? "" : (secretaryText || "-")),
3293
3616
  React.createElement(
3294
3617
  "span",
@@ -3552,14 +3875,96 @@ function readPlanIncludeDev() {
3552
3875
  }
3553
3876
  }
3554
3877
 
3555
- function savePlanIncludeDev(value) {
3556
- try {
3557
- const storage = typeof localStorage !== "undefined" ? localStorage : (typeof window !== "undefined" ? window.localStorage : null)
3558
- if (storage) storage.setItem(PLAN_INCLUDE_DEV_STORAGE_KEY, value ? "1" : "0")
3559
- } catch (_) {}
3560
- }
3561
-
3562
- class KoushuRateApp extends React.Component {
3878
+ function savePlanIncludeDev(value) {
3879
+ try {
3880
+ const storage = typeof localStorage !== "undefined" ? localStorage : (typeof window !== "undefined" ? window.localStorage : null)
3881
+ if (storage) storage.setItem(PLAN_INCLUDE_DEV_STORAGE_KEY, value ? "1" : "0")
3882
+ } catch (_) {}
3883
+ }
3884
+
3885
+ function HelpModal({ onClose, updateNotes }) {
3886
+ const sections = []
3887
+ if (updateNotes && updateNotes.length) sections.push({ title: PLUGIN_VERSION + "更新", items: updateNotes })
3888
+ sections.push(
3889
+ {
3890
+ title: "1.改修列表",
3891
+ items: [
3892
+ "显示每日改修装备,打开插件时默认自动跳转到当前星期数;",
3893
+ "装备行上最右侧为改修推荐星级,数据参考nga梦美改修2.0贴(插件版头有网站链接),点击表头可以按照星级排序;",
3894
+ "点击对应装备行可展开详细改修数据,默认只展开6-10级的改修(也可手动点开查看),最右侧确保推荐是按照螺丝期望自动计算的,未考虑稀有素材和紫菜的消耗情况(请注意甄别);",
3895
+ "点击装备行最左侧的星星可以收藏该装备,点亮插件右上角的收藏按钮会隐藏其他未收藏的装备;",
3896
+ "点击确保推荐列中的“推荐确保”/“不确保”按钮会显示消耗素材情况以及库存素材数量(默认不统计带有星级的素材),点击素材会弹出装备开发配方列表,点击库存可查看已拥有的素材详情(主要是查看星级);",
3897
+ ],
3898
+ },
3899
+ {
3900
+ title: "2.素材计算",
3901
+ items: [
3902
+ "只有在改修列表中点击收藏的装备才会显示在该页中;",
3903
+ "为了让插件看起来更加简洁,改修目标只设置了+6、max和进化三个选项,输入目标数量后,插件会自动计算所需的素材汇总情况;",
3904
+ "勾选“统计素材开发的紫菜消耗期望”会计算当所有改修素材均来自开发时所预计消耗的紫菜数量(图一乐功能);",
3905
+ "可对开发资材、螺丝以及素材汇总中的需求、库存和紫菜开发期望进行排序;",
3906
+ ],
3907
+ },
3908
+ {
3909
+ title: "3.我变强了!",
3910
+ items: [
3911
+ "所有收藏的装备均会在该页中显示(无论在素材计算页中是否勾选),插件会自动计算目标装备与库存装备的星级、数量差异,并默认倒序排列;",
3912
+ "已完成的装备会自动置顶并显示clear!;",
3913
+ "计划的完成情况会同步显示在改修列表页的装备行上;",
3914
+ "注意在素材计算页选择“进化”的装备会和进化后的同名装备分开计算,用来锚定阶段性目标。",
3915
+ ],
3916
+ },
3917
+ {
3918
+ title: "其他常规功能",
3919
+ items: [
3920
+ "模糊搜索,搜索栏支持输入简体中文;",
3921
+ "可在插件版头按照装备大类进行筛选;",
3922
+ "插件也加入了不可改修装备,同样可以收藏,并正常显示在我变强了页中。",
3923
+ ],
3924
+ },
3925
+ )
3926
+ const updateSection = sections[0] && sections[0].title === PLUGIN_VERSION + "更新" ? sections[0] : null
3927
+ const guideSections = updateSection ? sections.slice(1) : sections
3928
+ const renderSection = (section, key) =>
3929
+ React.createElement(
3930
+ "section",
3931
+ { key, className: "kr2-help-section" },
3932
+ React.createElement("div", { className: "kr2-help-section-title" }, section.title),
3933
+ React.createElement(
3934
+ "ul",
3935
+ { className: "kr2-help-list" },
3936
+ section.items.map((text, itemIndex) => React.createElement("li", { key: itemIndex }, text))
3937
+ )
3938
+ )
3939
+ return React.createElement(
3940
+ "div",
3941
+ { className: "kr2-modal-backdrop", onClick: onClose },
3942
+ React.createElement(
3943
+ "div",
3944
+ { className: "kr2-modal kr2-help-modal", role: "dialog", "aria-modal": "true", "aria-label": "使用说明", onClick: (e) => e.stopPropagation() },
3945
+ React.createElement(
3946
+ "div",
3947
+ { className: "kr2-modal-head" },
3948
+ React.createElement(
3949
+ "div",
3950
+ null,
3951
+ React.createElement("div", { className: "kr2-modal-title" }, "使用说明"),
3952
+ React.createElement("div", { className: "kr2-modal-sub" }, "螺丝计算器 v" + PLUGIN_VERSION)
3953
+ ),
3954
+ React.createElement("button", { type: "button", className: "kr2-modal-close", onClick: onClose, title: "关闭", "aria-label": "关闭使用说明" }, "×")
3955
+ ),
3956
+ React.createElement(
3957
+ "div",
3958
+ { className: "kr2-help-body" },
3959
+ updateSection ? renderSection(updateSection, "update") : null,
3960
+ React.createElement("div", { className: "kr2-help-lead" }, "功能主页"),
3961
+ guideSections.map((section, index) => renderSection(section, index))
3962
+ )
3963
+ )
3964
+ )
3965
+ }
3966
+
3967
+ class KoushuRateApp extends React.Component {
3563
3968
  state = {
3564
3969
  query: "",
3565
3970
  day: japanWeekdayKey(),
@@ -3583,13 +3988,22 @@ class KoushuRateApp extends React.Component {
3583
3988
  starSort: readUiState().starSort || null,
3584
3989
  activeTab: "list",
3585
3990
  improveableOpen: true,
3586
- nonImproveableOpen: false,
3587
- planSelection: readPlanSelection(),
3588
- includeDevExpected: readPlanIncludeDev(),
3589
- }
3590
-
3591
- componentDidMount() {
3592
- this.refresh()
3991
+ nonImproveableOpen: false,
3992
+ planSelection: readPlanSelection(),
3993
+ includeDevExpected: readPlanIncludeDev(),
3994
+ helpOpen: false,
3995
+ }
3996
+
3997
+ componentDidMount() {
3998
+ const uiState = readUiState()
3999
+ const legacyVersion = uiState.helpVersion ? String(uiState.helpVersion).split(".").slice(0, 3).join(".") : ""
4000
+ const seenVersion = String(uiState.pluginVersion || legacyVersion || "").trim()
4001
+ const firstInstall = !seenVersion
4002
+ const isUpdate = !!seenVersion && seenVersion !== PLUGIN_VERSION
4003
+ if (firstInstall || isUpdate) {
4004
+ this.setState({ helpOpen: true })
4005
+ }
4006
+ this.refresh()
3593
4007
  this.checkAkashiUpdate(false)
3594
4008
  refreshKcDevData().catch(() => {})
3595
4009
  this.loadKcDevExpectations()
@@ -3597,11 +4011,20 @@ class KoushuRateApp extends React.Component {
3597
4011
  this.inventoryTimer = setInterval(() => this.refreshInventory(), 1000)
3598
4012
  }
3599
4013
 
3600
- componentWillUnmount() {
3601
- if (this.inventoryTimer) clearInterval(this.inventoryTimer)
3602
- }
3603
-
3604
- loadKcDevExpectations = () => {
4014
+ componentWillUnmount() {
4015
+ if (this.inventoryTimer) clearInterval(this.inventoryTimer)
4016
+ }
4017
+
4018
+ openHelp = () => {
4019
+ this.setState({ helpOpen: true })
4020
+ }
4021
+
4022
+ closeHelp = () => {
4023
+ saveUiState({ helpVersion: HELP_VERSION, pluginVersion: PLUGIN_VERSION })
4024
+ this.setState({ helpOpen: false })
4025
+ }
4026
+
4027
+ loadKcDevExpectations = () => {
3605
4028
  loadKcDevData(false)
3606
4029
  .then((data) => this.setState({ kcDevData: data }))
3607
4030
  .catch(() => {})
@@ -3847,8 +4270,9 @@ class KoushuRateApp extends React.Component {
3847
4270
  if (ib >= 0) return 1
3848
4271
  return a.localeCompare(b, "zh-Hans-CN")
3849
4272
  })
3850
- const kcDevPopup = this.state.kcDevPopup
3851
- const inventoryPopup = this.state.inventoryPopup
4273
+ const kcDevPopup = this.state.kcDevPopup
4274
+ const inventoryPopup = this.state.inventoryPopup
4275
+ const helpOpen = this.state.helpOpen
3852
4276
  const kcDevSort = (kcDevPopup && kcDevPopup.sort) || { key: "rate", dir: "desc" }
3853
4277
  const kcDevFormulas = kcDevPopup && Array.isArray(kcDevPopup.formulas) ? kcDevPopup.formulas.slice() : []
3854
4278
  if (kcDevSort.key === "rate" || kcDevSort.key === "failRate") {
@@ -3857,10 +4281,21 @@ class KoushuRateApp extends React.Component {
3857
4281
  return kcDevSort.dir === "asc" ? diff : -diff
3858
4282
  })
3859
4283
  }
3860
- const inventoryByEquip = this.state.inventoryByEquip || {}
3861
- const useItemCounts = this.state.useItemCounts || {}
3862
- const favoritesOnly = !!this.state.favoritesOnly
3863
- let rows = this.state.rows.filter((row) => {
4284
+ const inventoryByEquip = this.state.inventoryByEquip || {}
4285
+ const useItemCounts = this.state.useItemCounts || {}
4286
+ const favoritesOnly = !!this.state.favoritesOnly
4287
+ const planSelection = this.state.planSelection || {}
4288
+ const favoriteRows = this.state.rows.filter((row) => !!this.state.favorites[row.id])
4289
+ const strongSummary = {}
4290
+ for (const item of buildStrongEntries(favoriteRows, inventoryByEquip, planSelection).entries) {
4291
+ const rowIds = item.sourceRowIds && item.sourceRowIds.length ? item.sourceRowIds : [item.row.id]
4292
+ for (const rowId of rowIds) {
4293
+ const summary = strongSummary[rowId] || (strongSummary[rowId] = { completed: 0, target: 0 })
4294
+ summary.completed += item.completed || 0
4295
+ summary.target += item.qty
4296
+ }
4297
+ }
4298
+ let rows = this.state.rows.filter((row) => {
3864
4299
  if (favoritesOnly && !this.state.favorites[row.id]) return false
3865
4300
  if (row.improveable !== false && day !== ALL_DAYS_KEY && (row.days || []).indexOf(day) < 0) return false
3866
4301
  if (category !== "all" && row.category !== category) return false
@@ -3899,9 +4334,10 @@ class KoushuRateApp extends React.Component {
3899
4334
  onInventoryClick: this.openInventoryPopup,
3900
4335
  isFavorite: !!this.state.favorites[row.id],
3901
4336
  onFavoriteClick: this.toggleFavorite,
3902
- secretaryText: secretaryText(row.secretaries, day),
3903
- kcDevData: this.state.kcDevData,
3904
- })
4337
+ secretaryText: secretaryText(row.secretaries, day),
4338
+ kcDevData: this.state.kcDevData,
4339
+ completion: strongSummary[row.id],
4340
+ })
3905
4341
 
3906
4342
  return React.createElement(
3907
4343
  "div",
@@ -3914,11 +4350,16 @@ class KoushuRateApp extends React.Component {
3914
4350
  React.createElement(
3915
4351
  "div",
3916
4352
  { className: "kr2-title-row" },
3917
- React.createElement(
3918
- "div",
3919
- null,
3920
- React.createElement("div", { className: "kr2-title" }, "螺丝计算器"),
3921
- React.createElement(
4353
+ React.createElement(
4354
+ "div",
4355
+ null,
4356
+ React.createElement(
4357
+ "div",
4358
+ { className: "kr2-title-line" },
4359
+ React.createElement("div", { className: "kr2-title" }, "螺丝计算器"),
4360
+ React.createElement("button", { type: "button", className: "kr2-help-btn", onClick: this.openHelp }, "使用说明")
4361
+ ),
4362
+ React.createElement(
3922
4363
  "div",
3923
4364
  { className: "kr2-title-note" },
3924
4365
  "螺丝确保仅供参考,稀有素材消耗请酌情考虑~(∠・ω< )⌒★"
@@ -4084,10 +4525,10 @@ class KoushuRateApp extends React.Component {
4084
4525
  ),
4085
4526
  this.state.activeTab === "plan"
4086
4527
  ? React.createElement(PlanningPage, {
4087
- rows: this.state.rows.filter((row) => !!this.state.favorites[row.id]),
4088
- inventoryByEquip,
4089
- useItemCounts,
4090
- selection: this.state.planSelection || {},
4528
+ rows: this.state.rows.filter((row) => !!this.state.favorites[row.id]),
4529
+ inventoryByEquip,
4530
+ useItemCounts,
4531
+ selection: planSelection,
4091
4532
  onToggle: this.togglePlanRow,
4092
4533
  onTarget: this.setPlanTarget,
4093
4534
  onEvo: this.setPlanEvo,
@@ -4099,14 +4540,17 @@ class KoushuRateApp extends React.Component {
4099
4540
  onToggleIncludeDevExpected: this.toggleIncludeDevExpected,
4100
4541
  })
4101
4542
  : null,
4102
- this.state.activeTab === "strong"
4103
- ? React.createElement(StrongPage, {
4104
- rows: this.state.rows.filter((row) => !!this.state.favorites[row.id]),
4105
- inventoryByEquip,
4106
- selection: this.state.planSelection || {},
4107
- })
4108
- : null,
4109
- kcDevPopup
4543
+ this.state.activeTab === "strong"
4544
+ ? React.createElement(StrongPage, {
4545
+ rows: this.state.rows.filter((row) => !!this.state.favorites[row.id]),
4546
+ inventoryByEquip,
4547
+ selection: planSelection,
4548
+ })
4549
+ : null,
4550
+ helpOpen
4551
+ ? React.createElement(HelpModal, { onClose: this.closeHelp, updateNotes: CURRENT_HELP_UPDATE_NOTES })
4552
+ : null,
4553
+ kcDevPopup
4110
4554
  ? React.createElement(
4111
4555
  "div",
4112
4556
  { className: "kr2-modal-backdrop", onClick: this.closeKcDevPopup },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-koushu-rate",
3
- "version": "2.0.17",
3
+ "version": "2.0.18",
4
4
  "description": "全装备改修资材计算与推荐",
5
5
  "main": "index.js",
6
6
  "files": [