@jx3box/jx3box-ui 2.4.2 → 2.4.4

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.
@@ -1,36 +1,10 @@
1
1
  <template>
2
2
  <transition name="box2-fade">
3
- <div v-if="status" class="c-jx3box2-mask" @click="closeBox">
3
+ <div v-if="status" class="c-jx3box2-mask">
4
4
  <section class="c-jx3box2" @click.stop>
5
- <div class="u-topline"></div>
6
-
7
- <div class="u-head">
8
- <div class="u-title-wrap">
9
- <div class="u-logo">
10
- <img svg-inline src="../../assets/img/common/logo.svg" alt="JX3BOX" />
11
- </div>
12
- <div>
13
- <h2 class="u-title">{{ $jx3boxT("jx3boxUi.box2.title", "魔盒矩阵") }}</h2>
14
- <p class="u-subtitle">THE BOX MATRIX NAVIGATION</p>
15
- </div>
16
- </div>
17
- <div class="u-search">
18
- <el-icon class="u-search-icon"><Search></Search></el-icon>
19
- <input
20
- v-model.trim="searchQuery"
21
- class="u-search-input"
22
- type="text"
23
- :placeholder="$jx3boxT('jx3boxUi.box2.searchPlaceholder', '搜索应用')"
24
- />
25
- <button v-if="searchQuery" class="u-search-clear" type="button" @click="searchQuery = ''">
26
- <el-icon><Close /></el-icon>
27
- </button>
28
- </div>
29
- </div>
30
-
31
5
  <div class="u-body">
32
6
  <ul class="u-list">
33
- <li v-if="homeMatched">
7
+ <li>
34
8
  <a class="u-item" href="/index">
35
9
  <div class="u-icon-wrap">
36
10
  <img class="u-pic" svg-inline :src="homeicon" />
@@ -38,7 +12,7 @@
38
12
  <span class="u-txt">{{ $jx3boxT("jx3boxUi.commonHeader.home", "首页") }}</span>
39
13
  </a>
40
14
  </li>
41
- <li v-for="(item, i) in filteredList" :key="i">
15
+ <li v-for="(item, i) in list" :key="i">
42
16
  <a class="u-item" :href="item.href" :target="getTarget(item.href)">
43
17
  <div class="u-icon-wrap">
44
18
  <img class="u-pic" :src="getBoxIcon(item.img)" />
@@ -46,7 +20,7 @@
46
20
  <span class="u-txt">{{ item.abbr }}</span>
47
21
  </a>
48
22
  </li>
49
- <li v-if="allMatched && filteredList.length > 0">
23
+ <li v-if="list.length > 0">
50
24
  <a class="u-item" href="/app">
51
25
  <div class="u-icon-wrap">
52
26
  <img class="u-pic" svg-inline :src="allicon" />
@@ -55,9 +29,12 @@
55
29
  </a>
56
30
  </li>
57
31
  </ul>
58
- <div v-if="searchQuery && !homeMatched && filteredList.length === 0" class="u-empty">
59
- {{ $jx3boxT("jx3boxUi.box2.empty", "未找到匹配应用") }}
60
- </div>
32
+ </div>
33
+ <div class="u-actions">
34
+ <button class="u-collapse" type="button" @click="closeBox">
35
+ <el-icon><Upload /></el-icon>
36
+ <span>{{ $jx3boxT("jx3boxUi.commonHeader.collapse", "收起") }}</span>
37
+ </button>
61
38
  </div>
62
39
  </section>
63
40
  </div>
@@ -69,11 +46,9 @@ import Bus from "../../utils/bus";
69
46
  import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
70
47
  import i18nMixin from "../../i18n/mixin";
71
48
  import box from "../../assets/data/box.json";
72
- import { getMenu } from "../../service/header.js";
73
- import axios from "axios";
74
- import { flatten } from "lodash";
49
+ import { loadBoxMenu } from "./box-menu";
75
50
 
