@jx3box/jx3box-editor 2.2.45 → 2.2.46

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,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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jx3box/jx3box-editor",
3
- "version": "2.2.45",
3
+ "version": "2.2.46",
4
4
  "description": "JX3BOX Article & Editor",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -3,29 +3,31 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
6
- <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
6
+ <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
7
7
  <meta name="renderer" content="webkit" />
8
8
  <title><%= htmlWebpackPlugin.options.title %></title>
9
9
  <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
10
10
  <script>
11
- window.RX_TINYMCE_ROOT = "https://cdn.jx3box.com/static/tinymce";
11
+ // window.RX_TINYMCE_ROOT = "https://cdn.jx3box.com/static/tinymce";
12
+ window.RX_TINYMCE_ROOT = "http://localhost:3000";
12
13
  </script>
13
- <script src="https://cdn.jx3box.com/static/tinymce/tinymce.min.js?v=1.9.0"></script>
14
+ <!-- <script src="https://cdn.jx3box.com/static/tinymce/tinymce.min.js?v=1.9.0"></script> -->
14
15
  <!-- TODO: -->
15
- <!-- <script src="http://localhost:3000/tinymce.min.js"></script> -->
16
+ <script src="http://localhost:3000/tinymce.min.js?v=1.9.0"></script>
16
17
  </head>
17
18
  <body>
18
19
  <div id="app"></div>
19
- <div id="c-ie-tips" style="display:none;background-color: #fef0f0;color: #f56c6c;padding:10px;font-weight:bold;text-align:center;width:60%;margin:200px auto;"><p>你的浏览器版本太低,可能无法正常使用本站,建议下载<a href="https://www.google.cn/intl/zh-CN/chrome/" target="_blank">Chrome</a>或其它极速浏览器</p><p>www.jx3box.com</p></div>
20
+ <div id="c-ie-tips" style="display: none; background-color: #fef0f0; color: #f56c6c; padding: 10px; font-weight: bold; text-align: center; width: 60%; margin: 200px auto">
21
+ <p>你的浏览器版本太低,可能无法正常使用本站,建议下载<a href="https://www.google.cn/intl/zh-CN/chrome/" target="_blank">Chrome</a>或其它极速浏览器</p>
22
+ <p>www.jx3box.com</p>
23
+ </div>
20
24
  <script>
21
25
  let ua = window.navigator.userAgent.toLocaleLowerCase();
22
26
  let isIE = ua.indexOf("trident") > 0 || ua.indexOf("msie") > 0;
23
27
  if (isIE) document.getElementById("c-ie-tips").style.display = "block";
24
28
  </script>
25
- <div style="display:none;">
26
- <script type="text/javascript"
27
- src="https://s9.cnzz.com/z_stat.php?id=1277938200&web_id=1277938200">
28
- </script>
29
+ <div style="display: none">
30
+ <script type="text/javascript" src="https://s9.cnzz.com/z_stat.php?id=1277938200&web_id=1277938200"></script>
29
31
  </div>
30
32
  </body>
31
33
  </html>
package/src/Article.vue CHANGED
@@ -66,6 +66,7 @@ import renderCode from "../assets/js/code";
66
66
  import renderImgPreview from "../assets/js/renderImgPreview";
67
67
  import renderPzIframe from "../assets/js/pz_iframe";
68
68
  import renderCombo from "../assets/js/combo";
69
+ import renderVoice from "../assets/js/voice";
69
70
 
70
71
  // 剑三
71
72
  import Item from "./Item";
@@ -215,6 +216,8 @@ export default {
215
216
  renderPzIframe();
216
217
  // 连招
217
218
  renderCombo();
219
+ // 音频
220
+ renderVoice();
218
221
  // 物品
219
222
  renderJx3Element(this);
220
223
  },
@@ -38,6 +38,7 @@ import Skill from "./Skill";
38
38
  import Npc from "./Npc";
39
39
  import renderJx3Element from "../assets/js/jx3_element";
40
40
  import renderImgPreview from "../assets/js/renderImgPreview";
41
+ import renderVoice from "../assets/js/voice";
41
42
 
42
43
  import {xssOptions} from '../assets/data/markdown_whitelist.json'
43
44
 
@@ -116,6 +117,8 @@ export default {
116
117
  renderImgPreview(this);
117
118
  // 语法高亮
118
119
  renderCode(`code[class=^'lang-']`)
120
+ // 音频
121
+ renderVoice();
119
122
  // 物品
120
123
  renderJx3Element(this);
121
124
  },
package/src/Tinymce.vue CHANGED
@@ -1,21 +1,27 @@
1
1
  <template>
