@jx3box/jx3box-vue3-ui 1.1.13 → 1.1.15
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/.eslintrc +2 -1
- package/assets/img/header/check.svg +1 -0
- package/assets/js/voice.js +238 -0
- package/assets/js/xss.js +130 -0
- package/package.json +4 -4
- package/src/Header.vue +2 -2
- package/src/header/UserInfo.vue +10 -1
- package/src/header/alternate.vue +285 -0
package/.eslintrc
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg t="1726565101981" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10379" width="200" height="200"><path d="M544.234145 146.178267c-202.342428 0-366.335433 163.993005-366.335433 366.335433 0 202.343451 163.993005 366.332363 366.335433 366.332363 202.366987 0 366.331339-163.988912 366.331339-366.332363C910.565485 310.171272 746.577596 146.178267 544.234145 146.178267L544.234145 146.178267zM475.964272 690.441065 312.25984 526.736633l37.042661-37.042661L475.964272 616.355743l263.221983-263.225053 37.042661 37.042661L475.964272 690.441065 475.964272 690.441065zM475.964272 690.441065" fill="#67C23A" p-id="10380"></path></svg>
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import $ from "jquery";
|
|
2
|
+
import { showAvatar } from "@jx3box/jx3box-common/js/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 渲染音频组件
|
|
6
|
+
* 将 e-audio 转换为实际的音频播放器
|
|
7
|
+
* @param {string} selector - 选择器,默认为 ".w-audio, .e-audio"
|
|
8
|
+
*/
|
|
9
|
+
function renderVoice(selector = ".w-audio, .e-audio") {
|
|
10
|
+
try {
|
|
11
|
+
$(selector).each(function (i, ele) {
|
|
12
|
+
const $audio = $(this);
|
|
13
|
+
const content = $audio.text().trim();
|
|
14
|
+
|
|
15
|
+
// 解析内容:name:xxx;author:xxx;user_id:xxx;src:xxx
|
|
16
|
+
const params = {};
|
|
17
|
+
content.split(";").forEach((item) => {
|
|
18
|
+
const [key, value] = item.split("|");
|
|
19
|
+
if (key && value !== undefined) {
|
|
20
|
+
params[key.trim()] = value.trim();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// 提取参数
|
|
25
|
+
const { name = "未命名音频", author = "未知", user_id = "", src = "" } = params;
|
|
26
|
+
|
|
27
|
+
if (!src) {
|
|
28
|
+
console.warn("音频源地址为空", content);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 生成唯一ID
|
|
33
|
+
const playerId = `audio-player-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
34
|
+
|
|
35
|
+
// 获取用户头像,如果没有则使用默认图片
|
|
36
|
+
const avatar = showAvatar(params.avatar, 240);
|
|
37
|
+
|
|
38
|
+
// 渲染音频播放器 - 使用项目中定义的样式结构
|
|
39
|
+
const html = `
|
|
40
|
+
<div class="w-audio-player" id="${playerId}" data-user-id="${user_id}">
|
|
41
|
+
<div class="m-item">
|
|
42
|
+
<div class="u-title">
|
|
43
|
+
<div class="clip">
|
|
44
|
+
<div class="marquee-wrapper">
|
|
45
|
+
<span class="marquee-text">${name}</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="u-author">
|
|
50
|
+
练习生:<a href="https://www.jx3box.com/author/${user_id}" target="_blank">${author}</a>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="m-record">
|
|
54
|
+
<img class="u-needle" src="https://cdn.jx3box.com/design/event/jx3cxk/web/item/needle.svg" />
|
|
55
|
+
<a href="https://www.jx3box.com/author/${user_id}" target="_blank" class="u-record">
|
|
56
|
+
<img class="u-avatar" src="${avatar}" />
|
|
57
|
+
</a>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="m-progress">
|
|
61
|
+
<div class="u-progress-bar">
|
|
62
|
+
<div class="u-progress-fill"></div>
|
|
63
|
+
<div class="u-progress-handle"></div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="m-play">
|
|
68
|
+
<div class="u-play-button">
|
|
69
|
+
<img class="u-icon" src="https://cdn.jx3box.com/design/event/jx3cxk/web/item/left.svg" />
|
|
70
|
+
<img class="u-icon u-play" src="https://cdn.jx3box.com/design/event/jx3cxk/web/item/play.svg" data-play-icon="https://cdn.jx3box.com/design/event/jx3cxk/web/item/play.svg" data-stop-icon="https://cdn.jx3box.com/design/event/jx3cxk/web/item/stop.svg" />
|
|
71
|
+
<img class="u-icon" src="https://cdn.jx3box.com/design/event/jx3cxk/web/item/right.svg" />
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<audio preload="metadata" src="${src}"></audio>
|
|
76
|
+
</div>
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
$audio.replaceWith(html);
|
|
80
|
+
|
|
81
|
+
// 初始化播放器功能
|
|
82
|
+
initAudioPlayer(playerId);
|
|
83
|
+
});
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error("音频渲染错误:", e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 初始化音频播放器功能
|
|
91
|
+
* @param {string} playerId - 播放器ID
|
|
92
|
+
*/
|
|
93
|
+
function initAudioPlayer(playerId) {
|
|
94
|
+
const $player = $(`#${playerId}`);
|
|
95
|
+
const audio = $player.find("audio")[0];
|
|
96
|
+
const $playBtn = $player.find(".u-play");
|
|
97
|
+
const $needle = $player.find(".u-needle");
|
|
98
|
+
const $avatar = $player.find(".u-avatar");
|
|
99
|
+
const $progressBar = $player.find(".u-progress-bar");
|
|
100
|
+
const $progressFill = $player.find(".u-progress-fill");
|
|
101
|
+
const $progressHandle = $player.find(".u-progress-handle");
|
|
102
|
+
|
|
103
|
+
if (!audio) {
|
|
104
|
+
console.warn("音频元素未找到", playerId);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let isPlaying = false;
|
|
109
|
+
let isDragging = false;
|
|
110
|
+
|
|
111
|
+
// 播放/暂停切换
|
|
112
|
+
$playBtn.on("click", function () {
|
|
113
|
+
const playIcon = $(this).data("play-icon");
|
|
114
|
+
const stopIcon = $(this).data("stop-icon");
|
|
115
|
+
|
|
116
|
+
if (isPlaying) {
|
|
117
|
+
audio.pause();
|
|
118
|
+
$(this).attr("src", playIcon);
|
|
119
|
+
$needle.removeClass("isPlaying");
|
|
120
|
+
$avatar.addClass("isPaused");
|
|
121
|
+
$player.removeClass("play");
|
|
122
|
+
} else {
|
|
123
|
+
audio.play();
|
|
124
|
+
$(this).attr("src", stopIcon);
|
|
125
|
+
$needle.addClass("isPlaying");
|
|
126
|
+
$avatar.removeClass("isPaused").addClass("isRotate");
|
|
127
|
+
$player.addClass("play");
|
|
128
|
+
}
|
|
129
|
+
isPlaying = !isPlaying;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// 音频播放状态改变
|
|
133
|
+
audio.addEventListener("play", function () {
|
|
134
|
+
isPlaying = true;
|
|
135
|
+
$playBtn.attr("src", $playBtn.data("stop-icon"));
|
|
136
|
+
$needle.addClass("isPlaying");
|
|
137
|
+
$avatar.removeClass("isPaused").addClass("isRotate");
|
|
138
|
+
$player.addClass("play");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
audio.addEventListener("pause", function () {
|
|
142
|
+
isPlaying = false;
|
|
143
|
+
$playBtn.attr("src", $playBtn.data("play-icon"));
|
|
144
|
+
$needle.removeClass("isPlaying");
|
|
145
|
+
$avatar.addClass("isPaused");
|
|
146
|
+
$player.removeClass("play");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 更新进度条
|
|
150
|
+
audio.addEventListener("timeupdate", function () {
|
|
151
|
+
if (!isDragging && audio.duration) {
|
|
152
|
+
const progress = (audio.currentTime / audio.duration) * 100;
|
|
153
|
+
$progressFill.css("width", progress + "%");
|
|
154
|
+
$progressHandle.css("left", progress + "%");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// 进度条拖拽和点击
|
|
159
|
+
function updateProgress(e) {
|
|
160
|
+
const rect = $progressBar[0].getBoundingClientRect();
|
|
161
|
+
const offsetX = e.clientX - rect.left;
|
|
162
|
+
const progress = Math.max(0, Math.min(1, offsetX / rect.width));
|
|
163
|
+
const newTime = progress * audio.duration;
|
|
164
|
+
|
|
165
|
+
$progressFill.css("width", progress * 100 + "%");
|
|
166
|
+
$progressHandle.css("left", progress * 100 + "%");
|
|
167
|
+
|
|
168
|
+
if (isDragging || e.type === "click") {
|
|
169
|
+
audio.currentTime = newTime;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
$progressBar.on("mousedown", function (e) {
|
|
174
|
+
isDragging = true;
|
|
175
|
+
updateProgress(e);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
$(document).on("mousemove", function (e) {
|
|
179
|
+
if (isDragging) {
|
|
180
|
+
updateProgress(e);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
$(document).on("mouseup", function (e) {
|
|
185
|
+
if (isDragging) {
|
|
186
|
+
updateProgress(e);
|
|
187
|
+
isDragging = false;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
$progressBar.on("click", function (e) {
|
|
192
|
+
updateProgress(e);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// 音频结束
|
|
196
|
+
audio.addEventListener("ended", function () {
|
|
197
|
+
isPlaying = false;
|
|
198
|
+
audio.currentTime = 0;
|
|
199
|
+
$playBtn.attr("src", $playBtn.data("play-icon"));
|
|
200
|
+
$needle.removeClass("isPlaying");
|
|
201
|
+
$avatar.removeClass("isRotate isPaused");
|
|
202
|
+
$player.removeClass("play");
|
|
203
|
+
$progressFill.css("width", "0%");
|
|
204
|
+
$progressHandle.css("left", "0%");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// 检查标题是否需要滚动
|
|
208
|
+
checkTextWidth($player);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 检查文本宽度,决定是否需要滚动动画
|
|
213
|
+
* @param {jQuery} $player - 播放器元素
|
|
214
|
+
*/
|
|
215
|
+
function checkTextWidth($player) {
|
|
216
|
+
const $wrapper = $player.find(".marquee-wrapper");
|
|
217
|
+
const $text = $player.find(".marquee-text");
|
|
218
|
+
|
|
219
|
+
if (!$text.length || !$wrapper.length) return;
|
|
220
|
+
|
|
221
|
+
const textWidth = $text[0].offsetWidth;
|
|
222
|
+
const containerWidth = $wrapper.parent()[0].clientWidth;
|
|
223
|
+
|
|
224
|
+
if (textWidth > containerWidth) {
|
|
225
|
+
$wrapper.addClass("marquee-active");
|
|
226
|
+
// 添加复制的文本用于无缝滚动
|
|
227
|
+
const copyText = $text.clone().addClass("copy");
|
|
228
|
+
$wrapper.append(copyText);
|
|
229
|
+
|
|
230
|
+
// 计算动画时长
|
|
231
|
+
const totalWidth = textWidth * 2 + 180; // 180px 是间距
|
|
232
|
+
const duration = totalWidth / 30; // 30px/s
|
|
233
|
+
$wrapper.css("animation-duration", `${duration}s`);
|
|
234
|
+
$wrapper.addClass("marquee-animate");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export default renderVoice;
|
package/assets/js/xss.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import sanitizeHtml from "sanitize-html";
|
|
2
|
+
import * as cheerio from "cheerio";
|
|
3
|
+
import postcss from "postcss";
|
|
4
|
+
import safeParser from "postcss-safe-parser";
|
|
5
|
+
|
|
6
|
+
const FORBID = new Set(["script", "object", "embed", "applet", "base", "meta", "link"]);
|
|
7
|
+
|
|
8
|
+
const EXTRA_TAGS = [
|
|
9
|
+
"img",
|
|
10
|
+
"h1", "h2", "h3", "h4", "h5", "h6",
|
|
11
|
+
"table", "thead", "tbody", "tr", "th", "td",
|
|
12
|
+
"blockquote", "pre", "code", "hr",
|
|
13
|
+
"video", "source",
|
|
14
|
+
"iframe", "style",
|
|
15
|
+
"colgroup", "col",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// 必须顶层的 at-rule(你说不需要动画,但 keyframes 也可能被编辑器/作者写进来,留着更稳)
|
|
19
|
+
const TOP_LEVEL_AT = new Set(["keyframes", "-webkit-keyframes", "font-face"]);
|
|
20
|
+
|
|
21
|
+
function stripDangerousCss(css) {
|
|
22
|
+
if (!css) return css;
|
|
23
|
+
return css
|
|
24
|
+
.replace(/@import\s+[^;]+;?/gi, "")
|
|
25
|
+
.replace(/url\s*\(\s*[^)]+\s*\)/gi, "")
|
|
26
|
+
.replace(/expression\s*\([^)]*\)/gi, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 暴力把 style 内容 nest 到 .c-article
|
|
30
|
+
function nestCssBrutally(cssText, scope = ".c-article") {
|
|
31
|
+
let css = stripDangerousCss(cssText || "");
|
|
32
|
+
if (!css.trim()) return css;
|
|
33
|
+
|
|
34
|
+
const root = postcss.parse(css, { parser: safeParser });
|
|
35
|
+
|
|
36
|
+
const top = [];
|
|
37
|
+
const rest = [];
|
|
38
|
+
|
|
39
|
+
(root.nodes || []).forEach((node) => {
|
|
40
|
+
if (node.type === "atrule" && TOP_LEVEL_AT.has(String(node.name).toLowerCase())) {
|
|
41
|
+
top.push(node.toString());
|
|
42
|
+
} else {
|
|
43
|
+
rest.push(node.toString());
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const restCss = rest.join("\n").trim();
|
|
48
|
+
if (!restCss) return top.join("\n").trim();
|
|
49
|
+
|
|
50
|
+
return `${top.join("\n")}\n${scope}{\n${restCss}\n}`.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function nestAllStyleTags(html, scope = ".c-article") {
|
|
54
|
+
const $ = cheerio.load(html, { decodeEntities: false });
|
|
55
|
+
|
|
56
|
+
$("style").each((_, el) => {
|
|
57
|
+
const oldCss = $(el).html() || "";
|
|
58
|
+
const newCss = nestCssBrutally(oldCss, scope);
|
|
59
|
+
$(el).text(newCss);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return $.html();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default function sanitizeRichText(html) {
|
|
66
|
+
if (!html) return html;
|
|
67
|
+
|
|
68
|
+
// 先把 <style> 内容暴力 nest 到 .c-article
|
|
69
|
+
html = nestAllStyleTags(html, ".c-article");
|
|
70
|
+
|
|
71
|
+
const allowedTags = sanitizeHtml.defaults.allowedTags
|
|
72
|
+
.concat(EXTRA_TAGS)
|
|
73
|
+
.filter((t, i, arr) => arr.indexOf(t) === i)
|
|
74
|
+
.filter((t) => !FORBID.has(t));
|
|
75
|
+
|
|
76
|
+
return sanitizeHtml(html, {
|
|
77
|
+
disallowedTagsMode: "discard",
|
|
78
|
+
allowedTags,
|
|
79
|
+
|
|
80
|
+
allowedAttributes: {
|
|
81
|
+
"*": ["class", "style", "title", "id", "data-*"],
|
|
82
|
+
a: ["href", "target", "rel", "title", "class", "style"],
|
|
83
|
+
img: ["src", "alt", "title", "width", "height", "class", "style", "loading", "decoding"],
|
|
84
|
+
video: ["controls", "width", "height", "class", "style"],
|
|
85
|
+
source: ["src", "type"],
|
|
86
|
+
iframe: ["src", "width", "height", "frameborder", "scrolling", "allowfullscreen", "sandbox", "referrerpolicy", "class", "style"],
|
|
87
|
+
td: ["colspan", "rowspan", "align", "valign", "class", "style"],
|
|
88
|
+
th: ["colspan", "rowspan", "align", "valign", "class", "style"],
|
|
89
|
+
col: ["span", "width", "class", "style"],
|
|
90
|
+
style: ["type", "media"],
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
94
|
+
allowProtocolRelative: true,
|
|
95
|
+
allowedSchemesByTag: { img: ["http", "https", "data"], iframe: ["http", "https"] },
|
|
96
|
+
|
|
97
|
+
transformTags: {
|
|
98
|
+
"*": (tagName, attribs) => {
|
|
99
|
+
const out = { ...attribs };
|
|
100
|
+
|
|
101
|
+
// 移除 on*
|
|
102
|
+
for (const k of Object.keys(out)) if (/^on/i.test(k)) delete out[k];
|
|
103
|
+
|
|
104
|
+
// style 属性:禁 @import / url(
|
|
105
|
+
if (typeof out.style === "string" && out.style) {
|
|
106
|
+
let s = out.style;
|
|
107
|
+
s = s.replace(/@import\s+[^;]+;?/gi, "");
|
|
108
|
+
s = s.replace(/url\s*\(\s*[^)]+\s*\)/gi, "");
|
|
109
|
+
s = s.replace(/;;+/g, ";").trim();
|
|
110
|
+
if (!s) delete out.style;
|
|
111
|
+
else out.style = s;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 兜底禁 javascript:
|
|
115
|
+
for (const key of ["href", "src"]) {
|
|
116
|
+
if (out[key] && /^\s*javascript:/i.test(out[key])) out[key] = "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 仅允许 data:image/*
|
|
120
|
+
if (tagName === "img" && typeof out.src === "string" && out.src.startsWith("data:")) {
|
|
121
|
+
if (!/^data:image\/(png|jpe?g|gif|webp|avif|bmp|svg\+xml);/i.test(out.src)) {
|
|
122
|
+
out.src = "";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { tagName, attribs: out };
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jx3box/jx3box-vue3-ui",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"description": "JX3BOX Vue3 UI",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@element-plus/icons-vue": "^2.1.0",
|
|
29
|
-
"@jx3box/jx3box-common": "^8.7.
|
|
30
|
-
"@jx3box/jx3box-data": "^3.
|
|
29
|
+
"@jx3box/jx3box-common": "^8.7.5",
|
|
30
|
+
"@jx3box/jx3box-data": "^3.9.2",
|
|
31
31
|
"@jx3box/jx3box-emotion": "^1.2.14",
|
|
32
32
|
"@jx3box/jx3box-macro": "^1.0.1",
|
|
33
|
-
"@jx3box/jx3box-talent": "^1.3.
|
|
33
|
+
"@jx3box/jx3box-talent": "^1.3.11",
|
|
34
34
|
"@jx3box/reporter": "^0.0.4",
|
|
35
35
|
"@tinymce/tinymce-vue": "^5.1.1",
|
|
36
36
|
"@vueuse/core": "^9.13.0",
|
package/src/Header.vue
CHANGED
|
@@ -36,7 +36,7 @@ import search from "./header/Search.vue";
|
|
|
36
36
|
import nav from "./header/Nav.vue";
|
|
37
37
|
import user from "./header/User.vue";
|
|
38
38
|
import Box from "../src/Box.vue";
|
|
39
|
-
import { isMiniProgram, miniprogramHack } from "@jx3box/jx3box-common/js/utils";
|
|
39
|
+
import { isMiniProgram, isApp as checkIsApp, miniprogramHack } from "@jx3box/jx3box-common/js/utils";
|
|
40
40
|
import miniprogram from "@jx3box/jx3box-common/data/miniprogram.json";
|
|
41
41
|
import User from "@jx3box/jx3box-common/js/user";
|
|
42
42
|
import { getGlobalConfig } from "../service/header.js";
|
|
@@ -66,7 +66,7 @@ export default {
|
|
|
66
66
|
const urlParams = new URLSearchParams(window.location.search);
|
|
67
67
|
const from = urlParams.get("from");
|
|
68
68
|
from && sessionStorage.setItem("from", from);
|
|
69
|
-
if (isMiniProgram()) {
|
|
69
|
+
if (isMiniProgram() || checkIsApp()) {
|
|
70
70
|
const appid = urlParams.get("appid");
|
|
71
71
|
const item = miniprogram?.find((item) => item.appid === appid);
|
|
72
72
|
const from = urlParams.get("_from");
|
package/src/header/UserInfo.vue
CHANGED
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
|
|
69
69
|
<el-button-group class="u-actions">
|
|
70
70
|
<a class="el-button el-button--default is-plain" href="/dashboard">个人中心</a>
|
|
71
|
-
<a class="el-button el-button--default is-plain"
|
|
71
|
+
<a class="el-button el-button--default is-plain" @click="changeAlternate">切换马甲</a>
|
|
72
72
|
<a class="el-button el-button--default is-plain" href="/dashboard/frame">主题风格</a>
|
|
73
73
|
</el-button-group>
|
|
74
74
|
|
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
</div>
|
|
98
98
|
</template>
|
|
99
99
|
</div>
|
|
100
|
+
<alternate></alternate>
|
|
100
101
|
</div>
|
|
101
102
|
</template>
|
|
102
103
|
|
|
@@ -109,10 +110,15 @@ import { getMyInfo } from "../../service/author";
|
|
|
109
110
|
import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
|
|
110
111
|
import { copyText } from "../../utils";
|
|
111
112
|
import { getMenu } from "../../service/header";
|
|
113
|
+
import Bus from "../../utils/bus";
|
|
114
|
+
import alternate from "./alternate.vue";
|
|
112
115
|
export default {
|
|
113
116
|
name: "HeaderUserInfo",
|
|
114
117
|
props: ["asset"],
|
|
115
118
|
emits: ["logout", "update"],
|
|
119
|
+
components: {
|
|
120
|
+
alternate,
|
|
121
|
+
},
|
|
116
122
|
data() {
|
|
117
123
|
return {
|
|
118
124
|
isPhone: window.innerWidth < 768,
|
|
@@ -221,6 +227,9 @@ export default {
|
|
|
221
227
|
console.log("loadPanel error", e);
|
|
222
228
|
}
|
|
223
229
|
},
|
|
230
|
+
changeAlternate: function() {
|
|
231
|
+
Bus.emit("showAlternate");
|
|
232
|
+
}
|
|
224
233
|
},
|
|
225
234
|
};
|
|
226
235
|
</script>
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dialog append-to-body v-model="visible" custom-class="c-alternate" width="320px" title="切换马甲">
|
|
3
|
+
<div class="c-alternate__content">
|
|
4
|
+
<div
|
|
5
|
+
class="c-alternate-item"
|
|
6
|
+
:class="{ 'is-active': profile.uid == item.uid, 'is-expired': isExpired(item.created_at) }"
|
|
7
|
+
v-for="item in alternate"
|
|
8
|
+
:key="item.uid"
|
|
9
|
+
@click="onSelectAlternate(item)"
|
|
10
|
+
>
|
|
11
|
+
<div class="m-avatar">
|
|
12
|
+
<img
|
|
13
|
+
class="u-active"
|
|
14
|
+
v-if="profile.uid == item.uid"
|
|
15
|
+
src="../../assets/img/header/check.svg"
|
|
16
|
+
alt=""
|
|
17
|
+
/>
|
|
18
|
+
<img class="u-avatar" :src="showAvatar(item.avatar)" alt="" />
|
|
19
|
+
</div>
|
|
20
|
+
<div class="m-misc">
|
|
21
|
+
<span class="u-name"><span class="u-label">用户昵称:</span>{{ item.name }}</span>
|
|
22
|
+
<span class="u-time">
|
|
23
|
+
<span class="u-label">上次登录:</span>{{ getFormatTime(item.created_at) }}
|
|
24
|
+
<span class="u-extra" v-if="isExpired(item.created_at)">(已过期)</span>
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="u-remove" @click.stop="onRemoveAlternate(item)" v-if="profile.uid != item.uid">
|
|
29
|
+
<i class="el-icon-close"></i>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="c-alternate-btn" :class="{ 'is-disabled': overLength }" @click="onAddAlternate">+</div>
|
|
33
|
+
</div>
|
|
34
|
+
</el-dialog>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script>
|
|
38
|
+
import Bus from "../../utils/bus";
|
|
39
|
+
import { showAvatar } from "@jx3box/jx3box-common/js/utils";
|
|
40
|
+
import dayjs from "dayjs";
|
|
41
|
+
import User from "@jx3box/jx3box-common/js/user";
|
|
42
|
+
import { __Links } from "@jx3box/jx3box-common/data/jx3box.json";
|
|
43
|
+
import { refreshAuth } from "../../service/cms"
|
|
44
|
+
export default {
|
|
45
|
+
name: "AlternateComponent",
|
|
46
|
+
data() {
|
|
47
|
+
return {
|
|
48
|
+
visible: false,
|
|
49
|
+
|
|
50
|
+
alternate: [
|
|
51
|
+
// {
|
|
52
|
+
// uid: 100,
|
|
53
|
+
// name: "马甲1",
|
|
54
|
+
// avatar: "https://avatar.jx3box.com/avatar/1",
|
|
55
|
+
// created_at: 1620000000000,
|
|
56
|
+
// },
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
computed: {
|
|
61
|
+
profile() {
|
|
62
|
+
return User.getInfo();
|
|
63
|
+
},
|
|
64
|
+
overLength() {
|
|
65
|
+
return this.alternate.length > 5;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
mounted() {
|
|
69
|
+
Bus.on("showAlternate", () => {
|
|
70
|
+
this.visible = true;
|
|
71
|
+
});
|
|
72
|
+
this.init();
|
|
73
|
+
},
|
|
74
|
+
methods: {
|
|
75
|
+
init() {
|
|
76
|
+
// 获取localStorage中以jx3box-alternate-开头的数据
|
|
77
|
+
try {
|
|
78
|
+
let keys = Object.keys(localStorage);
|
|
79
|
+
let alternate = keys.filter((key) => key.startsWith("jx3box-alternate-"));
|
|
80
|
+
|
|
81
|
+
alternate.forEach((key) => {
|
|
82
|
+
const item = JSON.parse(localStorage.getItem(key));
|
|
83
|
+
if (this.isExpired(item.created_at)) {
|
|
84
|
+
localStorage.removeItem(key);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!this.alternate.find((alt) => alt.uid == item.uid)) {
|
|
88
|
+
this.alternate.push(item);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 如果当前没有马甲,添加当前登录用户
|
|
93
|
+
if (!this.alternate?.length) {
|
|
94
|
+
const data = {
|
|
95
|
+
uid: this.profile.uid,
|
|
96
|
+
name: this.profile.name,
|
|
97
|
+
avatar: localStorage.getItem("avatar"),
|
|
98
|
+
created_at: Number(localStorage.getItem("created_at")),
|
|
99
|
+
group: ~~this.profile.group,
|
|
100
|
+
bind_wx: ~~this.profile.bind_wx,
|
|
101
|
+
token: localStorage.getItem("token"),
|
|
102
|
+
status: ~~this.profile.status,
|
|
103
|
+
};
|
|
104
|
+
this.alternate.unshift(data);
|
|
105
|
+
|
|
106
|
+
localStorage.setItem("jx3box-alternate-" + this.profile.uid, JSON.stringify(data));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 当前激活的排在第一
|
|
110
|
+
this.alternate.sort((a, _) => {
|
|
111
|
+
return a.uid == this.profile.uid ? -1 : 1;
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(error);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
showAvatar(url) {
|
|
118
|
+
return showAvatar(url, "m");
|
|
119
|
+
},
|
|
120
|
+
getFormatTime(time) {
|
|
121
|
+
return dayjs(time).format("YYYY-MM-DD HH:mm:ss");
|
|
122
|
+
},
|
|
123
|
+
// 判断是否已过期
|
|
124
|
+
isExpired(time) {
|
|
125
|
+
return dayjs().diff(time, "day") > 30;
|
|
126
|
+
},
|
|
127
|
+
// 选择马甲
|
|
128
|
+
onSelectAlternate(item) {
|
|
129
|
+
if (this.isExpired(item.created_at)) {
|
|
130
|
+
this.$message.error("该马甲已过期,请重新登录");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (this.profile.uid == item.uid) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.$confirm("确定要切换到该马甲吗?", "提示", {
|
|
137
|
+
confirmButtonText: "确定",
|
|
138
|
+
cancelButtonText: "取消",
|
|
139
|
+
type: "warning",
|
|
140
|
+
})
|
|
141
|
+
.then(() => {
|
|
142
|
+
User.update(item).then(async () => {
|
|
143
|
+
localStorage.setItem(
|
|
144
|
+
"jx3box-alternate-" + item.uid,
|
|
145
|
+
JSON.stringify({
|
|
146
|
+
...item,
|
|
147
|
+
created_at: Number(localStorage.getItem("created_at")),
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
await refreshAuth();
|
|
151
|
+
location.reload();
|
|
152
|
+
this.visible = false;
|
|
153
|
+
});
|
|
154
|
+
})
|
|
155
|
+
.catch(() => {});
|
|
156
|
+
},
|
|
157
|
+
// 删除马甲
|
|
158
|
+
onRemoveAlternate(item) {
|
|
159
|
+
this.$confirm("确定要删除该马甲吗?", "提示", {
|
|
160
|
+
confirmButtonText: "确定",
|
|
161
|
+
cancelButtonText: "取消",
|
|
162
|
+
type: "warning",
|
|
163
|
+
})
|
|
164
|
+
.then(() => {
|
|
165
|
+
localStorage.removeItem("jx3box-alternate-" + item.uid);
|
|
166
|
+
|
|
167
|
+
this.alternate = this.alternate.filter((alt) => alt.uid != item.uid);
|
|
168
|
+
})
|
|
169
|
+
.catch(() => {});
|
|
170
|
+
},
|
|
171
|
+
// 新增马甲
|
|
172
|
+
onAddAlternate() {
|
|
173
|
+
if (this.overLength) {
|
|
174
|
+
this.$message.error("最多只能添加5个马甲");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// 跳转至登录页
|
|
178
|
+
location.href = __Links.account.login + "?alternate=1";
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
</script>
|
|
183
|
+
|
|
184
|
+
<style lang="less">
|
|
185
|
+
.c-alternate {
|
|
186
|
+
.el-dialog__title {
|
|
187
|
+
.fz(12px);
|
|
188
|
+
}
|
|
189
|
+
.el-dialog__body {
|
|
190
|
+
padding: 0;
|
|
191
|
+
}
|
|
192
|
+
.el-dialog__header {
|
|
193
|
+
// padding: 10px;
|
|
194
|
+
border-bottom: #dcdfe6 1px solid;
|
|
195
|
+
}
|
|
196
|
+
.c-alternate__content {
|
|
197
|
+
max-height: 600px;
|
|
198
|
+
overflow-y: auto;
|
|
199
|
+
}
|
|
200
|
+
.c-alternate-item {
|
|
201
|
+
.flex;
|
|
202
|
+
gap: 10px;
|
|
203
|
+
.pointer;
|
|
204
|
+
padding: 10px;
|
|
205
|
+
.pr;
|
|
206
|
+
border-bottom: 1px solid #eee;
|
|
207
|
+
.fz(13px);
|
|
208
|
+
|
|
209
|
+
&:hover {
|
|
210
|
+
background-color: @bg-light;
|
|
211
|
+
|
|
212
|
+
.u-remove {
|
|
213
|
+
display: block;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.u-remove {
|
|
219
|
+
.pa;
|
|
220
|
+
right: 10px;
|
|
221
|
+
top: 5px;
|
|
222
|
+
color: #999;
|
|
223
|
+
.pointer;
|
|
224
|
+
font-size: 16px;
|
|
225
|
+
|
|
226
|
+
&:hover {
|
|
227
|
+
color: #333;
|
|
228
|
+
}
|
|
229
|
+
.none;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.m-avatar {
|
|
233
|
+
.pr;
|
|
234
|
+
}
|
|
235
|
+
.u-avatar {
|
|
236
|
+
width: 50px;
|
|
237
|
+
height: 50px;
|
|
238
|
+
border-radius: 50%;
|
|
239
|
+
}
|
|
240
|
+
.u-active {
|
|
241
|
+
.pa;
|
|
242
|
+
right: -2px;
|
|
243
|
+
top: -6px;
|
|
244
|
+
width: 20px;
|
|
245
|
+
height: 20px;
|
|
246
|
+
fill: #fff;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.m-misc {
|
|
250
|
+
.flex;
|
|
251
|
+
flex-direction: column;
|
|
252
|
+
gap: 10px;
|
|
253
|
+
}
|
|
254
|
+
.u-label {
|
|
255
|
+
font-size: 12px;
|
|
256
|
+
color: #999;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.u-extra {
|
|
260
|
+
font-size: 12px;
|
|
261
|
+
color: #999;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.is-expired {
|
|
265
|
+
.u-time {
|
|
266
|
+
color: #c00;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.c-alternate-btn {
|
|
271
|
+
// padding: 10px;
|
|
272
|
+
text-align: center;
|
|
273
|
+
.pointer;
|
|
274
|
+
// width: 100%;
|
|
275
|
+
// border-radius: 0;
|
|
276
|
+
.size(100%,74px);
|
|
277
|
+
.fz(40px,74px);
|
|
278
|
+
color: #999;
|
|
279
|
+
&:hover {
|
|
280
|
+
background-color: @bg-light;
|
|
281
|
+
color: #888;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
</style>
|