76
- const { __cdn, __imgPath } = JX3BOX;
51
+ const { __cdn } = JX3BOX;
77
52
 
78
53
  export default {
79
54
  name: "Box2",
@@ -81,7 +56,6 @@ export default {
81
56
  data: function () {
82
57
  return {
83
58
  status: false,
84
- searchQuery: "",
85
59
  data: [],
86
60
  client: location.href.includes("origin") ? "origin" : "std",
87
61
  };
@@ -110,50 +84,8 @@ export default {
110
84
  return matchedClient;
111
85
  });
112
86
  },
113
- filteredList: function () {
114
- const query = (this.searchQuery || "").toLowerCase();
115
- if (!query) return this.list;
116
-
117
- const compactQuery = query.replace(/\s+/g, "");
118
- return this.list.filter((item) => {
119
- const abbr = String(item.abbr || "").toLowerCase();
120
- const abbrCompact = abbr.replace(/\s+/g, "");
121
- const uuid = String(item.uuid || "").toLowerCase();
122
- return abbr.includes(query) || abbrCompact.includes(compactQuery) || uuid.includes(query);
123
- });
124
- },
125
- homeMatched: function () {
126
- const query = (this.searchQuery || "").toLowerCase().trim();
127
- if (!query) return true;
128
- return this.$jx3boxT("jx3boxUi.commonHeader.home", "首页").includes(query);
129
- },
130
- allMatched: function () {
131
- const query = (this.searchQuery || "").toLowerCase().trim();
132
- if (!query) return true;
133
- return this.$jx3boxT("jx3boxUi.commonHeader.all", "全部").includes(query);
134
- },
135
87
  },
136
88
  methods: {
137
- setPageScrollLock(locked) {
138
- const docEl = document.documentElement;
139
- const body = document.body;
140
-
141
- if (locked) {
142
- if (this.__prevHtmlOverflow === undefined) {
143
- this.__prevHtmlOverflow = docEl.style.overflow;
144
- }
145
- if (this.__prevBodyOverflow === undefined) {
146
- this.__prevBodyOverflow = body.style.overflow;
147
- }
148
- docEl.style.overflow = "hidden";
149
- body.style.overflow = "hidden";
150
- } else {
151
- docEl.style.overflow = this.__prevHtmlOverflow ?? "";
152
- body.style.overflow = this.__prevBodyOverflow ?? "";
153
- this.__prevHtmlOverflow = undefined;
154
- this.__prevBodyOverflow = undefined;
155
- }
156
- },
157
89
  closeBox() {
158
90
  this.status = false;
159
91
  },
@@ -169,28 +101,9 @@ export default {
169
101
  }
170
102
  },
171
103
  loadMenu() {
172
- try {
173
- const data = JSON.parse(sessionStorage.getItem("box"));
174
- if (data) {
175
- this.data = data;
176
- } else {
177
- axios.get(__imgPath + "logo/app.json").then((res) => {
178
- const _data = flatten(Object.values(res.data || {}));
179
- this.data = _data.map((item) => ({
180
- img: item.key + ".svg",
181
- uuid: item.key,
182
- abbr: item.label,
183
- href: item.link,
184
- client: item.client || ["std", "origin"],
185
- }));
186
-
187
- if (!this.data.length) this.data = box;
188
- sessionStorage.setItem("box", JSON.stringify(this.data));
189
- });
190
- }
191
- } catch (e) {
192
- this.data = box;
193
- }
104
+ loadBoxMenu(box).then((data) => {
105
+ this.data = data;
106
+ });
194
107
  },
195
108
  onEsc(e) {
196
109
  if (e.key === "Escape") {
@@ -210,6 +123,8 @@ export default {
210
123
  }
211
124
  };
212
125
  Bus.on("toggleBox", this.__toggleHandler);
126
+ this.__docClickHandler = () => this.closeBox();
127
+ document.addEventListener("click", this.__docClickHandler);
213
128
  window.addEventListener("keydown", this.onEsc);
214
129
  },
