@jx3box/jx3box-editor 3.2.10 → 3.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jx3box/jx3box-editor",
3
- "version": "3.2.10",
3
+ "version": "3.2.12",
4
4
  "description": "JX3BOX Article & Editor",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/Article.vue CHANGED
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="c-article-tinymce c-article-box">
2
+ <Author mode="popover" class="c-article-tinymce c-article-box" data-jx3-element-scope>
3
3
  <!-- <div id="c-article-origin" class="c-article-origin" ref="origin"><slot></slot></div> -->
4
4
 
5
5
  <div id="c-article" class="c-article" ref="article" v-if="pageable">
@@ -37,7 +37,7 @@
37
37
  :total="total"
38
38
  ></el-pagination>
39
39
 
40
- <div class="w-jx3-element-pop" :style="jx3_element.style">
40
+ <div ref="jx3ElementPop" class="w-jx3-element-pop" :style="jx3_element.style">
41
41
  <jx3-item :item_id="item.id" :jx3ClientType="item.client" v-show="jx3_element.type == 'item'" />
42
42
  <jx3-buff :client="buff.client" :id="buff.id" :level="buff.level" v-show="jx3_element.type == 'buff'" />
43
43
  <jx3-skill
@@ -47,15 +47,9 @@
47
47
  v-show="jx3_element.type == 'skill'"
48
48
  />
49
49
  <jx3-npc :client="npc.client" :id="npc.id" v-show="jx3_element.type === 'npc'" />
50
- <jx3-author
51
- :key="author.id"
52
- :uid="author.id"
53
- :visible="jx3_element.type === 'author' && jx3_element.style.display !== 'none'"
54
- v-show="jx3_element.type === 'author'"
55
- />
56
50
  <jx3-emotion-author :id="emotion.id" v-show="jx3_element.type === 'emotion'" />
57
51
  </div>
58
- </div>
52
+ </Author>
59
53
  </template>
60
54
 
61
55
  <script>
@@ -97,6 +91,33 @@ import Skill from "./Skill.vue";
97
91
  import Npc from "./Npc.vue";
98
92
  import renderJx3Element from "./assets/js/jx3_element.js";
99
93
 