2
- <div class="c-editor-tinymce">
3
- <slot name="prepend"></slot>
4
-
5
- <div class="c-editor-header">
6
- <Upload v-if="attachmentEnable" @insert="insertAttachments" />
7
- <Resource v-if="resourceEnable" @insert="insertResource" />
8
- <BoxResource v-if="resourceEnable" @insert="insertResource" :subtype="subtype" />
9
- </div>
10
- <Emotion class="c-editor-emotion" @selected="emotionSelected"></Emotion>
11
-
12
- <slot></slot>
13
-
14
- <editor id="tinymce" v-model="data" :init="init" class="c-tinymce" placeholder="✔ 图片可右键粘贴或拖拽至编辑器内自动上传 ✔ 支持word/excel内容一键粘贴" />
15
- <el-alert class="u-tutorial" type="warning" show-icon>进入特殊区域(代码块,折叠块等等)脱离或使用工具栏触发后,请使用键盘方向 → ↓ 键进行脱离,回车只是正常在区块内换行。去掉样式点击第二行第一个&lt;清除格式&gt;即可复位。<a href="/collection/31" target="_blank">[编辑器使用指南]</a> </el-alert>
16
-
17
- <slot name="append"></slot>
18
- </div>
2
+ <div class="c-editor-tinymce">
3
+ <slot name="prepend"></slot>
4
+
5
+ <div class="c-editor-header">
6
+ <Upload v-if="attachmentEnable" @insert="insertAttachments" />
7
+ <Resource v-if="resourceEnable" @insert="insertResource" />
8
+ <BoxResource v-if="resourceEnable" @insert="insertResource" :subtype="subtype" />
9
+ </div>
10
+ <Emotion class="c-editor-emotion" @selected="emotionSelected"></Emotion>
11
+
12
+ <slot></slot>
13
+
14
+ <editor id="tinymce" v-model="data" :init="init" class="c-tinymce" placeholder="✔ 图片可右键粘贴或拖拽至编辑器内自动上传 ✔ 支持word/excel内容一键粘贴" />
15
+ <el-alert class="u-tutorial" type="warning" show-icon
16
+ >进入特殊区域(代码块,折叠块等等)脱离或使用工具栏触发后,请使用键盘方向 → ↓ 键进行脱离,回车只是正常在区块内换行。去掉样式点击第二行第一个&lt;清除格式&gt;即可复位。<a
17
+ href="/collection/31"
18
+ target="_blank"
19
+ >[编辑器使用指南]</a
20
+ >
21
+ </el-alert>
22
+
23
+ <slot name="append"></slot>
24
+ </div>
19
25
  </template>
20
26
 
21
27
  <script>
@@ -36,133 +42,181 @@ import { draggable } from "../assets/js/drag";
36
42
  Vue.directive("draggable", draggable);
37
43
 
