@jx3box/jx3box-editor 2.2.45 → 2.2.47

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.
@@ -24,6 +24,7 @@
24
24
  @import "tinymce/qixue.less";
25
25
  @import "tinymce/pz.less";
26
26
  @import "tinymce/combo.less";
27
+ @import "tinymce/voice.less";
27
28
 
28
29
  @import "module/directory.less";
29
30
  @import "module/icon.less";
@@ -0,0 +1,331 @@
1
+ @design_url: "https://cdn.jx3box.com/design/";
2
+ @jx3cxk: "@{design_url}event/jx3cxk/";
3
+ // 编辑器内
4
+ .e-audio {
5
+ min-height: 24px;
6
+ border-radius: 4px;
7
+ font-size: 14px;
8
+ width: 100%;
9
+ box-sizing: border-box;
10
+ background-color: #f1f8ff;
11
+ border: 1px solid #c8e1ff;
12
+ color: #62a9ff;
13
+
14
+ line-height: 1.6 !important;
15
+ padding: 10px;
16
+ padding-right: 135px;
17
+ font-family: Monaco, Consolas, "Lucida Console", "Courier New", serif;
18
+ word-break: break-all;
19
+ white-space: pre-wrap !important;
20
+ overflow-wrap: break-word;
21
+
22
+ .pr;
23
+ &:after {
24
+ content: "JX3BOX·AUDIO";
25
+ position: absolute;
26
+ right: 8px;
27
+ top: 8px;
28
+ background-color: darken(#c8e1ff, 20%);
29
+ color: #fff;
30
+ border-radius: 3px;
31
+ padding: 2px 8px;
32
+ line-height: 21px;
33
+ }
34
+ }
35
+
36
+ // 渲染后的音频播放器样式
37
+ .w-audio-player {
38
+ .pr;
39
+ .r(24px);
40
+ box-sizing: border-box;
41
+ width: 376px;
42
+ background: rgba(255, 255, 255, 0.1);
43
+ transition: all 0.3s ease-in-out;
44
+ padding: 24px;
45
+ .m-item {
46
+ .pr;
47
+ .pointer;
48
+ .r(16px);
49
+ .clip;
50
+ .mb(24px);
51
+ color: #fff;
52
+ padding: 32px 36px;
53
+ box-sizing: border-box;
54
+ width: 100%;
55
+ background: linear-gradient(180deg, #7676f0 0%, #9495e9 100%);
56
+
57
+ a {
58
+ color: #fff;
59
+ text-decoration: none;
60
+
61
+ &:hover {
62
+ text-decoration: none;
63
+ box-shadow: none;
64
+ }
65
+ }
66
+
67
+ img {
68
+ padding: 0;
69
+ margin: 0;
70
+ border: none;
71
+ }
72
+
73
+ &::before {
74
+ content: "";
75
+ .pa;
76
+ .lt(0);
77
+ .tm(0);
78
+ .size(100%);
79
+ transition: opacity 0.3s ease-in-out;
80
+ background: linear-gradient(180deg, #f9c27f 0%, #9697ed 100%);
81
+ }
82
+ .u-title {
83
+ .flex;
84
+ .pr;
85
+ .bold;
86
+ .fz(24px);
87
+ .x;
88
+ justify-content: center;
89
+ align-items: center;
90
+ max-width: 300px;
91
+ box-sizing: border-box;
92
+ font-family: "ALIMAMASHUHEITI";
93
+
94
+ .clip {
95
+ .clip;
96
+ width: 100%;
97
+ }
98
+
99
+ .marquee-wrapper {
100
+ white-space: nowrap;
101
+ position: relative;
102
+ display: inline-block;
103
+ transition: transform 0s linear;
104
+
105
+ &.marquee-animate {
106
+ animation: marquee linear infinite;
107
+ animation-fill-mode: forwards;
108
+ }
109
+ }
110
+
111
+ .marquee-text {
112
+ display: inline-block;
113
+
114
+ &::after {
115
+ content: "》";
116
+ display: inline-block;
117
+ }
118
+
119
+ &::before {
120
+ content: "《";
121
+ display: inline-block;
122
+ }
123
+
124
+ &.copy {
125
+ margin-left: 180px;
126
+ display: none;
127
+
128
+ &::after,
129
+ &::before {
130
+ display: none;
131
+ }
132
+ }
133
+ }
134
+
135
+ &.marquee-active {
136
+ .marquee-text.copy {
137
+ display: inline-block;
138
+ }
139
+
140
+ .marquee-animate .marquee-text {
141
+ &::after,
142
+ &::before {
143
+ display: none;
144
+ }
145
+ }
146
+
147
+ &::after {
148
+ content: "》";
149
+ display: inline-block;
150
+ position: absolute;
151
+ right: -28px;
152
+ top: 50%;
153
+ transform: translateY(-50%);
154
+ z-index: 1;
155
+ }
156
+
157
+ &::before {
158
+ content: "《";
159
+ display: inline-block;
160
+ position: absolute;
161
+ left: -28px;
162
+ top: 50%;
163
+ transform: translateY(-50%);
164
+ z-index: 1;
165
+ }
166
+ }
167
+ }
168
+ .u-author {
169
+ .x;
170
+ .fz(14px);
171
+ .tm(0.75);
172
+ .mt(5px);
173
+ }
174
+ .m-record {
175
+ .pr;
176
+ padding: 42px 0 3px 0;
177
+ .u-needle {
178
+ .pa;
179
+ .lt(50%,0);
180
+ .size(120px,72px);
181
+ .ml(-10px);
182
+ user-select: none;
183
+ pointer-events: none;
184
+ transition: transform 0.3s ease-out, top 0.3s ease-out, margin 0.3s ease-out;
185
+ transform-origin: 0% 0%;
186
+ transform: rotate(0deg);
187
+ &.isPlaying {
188
+ top: -3px;
189
+ margin: 0;
190
+ transform-origin: 0% 0%;
191
+ transform: rotate(30deg);
192
+ display: inline-block;
193
+ }
194
+ }
195
+ .u-record {
196
+ .db;
197
+ .size(182px);
198
+ .auto(x);
199
+ user-select: none;
200
+ padding: 28px;
201
+ box-sizing: border-box;
202
+ background: url("@{jx3cxk}web/item/record.svg") no-repeat center center;
203
+ background-size: 100% 100%;
204
+ }
205
+ .u-avatar {
206
+ .r(50%);
207
+ .size(100%);
208
+ object-fit: cover;
209
+ transition: transform 0.6s ease-out;
210
+
211
+ &.isRotate {
212
+ animation: rotate 6s linear infinite;
213
+ &.isPaused {
214
+ animation-play-state: paused;
215
+ }
216
+ }
217
+ }
218
+ }
219
+ .m-progress {
220
+ .pr;
221
+ margin: 16px 0;
222
+
223
+ .u-progress-bar {
224
+ .pr;
225
+ height: 3px;
226
+ background: rgba(65, 61, 82, 0.3);
227
+ .r(3px);
228
+ .pointer;
229
+ overflow: visible;
230
+
231
+ .u-progress-fill {
232
+ .pa;
233
+ height: 100%;
234
+ background: #fff;
235
+ .r(3px);
236
+ width: 0%;
237
+ transition: width 0.1s linear;
238
+ }
239
+
240
+ .u-progress-handle {
241
+ .pa;
242
+ top: 50%;
243
+ left: 0%;
244
+ .size(12px);
245
+ background: #fff;
246
+ border: 2px solid rgba(65, 61, 82, 0.3);
247
+ .r(50%);
248
+ transform: translate(-50%, -50%);
249
+ .pointer;
250
+ transition: left 0.1s linear;
251
+ }
252
+ }
253
+ }
254
+ .m-play {
255
+ .pr;
256
+ .flex;
257
+ .pt(10px);
258
+ flex-wrap: wrap;
259
+ justify-content: space-between;
260
+ align-items: center;
261
+ .u-play-button {
262
+ .flex;
263
+ justify-content: space-between;
264
+ width: 70%;
265
+ box-sizing: border-box;
266
+ margin: 0 15% 20px 15%;
267
+ align-items: center;
268
+ }
269
+ .u-icon {
270
+ .size(24px);
271
+ }
272
+ .u-play {
273
+ .size(64px);
274
+ }
275
+ .u-like {
276
+ .tm(0.75);
277
+ .flex;
278
+ align-items: center;
279
+ gap: 5px;
280
+ .u-icon {
281
+ .size(20px);
282
+ }
283
+ }
284
+ .u-link {
285
+ .tm(0.75);
286
+ color: #fff;
287
+ }
288
+ }
289
+ }
290
+ &.play {
291
+ .m-item {
292
+ background: linear-gradient(180deg, #55c79f 0%, #9697ed 100%);
293
+ }
294
+ }
295
+ &:hover {
296
+ background: rgba(255, 255, 255, 0.15);
297
+ .m-item::before {
298
+ .tm(1);
299
+ }
300
+ }
301
+ }
302
+ @keyframes rotate {
303
+ from {
304
+ transform: rotate(0deg);
305
+ }
306
+ to {
307
+ transform: rotate(360deg);
308
+ }
309
+ }
310
+
311
+ @keyframes marquee {
312
+ 0% {
313
+ transform: translateX(0);
314
+ }
315
+ 100% {
316
+ transform: translateX(-100%);
317
+ }
318
+ }
319
+
320
+ // 渲染模式隐藏
321
+ .c-article-tinymce {
322
+ .e-audio {
323
+ display: none;
324
+ }
325
+ }
326
+ // tinymce编辑器内展示
327
+ .c-article-editor {
328
+ .e-audio {
329
+ display: block;
330
+ }
331
+ }
@@ -0,0 +1,249 @@
1
+ import $ from "jquery";
2
+ import { showAvatar } from "@jx3box/jx3box-common/js/utils";
3
+ import {getUserInfo} from "../../service/author";
4
+
5
+ /**
6
+ * 渲染音频组件
7
+ * 将 e-audio 转换为实际的音频播放器
8
+ * @param {string} selector - 选择器,默认为 ".w-audio, .e-audio"
9
+ */
10
+ function renderVoice(selector = ".w-audio, .e-audio") {
11
+ try {
12
+ $(selector).each(async function (i, ele) {
13
+ const $audio = $(this);
14
+ const content = $audio.text().trim();
15
+
16
+ // 解析内容:name:xxx;author:xxx;user_id:xxx;src:xxx
17
+ const params = {};
18
+ content.split(";").forEach((item) => {
19
+ const [key, value] = item.split("|");
20
+ if (key && value !== undefined) {
21
+ params[key.trim()] = value.trim();
22
+ }
23
+ });
24
+
25
+ // 提取参数
26
+ let { name = "未命名音频", author = "未知", user_id = "", src = "", avatar = "" } = params;
27
+
28
+ if (!src) {
29
+ console.warn("音频源地址为空", content);
30
+ return;
31
+ }
32
+
33
+ const user_info = await fetchUserInfo(user_id);
34
+ console.log("用户信息:", user_info);
35
+ avatar = showAvatar(user_info?.user_avatar || '', 240);
36
+ author = author || user_info?.display_name || '匿名用户';
37
+
38
+ // 生成唯一ID
39
+ const playerId = `audio-player-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
40
+
41
+ // 渲染音频播放器 - 使用项目中定义的样式结构
42
+ const html = `
43
+ <div class="w-audio-player" id="${playerId}" data-user-id="${user_id}">
44
+ <div class="m-item">
45
+ <div class="u-title">
46
+ <div class="clip">
47
+ <div class="marquee-wrapper">
48
+ <span class="marquee-text">${name}</span>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ <div class="u-author">
53
+ 练习生:<a href="https://www.jx3box.com/author/${user_id}" target="_blank">${author}</a>
54
+ </div>
55
+
56
+ <div class="m-record">
57
+ <img class="u-needle" src="https://cdn.jx3box.com/design/event/jx3cxk/web/item/needle.svg" />
58
+ <a href="https://www.jx3box.com/author/${user_id}" target="_blank" class="u-record">
59
+ <img class="u-avatar" src="${avatar}" />
60
+ </a>
61
+ </div>
62
+
63
+ <div class="m-progress">
64
+ <div class="u-progress-bar">
65
+ <div class="u-progress-fill"></div>
66
+ <div class="u-progress-handle"></div>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="m-play">
71
+ <div class="u-play-button">
72
+ <img class="u-icon" src="https://cdn.jx3box.com/design/event/jx3cxk/web/item/left.svg" />
73
+ <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" />
74
+ <img class="u-icon" src="https://cdn.jx3box.com/design/event/jx3cxk/web/item/right.svg" />
75
+ </div>
76
+ </div>
77
+ </div>
78
+ <audio preload="metadata" src="${src}"></audio>
79
+ </div>
80
+ `;
81
+
82
+ $audio.replaceWith(html);
83
+
84
+ // 初始化播放器功能
85
+ initAudioPlayer(playerId);
86
+ });
87
+ } catch (e) {
88
+ console.error("音频渲染错误:", e);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 初始化音频播放器功能
94
+ * @param {string} playerId - 播放器ID
95
+ */
96
+ function initAudioPlayer(playerId) {
97
+ const $player = $(`#${playerId}`);
98
+ const audio = $player.find("audio")[0];
99
+ const $playBtn = $player.find(".u-play");
100
+ const $needle = $player.find(".u-needle");
101
+ const $avatar = $player.find(".u-avatar");
102
+ const $progressBar = $player.find(".u-progress-bar");
103
+ const $progressFill = $player.find(".u-progress-fill");
104
+ const $progressHandle = $player.find(".u-progress-handle");
105
+
106
+ if (!audio) {
107
+ console.warn("音频元素未找到", playerId);
108
+ return;
109
+ }
110
+
111
+ let isPlaying = false;
112
+ let isDragging = false;
113
+
114
+ // 播放/暂停切换
115
+ $playBtn.on("click", function () {
116
+ const playIcon = $(this).data("play-icon");
117
+ const stopIcon = $(this).data("stop-icon");
118
+
119
+ if (isPlaying) {
120
+ audio.pause();
121
+ $(this).attr("src", playIcon);
122
+ $needle.removeClass("isPlaying");
123
+ $avatar.addClass("isPaused");
124
+ $player.removeClass("play");
125
+ } else {
126
+ audio.play();
127
+ $(this).attr("src", stopIcon);
128
+ $needle.addClass("isPlaying");
129
+ $avatar.removeClass("isPaused").addClass("isRotate");
130
+ $player.addClass("play");
131
+ }
132
+ isPlaying = !isPlaying;
133
+ });
134
+
135
+ // 音频播放状态改变
136
+ audio.addEventListener("play", function () {
137
+ isPlaying = true;
138
+ $playBtn.attr("src", $playBtn.data("stop-icon"));
139
+ $needle.addClass("isPlaying");
140
+ $avatar.removeClass("isPaused").addClass("isRotate");
141
+ $player.addClass("play");
142
+ });
143
+
144
+ audio.addEventListener("pause", function () {
145
+ isPlaying = false;
146
+ $playBtn.attr("src", $playBtn.data("play-icon"));
147
+ $needle.removeClass("isPlaying");
148
+ $avatar.addClass("isPaused");
149
+ $player.removeClass("play");
150
+ });
151
+
152
+ // 更新进度条
153
+ audio.addEventListener("timeupdate", function () {
154
+ if (!isDragging && audio.duration) {
155
+ const progress = (audio.currentTime / audio.duration) * 100;
156
+ $progressFill.css("width", progress + "%");
157
+ $progressHandle.css("left", progress + "%");
158
+ }
159
+ });
160
+
161
+ // 进度条拖拽和点击
162
+ function updateProgress(e) {
163
+ const rect = $progressBar[0].getBoundingClientRect();
164
+ const offsetX = e.clientX - rect.left;
165
+ const progress = Math.max(0, Math.min(1, offsetX / rect.width));
166
+ const newTime = progress * audio.duration;
167
+
168
+ $progressFill.css("width", progress * 100 + "%");
169
+ $progressHandle.css("left", progress * 100 + "%");
170
+
171
+ if (isDragging || e.type === "click") {
172
+ audio.currentTime = newTime;
173
+ }
174
+ }
175
+
176
+ $progressBar.on("mousedown", function (e) {
177
+ isDragging = true;
178
+ updateProgress(e);
179
+ });
180
+
181
+ $(document).on("mousemove", function (e) {
182
+ if (isDragging) {
183
+ updateProgress(e);
184
+ }
185
+ });
186
+
187
+ $(document).on("mouseup", function (e) {
188
+ if (isDragging) {
189
+ updateProgress(e);
190
+ isDragging = false;
191
+ }
192
+ });
193
+
194
+ $progressBar.on("click", function (e) {
195
+ updateProgress(e);
196
+ });
197
+
198
+ // 音频结束
199
+ audio.addEventListener("ended", function () {
200
+ isPlaying = false;
201
+ audio.currentTime = 0;
202
+ $playBtn.attr("src", $playBtn.data("play-icon"));
203
+ $needle.removeClass("isPlaying");
204
+ $avatar.removeClass("isRotate isPaused");
205
+ $player.removeClass("play");
206
+ $progressFill.css("width", "0%");
207
+ $progressHandle.css("left", "0%");
208
+ });
209
+
210
+ // 检查标题是否需要滚动
211
+ checkTextWidth($player);
212
+ }
213
+
214
+ /**
215
+ * 检查文本宽度,决定是否需要滚动动画
216
+ * @param {jQuery} $player - 播放器元素
217
+ */
218
+ function checkTextWidth($player) {
219
+ const $wrapper = $player.find(".marquee-wrapper");
220
+ const $text = $player.find(".marquee-text");
221
+
222
+ if (!$text.length || !$wrapper.length) return;
223
+
224
+ const textWidth = $text[0].offsetWidth;
225
+ const containerWidth = $wrapper.parent()[0].clientWidth;
226
+
227
+ if (textWidth > containerWidth) {
228
+ $wrapper.addClass("marquee-active");
229
+ // 添加复制的文本用于无缝滚动
230
+ const copyText = $text.clone().addClass("copy");
231
+ $wrapper.append(copyText);
232
+
233
+ // 计算动画时长
234
+ const totalWidth = textWidth * 2 + 180; // 180px 是间距
235
+ const duration = totalWidth / 30; // 30px/s
236
+ $wrapper.css("animation-duration", `${duration}s`);
237
+ $wrapper.addClass("marquee-animate");
238
+ }
239
+ }
240
+
241
+ function fetchUserInfo(userId) {
242
+ return getUserInfo(userId).then((res) => {
243
+ return res || null;
244
+ }).catch((err) => {
245
+ console.error("获取用户信息失败:", err);
246
+ });
247
+ }
248
+
249
+ export default renderVoice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jx3box/jx3box-editor",
3
- "version": "2.2.45",
3
+ "version": "2.2.47",
4
4
  "description": "JX3BOX Article & Editor",
5
5
  "main": "index.js",
6
6
  "scripts": {