94
+ const NETEASE_MUSIC_PLAYER_SRC_RE =
95
+ /(\s+src\s*=\s*)(["'])(?:https?:)?\/\/music\.163\.com\/(?:m\/)?outchain\/player(?=[?#]|\2)/i;
96
+
97
+ function isMobileArticleRuntime() {
98
+ if (typeof window !== "undefined" && typeof window.Capacitor?.isNativePlatform === "function") {
99
+ if (window.Capacitor.isNativePlatform()) return true;
100
+ }
101
+ if (typeof navigator === "undefined") return false;
102
+ if (navigator.userAgentData?.mobile === true) return true;
103
+
104
+ const userAgent = navigator.userAgent || "";
105
+ return (
106
+ /Android|iPhone|iPad|iPod|Mobile|HarmonyOS|OpenHarmony|ArkWeb/i.test(userAgent) ||
107
+ (/Macintosh/i.test(userAgent) && navigator.maxTouchPoints > 1)
108
+ );
109
+ }
110
+
111
+ function normalizeNeteaseMusicPlayerIframe(content) {
112
+ const playerUrl = isMobileArticleRuntime()
113
+ ? "https://music.163.com/m/outchain/player"
114
+ : "https://music.163.com/outchain/player";
115
+
116
+ return String(content || "").replace(/<iframe\b[^>]*>/gi, (iframe) =>
117
+ iframe.replace(NETEASE_MUSIC_PLAYER_SRC_RE, `$1$2${playerUrl}`)
118
+ );
119
+ }
120
+
100
121
  export default {
101
122
  name: "Article",
102
123
  props: {
@@ -176,9 +197,6 @@ export default {
176
197
  client: "std",
177
198
  id: "",
178
199
  },
179
- author: {
180
- id: "",
181
- },
182
200
  emotion: {
183
201
  id: "",
184
202
  },
@@ -187,7 +205,7 @@ export default {
187
205
  style: {
188
206
  top: 0,
189
207
  left: 0,
190
- display: "block",
208
+ display: "none",
191
209
  },
192
210
  type: "",
193
211
  },
@@ -238,10 +256,13 @@ export default {
238
256
  ...this.iframeWhitelist,
239
257
  ]);
240
258
 
241
- // 3. 处理图片懒加载
259
+ // 3. 避开网易云播放器在不同终端间跳转到 HTTP 地址
260
+ data = normalizeNeteaseMusicPlayerIframe(data);
261
+
262
+ // 4. 处理图片懒加载
242
263
  data = execLazyload(data, this.cdnDomain);
243
264
 
244
- // 4. 最后处理链接
265
+ // 5. 最后处理链接
245
266
  data = execFilterLink(data, this.linkWhitelist, this.linkStrict);
246
267
 
247
268
  return data;
@@ -299,7 +320,7 @@ export default {
299
320
  // 音频
300
321
  renderAudio();
301
322
  // 物品
302
- renderJx3Element(this);
323
+ renderJx3Element(this, '.w-jx3-element:not([data-type="author"])');
303
324
  },
304
325
  doDir: function () {
305
326
  // 显示局部
@@ -363,17 +384,19 @@ export default {
363
384
  }
364
385
  }
365
386
 
366
- if (version !== this.renderVersion) return;
387
+ if (version !== this.renderVersion) return null;
367
388
  this.data = result;
389
+ return version;
368
390
  },
369
391
  run: async function () {
370
392
  this.page = 1;
371
393
  this.all = false;
372
- await this.render();
373
- if (!this.data.length) return;
394
+ const version = await this.render();
395
+ if (!version || version !== this.renderVersion || !this.data.length) return;
374
396
 
375
397
  // 等待html加载完毕后
376
398
  this.$nextTick(() => {
399
+ if (version !== this.renderVersion) return;
377
400
  this.$emit("contentLoaded");
378
401
 
379
402
  // 统一DOM处理
@@ -399,12 +422,22 @@ export default {
399
422
  this.mode = params.get("mode") || "";
400
423
  this.run();
401
424
  },
425
+ beforeUnmount: function () {
426
+ this.renderVersion++;
427
+ if (typeof this.__jx3ElementCleanup === "function") {
428
+ this.__jx3ElementCleanup();
429
+ }
430
+ if (this.__jx3ElementPop?.length && this.__jx3ElementPop.parent()[0] === document.body) {
431
+ this.__jx3ElementPop.remove();
432
+ }
433
+ this.__jx3ElementPop = null;
434
+ },
402
435
  components: {
403
436
  "jx3-item": Item,
404
437
  "jx3-buff": Buff,
405
438
  "jx3-skill": Skill,
406
439
  "jx3-npc": Npc,
407
- "jx3-author": Author,
440
+ Author,
408
441
  "jx3-emotion-author": PostAuthor,
409
442
  },
410
443
  };
@@ -1,4 +1,5 @@
1
1
  import { load } from "cheerio";
2
+ import { resolveImagePath } from "@jx3box/jx3box-common/js/utils";
2
3
 
3
4
  function joinDomain(domain, path) {
4
5
  const d = String(domain || "").replace(/\/+$/, "");
@@ -16,7 +17,13 @@ function isAbsoluteLike(src) {
16
17
 
17
18
  function normalizeImgSrc(src, domain) {
18
19
  const s = String(src || "").trim();
19
- if (!s || !domain) return s;
20
+ if (!s) return s;
21
+
22
+ // 历史 OSS 图片域名先统一转换为当前 CDN,随后再处理可选的相对路径拼接。
23
+ const resolved = resolveImagePath(s);
24
+ if (resolved && resolved !== s) return resolved;
25
+
26
+ if (!domain) return s;
20
27
  if (isAbsoluteLike(s)) return s;
21
28
 
22
29
  // 最常见:/uploads/xxx
@@ -52,15 +59,12 @@ export default function lazyLoad(html, domain = "") {
52
59
  // 固定懒加载:无论如何都加/覆盖
53
60
  img.attr("loading", "lazy");
54
61
 
55
- // 只有传了 domain 才处理 src 拼接
56
- if (domain) {
57
- const src = img.attr("src");
58
- if (src) img.attr("src", normalizeImgSrc(src, domain));
62
+ // 无论是否传入 domain,都先统一历史 OSS/CDN 地址;相对路径仅在有 domain 时拼接。
63
+ const src = img.attr("src");
64
+ if (src) img.attr("src", normalizeImgSrc(src, domain));
59
65
 
60
- // 如果你有用 data-src 之类,也可以一起处理(可删)
61
- const dataSrc = img.attr("data-src");
62
- if (dataSrc) img.attr("data-src", normalizeImgSrc(dataSrc, domain));
63
- }
66
+ const dataSrc = img.attr("data-src");
67
+ if (dataSrc) img.attr("data-src", normalizeImgSrc(dataSrc, domain));
64
68
  });
65
69
 
66
70
  return $("root").html() || "";
@@ -1,25 +1,37 @@
1
1
  import $ from "jquery";
2
+ import { onBeforeUnmount } from "vue";
3
+
4
+ const SCOPE_ATTR = "data-jx3-element-scope";
5
+ const POP_OWNER_ATTR = "data-jx3-element-pop-owner";
6
+ const CLEANUP_REGISTERED = new WeakSet();
2
7
 
3
8
  function renderItem(vm, selector = ".w-jx3-element") {
9
+ if (typeof vm?.__jx3ElementCleanup === "function") {
10
+ vm.__jx3ElementCleanup();
11
+ }
12
+
4
13
  let outer, inner;
5
14
  const pop_class = ".w-jx3-element-pop";
6
15
  const scopeEl = vm && vm.$el ? vm.$el : document;
7
16
  const $scope = $(scopeEl);
8
- const uid = vm && vm._uid ? vm._uid : "default";
17
+ const uid = vm?.$?.uid ?? vm?._uid ?? "default";
18
+ const scopeId = `jx3box-element-${uid}`;
9
19
  const ns = `.jx3boxElement_${uid}`;
10
20
  let lastTappedElement = null;
11
21
  const debugPop = false;
12
22
 
23
+ if (scopeEl !== document && scopeEl?.setAttribute) {
24
+ scopeEl.setAttribute(SCOPE_ATTR, scopeId);
25
+ }
26
+
13
27
  const isMobile = () => {
14
28
  if (typeof window === "undefined") return false;
15
29
 
16
30
  const coarsePointer =
17
- typeof window.matchMedia === "function" &&
18
- window.matchMedia("(hover: none) and (pointer: coarse)").matches;
31
+ typeof window.matchMedia === "function" && window.matchMedia("(hover: none) and (pointer: coarse)").matches;
19
32
  const hasTouchEvent = "ontouchstart" in window;
20
33
  const mobileUA =
21
- typeof navigator !== "undefined" &&
22
- /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent || "");
34
+ typeof navigator !== "undefined" && /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent || "");
23
35
 
24
36
  return coarsePointer || (hasTouchEvent && mobileUA);
25
37
  };
@@ -31,9 +43,21 @@ function renderItem(vm, selector = ".w-jx3-element") {
31
43
  document.documentElement.contains(vm.__jx3ElementPop[0])
32
44
  ? vm.__jx3ElementPop
33
45
  : null;
34
- const $pop = cachedPop || $scope.find(pop_class).first();
46
+ const refPop = vm?.$refs?.jx3ElementPop;
47
+ const $refPop = refPop ? $(refPop.$el || refPop) : null;
48
+ const $ownedPop = $scope
49
+ .find(pop_class)
50
+ .filter(function () {
51
+ if (scopeEl === document) {
52
+ return !$(this).closest(`[${SCOPE_ATTR}]`).length;
53
+ }
54
+ return $(this).closest(`[${SCOPE_ATTR}]`)[0] === scopeEl;
55
+ })
56
+ .first();
57
+ const $pop = cachedPop || ($refPop?.length ? $refPop : $ownedPop);
35
58
 
36
59
  if (!$pop.length) return;
60
+ $pop.attr(POP_OWNER_ATTR, scopeId);
37
61
  if (isMobile() && vm) {
38
62
  vm.__jx3ElementPop = $pop;
39
63
  if ($pop.parent()[0] !== document.body) {
@@ -47,6 +71,12 @@ function renderItem(vm, selector = ".w-jx3-element") {
47
71
  return $(e.currentTarget || e.target).closest(selector);
48
72
  };
49
73
 
74
+ const isCurrentScopeTarget = ($target) => {
75
+ if (!$target?.length) return false;
76
+ const closestScope = $target.closest(`[${SCOPE_ATTR}]`)[0];
77
+ return scopeEl === document ? !closestScope : closestScope === scopeEl;
78
+ };
79
+
50
80
  const getDataTarget = ($target, e) => {
51
81
  const $eventTarget = $(e?.target);
52
82
  const $fromEvent = $eventTarget.closest("[data-type]");
@@ -73,16 +103,21 @@ function renderItem(vm, selector = ".w-jx3-element") {
73
103
  if (!type) return "";
74
104
 
75
105
  if (type === "item") {
106
+ if (!vm.item) return "";
76
107
  vm.item.id = $dataTarget.attr("data-id");
77
108
  vm.item.client = $dataTarget.attr("data-client") == "origin" ? 2 : 1;
78
109
  } else if (type === "author") {
110
+ if (!vm.author) return "";
79
111
  vm.author.id = $dataTarget.attr("data-id");
80
112
  } else if (type === "emotion") {
113
+ if (!vm.emotion) return "";
81
114
  vm.emotion.id = $dataTarget.attr("data-id");
82
115
  } else if (vm[type]) {
83
116
  vm[type].client = $dataTarget.attr("data-client");
84
117
  vm[type].id = $dataTarget.attr("data-id");
85
118
  vm[type].level = $dataTarget.attr("data-level");
119
+ } else {
120
+ return "";
86
121
  }
87
122
 
88
123
  vm.jx3_element.type = type;
@@ -90,12 +125,12 @@ function renderItem(vm, selector = ".w-jx3-element") {
90
125
  };
91
126
 
92
127
  const showPop = ($dataTarget, point = {}, $targetForRect = null) => {
128
+ const type = syncElementData($dataTarget, $targetForRect);
129
+ if (!type) return false;
130
+
93
131
  clearTimeout(outer);
94
132
  clearTimeout(inner);
95
133
 
96
- const type = syncElementData($dataTarget, $targetForRect);
97
- if (!type) return;
98
-
99
134
  const fallbackRect = $targetForRect && $targetForRect.length ? $targetForRect[0].getBoundingClientRect() : null;
100
135
  const fallbackX = fallbackRect ? fallbackRect.left + fallbackRect.width / 2 : 0;
101
136
  const fallbackY = fallbackRect ? fallbackRect.top + fallbackRect.height / 2 : 0;
@@ -136,6 +171,8 @@ function renderItem(vm, selector = ".w-jx3-element") {
136
171
  vm.jx3_element.style.maxHeight = "";
137
172
  vm.jx3_element.style.overflow = "";
138
173
  }
174
+
175
+ return true;
139
176
  };
140
177
 
141
178
  const hidePop = () => {
@@ -151,13 +188,16 @@ function renderItem(vm, selector = ".w-jx3-element") {
151
188
  $scope.on(`mouseenter${ns}`, selector, function (e) {
152
189
  if (isMobile()) return;
153
190
  const $target = getCurrentTarget(this, e);
191
+ if (!isCurrentScopeTarget($target)) return;
154
192
  const $dataTarget = getDataTarget($target, e);
155
193
  showPop($dataTarget, e, $target);
156
194
  });
157
195
 
158
196
  // 移除时
159
- $scope.on(`mouseleave${ns}`, selector, function () {
197
+ $scope.on(`mouseleave${ns}`, selector, function (e) {
160
198
  if (isMobile()) return;
199
+ const $target = getCurrentTarget(this, e);
200
+ if (!isCurrentScopeTarget($target)) return;
161
201
  outer = setTimeout(() => {
162
202
  hidePop();
163
203
  }, 380);
@@ -167,6 +207,7 @@ function renderItem(vm, selector = ".w-jx3-element") {
167
207
  if (!isMobile()) return;
168
208
 
169
209
  const $target = getCurrentTarget(this, e);
210
+ if (!isCurrentScopeTarget($target)) return;
170
211
  const $dataTarget = getDataTarget($target, e);
171
212
  if (!$dataTarget.length) return;
172
213
 
@@ -182,9 +223,15 @@ function renderItem(vm, selector = ".w-jx3-element") {
182
223
  return;
183
224
  }
184
225
 
226
+ const handled = showPop($dataTarget, point, $target);
227
+ if (!handled) {
228
+ lastTappedElement = null;
229
+ hidePop();
230
+ return;
231
+ }
232
+
185
233
  e.preventDefault();
186
234
  lastTappedElement = tappedElement;
187
- showPop($dataTarget, point, $target);
188
235
  });
189
236
 
190
237
  // POP内停留
@@ -206,13 +253,45 @@ function renderItem(vm, selector = ".w-jx3-element") {
206
253
  if (!isMobile()) return;
207
254
  const $target = $(e.target);
208
255
  const $trigger = $target.closest(selector);
209
- const isCurrentTrigger = $trigger.length && ($trigger[0] === scopeEl || $scope.has($trigger[0]).length);
256
+ const isCurrentTrigger = $trigger.length && isCurrentScopeTarget($trigger);
257
+ const isCurrentPop = $target.closest(pop_class)[0] === $pop[0];
210
258
 
211
- if (isCurrentTrigger || $target.closest(pop_class).length) return;
259
+ if (isCurrentTrigger || isCurrentPop) return;
212
260
 
213
261
  lastTappedElement = null;
214
262
  hidePop();
215
263
  });
264
+
265
+ const cleanup = () => {
266
+ clearTimeout(outer);
267
+ clearTimeout(inner);
268
+ $scope.off(ns);
269
+ $pop.off(ns);
270
+ $(document).off(ns);
271
+ if (vm?.__jx3ElementCleanup === cleanup) {
272
+ vm.__jx3ElementCleanup = null;
273
+ }
274
+ };
275
+
276
+ if (vm) {
277
+ vm.__jx3ElementCleanup = cleanup;
278
+ }
279
+
280
+ const internalInstance = vm?.$;
281
+ if (internalInstance && !CLEANUP_REGISTERED.has(internalInstance)) {
282
+ CLEANUP_REGISTERED.add(internalInstance);
283
+ onBeforeUnmount(() => {
284
+ if (typeof vm.__jx3ElementCleanup === "function") {
285
+ vm.__jx3ElementCleanup();
286
+ }
287
+ if (vm.__jx3ElementPop?.length && vm.__jx3ElementPop.parent()[0] === document.body) {
288
+ vm.__jx3ElementPop.remove();
289
+ }
290
+ vm.__jx3ElementPop = null;
291
+ }, internalInstance);
292
+ }
293
+
294
+ return cleanup;
216
295
  }
217
296
 
218
297
  export default renderItem;
@@ -1,6 +1,43 @@
1
1
  <template>
2
+ <div
3
+ v-if="isPopoverMode"
4
+ v-bind="$attrs"
5
+ data-jx3-author-pop-scope
6
+ @mouseover="onTriggerMouseOver"
7
+ @mouseout="onTriggerMouseOut"
8
+ @click="onTriggerClick"
9
+ >
10
+ <slot></slot>
11
+
12
+ <el-popover
13
+ ref="popover"
14
+ :visible="popoverVisible"
15
+ :virtual-ref="activeTrigger"
16
+ virtual-triggering
17
+ teleported
18
+ placement="bottom-start"
19
+ width="auto"
20
+ :offset="10"
21
+ :show-arrow="false"
22
+ :popper-style="popoverStyle"
23
+ popper-class="w-jx3-author-popper"
24
+ >
25
+ <div ref="popoverContent" @mouseenter="onPopoverMouseEnter" @mouseleave="onPopoverMouseLeave">
26
+ <Author
27
+ v-if="activeUid"
28
+ mode="card"
29
+ :key="activeUid"
30
+ :uid="activeUid"
31
+ :visible="popoverVisible"
32
+ :inside-popover="true"
33
+ @loaded="updatePopoverPosition"
34
+ />
35
+ </div>
36
+ </el-popover>
37
+ </div>
38
+
2
39
  <!-- @圈人pop:作者卡片 -->
3
- <div class="w-author" v-loading="loading">
40
+ <div v-else v-bind="$attrs" class="w-author" v-loading="loading">
4
41
  <div class="w-author-wrapper el-popover" :class="{ 'is-no-atcard': !bg }" v-if="data" :style="authorCardStyle">
5
42
  <div class="u-author">
6
43
  <Avatar
@@ -12,7 +49,14 @@
12
49
  />
13
50
  <div class="u-info">
14
51
  <a class="u-name" :href="authorLink(uid)" target="_blank">
15
- <el-tooltip class="item" effect="dark" content="签约作者" placement="top" v-if="isSuperAuthor">
52
+ <el-tooltip
53
+ class="item"
54
+ effect="dark"
55
+ content="签约作者"
56
+ placement="top"
57
+ :teleported="!insidePopover"
58
+ v-if="isSuperAuthor"
59
+ >
16
60
  <a class="u-superauthor" href="/dashboard/cooperation" target="_blank">
17
61
  <img :src="super_author_icon" alt="superauthor" />
18
62
  </a>
@@ -20,7 +64,7 @@
20
64
  <span class="u-displayname" :title="data.display_name">{{ data.display_name || "未知" }}</span>
21
65
  </a>
22
66
  <div class="u-extend">
23
- <el-tooltip class="item" effect="dark" placement="top">
67
+ <el-tooltip class="item" effect="dark" placement="top" :teleported="!insidePopover">
24
68
  <template #content>
25
69
  <span class="u-tips">经验值:{{ data.experience }}</span>
26
70
  </template>
@@ -33,7 +77,14 @@
33
77
  >Lv.{{ level }}</span
34
78
  >
35
79
  </el-tooltip>
36
- <el-tooltip class="item" effect="dark" :content="vipTypeTitle" placement="top" v-if="isVip">
80
+ <el-tooltip
81
+ class="item"
82
+ effect="dark"
83
+ :content="vipTypeTitle"
84
+ placement="top"
85
+ :teleported="!insidePopover"
86
+ v-if="isVip"
87
+ >
37
88
  <a class="u-vip" href="/vip/premium?from=sidebar_author" target="_blank">
38
89
  <i class="i-icon-vip on">{{ vipType }}</i>
39
90
  </a>
@@ -96,6 +147,7 @@ const DEFAULT_ATCARD_POSITION = "right top";
96
147
  const DECORATION_JSON = "decoration_json";
97
148
  const DECORATION_KEY = "decoration_me";
98
149
  const HONOR_KEY = "honor_me";
150
+ const AUTHOR_SCOPE_ATTR = "data-jx3-author-pop-scope";
99
151
  const POSITION_MAP = {
100
152
  lt: "left top",
101
153
  mt: "center top",
@@ -117,18 +169,33 @@ const POSITION_MAP = {
117
169
  const { __server, __imgPath, __userLevelColor, __cdn, __Root, __userLevel } = JX3BOX;
118
170
  export default {
119
171
  name: "Author",
172
+ inheritAttrs: false,
173
+ emits: ["loaded"],
120
174
  components: {
121
175
  Avatar,
122
176
  },
123
177
  props: {
124
178
  uid: {
125
179
  type: [String, Number],
126
- required: true,
180
+ default: "",
127
181
  },
128
182
  visible: {
129
183
  type: Boolean,
130
184
  default: true,
131
185
  },
186
+ mode: {
187
+ type: String,
188
+ default: "card",
189
+ validator: (value) => ["card", "popover"].includes(value),
190
+ },
191
+ selector: {
192
+ type: String,
193
+ default: '.w-jx3-element[data-type="author"]',
194
+ },
195
+ insidePopover: {
196
+ type: Boolean,
197
+ default: false,
198
+ },
132
199
  },
133
200
  data: () => ({
134
201
  data: null,
@@ -138,8 +205,24 @@ export default {
138
205
  bgPosition: DEFAULT_ATCARD_POSITION,
139
206
  honor: "",
140
207
  honorStyle: {},
208
+ activeUid: "",
209
+ activeTrigger: null,
210
+ popoverVisible: false,
211
+ hideTriggerTimer: null,
212
+ hidePopoverTimer: null,
213
+ lastTappedElement: null,
214
+ documentClickBound: false,
215
+ popoverStyle: {
216
+ padding: "0",
217
+ border: "none",
218
+ background: "transparent",
219
+ boxShadow: "none",
220
+ },
141
221
  }),
142
222
  computed: {
223
+ isPopoverMode: function () {
224
+ return this.mode === "popover";
225
+ },
143
226
  super_author_icon: function () {
144
227
  return __imgPath + "image/user/" + "superauthor.svg";
145
228
  },
@@ -194,8 +277,126 @@ export default {
194
277
  }
195
278
  },
196
279
  },
280
+ mode() {
281
+ if (!this.$el) return;
282
+ this.syncDocumentClickListener();
283
+ if (!this.isPopoverMode) {
284
+ this.lastTappedElement = null;
285
+ this.hidePopover();
286
+ this.clearPopoverTimers();
287
+ }
288
+ },
289
+ },
290
+ mounted() {
291
+ this.syncDocumentClickListener();
292
+ },
293
+ beforeUnmount() {
294
+ this.clearPopoverTimers();
295
+ document.removeEventListener("click", this.onDocumentClick);
296
+ this.documentClickBound = false;
197
297
  },
198
298
  methods: {
299
+ syncDocumentClickListener() {
300
+ if (this.isPopoverMode && !this.documentClickBound) {
301
+ document.addEventListener("click", this.onDocumentClick);
302
+ this.documentClickBound = true;
303
+ } else if (!this.isPopoverMode && this.documentClickBound) {
304
+ document.removeEventListener("click", this.onDocumentClick);
305
+ this.documentClickBound = false;
306
+ }
307
+ },
308
+ isMobile() {
309
+ const coarsePointer =
310
+ typeof window.matchMedia === "function" &&
311
+ window.matchMedia("(hover: none) and (pointer: coarse)").matches;
312
+ const hasTouchEvent = "ontouchstart" in window;
313
+ const mobileUA = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent || "");
314
+ return coarsePointer || (hasTouchEvent && mobileUA);
315
+ },
316
+ getOwnedTrigger(target) {
317
+ if (!(target instanceof Element)) return null;
318
+ const trigger = target.closest(this.selector);
319
+ if (!trigger || !this.$el.contains(trigger)) return null;
320
+ return trigger.closest(`[${AUTHOR_SCOPE_ATTR}]`) === this.$el ? trigger : null;
321
+ },
322
+ isSameTriggerTransition(trigger, relatedTarget) {
323
+ return relatedTarget instanceof Node && trigger.contains(relatedTarget);
324
+ },
325
+ onTriggerMouseOver(event) {
326
+ if (!this.isPopoverMode || this.isMobile()) return;
327
+ const trigger = this.getOwnedTrigger(event.target);
328
+ if (!trigger) return;
329
+ event.stopPropagation();
330
+ if (this.isSameTriggerTransition(trigger, event.relatedTarget)) return;
331
+ this.showPopover(trigger);
332
+ },
333
+ onTriggerMouseOut(event) {
334
+ if (!this.isPopoverMode || this.isMobile()) return;
335
+ const trigger = this.getOwnedTrigger(event.target);
336
+ if (!trigger) return;
337
+ event.stopPropagation();
338
+ if (this.isSameTriggerTransition(trigger, event.relatedTarget)) return;
339
+ clearTimeout(this.hideTriggerTimer);
340
+ this.hideTriggerTimer = setTimeout(this.hidePopover, 380);
341
+ },
342
+ onTriggerClick(event) {
343
+ if (!this.isPopoverMode || !this.isMobile()) return;
344
+ const trigger = this.getOwnedTrigger(event.target);
345
+ if (!trigger) return;
346
+ event.stopPropagation();
347
+
348
+ const href = trigger.closest("a[href]")?.getAttribute("href") || "";
349
+ if (href && this.lastTappedElement === trigger) {
350
+ this.lastTappedElement = null;
351
+ this.hidePopover();
352
+ return;
353
+ }
354
+
355
+ event.preventDefault();
356
+ this.lastTappedElement = trigger;
357
+ this.showPopover(trigger);
358
+ },
359
+ onDocumentClick(event) {
360
+ if (!this.isPopoverMode || !this.isMobile()) return;
361
+ const inPopover = this.$refs.popoverContent?.contains(event.target);
362
+ if (this.getOwnedTrigger(event.target) || inPopover) return;
363
+ this.lastTappedElement = null;
364
+ this.hidePopover();
365
+ },
366
+ onPopoverMouseEnter() {
367
+ if (this.isMobile()) return;
368
+ clearTimeout(this.hideTriggerTimer);
369
+ clearTimeout(this.hidePopoverTimer);
370
+ },
371
+ onPopoverMouseLeave() {
372
+ if (this.isMobile()) return;
373
+ clearTimeout(this.hidePopoverTimer);
374
+ this.hidePopoverTimer = setTimeout(this.hidePopover, 280);
375
+ },
376
+ showPopover(trigger) {
377
+ const uid = trigger.getAttribute("data-id");
378
+ if (!uid) return;
379
+
380
+ clearTimeout(this.hideTriggerTimer);
381
+ clearTimeout(this.hidePopoverTimer);
382
+ this.activeUid = uid;
383
+ this.activeTrigger = trigger;
384
+ this.popoverVisible = true;
385
+ this.updatePopoverPosition();
386
+ },
387
+ updatePopoverPosition() {
388
+ if (!this.popoverVisible) return;
389
+ this.$nextTick(() => {
390
+ this.$refs.popover?.popperRef?.popperInstanceRef?.update?.();
391
+ });
392
+ },
393
+ hidePopover() {
394
+ this.popoverVisible = false;
395
+ },
396
+ clearPopoverTimers() {
397
+ clearTimeout(this.hideTriggerTimer);
398
+ clearTimeout(this.hidePopoverTimer);
399
+ },
199
400
  loadData: function () {
200
401
  const promises = [getUserInfo(this.uid), getUserMedals(this.uid)];
201
402
  this.loading = true;
@@ -203,6 +404,7 @@ export default {
203
404
  this.data = res[0];
204
405
  this.medals = res[1];
205
406
  this.loading = false;
407
+ this.$nextTick(() => this.$emit("loaded"));
206
408
  });
207
409
  },
208
410
  loadUserInfo: function () {
@@ -446,11 +648,13 @@ export default {
446
648
  return {
447
649
  key: role?.id || role?.key || role?.val || name + index,
448
650
  name,
449
- server: role?.server || role?.role_server || role?.jx3_server || info?.server || info?.role_server || "",
651
+ server:
652
+ role?.server || role?.role_server || role?.jx3_server || info?.server || info?.role_server || "",
450
653
  url: this.normalizeLink(role?.url || role?.link || role?.href || info?.url || info?.link || ""),
451
654
  icon: this.normalizeRoleIcon(role, info),
452
655
  color: role?.color || info?.color || "",
453
- backgroundColor: role?.background_color || role?.bg_color || info?.background_color || info?.bg_color || "",
656
+ backgroundColor:
657
+ role?.background_color || role?.bg_color || info?.background_color || info?.bg_color || "",
454
658
  borderColor: role?.border_color || info?.border_color || "",
455
659
  };
456
660
  },
@@ -461,7 +665,8 @@ export default {
461
665
  return __cdn + url.replace(/^\/+/, "");
462
666
  },
463
667
  normalizeRoleIcon: function (role, info) {
464
- const mount = role?.mount || role?.school || role?.school_id || info?.mount || info?.school || info?.school_id || "";
668
+ const mount =
669
+ role?.mount || role?.school || role?.school_id || info?.mount || info?.school || info?.school_id || "";
465
670
  if (mount) return showSchoolIcon(mount);
466
671
 
467
672
  return this.normalizeRoleAsset(role?.icon || role?.logo || role?.image || info?.icon || info?.logo || "");
@@ -486,6 +691,13 @@ export default {
486
691
 
487
692
  <style lang="less">
488
693
  @import "../assets/css/module/author.less";
694
+ .w-jx3-author-popper.el-popover {
695
+ min-width: 0;
696
+ padding: 0;
697
+ border: none;
698
+ background: transparent;
699
+ box-shadow: none;
700
+ }
489
701
  .w-author {
490
702
  .w-author-wrapper {
491
703
  width: 300px;