38
44
  export default {
39
- name: "Tinymce",
40
- props: ["content", "height", "attachmentEnable", "resourceEnable", "subtype"],
41
- data: function () {
42
- return {
43
- data: this.content,
44
- init: {
45
- // 选择器
46
- selector: "#tinymce",
47
-
48
- // 语言
49
- language: "zh_CN",
50
-
51
- // 设置
52
- convert_urls: false,
53
-
54
- // 样式
55
- // TODO:
56
- content_css: process.env.VUE_APP_DEV_COMPONENT == "true" ? "/css/article.css" : `https://cdn.jx3box.com/static/jx3box-editor/css/article.css`,
57
- // content_css: `http://localhost:3000/skins/content/default/content.min.css`,
58
- body_class: "c-article c-article-editor c-article-tinymce",
59
- height: this.height || 800,
60
- autosave_ask_before_unload: false,
61
- content_style: "",
62
-
63
- // UI
64
- icons: "custom",
65
- menubar: false,
66
- branding: false,
67
- contextmenu: "",
68
- plugins: [
69
- "link autolink",
70
- "hr lists advlist table codeinline codesample checklist foldtext latex anchor",
71
- "image emoticons media videox macro qixue talent2",
72
- "code fullscreen wordcount powerpaste pagebreak printpage pz", // template anchor jx3icon autosave
73
- ],
74
- toolbar: [
75
- "undo | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough superscript subscript | link unlink | fullscreen code", //anchor restoredraft
76
- "removeformat | hr alignleft aligncenter alignright alignjustify indent outdent | bullist numlist checklist table blockquote foldtext codeinline codesample latex | emoticons image media videox | macro pz qixue talent2 pagebreak printpage", // template anchor jx3icon
77
- ],
78
- mobile: {
79
- toolbar_drawer: true,
80
- toolbar: ["undo emoticons bold italic underline strikethrough superscript subscript link unlink forecolor backcolor removeformat pagebreak fullscreen code", "hr alignleft aligncenter alignright alignjustify indent outdent bullist numlist checklist table blockquote foldtext codeinline codesample latex macro pz qixue talent2 media videox"],
81
- },
82
- block_formats: "段落=p;一级标题=h1;二级标题=h2;三级标题=h3;四级标题=h4;五级标题=h5;六级标题=h6;",
83
- fontsize_formats: "12px 14px 16px 18px 22px 24px 26px 28px 32px 48px 72px",
84
- color_map: ["FF99CC", "浅粉", "FF3399", "深粉", "FF0000", "正红", "CC99FF", "紫色", "9933ff", "深紫", "FFFF99", "浅黄", "FFFF00", "金黄", "FFCC00", "亮黄", "FFCC99", "浅桃", "FF6600", "橘色", "CCFFCC", "浅绿", "9bf915", "荧光绿", "00FF00", "辣眼绿", "49c10f", "深绿", "008080", "深青", "CCFFFF", "浅蓝", "00FFFF", "参考线", "00CCFF", "天蓝", "99CCFF", "蔚蓝", "0000FF", "辣眼蓝", "CC0000", "深红", "000000", "黑色"],
85
-
86
- codesample_languages: hljs_languages,
87
-
88
- // Image
89
- image_advtab: true,
90
- // paste_data_images: true,
91
- file_picker_types: "file image",
92
- images_upload_url: API,
93
- automatic_uploads: true,
94
- images_upload_credentials: true,
95
-
96
- // Hook
97
- // setup: this.setup,
98
- // init_instance_callback: this.ready,
99
-
100
- // Template
101
- // templates: [
102
- // {
103
- // title: "剑三宏",
104
- // description: "",
105
- // content: `
106
- // <pre class="e-jx3macro-area w-jx3macro">/cast 自绝经脉</pre>
107
- // `,
108
- // },
109
- // ],
110
- valid_children: "+body[style]",
111
- },
112
- mode: "tinymce",
113
- style: "",
114
- };
115
- },
116
- model: {
117
- prop: "content",
118
- event: "update",
119
- },
120
- watch: {
121
- data: function (newval) {
122
- this.$emit("update", newval);
123
- },
124
- content: function (newval) {
125
- this.data = newval;
126
- },
127
- style(val) {
128
- this.init.content_style = val;
129
- },
130
- },
131
- methods: {
132
- setup: function (editor) {
133
- // console.log("ID为: " + editor.id + " 的编辑器即将初始化.");
134
- },
135
- ready: function (editor) {
136
- // console.log("ID为: " + editor.id + " 的编辑器已初始化完成.");
137
- },
138
- insertAttachments: function (data) {
139
- tinyMCE.editors["tinymce"].insertContent(data.html);
140
- },
141
- insertResource: function (data) {
142
- tinyMCE.editors["tinymce"].insertContent(data);
143
- },
144
- emotionSelected: function (emotion) {
145
- let src = emotion.filename;
45
+ name: "Tinymce",
46
+ props: ["content", "height", "attachmentEnable", "resourceEnable", "subtype"],
47
+ data: function () {
48
+ return {
49
+ data: this.content,
50
+ init: {
51
+ // 选择器
52
+ selector: "#tinymce",
53
+
54
+ // 语言
55
+ language: "zh_CN",
56
+
57
+ // 设置
58
+ convert_urls: false,
59
+
60
+ // 样式
61
+ // TODO:
62
+ content_css:
63
+ process.env.VUE_APP_DEV_COMPONENT == "true" ? "http://localhost:3000/skins/content/default/content.min.css" : `https://cdn.jx3box.com/static/jx3box-editor/css/article.css`,
64
+ // content_css: `http://localhost:3000/skins/content/default/content.min.css`,
65
+ body_class: "c-article c-article-editor c-article-tinymce",
66
+ height: this.height || 800,
67
+ autosave_ask_before_unload: false,
68
+ content_style: "",
69
+
70
+ // UI
71
+ icons: "custom",
72
+ menubar: false,
73
+ branding: false,
74
+ contextmenu: "",
75
+ plugins: [
76
+ "link autolink",
77
+ "hr lists advlist table codeinline codesample checklist foldtext latex anchor",
78
+ "image emoticons media videox macro qixue talent2",
79
+ "code fullscreen wordcount powerpaste pagebreak printpage pz audio_x", // template anchor jx3icon autosave
80
+ ],
81
+ toolbar: [
82
+ "undo | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough superscript subscript | link unlink | fullscreen code", //anchor restoredraft
83
+ "removeformat | hr alignleft aligncenter alignright alignjustify indent outdent | bullist numlist checklist table blockquote foldtext codeinline codesample latex | emoticons image media videox audio_x | macro pz qixue talent2 pagebreak printpage", // template anchor jx3icon
84
+ ],
85
+ mobile: {
86
+ toolbar_drawer: true,
87
+ toolbar: [
88
+ "undo emoticons bold italic underline strikethrough superscript subscript link unlink forecolor backcolor removeformat pagebreak fullscreen code",
89
+ "hr alignleft aligncenter alignright alignjustify indent outdent bullist numlist checklist table blockquote foldtext codeinline codesample latex macro pz qixue talent2 media videox audio_x",
90
+ ],
91
+ },
92
+ block_formats: "段落=p;一级标题=h1;二级标题=h2;三级标题=h3;四级标题=h4;五级标题=h5;六级标题=h6;",
93
+ fontsize_formats: "12px 14px 16px 18px 22px 24px 26px 28px 32px 48px 72px",
94
+ color_map: [
95
+ "FF99CC",
96
+ "浅粉",
97
+ "FF3399",
98
+ "深粉",
99
+ "FF0000",
100
+ "正红",
101
+ "CC99FF",
102
+ "紫色",
103
+ "9933ff",
104
+ "深紫",
105
+ "FFFF99",
106
+ "浅黄",
107
+ "FFFF00",
108
+ "金黄",
109
+ "FFCC00",
110
+ "亮黄",
111
+ "FFCC99",
112
+ "浅桃",
113
+ "FF6600",
114
+ "橘色",
115
+ "CCFFCC",
116
+ "浅绿",
117
+ "9bf915",
118
+ "荧光绿",
119
+ "00FF00",
120
+ "辣眼绿",
121
+ "49c10f",
122
+ "深绿",
123
+ "008080",
124
+ "深青",
125
+ "CCFFFF",
126
+ "浅蓝",
127
+ "00FFFF",
128
+ "参考线",
129
+ "00CCFF",
130
+ "天蓝",
131
+ "99CCFF",
132
+ "蔚蓝",
133
+ "0000FF",
134
+ "辣眼蓝",
135
+ "CC0000",
136
+ "深红",
137
+ "000000",
138
+ "黑色",
139
+ ],
140
+
141
+ codesample_languages: hljs_languages,
142
+
143
+ // Image
144
+ image_advtab: true,
145
+ // paste_data_images: true,
146
+ file_picker_types: "file image",
147
+ images_upload_url: API,
148
+ automatic_uploads: true,
149
+ images_upload_credentials: true,
150
+
151
+ // Hook
152
+ // setup: this.setup,
153
+ // init_instance_callback: this.ready,
154
+
155
+ // Template
156
+ // templates: [
157
+ // {
158
+ // title: "剑三宏",
159
+ // description: "",
160
+ // content: `
161
+ // <pre class="e-jx3macro-area w-jx3macro">/cast 自绝经脉</pre>
162
+ // `,
163
+ // },
164
+ // ],
165
+ valid_children: "+body[style]",
166
+ },
167
+ mode: "tinymce",
168
+ style: "",
169
+ };
170
+ },
171
+ model: {
172
+ prop: "content",
173
+ event: "update",
174
+ },
175
+ watch: {
176
+ data: function (newval) {
177
+ this.$emit("update", newval);
178
+ },
179
+ content: function (newval) {
180
+ this.data = newval;
181
+ },
182
+ style(val) {
183
+ this.init.content_style = val;
184
+ },
185
+ },
186
+ methods: {
187
+ setup: function (editor) {
188
+ // console.log("ID为: " + editor.id + " 的编辑器即将初始化.");
189
+ },
190
+ ready: function (editor) {
191
+ // console.log("ID为: " + editor.id + " 的编辑器已初始化完成.");
192
+ },
193
+ insertAttachments: function (data) {
194
+ tinyMCE.editors["tinymce"].insertContent(data.html);
195
+ },
196
+ insertResource: function (data) {
197
+ tinyMCE.editors["tinymce"].insertContent(data);
198
+ },
199
+ emotionSelected: function (emotion) {
200
+ let src = emotion.filename;
146
201
  if (!emotion.filename.startsWith("http")) {
147
202
  src = `${__imgPath}emotion/output/${emotion.filename}`;
148
203
  }
149
- const IMAGE = `<img class="t-emotion" src="${src}" alt="${src}" style="max-width:60px;max-height:60px" />`;
150
- tinyMCE.editors["tinymce"].insertContent(IMAGE);
151
- },
152
- },
153
- mounted: function () {},
154
- components: {
155
- Editor,
156
- Upload,
157
- Resource,
158
- Emotion,
159
- BoxResource,
160
- },
204
+ const IMAGE = `<img class="t-emotion" src="${src}" alt="${src}" style="max-width:60px;max-height:60px" />`;
205
+ tinyMCE.editors["tinymce"].insertContent(IMAGE);
206
+ },
207
+ },
208
+ mounted: function () {},
209
+ components: {
210
+ Editor,
211
+ Upload,
212
+ Resource,
213
+ Emotion,
214
+ BoxResource,
215
+ },
161
216
  };
