@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 +1 -1
- package/src/Article.vue +53 -20
- package/src/assets/js/img.js +13 -9
- package/src/assets/js/jx3_element.js +92 -13
- package/src/components/Author.vue +220 -8
package/package.json
CHANGED
package/src/Article.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
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
|
-
</
|
|
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: "
|
|
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
|
-
//
|
|
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
|
-
|
|
440
|
+
Author,
|
|
408
441
|
"jx3-emotion-author": PostAuthor,
|
|
409
442
|
},
|
|
410
443
|
};
|
package/src/assets/js/img.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
|
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
|
|
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
|
|
256
|
+
const isCurrentTrigger = $trigger.length && isCurrentScopeTarget($trigger);
|
|
257
|
+
const isCurrentPop = $target.closest(pop_class)[0] === $pop[0];
|
|
210
258
|
|
|
211
|
-
if (isCurrentTrigger ||
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
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;
|