215
130
  beforeUnmount() {
@@ -217,13 +132,11 @@ export default {
217
132
  Bus.off("toggleBox", this.__toggleHandler);
218
133
  this.__toggleHandler = null;
219
134
  }
135
+ if (this.__docClickHandler) {
136
+ document.removeEventListener("click", this.__docClickHandler);
137
+ this.__docClickHandler = null;
138
+ }
220
139
  window.removeEventListener("keydown", this.onEsc);
221
- this.setPageScrollLock(false);
222
- },
223
- watch: {
224
- status(val) {
225
- this.setPageScrollLock(!!val);
226
- },
227
140
  },
228
141
  };
229
142
  </script>
@@ -233,17 +146,13 @@ export default {
233
146
  .c-jx3box2-mask {
234
147
  position: fixed;
235
148
  left: 0;
236
- top: 0;
237
- z-index: 1200;
238
- width: 100vw;
239
- height: 100vh;
240
- display: flex;
241
- justify-content: center;
242
- align-items: center;
243
- padding: 20px;
244
- background: rgba(0, 0, 0, 0.72);
245
- backdrop-filter: blur(12px);
246
- -webkit-backdrop-filter: blur(12px);
149
+ top: @header-height;
150
+ z-index: 800;
151
+ width: 100%;
152
+ max-height: calc(100vh - @header-height);
153
+ overflow: hidden;
154
+ background: rgba(21, 25, 35, 0.98);
155
+ box-shadow: 0 18px 42px rgba(0, 0, 0, 0.36);
247
156
  }
248
157
  @media screen and (max-width: @phone) {
249
158
  .c-jx3box2-mask {
@@ -253,147 +162,33 @@ export default {
253
162
 
254
163
  .box2-fade-enter-active,
255
164
  .box2-fade-leave-active {
256
- transition: opacity 0.25s ease;
257
- }
258
-
259
- .box2-fade-enter-active .c-jx3box2,
260
- .box2-fade-leave-active .c-jx3box2 {
261
- transition: transform 0.25s ease, opacity 0.25s ease;
165
+ transition: opacity 0.2s ease, transform 0.2s ease;
262
166
  }
263
167
 
264
168
  .box2-fade-enter-from,
265
169
  .box2-fade-leave-to {
266
170
  opacity: 0;
267
- }
268
-
269
- .box2-fade-enter-from .c-jx3box2,
270
- .box2-fade-leave-to .c-jx3box2 {
271
- transform: translateY(16px) scale(0.96);
272
- opacity: 0;
171
+ transform: translateY(-12px);
273
172
  }
274
173
 
275
174
  .c-jx3box2 {
276
175
  position: relative;
277
- width: min(1120px, calc(100vw - 40px));
278
- max-height: min(860px, calc(100vh - 40px));
176
+ width: 100%;
177
+ max-height: calc(100vh - @header-height);
279
178
  display: flex;
280
179
  flex-direction: column;
281
180
  overflow: hidden;
282
- border-radius: 36px;
283
- border: 1px solid rgba(255, 255, 255, 0.1);
284
- background: rgba(21, 25, 35, 0.9);
285
- box-shadow: 0 30px 100px rgba(0, 0, 0, 0.45);
181
+ background: transparent;
286
182
  }
287
183
 
288
184
  .c-jx3box2 {
289
- .u-topline {
290
- height: 6px;
185
+ .u-body {
291
186
  width: 100%;
292
- background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.6), transparent);
293
- }
294
-
295
- .u-head {
296
- padding: 28px 32px 24px;
297
- display: flex;
298
- align-items: center;
299
- gap: 20px;
300
- justify-content: space-between;
301
- flex-wrap: wrap;
302
- }
303
-
304
- .u-title-wrap {
305
- display: flex;
306
- align-items: center;
307
- gap: 14px;
308
- }
309
-
310
- .u-logo {
311
- width: 46px;
312
- height: 46px;
313
- border-radius: 14px;
314
- background: #fff;
315
- color: #fff;
316
- display: inline-flex;
317
- align-items: center;
318
- justify-content: center;
319
- box-shadow: 0 8px 24px rgba(37, 99, 235, 0.38);
320
- padding:5px;
321
- }
322
-
323
- .u-title {
324
187
  margin: 0;
325
- color: #fff;
326
- font-size: 28px;
327
- line-height: 1.1;
328
- font-weight: 800;
329
- }
330
-
331
- .u-subtitle {
332
- margin: 6px 0 0;
333
- color: #60a5fa;
334
- font-size: 10px;
335
- letter-spacing: 0.26em;
336
- font-weight: 700;
337
- }
338
-
339
- .u-search {
340
- width: min(460px, 100%);
341
- margin-left: auto;
342
- position: relative;
343
- .flex;
344
- align-items: center;
345
- }
346
-
347
- .u-search-input {
348
- width: 100%;
349
- height: 48px;
350
- border-radius: 16px;
351
- border: 1px solid rgba(255, 255, 255, 0.12);
352
- background: rgba(0, 0, 0, 0.3);
353
- color: #fff;
354
- outline: none;
355
- padding: 0 40px 0 40px;
356
188
  box-sizing: border-box;
357
- transition: all 0.2s ease;
358
- }
359
-
360
- .u-search-input::placeholder {
361
- color: #6b7280;
362
- }
363
-
364
- .u-search-input:focus {
365
- border-color: rgba(59, 130, 246, 0.5);
366
- box-shadow: 0 0 20px rgba(59, 130, 246, 0.2);
367
- }
368
-
369
- .u-search-icon {
370
- position: absolute;
371
- left: 14px;
372
- top: 50%;
373
- transform: translateY(-50%);
374
- color: #6b7280;
375
- font-size: 16px;
376
- }
377
-
378
- .u-search-clear {
379
- position: absolute;
380
- right: 10px;
381
- top: 50%;
382
- transform: translateY(-50%);
383
- border: 0;
384
- background: transparent;
385
- color: #798192;
386
- cursor: pointer;
387
- }
388
-
389
- .u-search-clear:hover {
390
- color: #fff;
391
- }
392
-
393
- .u-body {
394
- padding: 4px 32px 24px;
189
+ padding: 16px 24px 22px;
395
190
  flex: 1;
396
- min-height: 360px;
191
+ min-height: 0;
397
192
  display: flex;
398
193
  flex-direction: column;
399
194
  overflow-y: auto;
@@ -404,18 +199,18 @@ export default {
404
199
  padding: 0;
405
200
  list-style: none;
406
201
  display: grid;
407
- grid-template-columns: repeat(8, minmax(0, 1fr));
408
- gap: 14px;
202
+ grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
203
+ gap: 10px;
409
204
  align-content: start;
410
205
  }
411
206
 
412
207
  .u-item {
413
- height: 116px;
208
+ height: 88px;
414
209
  display: flex;
415
210
  flex-direction: column;
416
211
  align-items: center;
417
212
  justify-content: center;
418
- border-radius: 22px;
213
+ border-radius: 12px;
419
214
  background: rgba(255, 255, 255, 0.03);
420
215
  border: 1px solid rgba(255, 255, 255, 0.07);
421
216
  text-decoration: none;
@@ -423,16 +218,16 @@ export default {
423
218
  }
424
219
 
425
220
  .u-item:hover {
426
- transform: translateY(-4px);
427
- border-color: rgba(59, 130, 246, 0.5);
428
- background: rgba(59, 130, 246, 0.12);
429
- box-shadow: 0 14px 26px -12px rgba(0, 0, 0, 0.55), 0 0 16px rgba(59, 130, 246, 0.2);
221
+ transform: translateY(-2px);
222
+ border-color: fade(@v4primary500, 50%);
223
+ background: fade(@v4primary500, 12%);
224
+ box-shadow: 0 14px 26px -12px rgba(0, 0, 0, 0.55), 0 0 16px fade(@v4primary500, 20%);
430
225
  }
431
226
 
432
227
  .u-icon-wrap {
433
- width: 54px;
434
- height: 54px;
435
- border-radius: 14px;
228
+ width: 40px;
229
+ height: 40px;
230
+ border-radius: 10px;
436
231
  background: rgba(255, 255, 255, 0.05);
437
232
  display: flex;
438
233
  align-items: center;
@@ -441,17 +236,17 @@ export default {
441
236
  }
442
237
 
443
238
  .u-item:hover .u-icon-wrap {
444
- background: rgba(37, 99, 235, 0.18);
239
+ background: fade(@v4primary, 18%);
445
240
  }
446
241
 
447
242
  .u-pic {
448
- width: 30px;
449
- height: 30px;
243
+ width: 26px;
244
+ height: 26px;
450
245
  object-fit: contain;
451
246
  }
452
247
 
453
248
  .u-txt {
454
- margin-top: 10px;
249
+ margin-top: 7px;
455
250
  font-size: 12px;
456
251
  color: #a8b0be;
457
252
  line-height: 1.2;
@@ -461,13 +256,34 @@ export default {
461
256
  color: #fff;
462
257
  }
463
258
 
464
- .u-empty {
465
- flex: 1;
259
+ .u-actions {
260
+ width: 100%;
261
+ margin: 0;
262
+ padding: 0 24px 14px;
263
+ box-sizing: border-box;
466
264
  display: flex;
265
+ justify-content: flex-end;
266
+ }
267
+
268
+ .u-collapse {
269
+ display: inline-flex;
467
270
  align-items: center;
468
- justify-content: center;
469
- color: #8b95a7;
470
- font-size: 13px;
271
+ gap: 5px;
272
+ padding: 7px 12px;
273
+ border: 1px solid rgba(255, 255, 255, 0.1);
274
+ border-radius: 6px;
275
+ background: rgba(255, 255, 255, 0.06);
276
+ color: #a8b0be;
277
+ font-size: 12px;
278
+ line-height: 1;
279
+ cursor: pointer;
280
+ transition: all 0.2s ease;
281
+ }
282
+
283
+ .u-collapse:hover {
284
+ border-color: fade(@v4primary500, 45%);
285
+ background: fade(@v4primary500, 14%);
286
+ color: #fff;
471
287
  }
472
288
 
473
289
  .u-foot {
@@ -493,7 +309,7 @@ export default {
493
309
  width: 6px;
494
310
  height: 6px;
495
311
  border-radius: 50%;
496
- background: #3b82f6;
312
+ background: @v4primary500;
497
313
  }
498
314
 
499
315
  .u-close {
@@ -511,13 +327,13 @@ export default {
511
327
 
512
328
  @media screen and (max-width: 1200px) {
513
329
  .u-list {
514
- grid-template-columns: repeat(6, minmax(0, 1fr));
330
+ grid-template-columns: repeat(auto-fill, minmax(86px, 1fr));
515
331
  }
516
332
  }
517
333
 
518
334
  @media screen and (max-width: 992px) {
519
335
  .u-list {
520
- grid-template-columns: repeat(5, minmax(0, 1fr));
336
+ grid-template-columns: repeat(auto-fill, minmax(82px, 1fr));
521
337
  }
522
338
  }
523
339
  }
@@ -2,16 +2,22 @@ import { $cms } from "@jx3box/jx3box-common/js/api";
2
2
  import { isMiniProgram, isApp } from "@jx3box/jx3box-common/js/utils";
3
3
  import User from "@jx3box/jx3box-common/js/user";
4
4
  import { applyJx3boxClientStatRules } from "./client-stat-business";
5
+ import packageInfo from "../../package.json";
5
6
 
6
7
  const INSTANCE_ID_KEY = "jx3box:device_id";
7
8
  const REPORT_DATE_KEY = "jx3box:client-stat-report-date";
8
9
  const REPORT_SIGNATURE_KEY = "jx3box:client-stat-report-signature";
9
10
  const STAT_SDK_VERSION = "v0.0.2";
11
+ const PACKAGE_VERSION = packageInfo.version;
12
+ const WEB_VERSION = `jx3box-ui@${PACKAGE_VERSION}`;
10
13
  const STARTUP_DELAY_MIN = 3 * 1000;
11
14
  const STARTUP_DELAY_MAX = 15 * 1000;
12
15
 
13
16
  let reportTask = null;
14
17
  let scheduledTimer = null;
18
+ let trafficPermission = null;
19
+ let trafficPermissionCheckedAt = 0;
20
+ const TRAFFIC_PERMISSION_MAX_AGE = 5 * 60 * 1000;
15
21
 
16
22
  function compactPayload(payload) {
17
23
  return Object.fromEntries(
@@ -97,6 +103,12 @@ function resolveClient(platform, userAgent) {
97
103
  return "unknown";
98
104
  }
99
105
 
106
+ export function resolveCurrentClientSurface() {
107
+ const nav = window.navigator || {};
108
+ const userAgent = String(nav.userAgent || "");
109
+ return resolveClient(resolvePlatform(userAgent, nav.platform), userAgent);
110
+ }
111
+
100
112
  function parseBrowser(userAgent) {
101
113
  const candidates = [
102
114
  ["Edge", /EdgA?[\s/]([\d.]+)/i],
@@ -141,10 +153,6 @@ function resolveDisplayMode() {
141
153
  return "browser";
142
154
  }
143
155
 
144
- function resolveWebVersion() {
145
- return window.__APP_VERSION__ || process.env.VUE_APP_VERSION || process.env.VITE_APP_VERSION || null;
146
- }
147
-
148
156
  export function buildClientStatPayload(instanceId) {
149
157
  const nav = window.navigator || {};
150
158
  const screen = window.screen || {};
@@ -165,7 +173,9 @@ export function buildClientStatPayload(instanceId) {
165
173
  platform: context.platform,
166
174
  domain: window.location.hostname,
167
175
  channel: resolveChannel(),
168
- web_version: resolveWebVersion(),
176
+ app_version: "jx3box-ui",
177
+ app_build: PACKAGE_VERSION,
178
+ web_version: WEB_VERSION,
169
179
  sdk_version: STAT_SDK_VERSION,
170
180
 
171
181
  os_name: system.os_name,
@@ -208,7 +218,7 @@ export function buildClientStatPayload(instanceId) {
208
218
 
209
219
  function getReportSignature() {
210
220
  const uid = User.isLogin() ? Number(User.getInfo()?.uid || localStorage.getItem("uid") || 0) : 0;
211
- return `${uid}:${resolveWebVersion() || "unknown"}`;
221
+ return `${uid}:${WEB_VERSION}`;
212
222
  }
213
223
 
214
224
  export async function reportClientStat({ force = false } = {}) {
@@ -240,6 +250,24 @@ export async function reportClientStat({ force = false } = {}) {
240
250
  return reportTask;
241
251
  }
242
252
 
253
+ export async function resolveClientTrafficPermission({ force = false } = {}) {
254
+ if (!force && trafficPermission && Date.now() - trafficPermissionCheckedAt < TRAFFIC_PERMISSION_MAX_AGE) {
255
+ return trafficPermission;
256
+ }
257
+ try {
258
+ const result = await reportClientStat({ force: true });
259
+ const data = result?.data;
260
+ if (data && typeof data.traffic_allowed === "boolean") {
261
+ trafficPermission = data;
262
+ trafficPermissionCheckedAt = Date.now();
263
+ return data;
264
+ }
265
+ } catch (error) {
266
+ // Traffic remains fail-closed when heartbeat cannot confirm permission.
267
+ }
268
+ return null;
269
+ }
270
+
243
271
  function isLocalhost() {
244
272
  return ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname);
245
273
  }