162
217
  </script>
163
218
 
164
219
  <style lang="less">
165
220
  @import "../assets/css/tinymce.less";
166
221
  @import "../assets/css/tinymce/combo.less";
167
-
168
222
  </style>
@@ -0,0 +1,121 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>音频播放器测试</title>
7
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
8
+ <style>
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
11
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12
+ min-height: 100vh;
13
+ padding: 40px 20px;
14
+ margin: 0;
15
+ }
16
+
17
+ .container {
18
+ max-width: 800px;
19
+ margin: 0 auto;
20
+ }
21
+
22
+ h1 {
23
+ color: white;
24
+ text-align: center;
25
+ margin-bottom: 40px;
26
+ font-size: 32px;
27
+ }
28
+
29
+ .demo-section {
30
+ background: white;
31
+ border-radius: 15px;
32
+ padding: 30px;
33
+ margin-bottom: 30px;
34
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
35
+ }
36
+
37
+ .demo-section h2 {
38
+ margin-top: 0;
39
+ color: #333;
40
+ border-bottom: 2px solid #9b87f5;
41
+ padding-bottom: 10px;
42
+ }
43
+
44
+ .code-block {
45
+ background: #f5f5f5;
46
+ border-radius: 8px;
47
+ padding: 15px;
48
+ margin: 15px 0;
49
+ overflow-x: auto;
50
+ }
51
+
52
+ .code-block code {
53
+ font-family: "Monaco", "Courier New", monospace;
54
+ font-size: 14px;
55
+ color: #333;
56
+ }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <div class="container">
61
+ <h1>🎵 JX3 音频播放器演示</h1>
62
+
63
+ <div class="demo-section">
64
+ <h2>使用示例</h2>
65
+ <p>在文章内容中插入以下格式的文本,会自动渲染为音频播放器:</p>
66
+ <div class="code-block">
67
+ <code>name:123;author:1231;user_id:8719;src:https://cdn.jx3box.com/upload/voice/jx3cxk/2026/8719-1768912381447.mp3</code>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="demo-section">
72
+ <h2>实际效果</h2>
73
+ <div class="w-audio">name:123;author:1231;user_id:8719;src:https://cdn.jx3box.com/upload/voice/jx3cxk/2026/8719-1768912381447.mp3</div>
74
+ </div>
75
+
76
+ <div class="demo-section">
77
+ <h2>功能特性</h2>
78
+ <ul>
79
+ <li>✨ 美观的紫色渐变卡片设计</li>
80
+ <li>🎨 旋转的黑胶唱片动画效果</li>
81
+ <li>🎯 可拖拽的进度条</li>
82
+ <li>⏯️ 播放/暂停控制</li>
83
+ <li>⏱️ 实时时间显示</li>
84
+ <li>📱 响应式设计,支持移动端</li>
85
+ <li>🎼 唱针动画效果</li>
86
+ </ul>
87
+ </div>
88
+
89
+ <div class="demo-section">
90
+ <h2>在项目中使用</h2>
91
+ <p><strong>1. 导入模块:</strong></p>
92
+ <div class="code-block">
93
+ <code>import renderVoice from '@/assets/js/voice.js';</code>
94
+ </div>
95
+
96
+ <p><strong>2. 在组件挂载后调用:</strong></p>
97
+ <div class="code-block">
98
+ <code
99
+ >mounted() {<br />
100
+ &nbsp;&nbsp;renderVoice('.w-audio');<br />
101
+ }</code
102
+ >
103
+ </div>
104
+
105
+ <p><strong>3. HTML 中使用:</strong></p>
106
+ <div class="code-block">
107
+ <code>&lt;div class="w-audio"&gt;name:音频名称;author:作者;user_id:123;src:音频地址&lt;/div&gt;</code>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ <script type="module">
113
+ import renderVoice from "./assets/js/voice.js";
114
+
115
+ // 页面加载完成后渲染音频播放器
116
+ $(document).ready(function () {
117
+ renderVoice(".w-audio");
118
+ });
119
+ </script>
120
+ </body>
121
+ </html>