@jx3box/jx3box-ui 2.3.20 → 2.3.22

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.
@@ -305,15 +305,15 @@ export default {
305
305
  .u-game__icon {
306
306
  .flex(x);
307
307
  flex: 0 0 auto;
308
- width: 16px;
309
- height: 16px;
308
+ width: 20px;
309
+ height: 20px;
310
310
  margin-right: 12px;
311
311
 
312
312
  &,
313
313
  svg,
314
314
  img {
315
- width: 16px !important;
316
- height: 16px !important;
315
+ width: 20px !important;
316
+ height: 20px !important;
317
317
  }
318
318
 
319
319
  svg,
@@ -1,12 +1,18 @@
1
1
  <template>
2
2
  <div ref="trigger" class="w-qrcode" v-bind="$attrs" @click="togglePic" v-if="mode == 'cms'">
3
3
  <img class="u-icon" svg-inline src="../../assets/img/widget/qr-code.svg" />
4
- <span class="u-text">二维码</span>
4
+ <span class="u-text">{{ triggerText }}</span>
5
5
  </div>
6
6
  <teleport to="body">
7
- <div v-if="mode == 'cms' && active" class="u-qrcode u-qrcode-popup w-qrcode-static" :style="popupStyle" @click.stop>
7
+ <div
8
+ v-if="mode == 'cms' && active"
9
+ class="u-qrcode u-qrcode-popup w-qrcode-static"
10
+ :class="popupClass"
11
+ :style="popupStyle"
12
+ @click.stop
13
+ >
8
14
  <qrcode-vue class="u-pic" :value="value" :size="size" level="H"></qrcode-vue>
9
- <span class="u-txt">扫一扫即可访问</span>
15
+ <span class="u-txt">{{ popupText }}</span>
10
16
  </div>
11
17
  </teleport>
12
18
  <div class="w-qrcode-static" v-bind="$attrs" v-if="mode == 'static'">
@@ -38,6 +44,18 @@ export default {
38
44
  type: Number,
39
45
  default: 100,
40
46
  },
47
+ triggerText: {
48
+ type: String,
49
+ default: "二维码",
50
+ },
51
+ popupText: {
52
+ type: String,
53
+ default: "扫一扫即可访问",
54
+ },
55
+ popupClass: {
56
+ type: [String, Array, Object],
57
+ default: "",
58
+ },
41
59
  },
42
60
  data: function () {
43
61
  return {
@@ -1,11 +1,11 @@
1
1
  <template>
2
- <span class="c-game-price" :class="alignClass">
2
+ <span class="c-game-price" :class="rootClasses">
3
3
  <span class="u-neg" v-if="numericPrice < 0">- </span>
4
4
  <span class="u-price">
5
5
  <template v-for="part in priceParts" :key="part.unit">
6
6
  <span class="u-part" :class="`u-${part.unit}`">
7
7
  <span class="u-price-value">{{ part.value }}</span>
8
- <img :src="part.icon" :alt="part.alt" />
8
+ <img :src="part.icon" :alt="$jx3boxT(`jx3boxUi.gamePrice.${part.unit}`, part.alt)" />
9
9
  </span>
10
10
  </template>
11
11
  </span>
@@ -36,16 +36,38 @@ export default {
36
36
  type: [Boolean, String],
37
37
  default: false,
38
38
  },
39
+ variant: {
40
+ type: String,
41
+ default: "default",
42
+ validator: (value) => ["default", "surface", "plain"].includes(value),
43
+ },
44
+ size: {
45
+ type: String,
46
+ default: "medium",
47
+ validator: (value) => ["small", "medium", "large"].includes(value),
48
+ },
49
+ layout: {
50
+ type: String,
51
+ default: "inline",
52
+ validator: (value) => ["inline", "block"].includes(value),
53
+ },
54
+ wrap: {
55
+ type: Boolean,
56
+ default: true,
57
+ },
39
58
  },
40
59
  computed: {
41
60
  numericPrice() {
42
61
  const value = Number(this.price);
43
62
  return Number.isFinite(value) ? Math.trunc(value) : 0;
44
63
  },
45
- alignClass() {
46
- if (this.align === true) return "is-align";
47
- if (typeof this.align === "string" && this.align) return `is-${this.align}`;
48
- return "";
64
+ rootClasses() {
65
+ const classes = [`is-${this.variant}`, `is-size-${this.size}`, `is-layout-${this.layout}`];
66
+ if (this.align === true) classes.push("is-align");
67
+ if (typeof this.align === "string" && this.align) classes.push(`is-${this.align}`);
68
+ if (!this.wrap) classes.push("is-nowrap");
69
+ if (this.numericPrice < 0) classes.push("is-negative");
70
+ return classes;
49
71
  },
50
72
  priceParts() {
51
73
  const value = Math.abs(this.numericPrice);
@@ -75,21 +97,28 @@ export default {
75
97
 
76
98
  <style lang="less">
77
99
  .c-game-price {
100
+ --jx3box-ui-game-price-color: #333;
101
+ --jx3box-ui-game-price-icon-size: 18px;
102
+ --jx3box-ui-game-price-font-size: 12px;
103
+ --jx3box-ui-game-price-gap: 6px;
104
+ --jx3box-ui-game-price-negative-color: var(--jx3box-ui-game-price-color);
105
+
78
106
  display: inline-flex;
79
107
  align-items: center;
80
- color: #333;
108
+ color: var(--jx3box-ui-game-price-color);
81
109
  font-weight: 500;
82
110
 
83
111
  .u-price {
84
112
  display: inline-flex;
85
113
  align-items: center;
86
114
  flex-wrap: wrap;
87
- gap: 6px;
115
+ gap: var(--jx3box-ui-game-price-gap);
88
116
  }
89
117
 
90
118
  .u-neg {
91
119
  display: inline-flex;
92
120
  align-items: center;
121
+ color: var(--jx3box-ui-game-price-negative-color);
93
122
  }
94
123
 
95
124
  .u-part {
@@ -100,14 +129,15 @@ export default {
100
129
  }
101
130
 
102
131
  img {
103
- width: 18px;
104
- height: 18px;
132
+ width: var(--jx3box-ui-game-price-icon-size);
133
+ height: var(--jx3box-ui-game-price-icon-size);
105
134
  object-fit: contain;
106
135
  vertical-align: middle;
107
136
  }
108
137
 
109
138
  .u-price-value {
110
- .fz(12px,18px);
139
+ font-size: var(--jx3box-ui-game-price-font-size);
140
+ line-height: var(--jx3box-ui-game-price-icon-size);
111
141
  }
112
142
 
113
143
  &.is-align {
@@ -130,5 +160,32 @@ export default {
130
160
  justify-content: flex-end;
131
161
  width: 100%;
132
162
  }
163
+
164
+ &.is-size-small {
165
+ --jx3box-ui-game-price-icon-size: 14px;
166
+ --jx3box-ui-game-price-font-size: 11px;
167
+ --jx3box-ui-game-price-gap: 4px;
168
+ }
169
+
170
+ &.is-size-large {
171
+ --jx3box-ui-game-price-icon-size: 22px;
172
+ --jx3box-ui-game-price-font-size: 14px;
173
+ --jx3box-ui-game-price-gap: 8px;
174
+ }
175
+
176
+ &.is-layout-block {
177
+ display: flex;
178
+ width: 100%;
179
+ }
180
+
181
+ &.is-nowrap {
182
+ .u-price {
183
+ flex-wrap: nowrap;
184
+ }
185
+ }
186
+
187
+ &.is-surface {
188
+ --jx3box-ui-game-price-color: #334155;
189
+ }
133
190
  }
134
191
  </style>
@@ -11,7 +11,7 @@
11
11
  v-text="comment.user_nickname"
12
12
  ></a>
13
13
  <template v-if="comment.parent_id">
14
- <span>&nbsp;回复&nbsp;</span>
14
+ <span>&nbsp;{{ $jx3boxT("jx3boxUi.wiki.reply", "回复") }}&nbsp;</span>
15
15
  <a
16
16
  class="u-nickname"
17
17
  :href="comment.parent.user_id ? author_url(comment.parent.user_id) : null"
@@ -21,11 +21,11 @@
21
21
  </template>
22
22
  <span class="u-mark u-top" v-if="comment.is_top">
23
23
  <i class="el-icon-download"></i>
24
- 置顶
24
+ {{ $jx3boxT("jx3boxUi.wikiComment.top", "置顶") }}
25
25
  </span>
26
26
  <span class="u-mark u-star" v-if="comment.is_star">
27
27
  <i class="el-icon-star-on"></i>
28
- 精华
28
+ {{ $jx3boxT("jx3boxUi.wikiComment.star", "精华") }}
29
29
  </span>
30
30
  </div>
31
31
  <p class="u-content" v-html="comment.content"></p>
@@ -38,9 +38,11 @@
38
38
  class="u-reply"
39
39
  @click="comment.reply_form.show = !comment.reply_form.show"
40
40
  size="small"
41
+ :aria-expanded="true"
42
+ :aria-controls="`wiki-comment-reply-${comment.id}`"
41
43
  >
42
44
  <i class="el-icon-arrow-up"></i>
43
- <span>收起</span>
45
+ <span>{{ $jx3boxT("jx3boxUi.wikiComment.collapse", "收起") }}</span>
44
46
  </el-button>
45
47
  <el-button
46
48
  type="primary"
@@ -50,8 +52,10 @@
50
52
  @click="comment.reply_form.show = !comment.reply_form.show"
51
53
  icon="ChatDotRound"
52
54
  size="small"
55
+ :aria-expanded="false"
56
+ :aria-controls="`wiki-comment-reply-${comment.id}`"
53
57
  >
54
- <span>回复</span>
58
+ <span>{{ $jx3boxT("jx3boxUi.wiki.reply", "回复") }}</span>
55
59
  </el-button>
56
60
  <template v-if="isEditor && !comment.parent_id">
57
61
  <el-button
@@ -61,25 +65,43 @@
61
65
  plain
62
66
  :icon="comment.is_star ? 'StarFilled' : 'Star'"
63
67
  size="small"
64
- >{{ comment.is_star ? "取消加精" : "加精" }}</el-button
68
+ >{{
69
+ comment.is_star
70
+ ? $jx3boxT("jx3boxUi.wikiComment.cancelStar", "取消加精")
71
+ : $jx3boxT("jx3boxUi.wikiComment.setStar", "加精")
72
+ }}</el-button
65
73
  >
66
74
  <el-button type="primary" class="u-reply" @click="onTop(comment)" plain icon="Top" size="small">{{
67
- comment.is_top ? "取消置顶" : "置顶"
75
+ comment.is_top
76
+ ? $jx3boxT("jx3boxUi.wikiComment.cancelTop", "取消置顶")
77
+ : $jx3boxT("jx3boxUi.wikiComment.setTop", "置顶")
68
78
  }}</el-button>
69
79
  </template>
70
80
  <!-- 更新时间 -->
71
81
  <span class="u-time" v-text="ts2str(comment.updated)"></span>
72
82
  </div>
73
83
  <!-- 评论回复表单 -->
74
- <div class="m-reply-form" v-if="comment.reply_form && comment.reply_form.show">
75
- <textarea class="u-reply-content" v-model="comment.reply_form.content"></textarea>
84
+ <div
85
+ class="m-reply-form"
86
+ :id="`wiki-comment-reply-${comment.id}`"
87
+ v-if="comment.reply_form && comment.reply_form.show"
88
+ >
89
+ <textarea
90
+ class="u-reply-content"
91
+ v-model="comment.reply_form.content"
92
+ :aria-label="$jx3boxT('jx3boxUi.wiki.reply', '回复')"
93
+ ></textarea>
76
94
  <div class="u-author">
77
- <span>昵称:</span>
78
- <input v-model="comment.reply_form.user_nickname" type="text" />
95
+ <span>{{ $jx3boxT("jx3boxUi.wiki.nickname", "昵称:") }}</span>
96
+ <input
97
+ v-model="comment.reply_form.user_nickname"
98
+ type="text"
99
+ :aria-label="$jx3boxT('jx3boxUi.wiki.nickname', '昵称:')"
100
+ />
79
101
  </div>
80
102
  <el-button type="primary" class="u-submit" @click="create_comment(comment.reply_form, comment.id)">
81
103
  <i class="el-icon-check"></i>
82
- <span>提交</span>
104
+ <span>{{ $jx3boxT("jx3boxUi.common.submit", "提交") }}</span>
83
105
  </el-button>
84
106
  </div>
85
107
  </div>
@@ -117,7 +139,10 @@ export default {
117
139
  if (!app.create_comment) app = app.$parent;
118
140
  if (!app.create_comment) {
119
141
  this.$message({
120
- message: "发布评论异常,请联系管理员",
142
+ message: this.$jx3boxT(
143
+ "jx3boxUi.wikiComment.publishUnavailable",
144
+ "发布评论异常,请联系管理员"
145
+ ),
121
146
  type: "warning",
122
147
  });
123
148
  return;
@@ -132,7 +157,10 @@ export default {
132
157
  if (!app.star_comment) app = app.$parent;
133
158
  if (!app.star_comment) {
134
159
  this.$message({
135
- message: "操作异常,请联系管理员",
160
+ message: this.$jx3boxT(
161
+ "jx3boxUi.wikiComment.actionUnavailable",
162
+ "操作异常,请联系管理员"
163
+ ),
136
164
  type: "warning",
137
165
  });
138
166
  return;
@@ -144,7 +172,10 @@ export default {
144
172
  if (!app.top_comment) app = app.$parent;
145
173
  if (!app.top_comment) {
146
174
  this.$message({
147
- message: "操作异常,请联系管理员",
175
+ message: this.$jx3boxT(
176
+ "jx3boxUi.wikiComment.actionUnavailable",
177
+ "操作异常,请联系管理员"
178
+ ),
148
179
  type: "warning",
149
180
  });
150
181
  return;
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <WikiPanel class="c-wiki-comments" scene="detail">
2
+ <WikiPanel class="c-wiki-comments" scene="detail" :variant="variant">
3
3
  <template #head-title>
4
4
  <i class="u-icon el-icon-chat-line-round"></i>
5
5
  <span class="u-txt">{{ $jx3boxT('jx3boxUi.wikiComments.title', '百科评论') }}</span>
@@ -29,10 +29,18 @@
29
29
  <i class="el-icon-chat-dot-round"></i>
30
30
  <span>{{ $jx3boxT('jx3boxUi.wiki.reply', '回复') }}</span>
31
31
  </h4>
32
- <textarea class="u-reply-content" v-model="reply_form.content"></textarea>
32
+ <textarea
33
+ class="u-reply-content"
34
+ v-model="reply_form.content"
35
+ :aria-label="$jx3boxT('jx3boxUi.wiki.reply', '回复')"
36
+ ></textarea>
33
37
  <div class="u-author">
34
38
  <span>{{ $jx3boxT('jx3boxUi.wiki.nickname', '昵称:') }}</span>
35
- <input v-model="reply_form.user_nickname" type="text" />
39
+ <input
40
+ v-model="reply_form.user_nickname"
41
+ type="text"
42
+ :aria-label="$jx3boxT('jx3boxUi.wiki.nickname', '昵称:')"
43
+ />
36
44
  </div>
37
45
  <el-button type="primary" class="u-submit" @click="create_comment(reply_form)">
38
46
  <i class="el-icon-check"></i>
@@ -61,6 +69,11 @@ export default {
61
69
  type: [Number, String],
62
70
  default: 0,
63
71
  },
72
+ variant: {
73
+ type: String,
74
+ default: "default",
75
+ validator: (value) => ["default", "surface", "plain"].includes(value),
76
+ },
64
77
  },
65
78
  data() {
66
79
  return {
@@ -163,7 +176,9 @@ export default {
163
176
  });
164
177
  },
165
178
  getDefaultNickname() {
166
- return User.isLogin() ? User.getInfo().name : "神秘侠士";
179
+ return User.isLogin()
180
+ ? User.getInfo().name
181
+ : this.$jx3boxT("jx3boxUi.wiki.mysterious", "神秘侠士");
167
182
  },
168
183
  handleCurrentChange(page) {
169
184
  this.page = page;
@@ -1,9 +1,7 @@
1
1
  <template>
2
2
  <div
3
3
  class="c-wiki-panel"
4
- :class="{
5
- border: border,
6
- }"
4
+ :class="rootClasses"
7
5
  >
8
6
  <div class="m-panel-head">
9
7
  <slot name="head-before"></slot>
@@ -11,7 +9,13 @@
11
9
  <slot name="head-title"></slot>
12
10
  </div>
13
11
  <div class="m-panel-actions">
14
- <QRcode v-if="wikiPost && showQR" class="u-qr" />
12
+ <QRcode
13
+ v-if="wikiPost && showQR"
14
+ class="u-qr"
15
+ :trigger-text="$jx3boxT('jx3boxUi.wikiPanel.qrTrigger', '二维码')"
16
+ :popup-text="$jx3boxT('jx3boxUi.wikiPanel.qrScanToVisit', '扫一扫即可访问')"
17
+ popup-class="u-wiki-panel-qrcode-popup"
18
+ />
15
19
  <slot name="head-actions"></slot>
16
20
  </div>
17
21
  <slot name="head-after"></slot>
@@ -72,6 +76,11 @@ export default {
72
76
  type: String,
73
77
  default: "default",
74
78
  },
79
+ variant: {
80
+ type: String,
81
+ default: "default",
82
+ validator: (value) => ["default", "surface", "plain"].includes(value),
83
+ },
75
84
  border: {
76
85
  type: Boolean,
77
86
  default: true,
@@ -86,6 +95,15 @@ export default {
86
95
  stat: null,
87
96
  };
88
97
  },
98
+ computed: {
99
+ rootClasses() {
100
+ return {
101
+ border: this.border,
102
+ [`is-${this.variant}`]: true,
103
+ [`is-scene-${this.scene}`]: true,
104
+ };
105
+ },
106
+ },
89
107
  watch: {
90
108
  wikiPost: {
91
109
  immediate: true,
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <WikiPanel class="c-wiki-revisions" scene="detail">
2
+ <WikiPanel class="c-wiki-revisions" scene="detail" :variant="variant">
3
3
  <template #head-title>
4
4
  <i class="u-icon el-icon-time"></i>
5
5
  <span class="u-txt">{{ $jx3boxT('jx3boxUi.wikiRevisions.title', '历史版本') }}</span>
@@ -62,6 +62,11 @@ export default {
62
62
  type: [Boolean, Number],
63
63
  default: false,
64
64
  },
65
+ variant: {
66
+ type: String,
67
+ default: "default",
68
+ validator: (value) => ["default", "surface", "plain"].includes(value),
69
+ },
65
70
  },
66
71
  data: function () {
67
72
  return {
@@ -0,0 +1,113 @@
1
+ const assert = require("node:assert/strict");
2
+ const test = require("node:test");
3
+
4
+ require("@babel/register")({
5
+ babelrc: false,
6
+ configFile: false,
7
+ extensions: [".js"],
8
+ ignore: [/node_modules/],
9
+ plugins: ["@babel/plugin-transform-modules-commonjs"],
10
+ });
11
+
12
+ const {
13
+ createJx3boxUiI18n,
14
+ getJx3boxUiMessages,
15
+ mergeJx3boxUiMessages,
16
+ setJx3boxUiI18n,
17
+ } = require("../i18n");
18
+ const i18nMixin = require("../i18n/mixin").default;
19
+ const { createI18n } = require("vue-i18n");
20
+
21
+ const TARGET_PREFIXES = [
22
+ "wiki.",
23
+ "wikiPanel.",
24
+ "wikiComments.",
25
+ "wikiComment.",
26
+ "wikiRevisions.",
27
+ "gamePrice.",
28
+ ];
29
+
30
+ function flattenMessages(value, prefix = "", output = {}) {
31
+ Object.entries(value || {}).forEach(([key, child]) => {
32
+ const path = prefix ? `${prefix}.${key}` : key;
33
+ if (child && typeof child === "object" && !Array.isArray(child)) {
34
+ flattenMessages(child, path, output);
35
+ } else {
36
+ output[path] = child;
37
+ }
38
+ });
39
+ return output;
40
+ }
41
+
42
+ function placeholders(value) {
43
+ return [...String(value ?? "").matchAll(/\{(\w+)\}/g)].map((match) => match[1]).sort();
44
+ }
45
+
46
+ test("loads the target component messages for each supported locale", () => {
47
+ const messages = getJx3boxUiMessages();
48
+ Object.entries(messages).forEach(([locale, message]) => {
49
+ const flattened = flattenMessages(message);
50
+ TARGET_PREFIXES.forEach((prefix) => {
51
+ assert.ok(
52
+ Object.keys(flattened).some((key) => key.startsWith(prefix)),
53
+ `${locale} is missing ${prefix}`
54
+ );
55
+ });
56
+ });
57
+
58
+ const i18n = createJx3boxUiI18n({ locale: "en-US" });
59
+ assert.equal(i18n.global.t("jx3boxUi.wikiComment.setTop"), "Pin");
60
+ assert.equal(i18n.global.t("jx3boxUi.wikiPanel.qrTrigger"), "QR code");
61
+ assert.equal(i18n.global.t("jx3boxUi.wikiPanel.qrScanToVisit"), "Scan to open this page");
62
+ assert.equal(i18n.global.t("jx3boxUi.gamePrice.jin"), "gold");
63
+ });
64
+
65
+ test("merges target messages into a host i18n instance", () => {
66
+ const hostI18n = createI18n({
67
+ legacy: false,
68
+ locale: "en-US",
69
+ fallbackLocale: "zh-CN",
70
+ messages: {
71
+ "en-US": {
72
+ host: {
73
+ title: "Host",
74
+ },
75
+ },
76
+ },
77
+ });
78
+
79
+ mergeJx3boxUiMessages(hostI18n);
80
+ setJx3boxUiI18n(hostI18n);
81
+
82
+ assert.equal(hostI18n.global.t("host.title"), "Host");
83
+ assert.equal(hostI18n.global.t("jx3boxUi.wikiComments.title"), "Wiki Comments");
84
+ });
85
+
86
+ test("component mixin keeps the explicit fallback when the host misses a key", () => {
87
+ setJx3boxUiI18n(null);
88
+ const context = {
89
+ $t(key) {
90
+ return key;
91
+ },
92
+ };
93
+
94
+ assert.equal(
95
+ i18nMixin.methods.$jx3boxT.call(context, "jx3boxUi.missing.example", "欢迎 {name}", { name: "侠士" }),
96
+ "欢迎 侠士"
97
+ );
98
+ });
99
+
100
+ test("keeps four locale message keys and placeholders aligned", () => {
101
+ const messages = getJx3boxUiMessages();
102
+ const locales = Object.keys(messages);
103
+ const baseline = flattenMessages(messages["zh-CN"]);
104
+ const baselineKeys = Object.keys(baseline).sort();
105
+
106
+ locales.forEach((locale) => {
107
+ const current = flattenMessages(messages[locale]);
108
+ assert.deepEqual(Object.keys(current).sort(), baselineKeys, `${locale} message keys differ from zh-CN`);
109
+ baselineKeys.forEach((key) => {
110
+ assert.deepEqual(placeholders(current[key]), placeholders(baseline[key]), `${locale}:${key} placeholders differ`);
111
+ });
112
+ });
113
+ });